Skip to main content

radio-dm-gateway

❖ Communityv0.1.0

Answers an allowlisted direct message on your Meshtastic® radio.

Open in Hermes Desktop
hermes plugins install radio-dm-gateway

What it adds

Environment variables it needs 1

MESHTASTIC_URL

README

From the reviewed commit cc543e3 ↗; it updates when the author re-pins.

radio-dm-gateway

Talk to Hermes over a radio, built on the Meshtastic® Python API. Meshtastic® is a registered trademark of Meshtastic LLC. This site is not affiliated with or endorsed by the Meshtastic project. The repository was renamed on 2026-10-08 from hermes-plugin-meshtastic-gateway to hermes-plugin-radio-dm-gateway; GitHub redirects the old URL.

If you already installed the old name: run hermes plugins remove meshtastic-gateway, then install this repository again. In config.yaml, rename platforms.meshtastic-gateway to platforms.radio-dm-gateway and keep the same URL and allowlist. Delete plugin-data/meshtastic-gateway under the Hermes home. Uninstall does not delete it. New rows go in plugin-data/radio-dm-gateway.

A direct message that is addressed to this node, from a node id on the allowlist, is handed to Hermes only when the packet's pkiEncrypted flag is true. The reply is sent back to that node as one or more text packets. An empty allowlist answers nobody. There is no allow-all switch. Channel packets are never answered, because a channel key can impersonate any sender. Checked with a fake radio and the real Hermes adapter (platform_registry.create_adapter, then handle_message, then send) on Hermes v0.21.4 and on current main. Not tried on a physical radio.

This plugin does not flash firmware, does not publish MQTT, and does not send a message to ^all.

What was tested

Checked with a fake radio and the real Hermes adapter: platform_registry.create_adapter, then handle_message, then send, on Hermes v0.21.4 and on current main. That fake radio received the reply. Not tried on a physical radio. This repository does not include a completed two-radio packet exchange.

Checked against public sources on 2026-10-06:

  • TCPInterface in the meshtastic Python package opens hostname on port 4403 by default, and sendText takes a node id. wantAck defaults to false. This plugin passes wantAck=False.
  • DATA_PAYLOAD_LEN in the Meshtastic protobuf is 233. This plugin caps a text chunk at 200 UTF-8 bytes so a packet is not truncated by that limit.
  • The Meshtastic encryption page (docs 2.8) says direct messages since firmware 2.5 use public-key encryption after a key exchange, and that older firmware carried direct messages as channel packets. The same page says a channel key lets anyone impersonate a sender. Anyone who hears the radio can see the packet header, including the node ids. This plugin answers only when pkiEncrypted is true, never answers a channel packet, and does not pin a per-node public key.

Install

Needs Hermes 0.21.4 or later. plugin.yaml declares meshtastic>=2.7.9,<3 (GPL-3.0-only) in python_dependencies, so Hermes installs it with the plugin, inside Hermes' core constraints. This plugin does not vendor it. Every release before 2.7.9 (checked on PyPI: 2.5.0, 2.5.12, 2.6.0, 2.6.4, 2.7.0 to 2.7.8) requires packaging<25, which Hermes v0.21.4's core constraint packaging==26.0 rejects, so Hermes cannot install them. 2.7.9 and 2.7.11 require packaging>=24 and are GPL-3.0-only.

Set one URL and the nodes that may talk:

export MESHTASTIC_URL=tcp://radio.example:4403
export MESHTASTIC_ALLOWED_NODES='!aabbccdd'

serial:// and ble:// are recognized and refused. This plugin opens tcp:// only, because a serial or BLE open cannot be stopped. That TCP socket is not TLS. The radio's configuration, including channel keys, and the text this plugin hands to the radio travel on that socket without TLS. Over the air, encryption is the radio's own, and a direct message is still answered only when pkiEncrypted is true. If the radio opens but its own node number cannot be read, the connection is closed and nothing is answered. Disconnect drops the text subscription before it closes the radio. A second connect on the same adapter drops the previous subscription first. A tcp URL cannot carry a user name, password, path, or query. A tcp port must be 1 to 65535; :0 is refused, not read as 4403. A tcp open waits at most 20 seconds for that socket to connect, then clears the socket timeout, so a quiet radio does not stop the reader. After that socket is up, the library waits up to 30 seconds for the radio's own configuration. This plugin does not replace socket.create_connection. The library may reconnect on its own, and those later sockets are not given this 20 second limit. This plugin does not cap those retries. If the library reports the connection lost, this adapter stops running so Hermes can build another. plugins.isolation: host does not load this platform on current Hermes main. Hermes v0.21.4 has no host-isolation switch for platforms, so that key is ignored and this adapter runs in-process there. The default on both versions is in-process. Node ids in the allowlist may be !aabbccdd, !AABBCCDD, or a decimal number. Hermes compares the environment text as written, so this adapter also publishes the canonical !aabbccdd form. A send that already put one chunk on the radio is final for this adapter. If the library raises while a chunk is being handed over, this attempt is final too: it may have reached the radio, and Hermes is told not to send that reply again inside the turn. Hermes still tries to send the whole reply again after about 30 seconds and about 2.5 minutes. Those sends are outside the turn, so this adapter refuses them and they do not go on the radio. gateway.delivery_ledger: false stops those later attempts. Hermes reconnect builds a new adapter and connects that new one. The hourly count and the seen packet ids live on the adapter, so the new one starts them over. Connecting again on the same adapter does not clear them.

Who can use it

MESHTASTIC_ALLOWED_NODES is the whole list. Empty means every packet is dropped before Hermes sees it, including slash commands and approval answers. An unreadable id refuses the connection instead of being ignored. The allowlist is not proof of who sent the packet. A channel key can impersonate a node id, so channel packets are dropped. A direct message is also dropped unless pkiEncrypted is true. This plugin does not pin a public key. A node id that is not on the list is dropped before a session starts. The nodes on that list can ask the agent to use the tools Hermes enables for this platform. With nothing else configured, that list is browser, clarify, code_execution, computer_use, connections, cronjob, delegation, file, hermes-radio-dm-gateway, image_gen, memory, session_search, skills, terminal, todo, tts, vision, and web. hermes-radio-dm-gateway registers no tools. They can approve a command once. Turn those tools off per platform with hermes tools. A cron job the agent creates stays until you run hermes cron remove. Cron is not given a radio sender.

A reply to a direct message this process just accepted is transmitted without a second approval prompt, because that is the answer the allowlisted node asked for. Only the task that is producing that reply may transmit. A nested task, tool progress, or interim text is refused and does not go on the radio. Hermes's one-time notice that no home channel is set, which says to type /sethome, is interim text and is not sent. /sethome is not a radio command. Hermes text sent while a turn is still running, including stream chunks and the busy acknowledgement for a second message, is not transmitted. The task that accepted the packet transmits only a refusal this plugin wrote. The reply sent after that turn, an approval question for that node, and a rewritten confirmation for /new, /reset, or /undo, are. An approval question for that same node is transmitted, so the person can answer it. Hermes waits 15 seconds for an approval question to be sent. The question goes out as one short chunk, Reply /approve or /deny (once only). Run: <command> — <reason>, built from the command and the reason. The <command> in that line is sent only when it is exactly the command /approve will run. Hermes masks a secret before it shows the command, stores that masked copy as the approval it is holding, and /approve runs the original. A masked command is not sent. A mask is ***, [REDACTED, «redacted, or six non-space characters, three dots, and four non-space characters. Before the line is sent, this plugin reads the approval Hermes is holding. If that stored command differs from the line, if the line is masked, or if that read fails, nothing is sent. It is also not sent when the command or the reason contains a newline, a Unicode line break, a control or format character, a surrogate, a private-use or unassigned code point, or a backtick fence, when the text cannot be encoded as UTF-8, when the full line does not fit one chunk, when the radio is down, or when the hourly cap is used up. Whatever else goes wrong while that question is being built or sent, including a fault in this plugin, is reported to Hermes the same way: the question was not sent. Nothing is sent as plain text instead. The gap before the send and a full radio queue share one 12 second budget, because Hermes stops watching this send after 15 seconds. If that budget runs out, nothing is written and the chunk is not left in the library queue. Hermes then drops that approval, and /approve does not run the command. A character in an ordinary reply that has no UTF-8 form is replaced with ? before the reply is cut into chunks; that replacement is never used on an approval question, which is refused instead. A question that may already have reached the radio is not sent again, and /approve can still run that command. Over the radio, approval is one time only. /approve always and /approve session are answered with Once only. always and session are refused. and are not passed to Hermes, because Hermes keeps those approvals: always is written to command_allowlist and skips the prompt for that pattern from then on. A message that is only always or session (or Hermes' words for them) is answered with Add more words. always and session are refused. even when nothing is pending. /always and /remember are answered with That command is refused on the radio. and are not passed to Hermes. Over the radio, only these slash commands are accepted: /approve (no argument or once), /deny, /cancel, /stop, /new, /reset, /help, /status, /whoami, /retry, /undo; /yolo and /approvals <mode> are refused because they change approval for the session or the whole profile. A confirmation for /new is sent as /new discards history. /approve or /cancel. always refused. /reset is an alias of /new and discards the same history. Hermes resolves that alias and passes the title /new, so the line says /new. A confirmation for /undo says /undo drops last exchange. when the prompt names no count. It says /undo drops <N> turns. when the prompt names one number above 1, in any language, or when it says <N> turns. If the prompt names more than one number and does not say <N> turns, that confirmation is not sent and the command does not run. When the radio accepts another short send, the note Undo count is unclear. Command not run. goes out instead. The rest of that line matches the /new line. If that line does not fit one chunk, that confirmation is not sent. The command does not run. When the radio accepts another short send, the note Confirmation does not fit. Command not run. goes out instead. While a turn is still running, /new and /reset are refused with Turn still running. /new and /reset refused. Session not reset. The session is not reset. While a turn is still running, /stop, /undo, and /status get no reply on the radio. Idle /new and /reset still ask first. /always is not offered. A command that starts with ! is checked the same way, and any other command is answered with That command is refused on the radio. and not passed to Hermes. Plain-text phrases that Hermes turns into a command (for example restart gateway) are checked the same way. Operators can also set Hermes' allow_admin_from to limit admin commands on the Hermes side. Any other radio send is refused before the radio is called. The gateway does not wait on a person for that other send. If the reply fails before sendText is called, that packet id is forgotten, so the same packet can be accepted again. If the library raises while a chunk is being handed over, that packet id stays seen, because the attempt may have reached the radio and the same packet is not answered again. A chunk that already went out stays final, and that packet id stays seen.

This plugin registers no cron job. Cron is not given a radio sender. A cron job the agent creates with the cron tool stays after you remove this plugin, until you run hermes cron remove.

Radio limits

Default gap is 20 seconds, and it cannot be set under 10 or over 3600. Default cap is 12 sends in a rolling hour on that adapter, and it cannot be set above 30. It cannot be set below 1; 0 is read as 1. While the radio link is down (the library reported it lost, or its connected flag is clear), a send fails at once with "The radio is not connected. Nothing was sent." and is not sent again later. If the radio reports a transmit queue with no free slot, the send waits at most 12 seconds. The chunk is not written and is not left in the library queue. That attempt has not reached the radio. An approval question counts the gap and that queue wait against the same 12 second budget, so the answer comes back before Hermes stops watching at 15 seconds. Inside the library's own send there are waits this plugin does not set: up to 30 seconds for the link, and a close, one second sleep, and reopen with no limit of its own after a socket write error. An approval send waits one second past its 12 second budget for that call, then reports the question as not sent, so Hermes drops the approval and /approve cannot run the command. The library can still put that line on the air afterwards, and the one send slot is held until that call returns, so no other send reaches the library while it is in there. When that call returns, the next approval is declined once and is not put on the radio, so /approve cannot run it before its own line would have gone out. Radio sends run off the event loop, one at a time, so a slow radio does not stop the gateway. Hermes reconnect builds a new adapter and connects that new one. The hourly count and the seen packet ids live on the adapter, so the new one starts them over and a retransmission can be answered again. Connecting again on the same adapter does not clear them. The same adapter keeps only the last 100 packet ids, so an older retransmission can be answered again. nodes.json is not the counter. One reply uses at most 4 chunks of 200 UTF-8 bytes (8 is the ceiling). Hermes is asked to hand this plugin up to 800 characters in one send. That cap is characters, not bytes. When the reply does not fit, the chunks that do fit are sent, the last one ends with [cut], and the Hermes result stays success with error text that the reply was cut. The chunk size cannot be set under 64 bytes, so the approval prefix (42 bytes) and a short command fit in one chunk. If the radio stops after a chunk has already gone out, the error says how many of the chunks were sent. Inside a send that does fit, a reply that would break the hourly cap is not started. A later send in the same hour waits out the gap instead of pretending the earlier text was never sent. These numbers are not a duty-cycle calculation. Regional rules (for example a 1% or 10% limit, or a dwell-time limit) are the operator's to follow, and the caps above can be lowered. An unsolicited send is refused and does not wait. The gap between chunks of an accepted reply is at most 3600 seconds.

wantAck is false on sends this plugin makes. That does not prove the firmware will never retransmit for its own reasons.

What is stored

Under Hermes plugin_data_dir, nodes.json keeps up to 200 rows of node id, direction (in or out), byte count, and time. An inbound row is written only for a packet this radio accepted, after the handoff begins and before the reply is sent. If the event loop is not running, or the message cannot be handed over, the packet is not recorded and its id is not treated as already seen. Writes take a lock, write a temporary file, then replace nodes.json. A file that is not JSON, whose top level is not a list, whose bytes are not UTF-8, or that is nested deeper than the interpreter can walk, is left exactly as it is, and no new row is recorded until you delete it. The gateway log says once that such a file was left alone, and direct messages are still answered. Any JSON list this plugin can walk, including a list whose items are not row objects, is rewritten with the new row at the end and trimmed to the last 200 rows, so older rows in a longer file are dropped. The message text is not stored in that file. A chat display name is not stored there. The text you send is still in the Hermes session, because that is how the gateway turn runs. If plugin_data_dir cannot be loaded, nothing is written elsewhere. Uninstall does not delete nodes.json. Remove the radio-dm-gateway directory under Hermes plugin data to delete it.

Disclosure

This plugin reads gateway.run_busy.approval_input_words when that function exists, and gateway.platforms.base.coerce_plaintext_gateway_command. Before an approval question is sent, this plugin reads the approval Hermes is holding for that session. If a Hermes update changes that read, the radio approval is not sent. Whatever else goes wrong while an approval question is being built or sent, including a fault in this plugin, is reported to Hermes as a question that was not sent, so Hermes drops the approval instead of sending the same question as plain text. A nodes.json that is not a walkable JSON list stops the recording only: the direct message is still answered, the file is left alone, and the log says so once. If approval_input_words exists but cannot be read, a reply of at most 40 characters is refused. If the rewrite cannot be loaded, restart phrases are still refused. The agent can reply in the turn that accepted the message; agent-initiated sends are refused. There is no daily cap beyond the hourly send cap above. This plugin starts no child process and is not a sandbox. A reply to an accepted direct message does not wait on approval or on an extra model call. It can wait the configured gap between chunks, at most 3600 seconds each. Progress and interim text are not sent. Hermes's one-time notice that no home channel is set, which says to type /sethome, is interim text and is not sent. /sethome is not a radio command. Hermes text sent while a turn is still running, including stream chunks and the busy acknowledgement for a second message, is not transmitted. The task that accepted the packet transmits only a refusal this plugin wrote. An approval question for the node in that turn is sent, and so is a rewritten confirmation for /new, /reset, or /undo. An unsolicited send is refused and does not call Hermes approval, so the gateway does not wait on a person for that other send. register() registers one platform and no tool, hook, middleware, or CLI command. The nodes you allow can use the tool sets Hermes enables for this platform by default: browser, clarify, code_execution, computer_use, connections, cronjob, delegation, file, hermes-radio-dm-gateway, image_gen, memory, session_search, skills, terminal, todo, tts, vision, and web. hermes-radio-dm-gateway registers no tools. Turn those tools off per platform with hermes tools. A cron job the agent creates stays until hermes cron remove. tests/ is shipped and is not loaded by register(). nodes.json stays after uninstall; delete the radio-dm-gateway plugin-data directory to remove it. The Hermes session keeps the message text. nodes.json does not. Firmware 2.5+ direct messages are not readable with only the channel key after key exchange; older firmware's direct messages were. Checked with a fake radio and the real Hermes adapter on v0.21.4 and current main. Not tried on a physical radio.

Docs

ja-writing-guard❖ Community★ 0

Rule-based proofreading for Japanese business text: finds and fixes AI-style phrasing (stock phrases, monotonous sentence endings, overused symbols and Markdown, English-calque grammar) with line, column and a fix hint. It checks wording, not authorship, and rarely flags plain model output. A transform_llm_output hook checks every final Japanese answer and only reports by default; opt-in enforce mode rewrites flagged answers with the user's own model and delivers the rewrite only if numbers, names, URLs, code and negation survive and the score drops under the threshold. Disclosure: no network calls of its own; enforce mode sends each flagged answer to the user's configured model (one extra call, a second only when the first rewrite fails the checks or stays over the threshold; extra cost and up to 25 s delay); a read-only post_tool_call hook notes turns with MEDIA: lines so attachments from other plugins are kept; reports stay in process memory on local surfaces only. Disclosure — no network calls of its own; mode report (default) only logs scores; opt-in mode enforce sends each flagged final answer (with finding excerpts) to the user's configured model via ctx.llm (1–3 calls, up to 25 s delay) and replaces the delivered and stored answer when its preservation checks pass, which do not prove meaning is unchanged; reads plugins.hook_callback_timeout via a private read-only Hermes helper; eval/ dev scripts ship in the tree but are never loaded.

Tools
jp-charts❖ Community★ 0

Bar, line, stacked and 100% band charts as phone-sized PNGs for chat replies and reports, with Japanese text, 万/億 units and a required unit and source line. Every bar, point, label and tick is read back from the finished figure and compared with the parsed data before the image is returned. Disclosure: no network calls at run time; reads a local .csv/.tsv/.txt/.json only when given its path (never hidden files or the Hermes home); writes PNGs to the Hermes image cache; a transform_llm_output hook adds a forgotten MEDIA line to replies on chat platforms and cron deliveries (setting auto_attach, on by default).

Tools
jp-chatwork❖ Community★ 0

Talk to Hermes from Chatwork rooms, built on the Chatwork API: put Hermes in a Chatwork room and ask it with [To:] or a reply, like a colleague; the answer comes back as a Chatwork reply. Polls the Chatwork API (no public URL or webhook), never answers a message twice across restarts, and while CHATWORK_ALLOWED_USERS is unset only the token's own account can use Hermes commands beyond /help, /whoami, /new and /reset or answer approval prompts. Disclosure: talks only to api.chatwork.com with your own API token; polls every 5 s by default while the gateway runs; stores read positions and the ids of its posted messages in <HERMES_HOME>/plugin-data/jp-chatwork/state.json. Disclosure — while CHATWORK_ALLOWED_USERS is unset everyone in the rooms listed in CHATWORK_ROOMS (external guests included) can prompt the agent and use its enabled tools, with Hermes commands and approval answers kept to the token's own account; talks only to api.chatwork.com with your API token, polling every 5 s while the gateway runs; stores read positions and posted message ids in <HERMES_HOME>/plugin-data/jp-chatwork/state.json; logs each asker's name and account id.

Platforms
jp-corporate❖ Community★ 0

Japanese corporate lookup (法人照会): find or verify a company by name or corporate number and read its gBizINFO record (basic info, subsidies, government contracts, awards, certifications, financials, patents, workplace data). Disclosure — sends the company names, corporate numbers and prefectures you look up to api.info.gbiz.go.jp (METI gBizINFO) with your own GBIZINFO_API_TOKEN in a request header; no other host, nothing written to disk.

Tools
jp-edinet❖ Community

Japanese filings on the EDINET API v2. Disclosure — talks only to api.edinet-fsa.go.jp over HTTPS, and your EDINET_API_KEY rides in the URL as Subscription-Key. The edinet toolset is on for every platform, including gateways, so anyone a gateway admits can call these tools with your key. With CHATWORK_ALLOWED_USERS unset, everyone in CHATWORK_ROOMS can call them. Turn the toolset off per platform with hermes tools. There is no daily cap. The agent can call the three tools itself. A cron you add stays after uninstall until you remove it in Hermes. This plugin starts no child process and is not a sandbox. Search sends EDINET the filing date, and a download sends the document id and its type. Company name, securities code, EDINET code, and corporate number are filtered on your machine. found: false only means the figure is missing from the consolidated current-year rows the plugin reads, not that the document has no number. A past day's filing list can change. Lists, saved files, and a key fingerprint, not the key, stay at <HERMES_HOME>/plugin-data/jp-edinet per profile after uninstall. Delete that directory to remove them. The plugin relies on Hermes internals for the write check and the data folder. If an update moves those parts, the tools refuse and do not call EDINET. Test files are not loaded at startup.

Tools
jp-egov-law❖ Community★ 0

Japanese laws for Hermes Agent, built on the e-Gov Law API: find a law by name, abbreviation or phrase, read one article as in force today or on a given date, and check amendment history and enforcement dates, with the source credit e-Gov's terms ask for. Disclosure — sends the law names, article numbers, dates and search phrases you look up to laws.e-gov.go.jp (public API, no key); no other host, nothing written to disk.

Tools

← Back to the catalog · catalog built Oct 10, 2026