HERMES · emergency communications · system design whitepaper

Text messages that do not need the internet.
And a measurement record that earns its keep on every ordinary day in between.

A complete design for a small appliance that takes an ARRL radiogram, an ICS form, or a short message from a web browser and delivers it to another station: over the internet when there is one, over VHF packet where there is a path, and over HF with JS8Call when there is nothing else. Every node is also an unattended propagation and reachability instrument, which is the part that makes it worth running for the two years before it is needed. Hardware, enclosure, software, wire protocol, regulatory posture, forms and reporting, governance, and the arithmetic behind all of it. Published for review before anything is built.

% Information related to this document
documentHERMES, Ham Emergency Relay and Message Exchange System
version2.2, revised against a reference implementation
date12 August 2026
authorMark, K9MEV · markvd.net
hardwareRaspberry Pi 5 · QRP Labs QMX 80/60/40/30/20 m
transportsHTTPS over IP · AX.25 on VHF · JS8 on HF, in that order
statusimplemented and tested; never yet on the air
licenseMIT, see section 20
status

Draft for public review. This document describes a system that has not been built. Every performance figure marked as an estimate needs measurement, and the regulatory reading in section 3 is the author's own and is awaiting a second opinion from an ARRL Volunteer Counsel. Comments, corrections, and disagreement are all welcome, and section 20 lists the parts most worth attacking.

Contents

  1. Executive summary
  2. Requirements and traceability
  3. Regulatory framework
  4. System architecture
  5. Identity and naming
  6. Hardware design
  7. Enclosure design
  8. Software design
  9. The HERMES Message Protocol
  10. Internet transport and the hub
  11. Message profiles
  12. The reachability and propagation record
  13. Security architecture and hardening
  14. Concept of operations
  15. Capacity and performance
  16. Development plan
  17. Test and acceptance
  18. Risks
  19. Upgrade paths
  20. Decisions taken and items still open
  21. Governance, review, and license
  22. Implementation status
  23. Revision history
  24. Database schema
  25. RF frame grammar
  26. REST API summary
  27. Configuration file
  28. Enclosure parametric source (skeleton)
  29. Glossary
  30. References
section 01

Executive summary

HERMES is a small, self-contained appliance that accepts short text messages (an ARRL radiogram, an ICS-213, or a short email) from a local web browser and delivers them to another HERMES station, using the internet when it is available and amateur radio when it is not. Each unit is a Raspberry Pi paired with a QRP Labs QMX five-band QRP transceiver in a single 3D printed enclosure. There are no local applications to install and no operator skill required beyond typing a message and pressing send: the unit decides how to move the traffic.

The design rests on five ideas.

Transport indifference. A message is a signed, immutable object with a content-addressed identifier. Nothing in the message format knows or cares whether it crossed fiber, Wi-Fi, or 40 meters. A single message may traverse both in one hop each. This is what makes the internet-preferred, RF-fallback behavior tractable rather than a special case tangle.

Measurement before payload. Every station continuously advertises that it exists and continuously records who it hears, who hears it, and what the noise floor was at the time. The result is a measured reachability and propagation record: which stations, on which bands, at what hours, at what signal to noise ratio, and whether they also answer over IP. Routing decisions come from measured history rather than from prediction.

This deserves more emphasis than a design document normally gives to a database, so it has section 12 to itself. A messaging network is worth nothing until it has peers. A measurement network is worth something on the first day, with one node. Every unit is an unattended, calibrated, continuously operating HF path measurement instrument that happens to also pass traffic, and that property is what makes it rational to run one for the two years before the emergency that justifies it. The messaging capability is what the system is for; the measurement capability is why anyone would keep it running long enough for the messaging to matter.

Minimal, replaceable central infrastructure. The internet path uses a small rendezvous and mailbox service (the "hub"). The hub holds no long-term message content it does not need, is trivially self-hostable, and is federated. If every hub disappears, stations that have previously met each other still exchange traffic directly over IP or over RF. The hub is an optimization, not a dependency.

Verifiable delivery. Every message carries an Ed25519 signature from its originator. Every delivery produces a receipt signed by the destination and countersigned along the path. Over RF, where a 512 bit signature is prohibitively expensive, HERMES carries a 40 bit digest and issues a compact receipt that is automatically upgraded to a full cryptographic receipt the moment either party regains internet connectivity. Nothing in the message content is ever obscured, which keeps the design inside 47 CFR 97.113(a)(4).

Legal by construction. The RF behavior is designed against 47 CFR 97.221 rather than retrofitted to it. Unattended beaconing happens only inside the automatically controlled digital station segments. Automatic responses to interrogation elsewhere are held below 500 Hz occupied bandwidth by using JS8 Normal or Slow speed exclusively when unattended. Station identification, third party traffic rules, and the 60 meter channel restrictions are handled explicitly in the software rather than left to the operator.

Estimated cost per node is 250 to 300 USD for the core electronics and enclosure, and 400 to 500 USD for a complete off-grid station including antenna, battery, and a small solar panel. Expected throughput is one 25 word radiogram in roughly 6 to 11 minutes end to end over HF at JS8 Normal speed, a few seconds over a VHF packet link, and effectively instantaneous over IP.

1.1 What changed when this was built

Between revision 1.2 and this one, the whole design was implemented. That is the most useful thing that can happen to a design document, because a specification only finds out where it was wrong when something has to obey it. Three findings were significant enough to change the text rather than sit in an errata list.

An RF transport cannot answer synchronously. The transport interface in 4.1 has three verbs, and offer was specified as though a delivery attempt either succeeds or fails. Over IP that is true in a second; over HF an exchange takes ten minutes. Without a way to say "under way, do not start another path", the router will cheerfully put the same message on the air twice by two routes. The interface now carries a pending result (see 4.1) and the router leaves such a message alone until the transport reports back.

Compression cannot be a wire transform. Section 9.10 described abbreviation and ARL numbered text substitution as encoding steps. They are not, and cannot be: they change the message body, and the body is signed. Any transformation applied after signing breaks every signature over it. They are composition aids, applied before the message is created, and the transport's job is to refuse a body it cannot carry rather than quietly clean it up. This is corrected in 9.10.

The frame arithmetic in revision 1.2 understated the cost by about half. It charged one JS8 frame per protocol frame, but a 40 character fragment plus its header is around 55 characters, which is three JS8 frames at roughly 20 characters each. Section 15 now uses the corrected accounting, and its figures come from the real encoder and the real ARQ rather than from an estimate.

Three further deviations were made deliberately during implementation and are recorded in 22.1 rather than silently folded in, because a reader comparing the document to the code deserves to find the differences listed rather than discover them.

1.2 Where this sits among things that already exist

Honesty about prior art is cheaper before publication than after, and a reviewer who knows this landscape will otherwise stop reading at the first unearned claim of novelty.

System What it does Why this design is not it
Winlink The incumbent. Global gateway network, radio email, hybrid internet and RF, radio-only forwarding, and a forms library covering hundreds of agency documents Winlink is a client an operator drives, not an unattended appliance, and its best modems are closed or expensive. HERMES aims to interoperate with it (11.6) rather than displace it. The forms library is Winlink's real moat and 11.5 exists because of it
JS8Call Provides the RF layer used here: weak signal messaging, a store and forward inbox, relaying, and heartbeat networking A large fraction of the RF capability described in section 9 is JS8Call's, not this project's. What is added is orchestration, transport selection, verifiable delivery, a workflow, and the measurement record. That is a real contribution and it is not a new radio protocol
VarAC Popular HF messaging over the VARA modem, with store and forward Windows centric and dependent on a closed modem. Same functional territory, different constraints
NBEMS, fldigi with flmsg What many ARES groups actually use for forms, mostly on VHF Entirely operator driven, no automation, no unattended operation, no measurement
Reticulum with LXMF The closest architectural analogue: transport agnostic, cryptographic identity, signed objects, store and forward propagation nodes Encrypted end to end, which makes it unusable on US amateur HF under 97.113(a)(4). The plain-content, publicly-documented-encoding posture of this design is the difference, and it is the reason this is not simply a Reticulum deployment
Meshtastic, MeshCore Own the mindshare for off-grid text messaging, at LoRa ranges, without a license Different range class. A useful fourth transport rather than a competitor, see 6.11
Satellite, Starlink and Iridium What agencies increasingly buy instead of partnering with amateur radio The honest competitor. This design serves the tail case, the drill culture, and the measurement record, and it should not pretend otherwise

The defensible claims are narrow and worth stating precisely: an unattended appliance rather than an operator-driven client, automatic transport selection across IP, VHF, and HF, delivery verification without encryption, a Part 97 posture engine enforced below the user interface, and a continuous measurement record as a first-class product.


section 02

Requirements and traceability

The following table maps each stated requirement to the section that satisfies it. Any requirement that could not be satisfied cleanly is flagged and recorded in section 20 (decisions and open items).

# Requirement Where addressed Notes
R1 Pass short text: email or ARRL radiogram 11 Radiogram is the native object; email and ICS-213 are profiles of it
R2 Raspberry Pi based 6.1, 6.2 Pi 5 2 GB recommended, Pi 4 and Pi Zero 2 W variants specified
R3 QRP Labs QMX transceiver 6.3 80/60/40/30/20 m build recommended
R4 Inexpensive multi-band antenna, no tuning 6.5 40 m end fed half wave with 49:1 transformer, optional 80 m link
R5 Single attractive 3D printable case 7 Two piece shell, swappable call sign plate, parametric source
R6 Web interface for setup, all configuration, message control 8.4, 8.5, appendix C Local AP fallback, PWA, no cloud dependency
R7 Call sign identity with /N or -N instance suffix 5 Canonical form CALLSIGN-N over IP, CALLSIGN/N over RF, with cost analysis
R8 Dual mode, internet primary with limited central infrastructure 4, 10 Federated hub, direct peer paths, hub optional after first contact
R9 RF fallback over JS8Call 9.4 JS8Call TCP API, JS8 Normal and Slow only when unattended
R10 Idle beaconing and listening 9.5 Native JS8 heartbeat plus a HERMES capability advertisement
R11 Database of stations heard, band, time, internet reachability 8.3, appendix A Observations, links, and reachability tables with hourly aggregation
R12 Store and forward, intermediate stations 9.7 Explicit custody transfer with per hop receipts
R13 Verifiable delivery 9.8 Signed end to end receipts, RF digest receipts upgraded later
R14 Prefer internet over RF 9.6 Deterministic path scoring with internet weighted decisively
R15 Receive-only SDR as a supported addition 6.9 Removes the receive side of the band scheduling compromise
R16 Alternative radios supported 6.4 QMX+ and QDX and QDX-M through a driver abstraction
R17 Installable on a stock Raspberry Pi OS image 8.7 Signed APT repository, three commands, no custom image required
R18 Patchable and updatable, optionally automatic, on a read-only filesystem 8.8 Transactional update with rollback, offline bundles, policy interlocks
R19 Internet-facing components hardened against misuse 13 Full attack surface inventory and hardening baseline
R20 Served agency forms, and interoperation with Winlink 11.5, 11.6 Signed template engine, ICS set shipped, !WL bridge service
R21 Communications log for after-action reporting 11.7 ICS-309 and ICS-214 generated from data already held
R22 Net control view of the whole net 8.9 Traffic board and station board, visibility only, no command authority
R23 VHF and UHF last mile 6.11 AX.25 over Direwolf as a third transport, preferred over HF
R24 Delivery to people who do not have a node 11.8 Operator-asserted final delivery, service messages back to the originator
R25 Fleet provisioning and configuration for a district 8.10 Pull-based signed policy bundles, narrowly scoped, local override wins
R26 Propagation and reachability measurement as a product 12 Five collection streams, watched paths, aggregation, export
R27 Project governance and continuity 21.1 Spec and implementation split, key custody, succession, conformance suite
R28 Identity survives a failed card, and can be rotated 5.3 Encrypted backup, rotation certificates signed by the old key
R29 Clock failure is visible before it is fatal 6.7 Offset sampled and surfaced, warn at 0.5 s, fail at 1.5 s
R30 Battery state is measured, not assumed 6.10 Optional INA219; the update interlock reads it or treats it as unknown
R31 The operator is told when a message will reach a person 11.2, 11.7 Estimated from this node's history, reported afterwards from what happened
R32 A message cannot monopolize the channel 11.2 Airtime ceiling at composition, RF only, with shortening advice
R33 Nobody is telephoned twice about the same message 11.8 Delivery records checked before recording another
R34 Group name collisions stay courteous 9.12 Protocol frames held until a member is heard, with a recorded override
R35 Duplicate station identifiers are found and both sides told 13.5 Conflict record, dashboard banner, priority message to the identifier
R36 Exercises are run and measured 14.4 Real traffic, marked, per-participant report with findings in words
R37 A hub can be operated and federated 10.5 Authenticated operator endpoints; push federation that cannot rewrite a key

section 03

Regulatory framework

This section is the author's reading of the rules as they stand in August 2026 and is not legal advice. It should be reviewed before deployment, and the software is written so that every one of these constraints is a configuration value rather than a hard coded assumption, because rules change.

3.1 Automatic control (47 CFR 97.221)

The controlling text is short enough to quote in substance. Section 97.221(b) permits a station to be automatically controlled while transmitting a data emission on the 6 m and shorter bands, and on the 28.120-28.189, 24.925-24.930, 21.090-21.100, 18.105-18.110, 14.0950-14.0995, 14.1005-14.112, 10.140-10.150, 7.100-7.105, and 3.585-3.600 MHz segments. Section 97.221(c) permits automatic control on any other frequency authorized for the emission type, except the 60 m channels specified in 97.303(h), provided that both conditions hold: the station is responding to interrogation by a station under local or remote control, and no transmission from the automatically controlled station occupies a bandwidth of more than 500 Hz.

HERMES turns this into two operating postures, and the software will not let a node transmit unattended outside them.

Posture A, unattended beacon and general availability. All unsolicited transmissions (heartbeats, capability advertisements, unsolicited relay attempts, queue drain to a station that has not just interrogated us) occur only on the 97.221(b) segments. The default channel plan in section 9.3 places every automatic channel inside those segments with margin.

Posture B, unattended response. Anywhere else, including the conventional JS8 calling frequencies, the node may transmit only in direct response to an interrogation it has just received, and only at JS8 Slow (25 Hz) or Normal (50 Hz) speed, both of which are comfortably under the 500 Hz limit. JS8 Fast (80 Hz) and JS8 40 or JS8 60 remain under 500 Hz as well, but they are disallowed unattended by policy because they degrade sensitivity and because the heartbeat subsystem does not support them.

Posture C, attended. With a control operator present at the unit (asserted through the web interface with a timeout, described in 8.4), the node operates as any other JS8Call station on the conventional watering holes, at any speed.

3.2 No obscuring of meaning (97.113(a)(4))

No message content, header, or address in HERMES is ever encrypted, compressed with a private dictionary, or otherwise rendered unreadable to a third party monitoring the channel. Everything transmitted over RF is either plain uppercase text or a documented, publicly specified encoding of plain text.

Three features that could be mistaken for encryption deserve explicit treatment.

Digital signatures. An Ed25519 signature appended to a plainly readable message does not obscure the meaning of that message. It authenticates the origin. This is the same posture taken by digital voice systems that transmit authenticated but unencrypted frames. Over RF, HERMES carries only a truncated digest, not a signature, for bandwidth reasons.

Compression. HERMES uses only publicly specified, static transformations over RF: the base 32 encoding in appendix B, the ARL numbered radiogram texts, and a published abbreviation table. There is no adaptive or private dictionary. The published table is part of the protocol document and is shipped with the firmware, so any monitor can decode the transmission with public information alone.

Message authentication codes. An earlier draft used a truncated HMAC with a pairwise shared key for RF authentication. That is dropped. A shared secret key on the air invites an argument that the transmission is codified, and the operational benefit is small. RF frames carry a CRC for corruption detection and a truncated public digest for binding to a signed object, both of which use published algorithms with no key material.

3.3 Station identification (97.119)

Station identification is emitted at least every 10 minutes during a transmission series and at the end of each series. JS8Call directed messages carry the originating call sign in the frame itself, which satisfies this in normal operation. HERMES adds a belt and braces timer: if 9 minutes elapse in a transmitting session without the base call sign having gone out in clear, the node inserts an identification frame. The identification always uses the base call sign, not the instance suffix.

3.4 Third party traffic (97.115)

HERMES will routinely carry traffic originated by non-licensed third parties, which is the entire point of an emergency messaging system. Domestic third party traffic is permitted. International third party traffic is permitted only with countries having a third party agreement with the United States. The software therefore:

  1. Marks each message with an originator class (licensee, third party, served agency).
  2. Refuses to route a third party message over RF to a destination whose call sign prefix maps to a country without a third party agreement, and says so plainly in the user interface.
  3. Requires by default that a control operator be present for third party traffic, which is handled by the attended posture flag.

Emergency override. The control operator requirement for domestic third party traffic may be lifted by an active emergency declaration (3.7). While a declaration is in force, third party traffic is accepted, queued, and forwarded unattended. Every message carried under the override is stamped with the declaration identifier, and the message record, the transmission log, and the audit log all carry it, so that the after-action record shows exactly which traffic moved under which declaration and on whose authority.

The override is deliberately narrow:

  • It applies to domestic third party traffic only. The country filter for international third party traffic stays on, because that constraint rests on treaty arrangements rather than on the operator's convenience. A separate, separately logged toggle exists for an operator who concludes that 97.403 applies to a specific international message, and using it requires typing a justification.
  • It expires with the declaration, default 12 hours, renewable, and it cannot be made permanent through the interface.
  • It does not relax anything else. The band plan, identification, the encryption prohibition, and the transmit budget all still apply, although the declaration does raise the budget ceiling for emergency and priority traffic.
  • The user interface shows a persistent banner naming the declaration and its expiry, on every screen, for exactly as long as it is active.

3.5 Business and pecuniary interest (97.113(a)(3))

The message composer includes a plain language acknowledgment for any message that a served agency submits, and the node logs the acknowledgment. This is a policy control, not a technical one, but it belongs in the record.

3.6 60 meters

The 60 m band is channelized under 97.303(h) with an effective radiated power limit referenced to a dipole, and 97.221(c) explicitly excludes those channels from automatic control. HERMES therefore treats 60 m as attended only, never uses it for beaconing or automatic response, and requires a manual power and antenna gain declaration before it will enable the band. Given the QMX at 5 W into a wire, the ERP limit is not a practical constraint, but the automatic control exclusion is absolute.

3.7 Emergency provisions (97.401, 97.403, 97.405)

In a declared emergency involving immediate safety of life or protection of property, a station may use any means of radiocommunication at its disposal. HERMES exposes an explicit, logged, operator-asserted "distress override" that relaxes the band plan and priority scheduling. It does not relax the prohibition on obscuring meaning, because nothing in HERMES obscures meaning in the first place. Every use of the override is written to the audit log with a timestamp, the asserting operator, and a free text justification.


section 04

System architecture

4.1 The three planes

diagram
                        +-------------------------------+
                        |        HERMES NODE            |
   Browser  <--HTTPS--> |  web    (UI + REST + WS)      |
   (LAN/AP)             |  core   (message store,       |
                        |          routing, receipts)   |
                        |  netd   (IP transport)  <-----+---> hub / peers
                        |  rfd    (RF transport)  <--+  |
                        |  radiod (CAT, SWR, band)   |  |
                        +----------------------------+--+
                                                     |
                                              JS8Call TCP API
                                                     |
                                              USB audio + CAT
                                                     |
                                                   QMX  ---> antenna

The node is organized into three planes that are deliberately kept apart.

The message plane holds signed message objects and their delivery state. It never initiates anything. It exposes an append-only store with a small state machine per message.

The transport plane consists of independent, symmetric transports that each present the same three verbs to the core: probe(peer) returns a reachability estimate, offer(message, peer) attempts delivery, and poll() surfaces inbound messages, receipts, and failures. Adding a transport later (a LoRa link, a Meshtastic bridge, sneakernet by USB stick) means implementing three functions and touching nothing in the core. That claim was tested when VHF was added: it cost exactly three functions.

offer may return "pending", and this is not a detail. An HTTP POST answers in a second; an HF exchange takes ten minutes. A transport that has accepted a message but not finished with it says so, the router leaves that message alone rather than trying another path behind its back, and the outcome arrives later through poll. Without this the same message goes on the air twice by two routes, which wastes the scarcest resource the system has and confuses every station listening.

The knowledge plane is the station database: who exists, what they can do, when and how well we have heard them, and what worked. Both transports feed it; the router reads it. It is the piece with the longest useful life, because it accumulates into a genuinely valuable regional record of who is reachable how, at what hour, on what band.

4.2 Process model

Five long-lived services under systemd, all local, all communicating over a Unix domain socket bus with a simple length prefixed JSON framing:

Service Language Responsibility Restart policy
hermes-core Python 3.12 Message store, routing, receipts, scheduler, audit on-failure, 5 s
hermes-web Python (FastAPI) + static PWA UI, REST, WebSocket push, auth on-failure, 5 s
hermes-netd Python IP transport, hub registration, peer sessions, NAT traversal on-failure, 10 s
hermes-rfd Python JS8Call API client, framing, ARQ, beacon, neighbor tracking on-failure, 10 s
hermes-radiod Python Radio CAT, band switching, SWR qualification, PTT interlock on-failure, 10 s
hermes-scand Python Receive-only SDR channelizer and scan decoders, optional on-failure, 30 s
hermes-vhfd Python VHF and UHF transport over AX.25 via Direwolf, optional on-failure, 10 s

Python is chosen over Go or Rust for one reason that outweighs the others: the amateur community can read and modify it, and the existing JS8Call, Hamlib, and ADIF ecosystems are already there. The performance envelope is trivial (a few hundred messages per day, one 15 second radio frame at a time). Go remains a reasonable rewrite target for hermes-rfd if timing jitter ever becomes a problem, which is unlikely given that JS8Call itself is doing the hard real-time work.

A hardware watchdog (the Broadcom watchdog on the Pi, via systemd RuntimeWatchdogSec=30) covers total lockup. hermes-core additionally runs a liveness ping to each service every 15 seconds and restarts a service that misses three in a row.

4.3 Failure model

The design assumes, in order of likelihood: internet loss (routine, expected, the entire reason for the system), power loss (expected, handled by battery and by a filesystem that survives abrupt power removal), JS8Call crash (observed in the wild, handled by supervision and reconnection), band closure (constant, handled by the scheduler), and node loss (handled by relaying and by the fact that any two stations that have met can find each other again).

Everything on the Pi that matters is in SQLite in WAL mode with synchronous=FULL on the message store, on an overlay filesystem with a read-only root. A yanked power cord loses at most the current in-flight radio frame.


section 05

Identity and naming

5.1 Station identifiers

A HERMES station identifier is a base call sign and an instance number:

  • Canonical form, used in the database, the API, the web UI, and all IP transport: K9MEV-3. The instance number is 0 to 15. Instance 0 is written without a suffix, so K9MEV and K9MEV-0 are the same station and the bare form is preferred.
  • RF form, used inside JS8 frames: K9MEV/3. JS8Call handles a compound call sign suffix natively, at a cost described below.
  • Display form: K9MEV-3 (Batavia EOC) where a free text label has been configured.

The mapping between the two forms is mechanical and is performed at the transport boundary, so nothing above hermes-rfd ever sees a slash.

5.2 Instance identity on the air

Decision: per-instance RF identity is a hard requirement. Every node transmits under its own compound call sign, K9MEV/1, and is directly addressable on the air as that station. A message addressed to instance 3 is delivered to instance 3 over RF, not to a shared front door that forwards internally. This section documents what that costs and how the design pays for it, because the cost is real.

The cost. JS8 packs a standard call sign into a fixed field inside the 69 bit frame payload. A compound call sign, meaning anything with a prefix or a suffix, does not fit that field, so JS8Call sends the call sign in an additional frame. At Normal speed that is an extra 15 seconds. On a 20 frame message exchange it is roughly a 5 percent penalty. On a single-frame heartbeat it is a 100 percent penalty, because one frame becomes two.

How the design pays for it.

  1. Beacon rates are halved. The JS8 heartbeat interval for a compound-call node defaults to 30 minutes rather than 15, and the HERMES capability beacon defaults to 120 minutes rather than 60. Two frames every 30 minutes costs the same channel time as one frame every 15, so the aggregate spectrum occupancy of a HERMES population is unchanged. What changes is presence latency: a node's existence becomes known within 30 minutes rather than 15, which matters far less than it sounds, because presence is used to choose among paths rather than to discover the world from scratch.
  2. Instance 0 stays free. A single-node station uses the bare call sign, transmits one-frame heartbeats, and pays nothing. The compound cost is paid only by operators who genuinely run more than one node, which is the correct place for it to land.
  3. Directed exchanges amortize it. The extra frame is per transmission burst, not per fragment, so a long message pays it once. The 5 percent figure is the worst case for a short message and falls toward 2 percent for a full radiogram.
  4. Presence is not the only discovery mechanism. The directory in 10.2, the observation log, and any prior contact all establish that a station exists. The beacon keeps the propagation picture current; it is not the sole means of finding a peer.

The measured cost, now that the encoder exists. For a 25 word radiogram at JS8 Normal, a compound call sign adds roughly 12 percent to the total exchange rather than the 5 percent estimated in revision 1.2, because the extra frame is charged on each of the several transmission bursts an ARQ exchange involves rather than once. On a single-frame heartbeat it remains a 100 percent penalty. The halved beacon rates hold the aggregate spectrum occupancy unchanged; the message traffic penalty is real and is the price of the decision.

On VHF the penalty vanishes entirely. An AX.25 address field carries a call sign plus a 4 bit SSID, so K9MEV-1 is native and free. Every cost described in this section exists only on HF.

Co-site collision control. The most likely self-inflicted failure in this system is two of the operator's own nodes transmitting on the same channel at the same time, deafening each other and everyone nearby. The earlier draft solved this with a single RF transmit lease, which is no longer appropriate now that every instance has its own on-air identity. It is replaced by a co-site scheduler:

  • Nodes that can see each other on a LAN elect a coordinator and take turns. No two co-sited nodes transmit in the same 15 second window on the same band, and beacons are staggered.
  • Nodes that cannot see each other on a LAN fall back to a static slot derived from the instance number: beacon slot equals instance modulo 4, offset by 7 minutes per slot within the beacon interval, and the preferred audio offset is 500 Hz plus 200 Hz times the instance, which spreads co-sited nodes across the passband even when they cannot coordinate.
  • hermes-rfd monitors for its own other instances in the decode stream. Hearing a co-sited sibling at a very high SNR while attempting to transmit raises a co-site interference alarm in the user interface with the specific advice that matters, which is to separate the antennas or to put the nodes on different bands.

An open verification item. JS8Call supports compound call signs, and the extra-frame behavior described above is the documented cost. What has not been verified by the author is the exact behavior of the heartbeat networking subsystem, the automatic SNR reply, and directed message addressing when the station call sign is compound, across both the mainline release and the Improved fork. This is the first on-air test in phase 0 and it is carried in the risk table, because per-instance RF identity is now a hard requirement rather than a preference, and a surprise here changes the design rather than an option in it.

5.3 Keys and trust

Each node generates an Ed25519 identity key pair at first boot. The private key lives in /var/lib/hermes/identity.key, mode 0600, optionally sealed to a TPM or to the Pi's OTP if the hardware supports it. The public key is the node's cryptographic identity; the call sign is its human and legal identity, and binding the two is the trust problem.

Three binding mechanisms, in increasing order of strength, all supported:

Trust on first use. The first time a call sign is seen with a key, the binding is recorded and pinned. A later key change for the same call sign raises a loud, non-dismissible warning in the UI and marks all traffic from that identity as unverified until the operator accepts the change. This is adequate for a hobby network and is the default.

Hub attestation. A hub operator verifies a license (FCC ULS lookup plus an emailed challenge to the address of record, which is public) and issues a signed attestation binding call sign to public key with an expiry. Nodes carry attestations and present them. Hubs cross-sign each other, which is how federation works.

Out of band fingerprint. A 40 bit fingerprint rendered as eight base 32 characters and as a QR code in the UI, verified over the phone, in person at a club meeting, or on a repeater. This is the mechanism that survives the internet being gone, and it is the one to use for the stations that matter most, before the emergency.

Key rotation and backup. The identity key exists in one place, on an SD card, in a device that runs continuously. Losing it is a when, and losing it means losing every pinned trust relationship the station has built. Two mechanisms address that.

Encrypted backup. hermesctl identity backup writes the key sealed with a passphrase-derived key (scrypt, then ChaCha20-Poly1305). The station identifier is authenticated but not encrypted, so an operator with three sticks in a drawer can tell which is which without typing a passphrase. Restore refuses to overwrite a live identity unless forced, and keeps the superseded key rather than destroying it.

Rotation certificates. The old key signs a statement naming the new one. A peer holding the old key verifies it without asking anyone; a peer that never held the old key gains nothing and falls back to trust on first use, which is correct, because a rotation proves continuity rather than identity. A certificate that chains from a key we do not hold is recorded and not applied, and the operator is told why in words.

The interaction with 13.4 matters: a key change accompanied by a valid rotation certificate is not an identity conflict and is not reported as one.

5.4 Groups and addressing

Addresses come in four forms:

Form Example Meaning
Station K9MEV-3 One instance
Call sign K9MEV Any instance, first to acknowledge takes custody
Group @FRRL, @KANECO, @ARES-IL All subscribed members, no delivery guarantee to any individual
Service !EMAIL, !NTS, !APRS A gateway that translates outward

Group names are 2 to 8 uppercase characters and map directly onto JS8Call group call signs, so a HERMES group broadcast is visible to and usable by ordinary JS8Call operators who happen to be monitoring that group. Interoperability with plain JS8Call users is a design goal throughout, and is discussed in 9.9.


section 06

Hardware design

6.1 Bill of materials

Prices are August 2026 street prices in USD and are indicative.

Core node

Item Part Qty Unit Notes
Transceiver QRP Labs QMX, 80/60/40/30/20 m, assembled 1 152.47 Kit at 102.47 if building
Computer Raspberry Pi 5, 2 GB 1 50 Pi 4 2 GB acceptable, see 6.2
Storage 32 GB A2 microSD (industrial grade) 1 15 Or 128 GB NVMe on Pi 5 HAT
Time u-blox 7 or 8 USB GPS with PPS 1 18 Mandatory, see 6.7
Clock DS3231 RTC module with CR2032 1 5 Cold start without GPS
Power 12 V to 5 V 5 A buck (MP1584 or Pololu D24V50F5) 1 12 Low noise part, see 6.7
Power Anderson Powerpole set, fuse holder, 5 A ATC fuse 1 10
Cabling Short USB-C to USB-A, right angle 1 6 Pi to QMX
RF BNC bulkhead to BNC pigtail, RG-316 1 8
Suppression Fair-Rite 0431164281 clamp-on ferrites 6 2.50 See 6.8
Shielding Copper foil tape, 50 mm, adhesive conductive 1 10 Enclosure lining
Enclosure PETG or ASA filament, approx 220 g 1 6
Hardware M3 heat set inserts (x14), M3 screws, standoffs 1 8
Front panel 3 mm acrylic window, 20 x 60 mm 1 3 Over QMX LCD
Indicators 5 mm light pipes (x3), WS2812 or discrete LEDs 1 5
Core subtotal ~310 260 with QMX kit self-built

Station completion

Item Part Qty Unit Notes
Antenna 40/30/20 m fan dipole: wire, 1:1 balun, spreaders, insulators 1 55 Default, see 6.6
Antenna Alternative: 40 m linked EFHW with 49:1 transformer 1 45 Portable option, not the default
Feedline 15 m RG-58 or RG-8X with BNC ends 1 25
Balun FT240-43, 12 bifilar turns, 1:1 current balun at the feedpoint 1 14 Common mode control
Battery 12.8 V 20 Ah LiFePO4 with BMS 1 80 256 Wh
Charging 50 W folding panel plus 10 A MPPT controller 1 70
Deployment Throw line, 30 m paracord, weight, stakes 1 20
Station subtotal ~275

Optional receive-only SDR package (recommended, see 6.9)

Item Part Qty Unit Notes
Receiver RTL-SDR Blog V4 with dipole kit 1 40 Direct sampling HF, the default choice
Receiver Alternative: Airspy HF+ Discovery 1 169 Far better dynamic range, for relay and backbone nodes
Receiver Alternative: SDRplay RSP1B 1 110 14 bit, wider coverage, middle option
T/R switch 12 V SPDT RF relay board, keyed from the QMX PTT out jack 1 15 Only if sharing the transmit antenna
Protection SMA gas discharge tube, back to back 1N4148 pair, 100 ohm series 1 8 Mandatory when sharing, see 6.9
RX antenna 10 m insulated wire plus 9:1 unun, if using a separate receive antenna 1 20 Simpler and safer than sharing
Cabling RG-316 SMA to BNC pigtail 1 7
SDR subtotal ~70 to 90 With the RTL-SDR and one of the two connection methods

Total for a complete, off-grid capable station: approximately 585 USD, or approximately 480 USD building the QMX from the kit and reusing an existing battery. Adding the receive-only SDR brings the figure to roughly 660 USD, and it is the single best 70 USD that can be added to a relay node.

6.2 Computer selection

Option Idle power JS8Call decode headroom Recommendation
Pi Zero 2 W 0.7 W Marginal. Multi-decode across four speeds will drop frames under load Austere variant only, single speed decode, no web assets built locally
Pi 4, 2 GB 2.7 W Comfortable Best power to capability ratio, recommended for solar
Pi 5, 2 GB 3.5 W Ample, room for future SSTV or Winlink gateway Recommended default, active cooler required

The Pi 5 is the default because HERMES is doing more than running JS8Call: it is serving a web application, running a database, maintaining IP sessions, and decoding continuously. The extra watt buys a decade of headroom. The Pi 4 is the right answer for a node that must live on a 20 W panel through an Illinois December.

The Pi Zero 2 W variant is documented but constrained: it runs JS8Call with the multi-speed decoder disabled, serves a reduced UI, and is intended for a "go bag spare" role rather than a permanent installation.

6.3 QMX integration

The QMX presents to the Pi over a single USB-C cable as three devices: a 24 bit 48 ksps USB sound card, a virtual COM port for CAT (Kenwood TS-480 command dialect), and, in recent firmware, up to two additional virtual COM ports. This single-cable property is the reason the QMX is the right radio for this design. There is no sound card to buy, no VOX to configure, no ground loop between an external interface and the radio, and no analog audio path to pick up hash from the Pi.

Band build. The 80/60/40/30/20 m build is recommended. It covers all four of the automatic control segments that matter at HF (3.585-3.600, 7.100-7.105, 10.140-10.150, 14.1005-14.112) and gives a working set that spans night regional (80), day and night regional (40), the reliable 24 hour NVIS-to-medium-haul band (30), and daytime medium haul (20). The 20/17/15/12/11/10 m build is a poor fit: it lacks any usable night regional path for Illinois, and the entire premise of a regional emergency network is regional paths.

Mode. Digi mode (single-signal FSK), not SSB. The QMX in Digi mode generates a pure FSK carrier from the incoming audio tone with no unwanted sideband, no residual carrier, and no intermodulation, which is exactly what JS8 needs and is meaningfully cleaner than an SSB modulator at 5 W. JS8Call is configured for a low audio drive level, and the QMX firmware displays an error indicator when audio is insufficient, which hermes-radiod monitors.

Power output. 3 to 5 W at 12 V, drawing approximately 0.7 A on transmit and 80 mA on receive. The 5 W figure is realistic and is the basis for the link budget in section 15.5.

CAT usage by hermes-radiod. The service uses these commands (firmware 1_04_004 and later):

Command Use in HERMES
FA / FB Set and read dial frequency for band changes
BN Set band by band configuration table column, preferred over raw frequency
MD Confirm Digi mode, and MD8 to enter SWR tune mode during qualification
SW Read SWR meter during band qualification sweeps
PC Read power meter, verify actual output before trusting a link estimate
SM Read S meter for noise floor characterization per band per hour
SR Read and reset SWR protection state, critical for unattended operation
TM Read and set the real time clock
GP Read GPS position, date, and time when a GPS is attached to the radio
UI Read the STM32 unique serial number, used as a hardware asset tag
PS Power supply status and orderly shutdown
IF General status polling

The SR command is the one that makes unattended operation safe. If the antenna is damaged (a wire down after the same storm that took out the internet, which is precisely the scenario this system exists for), the QMX SWR protection trips and stays tripped. Without polling SR, the node would happily believe it was transmitting for hours. hermes-radiod polls SR after every transmission, records a protection event, marks the band as failed, attempts one reset, and after two failures on two different bands raises an antenna fault alarm in the UI and over the internet path if available.

6.4 Supported radios

The QMX is the reference radio and the one the enclosure, the parts list, and the test plan are built around. Two other QRP Labs transceivers are supported, because supporting them costs one driver class and materially widens the population of operators who can build a node from a radio they already own.

hermes-radiod implements a driver per model behind a capability interface. A driver declares what it can do, and the rest of the system adapts rather than assuming:

python
class RadioDriver:
    model: str
    bands: list[int]          # metres
    max_power_w: float
    has_swr_bridge: bool      # band qualification available
    has_smeter: bool          # noise floor characterization available
    has_gps_input: bool
    has_ptt_out: bool         # can key an external T/R switch
    band_switch: str          # "cat_band" | "cat_freq"
    standalone: bool          # usable without a computer attached
QMX (reference) QMX+ QDX / QDX-M
Bands 80/60/40/30/20 (confirmed build) 160 through 6 m 4 bands per build, commonly 80/40/30/20
Power 3 to 5 W 5 W with more headroom 4 to 5 W
Modes CW, digi FSK, SSB CW, digi FSK, SSB Digi FSK only
Sound card and CAT over one USB Yes Yes Yes
SWR bridge Yes Yes No
S meter over CAT Yes Yes No
Power meter over CAT Yes Yes No
PTT out for a T/R switch Yes Yes No
GPS input Yes Yes No
Front panel and standalone use Yes Yes No, headless by design
Approximate cost 102 kit, 152 assembled 200 kit range 79 to 99
HERMES support tier Reference Full Reduced, see below

QMX+. Functionally a superset. Same firmware family, same CAT dialect, same single-cable USB behavior, so the driver differences amount to the band table and the power ceiling. It adds 160 m and 6 m, neither of which has an automatically controlled digital station segment that this design uses at HF, but 160 m is a genuinely useful regional night band and 6 m is fully available for automatic control under 97.221(b), which makes a QMX+ node interesting for a metropolitan-scale network on 6 m in a way the QMX cannot be. The enclosure needs a wider radio compartment; the parametric source in appendix E has the compartment dimensions as variables, and the QMX+ figures must be taken from its own mechanical drawing rather than guessed.

QDX and QDX-M. Digital-only, cheap, and headless, which is exactly the shape of this application. Everything HERMES actually needs on transmit and receive is present. What is lost is instrumentation, and the losses are worth stating plainly because they degrade two features that this design leans on:

  • No SWR bridge means no automatic band qualification. The antenna fault detection in 6.6 and the "is the wire still in the tree" alarm both disappear. Mitigations: an inline QRP SWR meter checked by hand at commissioning and after any storm, and a software fallback that infers antenna trouble from a sudden collapse in successful exchanges across all bands at once. The software fallback is weaker, slower, and worth having anyway.
  • No PTT output means the receive-only SDR must use its own antenna rather than sharing the transmit antenna through a switch, or must use a switch keyed from a spare Pi GPIO with the timing derived from the JS8Call RIG.PTT event, which is riskier because it depends on software timing to protect a 30 USD receiver from 5 W.

A QDX node is a fully participating HERMES node with less self-knowledge. For a club buying ten nodes on a budget, that is a reasonable trade. For an unattended node at a remote site, the SWR bridge is worth the extra fifty dollars.

6.5 Assembly and interconnect

diagram
   12 V in ---[fuse 5A]---[reverse polarity]---+--- QMX 2.1 mm barrel
                                               |
                                               +--- Buck 12->5 V ---> Pi USB-C PD in
                                               |
                                               +--- ADC (INA219) ---> Pi I2C (voltage/current telemetry)

   Pi USB-A --[ferrite]--[ferrite]-- QMX USB-C     (audio + CAT)
   Pi USB-A ------------------------ GPS dongle    (time + position)
   Pi I2C   ------------------------ DS3231 RTC, INA219
   Pi GPIO 4 ----------------------- GPS PPS in
   Pi GPIO 17/27/22 ---------------- status LEDs (power, RF active, internet)
   Pi GPIO 26 ---------------------- momentary button (safe shutdown / AP mode)
   QMX BNC --[bulkhead]------------- antenna

The QMX and the Pi share a common 12 V source but are fed on separate branches from the fuse block, with the buck converter's input decoupled by a 470 uF low ESR capacitor and a common mode choke. The single most important assembly detail is that the QMX gets 12 V directly, not through the Pi's supply, so that the 0.7 A transmit current step does not modulate the Pi's rail.

6.6 Antenna

The requirement is inexpensive, multi-band, and no tuning. There is no antenna tuner in the QMX, and the QMX SWR protection will simply refuse to transmit into a bad match, so the antenna must present a usable impedance on every band the node intends to use, without operator action.

The default is a 40/30/20 m fan dipole. It covers exactly the three bands whose automatically controlled digital station segments this design uses in practice, it needs no walking to the far end of a wire to change bands, and it can be built for about 55 USD.

Element Half length per leg Total tip to tip Trim order
40 m, resonant near 7.10 MHz 10.06 m (33 ft 0 in) 20.1 m Third
30 m, resonant near 10.14 MHz 7.06 m (23 ft 2 in) 14.1 m Second
20 m, resonant near 14.10 MHz 5.04 m (16 ft 6 in) 10.1 m First

Construction notes that matter:

  • Feed with a 1:1 current balun, FT240-43 with 12 bifilar turns, or a coiled-coax choke of 5 turns at 200 mm diameter. Without it the feedline radiates, the pattern distorts, and the node's own computer noise gets a path straight back into the receiver.
  • Separate the elements by 100 to 150 mm using printed or PVC spreaders every 1.5 m. Elements too close together interact and the tuning becomes an afternoon of frustration.
  • Trim shortest first, from 20 m up. Each element affects the ones longer than it, so trimming in the other order means doing the work twice.
  • Inverted V with the apex at 8 to 10 m is entirely adequate and is what most operators will actually manage. Higher is better on 20 m and makes little difference to the 40 m NVIS path that regional traffic depends on.
  • An 80 m element can be added later for a four-band fan, 19.6 m per leg, but the 80 m element interacts strongly with the 40 m element and the whole antenna needs re-trimming. Add it only if the space and the patience are both available.

Documented alternative: the 40 m linked end fed half wave. For portable and go-bag use, where one support and one throw line is the whole installation, a 20.1 m end fed half wave with a 49:1 transformer on an FT240-43 core, a 100 pF capacitor across the primary, and a 2 m counterpoise gives resonant operation on 40, 20, 15, and 10 m. Adding a link and a further 20.5 m of wire makes it an 80 m antenna covering 80, 40, 20, 15, and 10 m. It is cheaper and far quicker to deploy. It is not the default, for two reasons: it does not cover 30 m, which is the band this design prefers whenever it is open, and changing between 80 and 40 means walking to the far end of a wire in whatever conditions produced the emergency.

SWR qualification. On first boot, on any configuration change, and every 24 hours thereafter, hermes-radiod sweeps each enabled band using the QMX built-in SWR bridge (MD8 to enter tune mode, SW to read, three points across each segment of interest, at reduced drive), records the result in the band_swr table, and marks bands above a configurable threshold, default 2.5:1, as unusable. The router will not select an unusable band. This turns "did the antenna fall down" from an unanswered question into a monitored, alarmed condition, and it makes the system tolerant of whatever wire the operator actually managed to get into a tree. On a QDX, which has no SWR bridge, this feature is unavailable and the fallback described in 6.4 applies.

6.7 Time and position

Time discipline is not optional and is the single most under-appreciated requirement in this design. JS8 requires transmit and receive windows aligned to within roughly 2 seconds. The Pi has no real time clock. In the scenario this system exists for, there is no NTP because there is no internet. A node that has been running on battery for three days with a drifting clock is a node that cannot decode anything, and it will not know why.

The solution is a three tier clock:

  1. GPS with PPS as primary. A u-blox USB dongle feeding gpsd, with PPS on GPIO 4, disciplined by chrony. Accuracy in the microseconds, entirely independent of any infrastructure. Approximately 18 USD and 0.3 W.
  2. DS3231 RTC as holdover. Temperature compensated, drift of roughly 2 ppm (about 1 minute per year), battery backed. Provides a correct clock at cold boot before GPS lock, which matters because JS8Call starts before the GPS has a fix.
  3. NTP when internet exists, and the JS8Call @TIME group drift synchronization as a last resort. JS8Call can estimate time drift from decoded signals and HERMES can consume that estimate through the API to at least warn the operator that the clock is wrong even when nothing else is available.

A wrong clock must be visible, not merely fatal. A node whose GPS has failed and whose real time clock has drifted does not stop: it transmits in the wrong windows, decodes nothing, and the operator sees "no decodes" and starts checking the antenna. The node therefore samples chrony every five minutes, records the offset rather than a boolean (because "synchronized" is not the useful question: a node can be synchronized to a source that is itself wrong), warns at 0.5 s of drift, fails at 1.5 s, and puts a clock card on the dashboard beside battery and antenna. The failure message names the two commands that diagnose it.

The GPS also supplies the Maidenhead grid square automatically, which populates the JS8Call station configuration, feeds the neighbor database with real distances, and lets the router reason about paths geographically (a 900 km path at 0300Z is a 40 m path, not a 20 m path).

The QMX has its own GPS input on the paddle jack for frequency calibration and clock setting, and a second GPS could be fitted, but a single dongle on the Pi with the Pi setting the QMX clock over CAT (TM) is the cheaper and simpler arrangement.

6.8 Electromagnetic compatibility

A Raspberry Pi 30 cm from a QRP receiver front end is a genuine engineering problem, and it will be the difference between hearing a station at -22 dB and not hearing it at all. A 5 W station cannot afford to lose 6 dB of receive sensitivity to its own computer.

Mitigations, in order of effectiveness per dollar:

  1. Ferrite on the USB cable between the Pi and QMX, two turns through a mix 31 clamp-on at each end. This cable is the most likely radiator because it is the only conductor bridging the noisy domain and the RF domain.
  2. Ferrite on the DC input and on the buck converter output.
  3. Conductive lining of the Pi compartment. Copper foil tape on the internal walls of the printed enclosure around the Pi, bonded to the chassis ground at a single point. This is a partial Faraday cage and gives 10 to 20 dB at HF for 10 USD. A printed enclosure has no shielding at all, and this is the price of the aesthetic requirement.
  4. A physical partition in the print between the Pi compartment and the QMX compartment, with the foil lining terminating at the partition.
  5. Disable unused radios and interfaces: Bluetooth off, Wi-Fi off when wired, HDMI off (vcgencmd display_power 0), and the Pi 5 UART console disabled after commissioning.
  6. Choose the buck converter deliberately. A fixed-frequency part with a known switching frequency (Pololu D24V50F5 at 400 kHz, or better, a low noise LDO cascade if the thermal budget allows) beats a random module. The harmonics land at known frequencies that can be checked against the band plan.
  7. Measure. Commissioning includes a noise floor sweep on each band with the Pi off, the Pi idle, and the Pi under load, recorded in the band_noise table through the QMX SM command. If the difference exceeds 3 dB, the build is not finished. This test is scripted and is part of the acceptance procedure in section 17.

6.9 Receive-only SDR (recommended addition)

The QMX hears one band at a time. That constraint is the reason section 9.3 needs a band schedule, and the band schedule is the largest single compromise in the RF design: a node monitoring 40 m is deaf to a station calling it on 30 m, and both stations waste transmissions discovering that.

A cheap receive-only software defined radio removes the receive half of that compromise entirely. A node with an SDR listens on every band at once and transmits on one, which changes the network from a set of stations that must agree on where to meet into a set of stations that can be found wherever they happen to be calling.

What it buys, concretely:

  1. Continuous multi-band presence collection. Every heartbeat on every band lands in the observations table, so the link quality picture in 8.3 fills out in days instead of weeks and covers bands the node has never transmitted on.
  2. Correct band selection on the first try. The router stops guessing from a stale schedule and starts choosing from what is being heard right now.
  3. Noise floor measurement that is independent of the transceiver, including while the QMX is transmitting, which is what makes the EMC acceptance test in 6.8 repeatable rather than a one-off.
  4. A waterfall in the web interface, which is the feature that makes an operator trust the box, because they can see it working.
  5. A byproduct contribution to the community, since the same receive chain can report spots to PSKReporter and decode WSPR without any additional hardware.
Receiver Cost HF performance Recommendation
RTL-SDR Blog V4 40 8 bit, direct sampling HF, adequate with a modest antenna, will overload near strong broadcast signals The default. Buy it, add an attenuator or a broadcast band reject filter if 49 m band overload appears
SDRplay RSP1B 110 14 bit, much better dynamic range, wide coverage Middle option, good for a home node in a noisy suburb
Airspy HF+ Discovery 169 Excellent HF dynamic range, the right answer in a crowded band Backbone and relay nodes, where hearing weak stations under strong ones is the whole job

Connecting it to an antenna, which is where the danger lives. Five watts into an RTL-SDR input destroys it instantly. There are two safe approaches and one unsafe one.

Approach A, separate receive antenna (recommended, and the default in the parts list). A 10 m insulated wire with a 9:1 unun, or a small passive loop, on its own coax. It costs 20 USD, it can never be connected to the transmitter, and it decouples the receive chain from any transmit antenna fault. A receive antenna does not need to be efficient, because on HF below 20 m the received noise floor dominates, not the antenna gain.

Approach B, shared antenna with a hardware T/R switch. A 12 V SPDT RF relay board, keyed by the QMX PTT out jack, disconnects the SDR and grounds its input during transmit. Note that Rev 1, 2 and 3 QMX PCBs need the QRP Labs grounded PTT output modification for this jack to work, and Rev 4 and later do not. Whichever approach is used, the SDR input gets a protection stack regardless: a gas discharge tube to ground, a 100 ohm series resistor, and back to back small signal diodes to ground. Relays bounce, firmware hangs, and cables get swapped, and the protection stack is what stands between a moment of any of those and a dead receiver.

The unsafe approach, which is not supported: keying a switch from software timing alone, with no hardware interlock from the transmitter. A QDX node has no PTT output and therefore has no safe way to share the transmit antenna, so a QDX node uses approach A.

Software. A new service, hermes-scand:

diagram
rtl_sdr / SoapySDR  ->  ka9q-radio or csdr channelizer
                    ->  N virtual sound devices (snd-aloop)
                    ->  N headless JS8Call instances, transmit disabled,
                       one per band, API ports 2443, 2444, 2445 ...
                    ->  hermes-rfd collects decodes into observations

Every scan instance has transmit hard-disabled in three places: the JS8Call configuration, a hermes-scand policy flag, and the absence of any transmit path in the hardware. This is deliberate belt, braces, and a third fastener, because a receive-only chain that somehow transmits is both an interference incident and a violation.

Cost in power and CPU. The RTL-SDR draws about 1.3 W, and each additional decoder instance costs roughly 0.4 W of CPU on a Pi 5. Two extra bands is comfortable. Three or four needs measurement, and is a phase 8 test item. On a solar-powered node the honest arithmetic is that the SDR package adds roughly 2 to 2.5 W, or 55 Wh per day, which is a 40 percent increase over the base budget in 6.10 and needs a bigger panel. On mains power, fit it to every node. On solar, fit it to relay nodes and size the panel accordingly.

6.10 Power budget

At 12.0 V nominal:

State QMX Pi 5 (via buck at 88 percent) GPS Total Notes
Receive, idle 0.08 A 0.33 A 0.03 A 0.44 A (5.3 W) Continuous baseline
Receive, decoding 0.08 A 0.45 A 0.03 A 0.56 A (6.7 W) Multi-speed decode bursts
Transmit 0.70 A 0.40 A 0.03 A 1.13 A (13.6 W) 5 W out

Duty cycle assumptions for a node in a quiet posture: 96 percent receive idle, 3 percent decoding, 1 percent transmit. Average draw approximately 0.45 A, or 5.5 W, or 132 Wh per day.

With the Pi 4 substituted, the figures fall to roughly 0.33 A average, 4.0 W, and 96 Wh per day.

Battery Capacity Pi 5 runtime Pi 4 runtime
12.8 V 20 Ah LiFePO4 256 Wh 1.9 days 2.7 days
12.8 V 50 Ah LiFePO4 640 Wh 4.8 days 6.7 days

For indefinite operation, a 50 W panel in Batavia yields roughly 150 to 200 Wh per day averaged over a year and considerably less in December, when the practical figure is closer to 60 to 90 Wh per day for a fixed south-facing panel. A 100 W panel and a 50 Ah battery is the correct specification for a genuinely indefinite winter installation in northern Illinois, and the 50 W and 20 Ah figures in the bill of materials should be understood as a summer or short-duration configuration.

If the receive-only SDR package of 6.9 is fitted, add approximately 2.0 to 2.5 W continuous, or roughly 55 Wh per day, and size the panel and battery accordingly. That is the honest cost of the single most useful optional addition in this design.

Power monitoring, and the interlock that depended on it. An INA219 on the I2C bus reports bus voltage and shunt current, from which the node derives power, a coarse state of charge against a per-chemistry curve, and an estimate of hours remaining. It is optional, because most nodes are on mains and the part can be left off the board, and it is enabled with power.monitor.enabled.

What is not optional is honesty about what depends on it. Earlier revisions specified a battery floor interlock on software updates, and the first implementation only evaluated it when a number was passed by hand, which nobody does. An interlock that never fires is worse than none, because it creates the belief that the node is protecting itself. The interlock now reads the most recent sample, treats a reading older than fifteen minutes as unknown rather than as full, and defers the update when the battery is genuinely below the floor.

The state of charge is deliberately coarse and labelled as such. A proper fuel gauge needs coulomb counting, and a coarse number that is honestly labelled beats a precise one that is wrong under load. Terminal voltage sags under transmit, so the estimate adds back a correction rather than reporting a node mid-transmission as about to die.

A power-saving posture is implemented for battery operation: the Pi drops to a reduced governor, JS8Call decodes only Normal and Slow, and the node adopts a duty schedule (listen for 10 minutes in every 30, transmit heartbeats every 60 minutes rather than every 15). This roughly halves consumption at the cost of latency, and the router accounts for scheduled sleep when estimating delivery time. The schedule is advertised in the capability beacon so that peers know when to try.

6.11 The VHF and UHF last mile

Most emergency communications traffic travels five miles, not three hundred. A shelter reports to the county emergency operations center, a hospital reports to the health department, a damage assessment team reports to the incident commander. Building an HF-only messaging network for that job is a design error of scale, and revision 1.1 made it by treating VHF as a future upgrade rather than as a transport.

A VHF or UHF link is added as a third transport, and on a node that has one it becomes the preferred RF path. Three facts make this much easier than the HF side:

  1. The automatic control rules stop being a constraint. 97.221(b) permits automatic control while transmitting data on the 6 m and shorter wavelength bands with no segment restriction at all. An unattended node on 2 m operates anywhere in the band, at any speed, with no posture engine, no channel plan, and no interpretation question.
  2. AX.25 already has the addressing this design needed. An AX.25 call sign field carries a call sign plus a 4 bit SSID, so K9MEV-1 is native, costs nothing, and needs none of the compound call sign machinery of 5.2. The instance number maps directly onto the SSID.
  3. It is roughly two hundred times faster. 1200 bps against JS8 Normal's effective 40 to 50 characters per minute. A radiogram that takes 9 minutes on HF takes about 4 seconds on 2 m.
Option Speed Hardware Recommendation
AX.25 at 1200 baud, Direwolf software TNC 1200 bps Any FM radio plus a sound interface The default. Mature, forgiving of cheap radios, works through existing digipeaters
AX.25 at 9600 baud 9600 bps A radio with a true 9600 data port Better where the radios support it, which most cheap ones do not
JS8 over FM simplex 40 to 90 cpm Same as HF Works, and there is no reason to accept HF speeds on a line of sight path
LoRa bridge, Meshtastic or MeshCore Low LoRa module Interesting as a fourth transport for last-hundred-metres and for unlicensed participants
AREDN or Wi-Fi mesh Megabits Rooftop dishes, line of sight The right answer between fixed sites that can see each other, and a different project

Recommended hardware, added to the parts list as an option:

Item Part Qty Unit
Radio Used commercial 2 m mobile, 25 W, or an inexpensive dual band mobile 1 60 to 150
Interface Digirig Mobile, or a USB sound interface plus a PTT cable 1 60
Antenna 2 m or dual band ground plane, plus coax 1 60
Power Uprated DC branch and fuse, 10 A allowance for transmit 1 10

Software. A new service, hermes-vhfd, running Direwolf as a KISS TNC on a serial or network socket, carrying HMP fragments in AX.25 UI frames with the same session, fragmentation, and acknowledgment logic as the HF transport. Connectionless UI frames rather than connected AX.25 mode, because HERMES already has its own selective repeat scheme and layering one retransmission mechanism over another produces the worst of both.

Power is the honest cost. A 25 W mobile draws 8 to 10 A on transmit. The duty cycle is very low, since transmissions last seconds rather than minutes, so the energy cost is small, but the peak current means a bigger fuse, thicker wire, and a battery that can supply it. A 5 W handheld with a proper interface is a perfectly reasonable alternative for a node that only needs to reach a repeater or a neighbor a few miles away.

Digipeaters and existing infrastructure. A node can route through existing AX.25 digipeaters exactly as APRS does, which means an established regional packet network becomes usable HERMES infrastructure without anybody's permission and without any new hardware. Where a local group already runs a packet BBS or a digipeater on a good site, that site is already a HERMES relay.


section 07

Enclosure design

7.1 Constraints

The QMX bare PCB fits an enclosure of 95 x 63 x 25 mm without protrusions, which sets the minimum dimension of the radio compartment. The Pi 5 board is 85 x 56 mm and needs 25 mm of height with an active cooler. The QMX front panel carries the LCD, two rotary encoders with push, and 3.5 mm jacks; the rear carries the BNC, the 2.1 mm power jack, and USB-C.

The design decision is whether to enclose the bare QMX PCB or to place the QMX in its own aluminum enclosure inside a printed shell. Enclose the bare PCB. Nesting an aluminum box inside a printed box wastes 40 mm in every dimension, doubles cost, and produces something that looks like a project rather than a product. The trade is that the printed shell must provide the mechanical protection and the electromagnetic mitigation that the aluminum would have given, which is what the copper foil lining and the internal partition in 6.8 are for.

7.2 Form and finish

Overall envelope: 175 mm wide x 125 mm deep x 62 mm high, excluding the BNC and knobs.

diagram
   FRONT                                          REAR
   +---------------------------------+            +---------------------------------+
   |  [====  QMX LCD window  ====]   |            |   (o)DC   [USB][USB]   [ETH]    |
   |                                 |            |                                 |
   |   (O)          (O)      * * *   |            |        (O) BNC       [vent]     |
   |  TUNE         VOL      P R I    |            |                                 |
   |                                 |            |                                 |
   |   +-------------------------+   |            +---------------------------------+
   |   |  K 9 M E V - 1          |   |
   |   +-------------------------+   |   <-- swappable call sign plate
   +---------------------------------+

Aesthetic direction, since visual appeal is an explicit requirement:

  • Two-piece shell plus a separate faceplate. The faceplate prints flat, face down, in a contrasting filament, which is where all the visual quality comes from: no supports, no seams, and crisp text.
  • A 3 mm chamfer on every vertical edge and a 2 mm fillet on the top edges. Chamfers print cleanly without support; large fillets do not. This single choice is most of the difference between "3D printed box" and "product".
  • A swappable call sign plate, 100 x 18 mm, held by two M2 screws or a friction dovetail, printed in the accent color with embossed 8 mm characters. It is the identity of the unit, it is the thing an operator looks for on a table of six identical boxes, and it costs 20 minutes of print time.
  • Fuzzy skin or a fine diagonal knurl on the top surface only, which hides layer lines, gives the box a tactile quality, and photographs well.
  • Three 5 mm light pipes (printed in clear PETG or bought as acrylic rod) for power, RF active, and internet status, recessed 1 mm so they read as deliberate rather than as holes.
  • A recessed 2 mm channel around the faceplate so that the two colors meet at a shadow line rather than at a butt joint, which forgives print tolerance.
  • Ventilation as a louver array on the rear and the underside, never on the top, with 1.6 mm slots at 3 mm pitch. Louvers angled at 45 degrees print without support and keep water off the electronics if the unit is used under a tarp.

7.3 Internal layout

Two compartments separated by a 3 mm printed partition:

  • Radio compartment (left, 100 x 68 x 30 mm): QMX PCB on four M3 standoffs, front edge registered against the faceplate so the LCD and encoder shafts align without adjustment.
  • Computer compartment (right, 92 x 62 x 40 mm): Pi on four M3 heat set inserts, active cooler exhausting toward the rear louvers, GPS dongle on a right-angle USB adapter, DS3231 and INA219 on a small perfboard on the compartment wall.
  • Cable channel across the rear, 12 mm deep, for the USB link, the DC harness, and the RF pigtail, with printed cable combs so a service technician can get in and out without a fight.
  • Copper foil lining on the computer compartment walls and the partition face, bonded at a single M3 screw to the DC ground.

7.4 Print parameters

Parameter Value Reason
Material PETG (indoor) or ASA (vehicle, outdoor cabinet) Heat resistance above PLA, less brittle than ABS
Layer height 0.2 mm, 0.16 mm on the faceplate Faceplate is the visible surface
Walls 4 perimeters (1.6 mm) Stiffness and screw boss strength
Infill 25 percent gyroid Isotropic, quiet, adequate
Top and bottom 5 layers Solid surface for fuzzy skin
Supports None required by design Every overhang is at 45 degrees or bridged under 8 mm
Inserts M3 x 5 x 4 mm brass heat set, 14 places Repeated disassembly without stripping
Print time approximately 14 hours total, 3 parts
Material approximately 220 g

Appendix E contains a parametric OpenSCAD skeleton with every dimension exposed as a variable, including a verify_against_mechanical_drawing block listing the seven measurements that must be confirmed against the actual QMX PCB revision before the first print, because QMX PCB revisions have moved connector positions.


section 08

Software design

8.1 Platform

  • Stock Raspberry Pi OS Lite, 64 bit, Debian 13 base, no desktop and no custom image. Installation is three commands against a signed APT repository, described in 8.7. A prebuilt appliance image exists for people who want one, but nothing in this design requires it, and requiring a custom image is how community projects die.
  • Two supported disk layouts, described in 8.8: a plain read-write root for a node someone can reach, and a read-only root with an overlay for an unattended field node. /var/lib/hermes is always a separate ext4 partition mounted data=journal, which is what makes power removal survivable.
  • JS8Call installed from the maintainer's package. Both mainline JS8Call 2.2 and the JS8Call-Improved fork are supported; the API port differs (2442 mainline, 2242 Improved by default) and is a configuration value.
  • No X server. JS8Call is run headless under xvfb with the GUI never rendered, controlled exclusively through its TCP API. This is a well-trodden path and is how every JS8Call automation project operates.
  • Unattended security updates for the base OS, with HERMES itself updated through a signed image (A/B partition scheme) so that a failed update reverts on the next boot rather than bricking a node that is 40 km away in a park district building.

8.2 Service responsibilities in detail

hermes-core owns the message store and is the only writer to the messages table. It runs the router (9.6), the scheduler, the receipt engine, and the audit log. It is deliberately transport-ignorant.

hermes-rfd maintains a persistent TCP connection to the JS8Call API, subscribes to RX.DIRECTED, RX.ACTIVITY, RX.SPOT, RX.CALL_ACTIVITY, and RIG.PTT, and issues TX.SEND_MESSAGE, RIG.SET_FREQ, MODE.SET_SPEED, and STATION.SET_*. It implements the framing, fragmentation, and ARQ described in 9.4, maintains the transmit budget, and feeds every decode into the observations table whether or not it is HERMES traffic. That last point matters: a node accumulates a propagation database from ordinary JS8Call activity as well as from its own network, which is what makes the database useful within days rather than months.

hermes-netd registers with the configured hubs, maintains a WebSocket to each, holds direct sessions to peers it can reach, and performs the reachability probing that populates the internet side of the station database. It also handles the case that matters most in practice: a node behind CGNAT with no inbound reachability at all, which is the majority case, and which is exactly why a hub exists.

hermes-radiod owns the CAT port. Nothing else touches it. It arbitrates band changes against the transmit budget, runs the SWR and noise sweeps, enforces the interlock (no band change during transmit, no transmit when SWR protection is latched, no transmit on 60 m unless the attended flag is set), and publishes radio telemetry to the bus.

hermes-web serves the UI and the REST API, holds WebSockets to browsers, and enforces authentication and authorization. It never touches the radio or the message store directly; it calls the core over the bus. This separation means a compromised web layer cannot transmit.

8.3 Data model

Full DDL is in appendix A. The important structures:

stations is the roster: canonical identifier, base call sign, instance, public key, key state (pinned, attested, unverified), display name, grid, capabilities, and the last known values of everything. One row per station, ever.

observations is the raw log: every time we heard anything from anyone, with timestamp, band, dial frequency, offset, SNR, speed, mode, and whether the decode was addressed to us. This table grows and is the most valuable thing the node produces. Retention is 400 days by default with hourly rollup after 30 days, which keeps a full seasonal cycle at full resolution and a multi-year record in summary.

links is the aggregation: for each (peer, band, hour-of-day-UTC, month) tuple, the count of decodes, the mean and best SNR, the success rate of attempted exchanges, and a computed quality score. This is what the router actually reads. It is rebuilt incrementally, not recomputed.

reachability records the IP side: last successful direct connection, last hub-mediated contact, observed latency, whether the peer accepts inbound connections, and the peer's advertised duty schedule.

messages, message_parts, receipts, custody, and transmissions carry the traffic and its proof. transmissions in particular records every frame the node sent, with timestamps, which is both the ARQ state and a defensible operating log.

band_swr, band_noise, tx_budget, config, users, and audit_log complete the schema.

SQLite, WAL mode, one file, approximately 40 MB after a year of typical operation. It is trivially backed up (the backup button in the UI produces a single signed tarball), trivially inspected, and needs no administration.

8.4 Web interface

Served over TLS on port 443 with a self-signed certificate by default, replaced by a real certificate when the node has a name and reachability. Reachable three ways, in this order of preference: over the LAN at https://hermes-k9mev-1.local through mDNS, over a Tailscale or WireGuard overlay if configured, and over the node's own Wi-Fi access point at https://192.168.44.1 when it detects no usable network. The 44 in that address is not an accident and the AP SSID defaults to HERMES-<callsign>.

First boot wizard, six screens, each of which can be revisited later:

  1. Identity. Call sign, instance number, license class, grid (offered from GPS), station name. Generates the key pair and displays the fingerprint with a QR code, with an instruction to write it down.
  2. Radio. Detects the QMX over USB, reads the firmware version and band configuration, runs the SWR sweep, and shows the operator which bands actually work. This screen is where a bad antenna is discovered, on day one, rather than during an activation.
  3. Time. Detects GPS, shows lock status and satellite count, shows current offset from GPS, and refuses to proceed to RF enablement without a time source. This is a hard gate.
  4. Network. Wi-Fi or Ethernet, hub selection (with the operator's own hub as an option), and a reachability test.
  5. Policy. Automatic control posture, transmit budget, quiet hours, duty schedule, third party traffic settings, and the emergency override acknowledgment.
  6. Peers. Import a peer list from a file or a QR code, or start empty and discover.

Main screens:

  • Dashboard. Traffic light status for radio, time, internet, antenna, and battery. Queue depth by priority. Last 10 events. Two large buttons: New Message, and Attended Mode.
  • Compose. Three tabs matching the three message profiles: Radiogram (a proper ARRL form with the check computed automatically and ARL numbered text lookup), Message (free text with a subject, which is the email profile), and ICS-213. Live estimate of transmission time and path as the operator types, which is the feature that teaches operators brevity better than any training document.
  • Traffic. Inbox, outbox, and in-transit, with a per-message timeline showing every custody transfer, every hop, and every receipt, rendered as a vertical trace with times and SNRs. Delivery state is never a single opaque word.
  • Stations. The database made visible: a sortable roster, and for any station a band-versus-hour heat map of observed SNR that tells an operator at a glance that K9ABC is a 40 m station after dark and a 20 m station at midday. A map view using cached offline tiles.
  • Radio. Current band, offset, speed, SWR per band, noise floor per band, transmit budget consumed, and a live JS8Call band activity view.
  • Network. Hub status, peer sessions, key fingerprints and their verification state.
  • System. Logs, updates, backup and restore, power telemetry and battery projection, and a factory reset.

Attended mode is a toggle with a countdown, defaulting to 60 minutes, extendable, that asserts a control operator is present. It unlocks the conventional calling frequencies, higher speeds, 60 m, and third party traffic. It expires by itself, because an operator who forgets is the normal case, and an expired assertion returns the node to a legal unattended posture rather than leaving it in an illegal one.

Progressive web app, installable, fully functional offline against the local node, with the message composer working while disconnected and syncing when the browser reconnects. This matters because the operator's phone may be the only user interface available.

8.5 API surface

A REST API under /api/v1 with a WebSocket at /api/v1/events for push. Full listing in appendix C. Notable design choices:

  • Every mutating endpoint requires a session cookie plus a CSRF token, or a bearer token for automation.
  • Roles are admin (everything), operator (compose, send, attend), and observer (read only). A served agency volunteer gets operator, not admin.
  • The API is the same interface the UI uses. There is no privileged back channel, which means anything the UI can do can be scripted, which is how a club builds a bulk radiogram importer or a net control dashboard without asking anyone's permission.
  • An optional Winlink-style SMTP submission listener on localhost:2525 lets a local mail client submit a message, for operators who want to use a familiar tool.

8.6 Security posture

Security is treated as a first-class part of the design rather than a section at the end, and it is documented in full in section 13, which covers the attack surface inventory, the node hardening baseline, the network abuse controls, and the hub hardening requirements. Two properties belong here because they shape the software architecture itself:

  • hermes-web cannot transmit. It runs as its own unprivileged user with no access to the CAT serial port, no access to the JS8Call socket, and no ability to reach the radio except by asking hermes-core over the bus, where every request is validated independently. A compromise of the web layer is a serious problem, and it is not a transmitter.
  • Nothing received over the air or over the network is ever executed, interpreted as a path, or passed to a shell. Received data is parsed by a strict schema into typed fields and stored. Message bodies are rendered as text, never as markup. This is stated as an invariant because it is the one that, if violated once, turns a messaging network into a remote code execution network.

8.7 Packaging and installation

The target is an operator who has just written Raspberry Pi OS Lite to a card, booted it, and logged in over SSH. From there, HERMES installs in three commands.

A signed APT repository carries every component. The repository key is distributed as a keyring package and as a fingerprint published in this document and on the project site, so that a fresh install can be verified against something other than the server it came from.

shell
# 1. Trust the repository key
curl -fsSL https://apt.<project-domain>/hermes-archive-keyring.gpg \
  | sudo tee /usr/share/keyrings/hermes-archive-keyring.gpg > /dev/null

# 2. Add the repository
echo "deb [signed-by=/usr/share/keyrings/hermes-archive-keyring.gpg] \
  https://apt.<project-domain>/hermes trixie main" \
  | sudo tee /etc/apt/sources.list.d/hermes.list

# 3. Install
sudo apt update && sudo apt install hermes-node

# 4. Configure, interactively, once
sudo hermes-setup

hermes-setup asks for the call sign, instance number, and grid, forces the creation of a web administrator password, generates the identity key pair, prints the fingerprint, and prints the URL to open. Everything else happens in the browser wizard of 8.4.

Packages:

Package Contents
hermes-common Message library, canonicalization, crypto, schema, shared config
hermes-core Message store, router, receipts, scheduler
hermes-web User interface, REST API, PWA assets
hermes-rfd JS8Call transport
hermes-radiod Radio drivers for QMX, QMX+, QDX
hermes-netd IP transport
hermes-scand Receive-only SDR scanning, pulls in rtl-sdr or soapysdr
hermes-hub The hub service, for anyone running one
hermes-doc This document and the operator manual, offline
hermes-node Metapackage: common, core, web, rfd, radiod, netd, plus js8call
hermes-node-sdr hermes-node plus hermes-scand

What the packages do and deliberately do not do. The postinstall creates the service users, creates /var/lib/hermes and /etc/hermes, and installs the systemd units disabled. It does not generate keys, does not open a port, does not start a listener, and does not set a password. A machine that has hermes-node installed but has never had hermes-setup run on it is inert. Keys are generated at first setup, on the machine, never at package build or install time, so that no two nodes can ever share an identity.

Configuration lives in /etc/hermes/hermes.yaml as a dpkg conffile, so an upgrade never silently overwrites an operator's settings.

On the convenience one-liner. A curl | sudo bash installer is published because people will ask for one, and the documentation says plainly that the four commands above are the preferred path, prints the script's SHA-256 next to the link, and explains that piping a remote script into a root shell means trusting the server completely at that instant. Offering it and being honest about it is better than pretending nobody does it.

Other platforms. The same packages build and run on Debian and Ubuntu on x86-64, which is how a hub gets deployed on a virtual machine and how a test node runs on a laptop without a radio.

8.8 Updates, patching, and the read-only filesystem

An emergency communications appliance that is three years behind on security updates is a liability sitting on a public address. An appliance that reboots into a broken kernel during an activation is worse. This section resolves the tension.

Two layouts.

Layout A, plain read-write root. The default for a node the operator can physically reach. OS security updates come from unattended-upgrades with the Debian security suites plus the HERMES repository. HERMES updates come from apt. Nothing exotic.

Layout B, read-only root with an overlay. The recommended layout for an unattended field node, and the one that needs the machinery below. The root filesystem is mounted read-only with a tmpfs overlay, so an abrupt power loss can never corrupt it, and no runtime process can modify the system.

The update pipeline, identical in both layouts, run by hermes-update on a timer:

  1. Check policy. Is this inside the configured maintenance window, is battery state of charge above the floor, is there no active emergency declaration, is the queue free of emergency and priority traffic, and is this node not currently the only known relay path for something in flight? Any answer of no defers the update and records why.
  2. Fetch and verify. The repository index and every package are signature-checked. There is no unauthenticated fallback and no plain HTTP mirror.
  3. Snapshot. Layout A takes a filesystem snapshot where the underlying filesystem supports it. Layout B applies the change to the inactive root slot of an A/B pair rather than to the running system.
  4. Apply. Layout B remounts nothing on the running system; the update is written to the inactive slot. Layout A remounts read-write only for the duration of the transaction and returns to read-only immediately afterward.
  5. Health check. After the change, or after a boot into the new slot, a scripted check confirms that every service is running, the JS8Call API answers, the CAT port enumerates and the radio responds, the web interface returns a page, and the database opens. A failure rolls back: the snapshot is restored, or the bootloader falls back to the previous slot.
  6. Report. The result goes to the audit log, to the System screen, and, if the node has connectivity, to the operator by whatever notification channel is configured.

A/B slots on the Pi. The Pi 5 and Pi 4 bootloaders support a try-boot mechanism: boot the new slot once, and if the running system does not confirm success by setting a flag, the next boot reverts automatically. A node that receives a bad kernel update comes back on the old kernel by itself, without a site visit, which for a node in a park district building 40 km away is the difference between an inconvenience and a lost node.

Reboots are policy-controlled: never, window, or required-only. A node that needs a reboot announces it to its peers as a status message with the expected downtime, so the network's routing does not spend that window trying to reach it, and defers if it is carrying custody of anything above routine precedence.

Offline and sneakernet updates. This is not optional in a system whose premise is that the internet fails. A signed update bundle can be built by anyone with the repository contents and applied from removable media:

shell
sudo hermes-update --bundle /media/usb/hermes-2026-09-security.hupd

The bundle is signed with a separate offline signing key whose public half ships with the packages. Bundle application runs the same verify, apply, health check, and rollback pipeline as a network update. A volunteer walking a USB stick to six nodes after a two-week outage is a supported and documented workflow.

Configuration.

yaml: configuration
vhf:                      # section 6.11
  enabled: false
  tnc: direwolf
  kiss: 127.0.0.1:8001
  callsign_ssid: 1        # AX.25 SSID, matches the instance number
  frequency_hz: 144390000 # example only, choose a local data frequency
  digipeat_path: ["WIDE1-1"]

measurement:              # section 12
  retention_days: 400
  rollup_after_days: 30
  pskreporter: false
  aggregator: null        # station id of an opt-in group aggregator
  share_grid_precision: 4

path_watch:
  - peer: N9ABC-0
    label: "County EOC"
    expect_reachable_pct: 90
    expect_bands: [40, 80]
    degraded_snr_db: -18
    alert_after_h: 12

net:
  role: node              # node | net_control | aggregator
  report_to: null         # net control station id, opt in
  report_interval_ip_min: 5
  report_interval_rf_min: 30

fleet:
  coordinator: null       # pinned identity, opt in
  accept_classes: [channels, groups, peers, watched_paths, forms]

updates:
  mode: security            # off | security | full | manual
  window_utc: "09:00-11:00"
  days: [Tue, Wed, Thu]
  stage_days: 3             # wait this long after a release before taking it
  battery_floor_pct: 60
  reboot_policy: window     # never | window | required-only
  defer_on_emergency: true
  defer_on_priority_queue: true
  offline_bundles_allowed: true

Visibility. The dashboard carries a "days since last successful update" indicator with the same weight as battery state and antenna health, because an unpatched node is a failure mode, and a failure mode that nobody can see is one that nobody fixes.

8.9 Net control and the operational picture

The design so far is node-centric, and emergency communications is organizational. Somebody is running the net, and that person needs to see all of it: what traffic exists, who is holding it, what is stuck, who is on the air, and what has actually been delivered to a human being.

Net control is a role, not a special build. Any node can be designated net control for a group and for an incident. It runs the same software with an additional screen.

The traffic board is the main view: every message the net control node knows about, in a sortable table showing precedence, age, originator, destination, current state, current custody holder, transport used so far, and time in current state. Rows turn amber past a precedence-dependent age threshold and red past a second one, which is how a stuck priority message announces itself rather than waiting to be noticed.

The station board shows every group member with last heard time by transport, queue depth by precedence, battery state, antenna health, and whether they are attended right now. A station that has gone quiet on every transport at once is highlighted, because that pattern is about the incident rather than about the radio.

How the data gets there. Group members send a periodic netstat profile message to the net control identity: their queue summary, custody list by message identifier, station health, and reachability summary. Signed, small, and over IP where possible. Over RF the same content compresses into one or two ~S frames, at a rate that respects the transmit budget: every 5 minutes over IP, every 30 minutes over RF, and immediately on any state change above routine precedence.

Consent and authority, stated explicitly. Reporting to a net control station is opt in, per group, and expires with the incident. And net control has no command authority over any node, which is the same invariant as 13.6: it can see, and it can send messages like anyone else. Traffic is assigned by sending someone a message asking them to handle it, not by reaching into their node. This is a deliberate refusal of a feature that every operational user will eventually ask for, and the reason is that a remote command channel into every node in a district is the single most dangerous thing this design could contain.

Output. The net control node produces the group-wide ICS-309 from 11.7, the after-action summary, and the delivery statistics by precedence and transport, for the whole net rather than for one station.

8.10 Fleet provisioning and configuration

A district running forty nodes cannot configure them one browser session at a time, and cannot rely on forty volunteers to keep band plans, group memberships, form templates, and update windows aligned. The obvious solution, a management server that pushes configuration, is exactly the remote command channel that 13.6 forbids. The resolution is that the direction of control is inverted.

Policy is pulled, signed, and narrowly scoped.

  1. A coordinator publishes a policy bundle: a signed document, versioned, carrying only the classes of settings listed below. It is published wherever nodes can fetch it, which in practice is a hub, a web server, or a USB stick.
  2. A node subscribes to a coordinator identity, explicitly, once, by pinning that identity's key. Subscription is per-node consent and can be withdrawn at any time from the local interface.
  3. On fetch, the node verifies the signature, checks the version is newer, and applies only the policy classes the operator has enabled.
Class Contents Default
channels Automatic and attended channel plans, band schedule Enabled
groups Group memberships and the group directory Enabled
peers Directory entries, peer keys, hub list Enabled
watched_paths The path watch expectations of 12.4 Enabled
forms Form template set and versions Enabled
updates Maintenance window, staging delay, reboot policy Disabled by default
blocklist Advisory blocklist entries Disabled by default

What a policy bundle can never contain: executable code, package sources, cryptographic identity material, credentials, transmit commands, or any instruction that causes an immediate transmission. Those are not in the schema, so a hostile or compromised coordinator can misconfigure a node's band plan and cannot make it do anything else. A local setting always overrides a policy setting, every application is written to the audit log with a diff, and one button reverts to local configuration.

Provisioning at scale. An enrollment token, presented as a QR code or a string, carries the coordinator identity, the group, the hub list, and the policy subscription. sudo hermes-setup --enroll <token> takes a node from a fresh image to a configured group member in one step, leaving only the call sign, instance number, and the administrator password to be entered locally. Identity keys are always generated on the node at first setup and never in an image, so a district can flash forty identical cards without ever creating two nodes that share an identity.

Health visibility. A fleet dashboard is simply a node with the net control role and a permanent group scope: nodes push health telemetry to it, opt in, and it displays. Read only, in the same way and for the same reason.


section 09

The HERMES Message Protocol

9.1 The message object

Everything in HERMES is one object type. A radiogram, an email, an ICS-213, a receipt, and a capability advertisement are all messages with different profiles.

json
{
  "v": 1,
  "id": "01J8Z4K7QW3M9XR2",
  "from": "K9MEV-1",
  "to": ["N9ABC-0"],
  "profile": "radiogram",
  "prec": "P",
  "hlim": 4,
  "created": "2026-08-10T14:32:07Z",
  "expires": "2026-08-13T14:32:07Z",
  "subject": "WELFARE CHECK OAK ST",
  "body": "ARL SIXTY SIX WITH TRUCK X ALL WELL X",
  "meta": {
    "check": 9,
    "place": "BATAVIA IL",
    "filed": "1432Z AUG 10",
    "signature_line": "MARK K9MEV",
    "handling": "HXG"
  },
  "orig_class": "third_party",
  "sig": "base64-ed25519-signature-over-canonical-form"
}

Identifier. A 16 character Crockford base 32 string derived from the first 80 bits of SHA-256 over the canonical serialization, with the top 48 bits replaced by a millisecond timestamp so that identifiers sort chronologically and collide with negligible probability. The result is content-addressed enough for deduplication and time-ordered enough for a queue.

Canonical form. JSON Canonicalization Scheme (RFC 8785), so that the same object signed on two nodes produces byte-identical input to the signature. This is unglamorous and absolutely load bearing: it is the difference between signatures that verify and signatures that mysteriously do not.

Precedence follows the ARRL and NTS convention: R routine, W welfare, P priority, E emergency. It drives queueing, transmit budget allocation, band selection aggressiveness, and retry intervals.

Hop limit (hlim) decrements at each custody transfer and prevents loops in combination with the path list carried in the custody record.

9.2 Transports

Property HMP/IP HMP/RF
Underlying TLS 1.3 over TCP, WebSocket framing JS8 over SSB-suppressed FSK, via JS8Call
Throughput Effectively unlimited 40 to 90 characters per minute
Latency Under 1 second 15 seconds to several hours
Reliability TCP plus application receipts Selective repeat ARQ plus receipts
Signature carried Full Ed25519, 64 bytes 40 bit digest only
Framing Complete JSON object Fragmented, encoded, compressed
Addressing Canonical CALL-N RF form CALL/N, or CALL

The core hands the same object to either. The transports differ entirely in what they can afford.

9.3 RF channel plan

Every automatic channel is placed inside a 97.221(b) segment with at least 1 kHz of margin at both ends, computed for JS8Call audio offsets in the range 200 to 2400 Hz.

Band Posture Dial (USB) Resulting RF Inside segment Use
80 m Automatic 3.5860 3.5862 - 3.5884 3.585 - 3.600 Night regional, NVIS, primary after dark
40 m Automatic 7.1005 7.1007 - 7.1029 7.100 - 7.105 Day and night regional, most reliable
30 m Automatic 10.1405 10.1407 - 10.1429 10.140 - 10.150 24 hour workhorse, low QRM, no contests
20 m Automatic 14.1015 14.1017 - 14.1039 14.1005 - 14.112 Day medium haul, out of region
60 m Attended only channelized per 97.303(h) not permitted Excluded from automatic control
80 m Attended 3.5780 conventional JS8 n/a Interoperability with JS8Call community
40 m Attended 7.0780 conventional JS8 n/a Interoperability
30 m Attended 10.1300 conventional JS8 n/a Interoperability
20 m Attended 14.0780 conventional JS8 n/a Interoperability

The 40 m automatic segment is only 5 kHz wide and is shared with every other automatically controlled digital station in the country. This is the most contended piece of spectrum in the plan, and the software mitigates by preferring 30 m whenever 30 m is open, which for regional Illinois paths is most of the time.

Band scheduling. A node in automatic posture spends its time according to a schedule derived from the links table, defaulting before any data is collected to: 80 m from 0200Z to 1100Z, 40 m from 1100Z to 1400Z and 2200Z to 0200Z, 30 m from 1400Z to 1700Z, 20 m from 1700Z to 2200Z. The schedule is advertised in the capability beacon so that a peer knows where to find the node and does not waste a transmission on a band the node is not monitoring. Within any 30 minute window the node also makes one 3 minute excursion to the next band in rotation to keep the database fresh, which is how the schedule improves itself.

9.4 RF framing

A HERMES frame is carried inside a JS8Call directed message. The sigil ~ is chosen because it is not used by JS8Call protocol syntax and reads clearly in a monitor's window.

diagram
<TO>: <FROM> ~<T><flags> <fields...>

For example, a data fragment:

diagram
N9ABC: K9MEV ~D 7QK2 03/11 ARL SIXTY SIX WITH TRUCK X ALL W

Frame types:

T Name Direction Purpose
H Hello broadcast Capability advertisement: version, bands, schedule, queue depth, hub reachability, key fingerprint prefix
Q Query directed Do you have traffic for me, or for anyone I can reach
O Offer directed I hold message <sid>, <n> fragments, precedence <p>, for <dest>
A Accept directed Send it, starting at fragment <k>
D Data directed Fragment <k> of <n> for session <sid>
K Ack directed Fragments received, as a bitmap, plus a request for the missing ones
R Receipt directed Custody accepted, or final delivery confirmed, with digest
X Reject directed Refused, with a reason code (full, out of budget, wrong destination, unverified)
I Identify broadcast Explicit station identification for 97.119 compliance
S Status directed Queue and reachability state, sent in response to Q when there is no traffic

Session identifier <sid> is four base 32 characters, derived from the message identifier, unique enough within the lifetime of an exchange and cheap enough to repeat in every fragment.

Fragmentation. The payload is encoded (9.10), then split into fragments sized to fill an integral number of JS8 frames. At JS8 Normal, a directed message carries roughly 20 characters of free text in the first frame after the addressing overhead, and roughly 20 in each subsequent frame. HERMES uses a fragment payload of 40 characters, which is two JS8 frames plus the fragment header, giving a natural unit of about 45 seconds. Larger fragments waste less overhead but retransmit more on failure; 40 characters is the point where the two curves cross at typical emergency-conditions error rates.

ARQ. Selective repeat with a window of six fragments. The sender transmits up to six, the receiver acknowledges with a 6 bit bitmap in a K frame, the sender retransmits only the gaps. Timeout is four frame periods plus the peer's advertised turnaround. Three consecutive failed windows abandons the session, records the failure against the link in the database, and returns the message to the router, which will try a different band, a different path, or a different transport.

Integrity. Each fragment carries a 10 bit CRC rendered as two base 32 characters. The reassembled payload carries a 40 bit SHA-256 prefix digest, rendered as eight base 32 characters, which is checked against the digest in the O frame. Both algorithms are public and neither carries a key.

Speed selection. Normal (50 Hz, 15 s, about -24 dB threshold) is the default. Slow (25 Hz, 30 s, about -28 dB) is selected automatically when the link table shows a mean SNR below -18 dB for that peer on that band, and for the very long paths where the extra 4 dB is the difference between a message and nothing. Fast is used only in attended mode. JS8 40 and JS8 60 are never used, because they do not participate in heartbeat networking.

9.5 Presence

Two mechanisms operate together, and the choice to use the native one for the common case is deliberate.

Native JS8Call heartbeat. The node participates in ordinary JS8 heartbeat networking, transmitting HB in the heartbeat sub-band on a configurable interval (default 30 minutes for a compound call sign, 15 minutes for a bare call sign, 60 minutes in power saving posture, per the arithmetic in 5.2) and answering heartbeats it hears with SNR reports. This costs one frame, is understood by every JS8Call station in the world, and populates RX.CALL_ACTIVITY, which hermes-rfd polls every 60 seconds and writes into observations. A HERMES node therefore appears to the wider JS8Call community as a well-behaved ordinary station, and a plain JS8Call operator can see it, call it, and leave it a message without knowing HERMES exists.

HERMES capability beacon. A ~H frame to the group @HERM, at a much lower rate (default 120 minutes for a compound call sign, 60 minutes for a bare one, or immediately after a band change), carrying what the heartbeat cannot: protocol version, enabled bands, duty schedule, current queue depth by precedence, whether the node currently has internet, and the first 20 bits of its key fingerprint. Example:

diagram
@HERM: K9MEV/1 ~H 1 1110 SCH2 Q0102 NET1 F7K2A

decoded as version 1, bands 80/40/30/20 enabled as a bitmask, schedule profile 2, queue of 0 emergency, 1 priority, 0 welfare, 2 routine, internet available, fingerprint prefix F7K2A. With a compound call sign that is two JS8 frames at Normal speed, 30 seconds, every two hours, which is 15 seconds per hour of channel occupancy per station. A hundred stations in a region cost 25 minutes of aggregate channel time per hour, which is why the interval is measured in hours rather than minutes.

Listening is continuous and free. Every decode of any kind, HERMES or not, is written to observations. A node that has been running for a week in Batavia knows which of its neighbors are audible on which band at which hour, and so does every other node, and that shared knowledge is what makes the routing in 9.6 possible without any central coordination.

9.6 Routing and path selection

The router runs whenever a message enters the queue, whenever a receipt arrives, whenever presence data changes materially, and on a 60 second tick. For a message m with destination d, it enumerates candidate paths and scores them.

diagram
score(path) = W_transport
            + W_directness
            + W_quality
            + W_freshness
            - W_cost
            - W_hops

with the following weights, all configurable:

Term Value Rationale
W_transport +1000 IP direct, +900 IP via hub, +400 VHF direct, +300 VHF digipeated, +100 HF direct, +60 HF relayed The 500 point gap makes the internet preference decisive rather than advisory, which is the stated requirement, and VHF sits decisively above HF for the same reason: it is two orders of magnitude faster and legally simpler
W_directness +50 if the destination itself, +0 if a relay
W_quality 40 + SNR for the relevant band and hour, clamped to 0 to 60 A -20 dB path scores 20, a 0 dB path scores 40
W_freshness +30 if heard in the last hour, +15 within 6 hours, +5 within 48 hours, 0 beyond
W_cost estimated frames to send, times 2 Long messages prefer better paths
W_hops 25 per hop

The highest scoring path is attempted. On failure, the path is penalized in the link table and the router re-runs, which produces automatic, memory-bearing fallback without any explicit state machine.

The internet preference in practice. Any live IP path outscores any RF path by at least 800 points, so RF is used only when no IP path exists or every IP path has failed. That is the requirement. But there is a subtlety worth stating: hermes-netd must distinguish "the internet is down" from "this peer is down". It probes three independent anchors (the configured hubs, a well-known public resolver, and the last-known-good direct peer) and only declares internet loss when all three fail, which prevents a single hub outage from pushing a region onto HF unnecessarily.

Scheduled delivery. If the best path is a peer that is asleep under a duty schedule, the router does not burn transmissions calling into the void. It schedules the attempt for the peer's next advertised window and displays that time in the UI, so the operator sees "next attempt 0215Z via N9ABC on 80 m" rather than an unexplained pause.

Relay selection. A relay candidate must have advertised the relay capability, must have been heard within the freshness window, and must itself have a scored path to the destination that it has advertised in a ~S frame or that is inferable from the local database. Two hops is the default limit for automatic relaying, extendable to four for emergency precedence. Every relay decision is logged with its reasoning, and the message timeline in the UI shows why each hop was chosen, because an emergency communications system whose routing cannot be explained after the fact is a liability during the after-action review.

9.7 Store and forward, and custody

HERMES uses explicit custody transfer, borrowed from delay tolerant networking, rather than best-effort forwarding.

  1. Node A holds message m for destination D and finds a path through relay B.
  2. A sends ~O offering m. B evaluates against its storage quota, its transmit budget, its policy (does it relay for this precedence, this origin class), and its own path to D.
  3. B accepts with ~A, A transmits fragments, B verifies the digest.
  4. B issues ~R with custody acceptance. At this moment responsibility transfers. A marks the message as custody_transferred, stops attempting delivery, but retains the message and the custody record for the message lifetime in case B never delivers.
  5. B is now responsible and applies the same routing logic.
  6. When D finally receives the message, D issues an end to end receipt addressed to the originator, which propagates back along any available path, not necessarily the forward path, and typically over IP once anyone in the chain regains internet.

Custody timeout. Each custody record carries a deadline derived from the message expiry and the hop limit. If A does not see an end to end receipt before the deadline, A resumes responsibility and re-routes, avoiding B. This means a relay that is destroyed, powered down, or simply out of range does not silently swallow traffic, which is the classic failure of naive store and forward systems.

Storage quotas. Default 200 messages or 2 MB of relayed traffic, whichever comes first, with emergency and priority traffic exempt from the quota up to a hard ceiling. Full storage produces ~X with reason FULL, which the offering node records so it does not keep asking.

9.8 Delivery verification

Four levels of proof, and the user interface distinguishes all four rather than showing a single checkmark.

Level Name Meaning Cryptographic strength
1 Transmitted The frames left this station None, local log only
2 Received by next hop ARQ completed and digest matched at the neighbor 40 bit digest, CRC per fragment
3 Delivered The destination has the message and issued a receipt 40 bit digest over RF, full signature over IP
4 Verified A full Ed25519 receipt from the destination's pinned key is held 128 bit security

Receipt upgrade is the mechanism that makes level 4 achievable in a network where RF cannot carry a signature. When D receives a message over RF, it issues a compact ~R receipt containing the message identifier and the 40 bit digest. It simultaneously queues a full signed receipt object. The moment D has any IP path (its own, or a relay's), the full receipt flows to the originator, and the originator's UI transitions the message from Delivered to Verified. If IP never returns, the compact receipt stands, and the UI is honest about what it does and does not prove.

Non-repudiation is bounded and stated as such. A 40 bit digest proves the destination saw the exact bytes. It does not prove which station issued the receipt, because RF frames carry no signature. An adversary with a transmitter could forge a compact receipt. The UI therefore never claims Verified on RF-only evidence, and for traffic where proof of delivery genuinely matters (a served agency resource request), the operator can mark the message require_verified, which keeps the message active and retrying until a full signed receipt is obtained.

9.9 Interoperability with plain JS8Call

A HERMES node must be a good neighbor and a useful one to operators who have never heard of it.

  • It answers ordinary directed messages, SNR?, GRID?, STATUS?, and HEARING? queries with sensible values.
  • It accepts ordinary JS8Call MSG store-and-forward inbox traffic and surfaces it in the HERMES inbox, so a plain JS8Call user can leave a message for a HERMES station using nothing but stock software.
  • It relays for plain JS8Call stations using the native > relay syntax when asked, subject to the same budget and policy controls.
  • Its beacons are ordinary heartbeats. Its ~H frames go to a group, which plain stations can subscribe to or ignore.
  • When a plain JS8Call operator sends free text to a HERMES node, the node replies in plain English, not in protocol, with a short message identifying itself and how to leave traffic.

This is not a courtesy. It is a survival property: a network of six HERMES nodes is nearly useless, and a network of six HERMES nodes embedded in a regional JS8Call population of two hundred is a working system on day one.

9.10 Payload encoding, and why compression is not a wire transform

JS8 encodes text with a variable-length code averaging about 3.4 bits per character over its efficient alphabet, packing roughly 69 bits of payload into each frame. Characters outside that alphabet cost dramatically more or cannot be transmitted at all. Every RF payload is therefore confined to A-Z, 0-9, space, and the punctuation . , ? / - +.

Revision 1.2 got the layering wrong here, and the correction matters. It described abbreviation and ARL numbered text substitution as encoding steps performed by the transport. They cannot be. Those transformations change the message body, and the body is signed. Anything applied to it after signing breaks every signature over it, and a message whose signature fails is worse than no message at all.

The correct arrangement, and the one implemented:

Composition aids, applied before the message is created and signed. The composer folds text into the wire alphabet, applies the ARRL radiogram punctuation convention, suggests ARL numbered texts, and offers the published abbreviation table. The operator sees the result, that result is what gets signed, and that is what goes on the air. There is no transformation in between.

  1. Case folding to uppercase, free because radiograms are uppercase by convention.
  2. Punctuation substitution per the radiogram convention: a period becomes X, other characters are spelled out. Not compression, an existing standard operators already know.
  3. ARL numbered text substitution. ARL SIXTY SIX is a complete standard sentence in thirteen characters, the largest single win available and requiring no invention. The composer suggests them as the operator types and shows the saving.
  4. Abbreviation table. A published static table of emergency communications terms, shipped with the software, applied only where it creates no ambiguity.

The transport refuses rather than transforms. A body containing characters JS8 cannot carry is rejected at the point of transmission with a message naming the offending characters and saying where to fix it. That is a worse user experience than silently cleaning it up, and it is the only correct behavior: the alternative is a message that arrives with a signature that does not verify.

Binary fields use Crockford base 32, which is cheap in the JS8 alphabet and excludes the ambiguous letters so a fingerprint survives being read aloud on a repeater.

There is deliberately no general-purpose binary compression. A DEFLATE stream base 32 encoded expands by 60 percent before it compresses, which for messages under 300 characters is a net loss, and it would raise a question under 97.113(a)(4) that a published substitution table does not.

9.11 Transmit budget

Unattended stations that transmit without restraint are how automatic digital operation gets a bad name, and 40 m ACDS spectrum is 5 kHz wide. Every node enforces a budget:

  • Default 15 percent transmit duty cycle measured over any rolling 60 minutes, and 8 percent over any rolling 24 hours.
  • Priority allocation: emergency traffic may consume the full budget and may borrow from the next period; routine traffic may consume no more than 40 percent of it.
  • Listen before transmit: hermes-rfd consults the JS8Call band activity window and defers if the intended offset is occupied, hunting for a clear offset in 100 Hz steps.
  • Backoff on collision: a failed exchange doubles the wait before retrying the same peer on the same band, up to 32 minutes.
  • Quiet hours, configurable, during which only priority and emergency traffic transmits.

The budget is visible on the Radio screen as a consumed-versus-available bar, because an operator who can see the budget will manage it, and an operator who cannot will assume it does not exist.

9.12 The group namespace

JS8Call group names are a flat, uncoordinated, first-come namespace. @ALLCALL, @HB, and a handful of others are reserved by the software; everything else is whatever a local community decided to type. There is no registry with any authority behind it.

Decision: accept collisions. Do not claim a prefix.

The alternative, prefixing every HERMES group (@H-FRRL rather than @FRRL), buys uniqueness at the cost of the property that section 9.9 argues is the system's best chance of being useful, which is that a plain JS8Call operator monitoring their club's group should see HERMES stations in it and be able to talk to them. A parallel prefixed namespace makes HERMES a private club inside a public band, which is both antisocial and self-defeating in a network whose whole problem is having too few participants.

Collisions are benign here, and the reason is that HERMES frames are self-identifying:

  1. The ~ sigil disambiguates without a namespace. A HERMES protocol frame is unmistakably a HERMES protocol frame. A human reading the group window sees a short machine string and ignores it, the same way they already ignore heartbeats and SNR reports.
  2. Non-~ group traffic is never parsed as protocol. Plain text arriving in a shared group is surfaced in the HERMES inbox as an ordinary message from an ordinary station, which is exactly right.
  3. Protocol frames are never sprayed into a group that has no HERMES members. A node sends ~ frames to a group only after it has heard at least one member of that group advertise HERMES capability. A club that has never heard of this project never sees a byte of it in their group window, which is the courtesy that makes accepting collisions defensible.
  4. The directory records who uses which group, which gives soft coordination without anyone allocating anything.

The rule is enforced, not merely stated. A node sends protocol frames to a group only after it has heard a HERMES-capable station in that group within the last thirty days. Revision 1.2 wrote this promise down and the first implementation beaconed unconditionally, which would have made the whole position dishonest.

The awkward case is the first node in a region, which by definition has heard nobody, and refusing forever would mean the network can never start. So there is a deliberate operator override, hermesctl group enable @HERM, which states the consequence at the point of use ("this puts machine traffic into a group that may belong to someone else") and records who enabled it and why. Correct for the first node in a county; discourteous otherwise, and now attributable either way.

One reservation, and it needs checking. @HERM is used as the control group for capability beacons. That is a claim on a name in a namespace where claims have no force, so before release the name must be checked against actual on-air usage and against the JS8Call community's informal lists. If it is already in use by someone else, the project picks a different one and moves on. This is an open item in section 20.

Group names are 2 to 8 uppercase characters, and two-character names are avoided because several short names are reserved or conventional in JS8Call.


section 10

Internet transport and the hub

10.1 Design principle and ownership

The stated requirement is "limited centralized infrastructure". The interpretation adopted here is that the central component must be small enough to run on a 5 USD per month virtual machine, replaceable by any operator in an afternoon, federated so that no single instance is authoritative, and unnecessary once two stations have met.

Ownership is left to whoever deploys. This project operates no hub, ships with no hub configured, and names no default. A freshly installed node works out of the box with no hub at all: it talks to peers directly where it can, over RF where it cannot, and asks the operator to add a hub only when they want the convenience of rendezvous and mailbox. That is a deliberate choice about power as much as about architecture. A project-operated default hub becomes a chokepoint, a dependency, a privacy question, and eventually somebody's unpaid job.

The recommended pattern for an operator or a club who wants one is in 10.7: run the hub on the node itself, on 44Net address space.

10.2 What the hub does

  1. Rendezvous. Maps a station identifier to its current endpoints, so that two nodes behind NAT can find each other. This is the function that genuinely requires a third party.
  2. Presence. Holds a short-lived record of which stations are currently online, refreshed by a WebSocket keepalive, so that presence is a byproduct of the connection rather than a separate poll.
  3. Relay of last resort. When neither node can accept an inbound connection (the common case), the hub forwards frames between two connected sessions without storing them.
  4. Mailbox. Holds messages for a station that is offline, for a bounded period (default 7 days) and a bounded size (default 20 MB per station), then expires them.
  5. Attestation. Optionally, license verification and signed call-sign-to-key attestations, plus cross-signatures with peer hubs.
  6. Directory. A signed, cacheable snapshot of known stations, their public keys, and their capabilities, which every node downloads and keeps, so that a node that has synchronized once can operate indefinitely without any hub at all. This is the property that makes the hub non-essential.

10.3 What the hub does not do

It does not route, it does not make decisions about transports, it does not hold long-term message archives, it does not sign as a station, it does not touch RF, and it does not have any privileged view that a participating node lacks. A hub operator who turns hostile can deny service and can read traffic, and traffic is not confidential anyway.

10.4 Implementation

A single Python module behind a reverse proxy, roughly 400 lines including the quota logic. Endpoints: /register, /mailbox/{station}, /mailbox/{station}/collect, /directory, /health, /stats. Station authentication is by signed challenge against the station's Ed25519 key, which is stronger than a password and needs no account system.

Deviation from earlier revisions: REST and polling, not a WebSocket. A WebSocket remains the right answer for a busy hub and can be added without changing a node. It is not what was built, for three reasons. Polling survives every corporate proxy, captive portal, and flaky link this system exists for. A mailbox checked every thirty seconds is indistinguishable from a push for traffic that took nine minutes to cross forty metres. And a polling client has no reconnection state machine to get wrong, which on an appliance that must run unattended for months is worth more than the latency it costs.

The challenge is client generated, not server issued. A server nonce needs server state, an extra round trip, and a cleanup job. A client nonce, a bounded timestamp, and a replay cache give the same guarantee in one request. The cost is that both ends need a roughly correct clock, which a JS8 node has anyway because JS8 does not work without one. The token binds the station, the action, and a digest of the request body, so a captured token cannot be replayed to push different content.

Federation is deliberately simple: each hub holds a list of peer hubs, pushes directory deltas to them every 5 minutes, and forwards mailbox deposits for stations it does not know to the hub that claims them. There is no consensus protocol and no attempt at global consistency, because the failure mode of inconsistency (a message takes the RF path instead of the IP path) is benign.

Self-hosting is the intended default for any organized group. A club, an ARES district, or an individual operator runs a hub for their own region. The reference deployment is a container plus a Caddy configuration, and the documentation includes an AMPRNet variant with the hub reachable on 44.72.0.0/16 space, which gives an ARDC-allocated, globally routed, non-commercial path that is entirely independent of any consumer ISP. For a network whose premise is infrastructure failure, having the hub reachable over an amateur-operated network rather than only over a commercial one is a meaningful additional layer.

10.5 Operating a hub

A hub is run by a volunteer on hardware that does something else too, and until recently its operator could see /stats and journald and nothing more. They could not list registrations, remove a stale one, issue an invite, or block an abusive station without editing SQLite by hand.

The operator interface is a small set of authenticated endpoints: list stations with their mailbox depth, block or unblock, remove a registration, issue and list invite codes, manage federation peers, and read the action log. Authentication is a single bearer token rather than an account system, because the hub has exactly one operator and an account system here would be more code to get wrong than the thing it protects. With no token configured the interface is disabled, not open.

Removing a registration is deliberately manual and deliberately explicit. The hub refuses a key change on purpose (10.2); undoing that refusal should be a person's decision after they have confirmed the change out of band, and the response says so.

Federation is a push rather than a pull: each hub sends its directory to its configured peers on a timer. Push, because a hub behind NAT can reach its peers even when they cannot reach it. Two constraints keep it safe. A hub accepts federation only from peers it has been configured with, so the directory is not writable by the internet. And a federated entry never overwrites a key already held: a hub that has been taken over must not be able to tell its peers that a station's key has changed. That is what rotation certificates are for, and they are verified by the node rather than by any hub.

10.6 IP transport details

  • TLS 1.3, certificate pinning against the hub's key after first contact.
  • WebSocket with a 30 second keepalive and exponential reconnect backoff capped at 5 minutes.
  • Direct peer sessions preferred where possible: the node advertises its endpoints (including an IPv6 global address, which usually works where IPv4 NAT does not, and an AMPRNet address where allocated), and attempts direct connection before falling back to hub relay.
  • Optional WireGuard overlay for groups that want a flat, always-on private network between their nodes, which subsumes the NAT traversal problem entirely and is the recommended arrangement for a fixed set of club stations.

10.7 Recommended pattern: your own hub on the node, over 44Net

Most amateurs sit behind a consumer internet connection with dynamic addressing, carrier grade NAT, or both, which means no inbound reachability, which is the entire reason a rendezvous service exists. There is a way out of that specific to this hobby, and an operator who already runs 44Net Connect has it available today.

The idea. Take a small block of 44Net (AMPRNet) address space, carry it home over WireGuard tunnels to the 44Net Connect points of presence, announce it with BGP from a small router, and put the node on it. The node then has a real, globally routed, static, publicly reachable IPv4 address that has nothing to do with the local ISP. Peers connect to it directly. If it also runs hermes-hub, other operators can use it as their rendezvous service.

Why this fits the design's premises unusually well:

  • It is amateur-operated infrastructure end to end, which is a better philosophical and practical match for an emergency network than a commercial cloud tenancy.
  • It is independent of the local ISP's NAT policy, which is the specific failure this design keeps running into.
  • A /28 from the user-requestable pool is sufficient for a node, a hub, and room to grow, and networks that size are effectively auto approved in the portal.
  • Two tunnels to two points of presence gives a measure of path redundancy on the internet side, which is the side that fails first in the scenarios this system exists for.

The honest caveats:

  • 44Net space is directly reachable from the entire internet with no NAT in front of it. There is no accidental protection. Everything in section 13 becomes mandatory rather than advisory the moment a node sits on a 44 address. The node will be port-scanned within hours of the prefix being announced, and it will be probed continuously thereafter.
  • The tunnels and the BGP session depend on the internet being up, so this improves the blue sky and degraded cases and does nothing at all for the dark case. It is not a resilience measure, it is a reachability measure.
  • It is real network engineering: an ASN, BGP, WireGuard, policy routing, and a firewall that has to be right the first time.

IPv6 is the simpler alternative for anyone who does not want to run BGP. Most residential connections now carry a routable IPv6 prefix, most CGNAT deployments do not apply to IPv6, and a node with a stable IPv6 address and a properly restrictive firewall solves the same reachability problem with a tenth of the work. The design prefers IPv6 endpoints where both peers have them. The same warning applies: a globally routable address is a globally reachable address.

10.8 Hub hardening

A hub is a public service with a listening socket, an account model, and a store of other people's messages, run by a volunteer, usually on hardware that does something else too. It is the most attractive target in the system and the piece most likely to be neglected. The hardening requirements for it are specified in 13.6, and they are requirements rather than suggestions: the reference implementation ships with them applied, refuses to start with a default configuration, and logs a prominent warning if the operator disables any of them.


section 11

Message profiles

11.1 Radiogram

The native profile. The composer implements the full ARRL form: number, precedence, handling instructions (HXA through HXG), station of origin, check, place of origin, time filed, date, address block, text, and signature.

  • The check is computed automatically by counting groups in the text, with the composer showing the count live and refusing to send a message whose declared check disagrees with the actual count, which eliminates the single most common error in traffic handling.
  • ARL numbered texts are offered as an autocomplete list, with the numbered form substituted into the transmitted text and the plain English rendered for the reader on both ends. The operator writes plain English, the radio sends ARL SIXTY SIX, and the recipient reads plain English.
  • Format on receipt is a printable radiogram form, as a PDF, ready to be delivered by hand or telephone.
  • NTS interoperability: the profile maps cleanly onto standard NTS traffic and the node can produce and consume standard formatted traffic for handoff to voice or CW nets. A HERMES node is a legitimate NTS digital station.

11.2 Short message (the email profile)

Subject plus body, with a soft limit of 600 characters and a hard limit of 2000.

The composer shows two numbers, because they answer different questions.

Airtime is what the encoder costs: frames times slot time, the arithmetic of section 15. It is the cost to everyone else on the channel.

Time to a person is what an emergency coordinator actually asks. It adds the queue ahead of this message at this precedence, the transport the router will choose, and the interval between a message reaching the destination node and an operator there telephoning the addressee. It comes from this node's own measured history with this peer, labelled measured when there are ten or more samples, few samples between three and ten, and no history when the node is falling back to arithmetic. Frequently it is an order of magnitude larger than the airtime, and it is the honest answer.

The counter is the feature that teaches brevity better than any training document, and giving the second number is what stops an operator quoting the first one to somebody who then expects the message to have arrived.

Airtime is also a limit, not only a warning. Nothing previously stopped an operator queueing a 2000 character message for HF, which is roughly fifty minutes of transmission on a segment five kilohertz wide shared nationally, several times the entire hourly budget. Composition now refuses above a configurable ceiling, defaulting to fifteen minutes of airtime and forty five for emergency precedence, with the refusal naming the ARL numbered text or abbreviation that would shorten it. The limit applies only when the message would actually go over RF: length costs nobody anything over the internet, and a node with an IP path to the destination is not restricted.

Addresses may be HERMES station identifiers or, through the !EMAIL service address, ordinary internet email delivered by a gateway node that has connectivity. Inbound email to callsign@hermes.example.org is accepted, stripped to plain text, truncated with an explicit marker, and injected as a message. HTML, attachments, and quoted reply chains are discarded rather than truncated, and the sender receives a bounce explaining why.

11.3 ICS-213

The standard general message form: to, from, subject, date, time, message, approved by, reply. Field-for-field, so that a served agency receives something their people already recognize, and so that a message can be handed to an emergency manager without translation. Renders to a printable ICS-213 PDF on receipt.

11.4 Position and status

A compact profile carrying grid, battery state, queue depth, and a short status line, used for automated check-in nets. This is what makes the system useful on an ordinary Tuesday: a weekly automated check-in where twelve nodes report in and the net control station sees a table rather than taking roll for twenty minutes. Optionally bridged to APRS through the !APRS service address, which is a natural fit given an existing igate.

11.5 The forms library

Three profiles are not enough for a served agency, and this is the gap that most often decides whether an emergency coordinator pilots a system or politely declines. Winlink's real advantage over every competitor is not its modem, it is that an emergency manager can ask for an ICS-213RR and get one.

HERMES therefore carries a form template engine rather than a fixed set of hard coded profiles.

A template is a signed YAML document declaring fields, types, validation, the printable layout, and the wire encoding:

yaml: configuration
form: ICS-213RR
version: 3
title: Resource Request
fields:
  - {id: incident,  label: Incident Name,        type: text,  max: 40, required: true}
  - {id: date_req,  label: Date/Time Prepared,   type: datetime, required: true}
  - {id: qty,       label: Qty, type: int, repeat: 8}
  - {id: kind,      label: Kind/Type, type: text, max: 30, repeat: 8}
  - {id: descr,     label: Detailed Description, type: text, max: 80, repeat: 8}
  - {id: arrival,   label: Requested Date/Time,  type: datetime}
  - {id: deliver,   label: Deliver To,           type: text, max: 40}
  - {id: priority,  label: Priority, type: enum, values: [Urgent, Routine, Low]}
  - {id: approved,  label: Approved By,          type: text, max: 40}
wire:
  order: [incident, date_req, priority, qty, kind, descr, arrival, deliver, approved]
  separator: "|"
render: ics213rr.html

Design consequences worth stating:

  • Only the field values cross the air, never the labels or the layout. A completed ICS-213RR is a pipe-separated field vector of maybe 200 characters, not a 4 kB form. Both ends hold the template, identified by name and version, and a receiving node that does not hold the template asks for it over IP or displays the raw field vector with its identifiers, which is ugly but never lossy.
  • Templates are versioned and signed by whoever publishes the set, and they update through the same package mechanism as everything else. An agency with a local form publishes a template; nobody has to modify the software.
  • Validation happens at composition time, on the originating node, because a form that fails validation after a 12 minute HF transmission is a very expensive mistake.

The shipped set at release: ICS-213 (general message), ICS-213RR (resource request), ICS-205 (communications plan), ICS-214 (activity log), ICS-309 (communications log, generated rather than composed, see 11.7), the ARRL radiogram, and a welfare inquiry profile. That is the minimum credible set. A club or district adds its own without touching code.

11.6 The Winlink bridge

Winlink is the incumbent, it is what served agencies and most ARES groups already know, and a system that cannot exchange traffic with it is a system that operates in a corner by itself. Interoperating is worth more than competing.

The bridge is a service address, !WL, implemented on any node that has both a HERMES identity and Winlink connectivity. Outbound, a HERMES message addressed to !WL/W1AW or !WL/name@example.org is rendered into a Winlink message and submitted through the gateway node's own Winlink client session. Inbound, mail arriving at the gateway operator's Winlink address with a HERMES address on the first line is injected into the network.

The honest constraints, which belong in the document rather than in a support forum later:

  • The bridge is an operator's account, not an anonymous relay. Traffic crossing it is sent under the gateway operator's Winlink identity and is subject to Winlink's own rules and their acceptable use expectations. The gateway node makes that visible in the user interface and requires the operator to opt in explicitly, with their call sign attached to the decision.
  • Attachments and forms do not survive in general. Plain text crosses cleanly. A Winlink form maps to a HERMES form only where a template with the same identifier exists on both sides, and the shipped set is chosen partly for that reason.
  • The bridge runs over the internet or over the gateway's own Winlink RF session. HERMES does not attempt to speak the Winlink RF protocols. That is a deliberate limit: reimplementing someone else's radio protocol badly helps nobody.
  • Delivery verification degrades at the boundary. Once a message leaves for Winlink, HERMES can prove it handed it over and nothing further. The interface says so rather than showing a checkmark it cannot justify.

11.7 The communications log (ICS-309)

Every activation ends with paperwork, and the ICS-309 Communications Log is the piece an agency actually asks for. The node already records every message, every transmission, every receipt, and every timestamp, so producing an ICS-309 costs nothing beyond the rendering.

GET /api/v1/reports/ics309?from=&to=&incident= produces a completed form as CSV and as a print-ready HTML view. Deviation: not server-side PDF. A PDF renderer is a heavy dependency on a Raspberry Pi, and a browser prints the HTML to PDF in one click; a pdf extra can add server-side generation later for anyone who wants it. The content is incident name, operational period, radio network, station identifier, operator, and then the chronological log of time, from, to, and subject for every message handled. The System screen has a button. This is the single highest-value-per-line-of-code feature in the whole design, because it converts an after-action chore that normally takes an evening into a file, and because it is the artifact that makes an emergency coordinator take the rest of it seriously.

The same engine produces an ICS-214 activity log for the operator and an after-action summary.

The summary carries delivery rates by precedence and by transport, and the two elapsed times that matter: from composition to the destination node, and from composition to a person being told. Median, ninetieth percentile, fastest and slowest, per precedence. The gap between those two figures is usually the finding: six minutes to cross a county and forty one until anyone was told is a difference made of operators rather than radios, and no amount of protocol work shortens it. Reporting only the first number, as most systems do, quietly hides the part an emergency coordinator is asking about. Duplicate final deliveries (11.8) are listed too.

11.8 Delivery to people who do not have a node

A radiogram is not delivered when a computer receives it. It is delivered when a human being is told what it says. Revision 1.1 tracked custody all the way to the destination node and then stopped, one hop short of reality.

The message state machine therefore extends past delivered:

State Meaning
delivered The destination node holds the message and issued a receipt
assigned An operator at the destination has taken responsibility for final delivery
served An operator asserted final delivery to the addressee, with method, time, and who received it
undeliverable Final delivery attempted and failed, with a reason

Final delivery is an operator assertion, captured in the interface in about fifteen seconds: method (telephone, in person, email, hand carried, left with a third party), time, the name of the person who received it, and free text. It is signed by the delivering node and propagated back to the originator as a service message, which is exactly how NTS has always closed the loop, and it appears on the originator's message timeline as the final entry.

undeliverable produces a service message too, carrying the reason, because "we tried three times and the phone is dead" is itself information the originator needs, and an emergency network that silently swallows failures is worse than one that fails loudly.

Delivering the same message to the same person twice is caught. If two nodes both hold a message for one addressee, which happens with group traffic and with a relay that is also local, two operators may both make the call. The recipient hears the same thing twice from two different hams, which reads as a shambles. Recording a delivery therefore checks what has already been recorded: the same recipient is refused with the previous operator, time, and method named, and a different recipient warns and proceeds. It is advisory rather than absolute, because two people genuinely may need telling, and a system that blocks a legitimate second delivery is more annoying than one that asks. Duplicates that do occur appear on the after-action report.

Delivery assertions and their timestamps flow into the ICS-309 in 11.7, which means the log an agency receives shows when a message reached a person rather than when it reached a machine.


section 12

The reachability and propagation record

12.1 Why this is a headline capability and not a byproduct

A messaging network is worth nothing until it has peers. A measurement network is worth something on the first day, with one node, and its value grows monotonically from there. That asymmetry is the most important practical fact about this design, and revision 1.1 buried it in a data model section.

Every HERMES node is a continuously operating, unattended, calibrated HF path measurement instrument that happens to also pass traffic. It records what it hears, from whom, on what band, at what hour, at what signal to noise ratio, against what noise floor, with a disciplined clock and a known antenna. It records what it transmitted and whether that transmission succeeded. With a receive-only SDR fitted (6.9) it does this on four bands at once, continuously, forever.

The distinction that matters is between prediction and measurement. VOACAP will tell an emergency coordinator what should work between two grid squares. It will not tell them that the county EOC's antenna has been detuned since the ice storm, that a new solar installation raised the noise floor at the shelter by 9 dB last March, or that the 40 m path to the hospital closes ninety minutes earlier in November than the model suggests. A network of nodes that have been listening to each other for a year knows all three, as fact rather than as forecast.

12.2 What is collected

Five independent streams, four of which cost nothing beyond what the node is doing anyway.

Stream Source Cost What it answers
Decodes Every JS8 decode, HERMES or not, on every monitored band Free Who can I hear, when, how well
Reverse spots SNR reports other stations return to our heartbeats, plus PSKReporter query when online Free Who can hear me, which is a different question
Exchange outcomes Every attempted transfer, its result, retries, and elapsed time Free Does the path carry traffic, not merely a beacon
Noise floor S meter sampled per band per hour with no signal present, plus wideband SDR floor Free Why a path degraded, and whether the cause is local
Station health SWR per band, power out, supply voltage, temperature, uptime Free Whether a measurement can be trusted at all

Asymmetry is measured explicitly and is not an edge case. Hearing a station is not the same as being heard by it, and on a network of 5 W stations with mismatched antennas and mismatched noise floors the two diverge constantly. The database stores both directions separately and the router uses the outbound direction, which is the one that determines whether a message will actually arrive.

Every measurement carries its own provenance: which radio, which antenna profile, which firmware, whether the SDR or the transceiver produced it, and whether the node's clock was disciplined at the time. A measurement whose clock was free running is retained and flagged, never silently mixed with good data.

12.3 What is derived

Derived product Definition Used by
Link quality Composite of decode rate, mean and best SNR, and exchange success, per peer, band, hour, and month The router, continuously
Path availability Percentage of hours in a period in which a peer was reachable at all, by band Planning, and the reachability report
Opening and closing times First and last reliable decode per band per peer, tracked as a distribution rather than a single time, with seasonal drift Net scheduling
Band ranking by hour Which band to try first, right now, for this specific peer The router, and the operator's band button
Noise floor trend Per band, per hour, with change point detection RFI hunting, and explaining a degraded path
Coverage gaps Peers or areas that no node in a group can reach at any hour on any band Deciding where a relay node needs to exist
Prediction error Measured SNR minus VOACAP or ITU predicted SNR for the same path and hour Anyone who cares whether the models are right, which is a real research community

12.4 Watched paths, and treating reachability as a monitored service

This is the feature that converts a database into an operational tool, and it is borrowed wholesale from network monitoring practice.

An operator designates certain paths as watched: the county EOC, the hospital, the district's two backbone relays. For each, they set an expectation:

yaml: configuration
path_watch:
  - peer: N9ABC-0
    label: "Kane County EOC"
    expect_reachable_pct: 90        # over any rolling 7 days
    expect_bands: [40, 80]
    expect_hours_utc: [0000-0400, 1200-1600]
    degraded_snr_db: -18
    alert_after_h: 12

The node then continuously evaluates whether that expectation is being met and raises an alarm when it is not. "We have not been able to reach the EOC on any band for fourteen hours" is a finding that arrives on a Tuesday in April rather than a discovery made during an activation in July. The alarm goes to the dashboard, and out by email or to a group over the network if configured.

Each watched path gets a monthly one-page report: availability by band and hour, mean and worst SNR, the number of messages exchanged, the number of failures, and the trend against the previous three months. That page is a communications plan's evidence base, and it is exactly the artifact that turns "amateur radio can reach the shelter" from an assertion into a claim with numbers behind it.

12.5 Reporting, sharing, and export

Local first. Everything is queryable from the node with no external service: the API in appendix C, CSV and JSON export, and ADIF export so decodes can go into a normal logging program.

Community contribution, opt in. A node with connectivity can report spots to PSKReporter, which costs nothing, benefits the wider propagation research community, and gives the operator a second view of their own reverse path for free. WSPR reception through the SDR chain is the same idea with better science behind it.

Group aggregation, opt in, signed, and coarse. A club or district can nominate an aggregator that collects the link tables (not the messages, not the raw decode text) from consenting nodes and produces the regional picture: a matrix of who can reach whom, by band and hour, for the whole group. This is what makes coverage gap analysis possible, and it is the thing an emergency coordinator will actually want on a wall. Aggregation is off by default, is per-node consent, and carries the same "we see everything you send us" honesty as the hub.

Privacy, in a hobby where call signs are public and addresses are in a federal database anyway. Position is shared at the precision the operator chooses, defaulting to a four character grid square rather than a six character one for aggregation, and a node can participate in measurement while suppressing its own position entirely. Nothing about message content is ever included in a propagation export.

12.6 Storage and retention

Observations are the largest table and the most valuable one, so it is sized deliberately rather than by accident.

Node type Decodes per day Raw storage per year After rollup
Single band, quiet area 2,000 to 5,000 60 to 150 MB 15 MB
Single band, 40 m evening 15,000 to 30,000 400 to 800 MB 40 MB
Four bands with SDR 50,000 to 120,000 1.5 to 3.5 GB 120 MB

Retention defaults to 400 days of full resolution, which covers a complete seasonal cycle plus a margin for year over year comparison, followed by hourly rollups kept indefinitely. A 128 GB card holds decades of rollups and several years of raw data. The rollup is incremental and runs nightly, and the raw table is never deleted without the rollup having succeeded first.

The dataset is the asset. The backup in 8.5 includes it, the export is documented, and the schema is published, because a decade of continuous multi-band regional reachability measurements from fixed stations with known antennas is a genuinely rare thing, and it should outlive both the software and the operator's interest in the software.

12.7 Use cases, written out

The whitepaper has so far described a database. This is what it is for.

Before an event, choosing a communications plan. A district writes an ICS-205 that says the tactical net meets on 40 m at 0100Z and the alternate is 80 m. Today that choice is made from a propagation model, a rule of thumb, and whatever worked once in 2019. With a year of measurement across the actual stations that will be on the net, the choice is made from a table showing that 40 m at 0100Z reached seven of eight stations 94 percent of the time last winter, and that the eighth needs 80 m. The plan stops being a guess.

During an event, routing. The router picks the band and the relay from what is being heard right now rather than from a schedule. This is the use described in 9.6 and it is the least interesting of the seven.

During an event, situational awareness for net control. A net control station looking at the group matrix sees immediately that three stations in the northwest of the county have gone quiet on all bands simultaneously, which is a power or a weather event rather than a propagation event, and which is actionable intelligence about the incident and not merely about the radio network.

After an event, the after-action report. Delivery rates by precedence and transport, which paths carried the load, which failed, and how long messages actually took. Attached to the ICS-309 from 11.7, this is a communications section that says something.

Between events, finding the network's holes. Coverage gap analysis answers the question every ARES group struggles with: where does the next node need to be, and does it need a better antenna or just a better location. A measured answer changes how a limited equipment budget is spent.

At home, hunting your own noise. The noise floor trend with change point detection will tell an operator the exact day their neighbor's new solar inverter came online, and the band and hour signature will usually tell them what class of device it is. This is the use case that will get people to install a node who do not care about emergency communications at all, and that is not a joke: a node that is useful on ordinary days is a node that is still running when the extraordinary day arrives.

For the propagation community, measurement against prediction. Fixed stations, known power, known antennas, disciplined clocks, continuous operation, multi-band, multi-year, with the raw data exportable. Comparing that against VOACAP and against the current solar indices is real work that people are already doing with WSPR and PSKReporter data, and a HERMES network contributes to it as a side effect of existing.

12.8 What this means for the network's value curve

With one node, the operator gets a noise and reachability record of their own station and contributes spots to PSKReporter. Useful on day one.

With three to five nodes in a club, the group gets a real matrix of who can reach whom, which bands work when, and an evidence base for a communications plan. Useful within a month, and useful without a single emergency message ever being sent.

With fifteen to forty nodes across a district, the group gets coverage analysis, watched path alarms on the links that matter, and a messaging capability that is exercised continuously rather than assumed.

The messaging capability is what the system is for. The measurement capability is why anyone would run it long enough for the messaging to matter. Both belong in the first paragraph of any description of this project, and the ordering above is the ordering in which the value actually arrives.


section 13

Security architecture and hardening

13.1 Principles

  1. Assume the node is on a public address. Whether by 44Net, IPv6, a port forward, or a hotel network, some nodes will be directly reachable from the internet. The baseline is written for that case rather than for a comfortable one behind NAT.
  2. Nothing on the RF side is confidential, and the interface says so. That is a legal constraint, and pretending otherwise would be worse than stating it.
  3. Fail closed. A node that cannot verify, cannot authenticate, or cannot determine its legal posture does not transmit and does not accept.
  4. No default credentials, ever. Not a printed one, not a well-known one, not a blank one. Setup will not complete without a password the operator chose.
  5. Least privilege between components, so that the blast radius of any one compromise stops at that component.
  6. Nothing received is ever executed. Not from RF, not from a peer, not from a hub.

13.2 Attack surface inventory

Every listening socket in the system, its default binding, and what protects it.

Service Port Default bind Exposure Authentication
Web UI and REST 443/tcp LAN and WireGuard interfaces only Public exposure is opt-in, with a warning and a confirmation Password (argon2id) plus optional TOTP, session cookie, CSRF token
Peer transport 8443/tcp All interfaces, but only when enabled Public by design TLS 1.3 plus Ed25519 signed challenge, both directions
AP mode UI 443/tcp on 192.168.44.1 Only while the AP is active Local RF range Same as web UI, plus a WPA2 passphrase generated at setup
JS8Call API 2442/tcp 127.0.0.1 only None None needed; loopback only, enforced in the JS8Call config and by nftables
Scan decoders 2443+/tcp 127.0.0.1 only None As above
SMTP submission 2525/tcp 127.0.0.1 only None Off by default, enabled explicitly
SSH 22/tcp LAN and WireGuard only Operator choice Public key only, passwords disabled, root login disabled
hermes-hub 443/tcp All interfaces Public by design Signed challenge, optional invite or attestation

Anything not in this table does not listen. hermes-core, hermes-rfd, hermes-radiod, and hermes-scand communicate only over a Unix domain socket with filesystem permissions, and have no TCP listener at all.

13.3 Node hardening baseline

Shipped applied, not documented as an exercise for the operator.

Network.

  • nftables with a default-deny input policy, an explicit allowlist per the table above, connection rate limiting per source address, a per-source concurrent connection cap, and SYN flood protection. ICMP and ICMPv6 are permitted, because breaking path MTU discovery to look tough is a self-inflicted wound.
  • No UPnP, no automatic port forwarding, no hole punching. If an operator wants inbound reachability they configure it deliberately.
  • Remote administration is expected to happen over WireGuard rather than by exposing the web interface, and the setup wizard offers to generate a WireGuard profile and a QR code for a phone.
  • Outbound is restricted for the services that do not need it: IPAddressAllow and IPAddressDeny on the systemd units confine hermes-netd to the configured hubs and peers.

Operating system.

  • SSH: public key only, PermitRootLogin no, PasswordAuthentication no, AllowUsers limited, sshguard or fail2ban fitted. The port is left at 22, because moving it is not a security control and it breaks things.
  • Automatic security updates per 8.8, with the visibility indicator that makes neglect obvious.
  • No compiler, no development toolchain, and no unnecessary daemons in the appliance profile.
  • AppArmor profiles ship for every service.
  • Read-only root and overlay where the layout supports it.

Service isolation. Each service runs as its own unprivileged user with no shell and no sudo, under systemd sandboxing:

systemd unit
[Service]
User=hermes-web
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectProc=invisible
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
LockPersonality=true
MemoryDenyWriteExecute=true
SystemCallFilter=@system-service
SystemCallArchitectures=native
CapabilityBoundingSet=
UMask=0077
ReadWritePaths=/var/lib/hermes/web

hermes-radiod is the only unit with DeviceAllow for the serial port, and it is scoped to that one device node. PrivateDevices is relaxed only there.

Web application.

  • TLS 1.3 only. HSTS. A content security policy with script-src 'self', no inline script and no inline event handlers, which the progressive web app is built to satisfy rather than worked around. X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, a restrictive Permissions-Policy, and cross-origin isolation headers.
  • Session cookies HttpOnly, Secure, SameSite=Strict. CSRF tokens on every mutating request. No CORS, at all.
  • Password hashing with scrypt at memory-hard parameters, account lockout with backoff, and optional TOTP for the administrator role. Deviation: argon2id is the better algorithm and is what earlier revisions specified, but it is a compiled dependency on an appliance that must install from a plain apt repository onto a Pi. scrypt is in the Python standard library, is memory hard, and at these parameters is far beyond what anyone will spend attacking a hobby node's local interface. Recorded rather than substituted silently.
  • Sessions are stored as a hash of the token, never the token, so a stolen database does not hand over live sessions. A wrong user name costs the same time as a wrong password, so the difference is not detectable by timing.
  • Every mutating route carries a CSRF token, including sign out. A forced sign out is only a nuisance, and "only a nuisance" is how the habit of exempting one route begins.
  • Request body size caps, JSON depth and element caps, strict schema validation with unknown fields rejected rather than ignored, and per-user and per-endpoint rate limits.
  • Message bodies are rendered as plain text. Never as HTML, never as Markdown with raw HTML enabled, never into an innerHTML. A message body arriving over the air is hostile input by default.
  • Uploaded and received data never becomes a filename, a path component, a shell argument, or an SQL fragment. Parameterized queries throughout.

Secrets. The identity private key is mode 0600, owned by hermes-core, outside the web service's ReadWritePaths, and sealed to a TPM where one exists. WireGuard keys, hub credentials, and the TOTP secrets live in separate files with separate ownership. Nothing sensitive is in the conffile that gets backed up in plain form; the backup archive is signed and optionally passphrase-protected, and the UI warns that a backup contains the node's identity.

13.4 Abuse resistance on the network

The realistic adversary is a bored person with a transmitter or a script, not a state actor. The controls are proportionate.

Vector Control
Forged messages injected over RF RF frames carry no signature, so they are marked unverified, are never auto-relayed above routine precedence, and are shown as unverified in the interface until a signed object confirms them
Replay of captured traffic Content-addressed identifiers, monotonic per-origin sequence, a clock-skew-bounded acceptance window, and a duplicate store that remembers identifiers for the message lifetime
Relay amplification, one message becoming a storm Hop limit, path list, deduplication, per-origin fan-out cap, and the rule that a node relays only toward a destination it has a scored path to
Storage exhaustion at a relay Custody quotas by count and by bytes, precedence-aware admission, and explicit ~X FULL rejection so the offering node stops asking
Channel exhaustion, deliberate or accidental Transmit budget, listen before transmit, exponential backoff, and per-peer exchange limits
A station that misbehaves Automatic demotion in the router after repeated failures, an operator blocklist, and signed blocklist sharing through the directory that is advisory only, because automatic distributed banning is a governance problem wearing a technical costume
Hostile hub It can deny service and read traffic that is not confidential anyway; it cannot sign as a station, cannot forge receipts, and cannot alter a message without breaking its identifier
Peer connection flooding Connection caps, per-source rate limits, a proof-of-identity challenge before any state is allocated, and no unauthenticated allocation of memory or disk
Stolen node Key rotation certificates published to hubs and gossiped over RF, configuration changes gated behind the administrator password, and an operator playbook for revocation

13.5 Duplicate station identifiers

Two nodes configured as K9MEV-1 is a silent failure with ordinary causes: a restored backup running alongside the original, a spare built from a copied card, or two people in a club who both picked instance 1. Traffic partly works, because messages deduplicate by identifier; the hub refuses the second registration with an error about a different key; and neither operator knows why.

A node that sees a second key claiming an identifier it holds records the conflict with its source, raises it on the dashboard as a non-dismissible banner, and sends a priority message to the disputed identifier. That last part is the point: a message addressed to the identifier reaches whichever node is currently answering to it, which is exactly the node that needs telling, and it is the only channel guaranteed to do so. The message names both fingerprints and says plainly that one of the two must change its instance number.

A key change accompanied by a valid rotation certificate (5.3) is not a conflict and is not reported as one.

13.6 Hub hardening requirements

The reference hub ships with all of the following applied and refuses to start if the configuration still contains placeholder values.

  • Runs as a container or a dedicated unprivileged user, read-only filesystem, no shell in the image, all capabilities dropped, seccomp profile applied, its own data volume.
  • If it runs on the same Pi as a node, it runs as a different user, in its own namespace, with no read access to the node's identity key or message store. Colocation is convenient and it is not a trust relationship.
  • Behind a reverse proxy (Caddy or nginx) terminating TLS 1.3, with request size limits, header limits, connection caps, and aggressive timeouts.
  • Registration requires a signed challenge proving control of the station key, and optionally an invite code or a license attestation. Anonymous unlimited registration is not offered, because it is how a mailbox service becomes a spam relay.
  • Per-station quotas: mailbox size, message count, message size, deposit rate, and connection count, all enforced server side and all returning a clear error rather than silently dropping.
  • Retention is short and stated: mailbox contents expire on a fixed schedule, and connection metadata logging is minimized and time-limited. The hub publishes what it keeps and for how long, in the same spirit as a privacy policy for a website.
  • No message content in logs. Ever. Identifiers and sizes only.
  • Monitoring and alerting on error rate, queue depth, disk, and authentication failures, plus fail2ban or equivalent on the proxy.
  • Backups encrypted, restore tested, and the signing keys held offline.
  • A published security contact, an RFC 9116 security.txt, and a stated disclosure practice. A volunteer-run service with no way to report a problem is a service that finds out from an incident.
  • Denial of service is accepted as survivable rather than solved. A hub on a home connection or a small virtual machine cannot absorb a volumetric attack, and the design's answer is that a hub going away is not an outage: nodes fall back to direct peering, to another hub, and to RF. Anyone wanting more can put the hub behind a provider that offers volumetric protection, and nothing in the design prevents that.

13.7 What is deliberately not done

  • No encryption of message content, anywhere, including over IP. TLS protects the transport, and the content is signed and readable. This keeps one message format across both transports and removes any argument about 97.113(a)(4). It is a real limitation and operators must be trained to it.
  • No remote shell, remote command, or remote configuration from a hub to a node. A hub can hold mail and introduce peers. It cannot change a node's behavior. This removes the single most attractive reason to attack a hub.
  • No automatic trust upgrades. A key change is always surfaced to a human, never quietly accepted.
  • No telemetry to the project. Nothing phones home. The only outbound connections a node makes are to the hubs and peers the operator configured, and to the update repository.

section 14

Concept of operations

14.1 The three states of the network

Blue sky. Internet is up everywhere. All traffic goes over IP. RF is used only for the heartbeat and the capability beacon, which continue anyway because their purpose is to keep the propagation database current and to prove the RF path works. This is the state 99 percent of the time, and the discipline it enforces is that the RF path is exercised continuously rather than discovered to be broken during an activation.

Degraded. Some nodes have internet, some do not. This is the realistic disaster state, and it is where the design earns its keep. A node in the affected area sends over RF to a node outside the affected area, which forwards over IP to the destination, which may be anywhere. One RF hop of 200 km buys access to the entire internet. The hub is reachable by the unaffected nodes, so the directory and the mailbox still work for most of the network.

Dark. No internet anywhere in the region. Pure RF, store and forward, relaying, scheduled windows. Throughput falls to a few messages per hour per node. Precedence discipline becomes the governing factor, and the transmit budget and priority queueing are what stop the network from collapsing into mutual interference.

14.2 Deployment patterns

Pattern Nodes Notes
Personal 1 to 2 Home plus go-bag. Useful only as part of a wider population
Club 6 to 20 The natural unit. A club hub, a shared group, a monthly exercise
ARES or RACES district 20 to 60 Served agency sites, EOC, hospitals, shelters. Requires a training program and a designated network coordinator
Regional backbone 3 to 8 High-sited, well-antenna'd, always-attended-capable nodes that exist specifically to relay. These deserve better antennas and 100 W amplifiers, discussed in 19

14.3 The value of a node on an ordinary day

Every emergency communications system faces the same problem: it must be maintained for years to be useful for hours, and volunteers do not maintain things that do nothing between events. HERMES answers this deliberately rather than by exhortation.

On an ordinary Tuesday a node is: a propagation and noise measurement instrument for its own station (12.7), a contributor to PSKReporter, a participant in an automated weekly check-in net (11.4), a monitored watch on the paths its operator cares about (12.4), and an ordinary JS8Call presence that other operators can call and leave messages with (9.9). None of that requires an emergency, and all of it produces something the operator can look at.

A system that is only useful during a disaster will not be working when the disaster arrives. This is the single most important operational claim in the document, and the measurement capability of section 12 is the mechanism by which it is made true.

14.4 The exercise requirement

An emergency communications system that is not exercised is a decoration. The design supports this directly: a scheduled exercise mode generates synthetic traffic between all participating nodes at a configured rate, measures end to end delivery time and success rate by path and by transport, and produces an after-action report showing which links worked, which did not, and how the network would have performed with the internet removed. A monthly exercise that takes the operator ten minutes is the difference between a working network and a shelf full of boxes.

How it works. An exercise sends real messages over real transports through the real router. It is not a simulation; the only differences are that the traffic is marked, it goes to a participant list on a schedule, and there is a report at the end.

Every body opens with EXERCISE EXERCISE EXERCISE. The marker is not decoration: people have acted on drill traffic, and anyone who sees a message during a real event needs to know within three words that it is not real. Exercise traffic may not use emergency precedence, and the software refuses it, because a drill that takes the whole transmit budget and jumps every real queue is the one thing a drill must never do.

The report is per participant, because an overall rate is not a finding. An exercise reporting 80 percent delivery tells an operator nothing they can act on; one that names which station was the missing 20 percent tells them where the next relay goes. Each participant gets a verdict (reliable, intermittent, poor, never reached), a median time, and the transports that carried the traffic, and the report states in words what the numbers mean:

status

never reached: KB9FAR. Either a relay is needed between here and there, or their station is not receiving. Check whether they heard us at all.

every message reached a node and none was recorded as delivered to a person. That is the half of the drill that usually gets skipped, and it is the half an agency asks about.

Once every few drills, unplug the network cable first. That is the state the whole system exists for, it is the state nobody tests, and the first time should not be the day it happens for real.


section 15

Capacity and performance

15.1 Measured throughput

Revision 1.2 estimated these figures. They are now measured, by running the reference implementation's encoder, fragmentation, and ARQ across a simulated channel with injected frame loss. The simulator charges time per JS8 frame slot, which is how JS8 actually bills: a lost frame costs the same fifteen seconds as a good one.

A 25 word radiogram, JS8 Normal, compound call sign, end to end including the handshake and the receipt:

Frame loss Delivered Median minutes Median JS8 frames
0 percent 100 percent 7.0 28
5 percent 81 percent 7.0 28
10 percent 86 percent 8.5 34
20 percent 81 percent 9.5 38
30 percent 52 percent 13.0 52

At JS8 Slow with 10 percent loss, 19 minutes.

Revision 1.2's estimate of 6 to 11 minutes was about right, which is reassuring, but it was right for the wrong reason: the frame accounting was understated by roughly half and the fragment overhead by roughly the same, and the two errors cancelled. Section 1.1 describes the correction.

Two caveats on these numbers. The success rate is non-monotonic at low loss because each point is only 21 runs; a publishable table needs a few hundred. And a uniform random loss model is optimistic: real HF loss is correlated, arriving in bursts as fading takes out several consecutive frames, which an ARQ window handles worse than the same average loss spread evenly. Expect real-world figures to be worse than the 20 percent row, and treat the 30 percent row as the realistic edge of usefulness rather than the worst case.

15.2 Where the time goes

For the zero loss case, the 28 frames break down as:

Stage JS8 frames Why
Offer 2 Session, count, precedence, destination, digest, plus the compound call sign frame
Accept 2
Data, 5 fragments 18 40 characters plus a 15 character header is 3 frames each
Acknowledgment 4 One per window, plus the call sign frame
Receipt 2

The fragment header is 27 percent of the payload cost. A larger fragment amortizes it better and retransmits more on failure; 40 characters is where the two curves cross at the error rates HF produces. This is the single most obvious place to optimize, and the reason not to is that the crossover moves with the error rate, so a fixed larger fragment is better on a good channel and worse on the channel this system exists for.

15.3 Network capacity

A single JS8 channel occupies 50 Hz, and JS8Call decodes the entire 2.5 kHz passband simultaneously, so roughly 20 to 30 signals can coexist at different offsets before mutual interference becomes limiting, with practical experience suggesting 8 to 12 concurrent conversations is the realistic ceiling before collisions dominate.

At 9 minutes per radiogram and 10 concurrent sessions, one band supports approximately 60 to 65 radiograms per hour region-wide in ideal conditions, and realistically 25 to 40. Across four bands, and accounting for the fact that not all bands are open at once, a regional network's realistic dark-state capacity is 50 to 80 messages per hour.

That is the number that should govern operational planning. It is enough for command and coordination traffic for a county. It is not enough for individual welfare traffic at scale, and the priority system exists precisely so that the emergency and priority traffic gets through while welfare traffic queues, which is exactly the discipline NTS has used for a century.

15.5 Link budget sanity check

5 W into an end fed half wave at 10 m height on 40 m at 300 km, at night, with a typical noise floor of -100 dBm in a suburban location, yields a predicted SNR at the receiver in the range of -5 to +10 dB by NVIS. JS8 Normal decodes to -24 dB and Slow to -28 dB, so the margin is 15 to 35 dB. QRP is not the limiting factor on regional paths. Antenna height, noise floor, and time of day dominate, in that order, and the system's own database will demonstrate this to the operator within a week.

The 1000 km and beyond paths are where 5 W becomes marginal, and that is where relaying and the Slow speed exist.


section 16

Development plan

Phase Deliverable Effort Depends on
0 Bench prototype: Pi, QMX, JS8Call headless, API round trip proven 1 week Hardware in hand
1 hermes-core message object, store, signatures, canonicalization, CLI 2 weeks
2 hermes-rfd: framing, fragmentation, ARQ, two-node file transfer over the air 3 weeks 1
3 hermes-radiod: CAT, SWR qualification, band scheduling, posture enforcement 2 weeks 0
4 hermes-netd and reference hub: rendezvous, mailbox, directory 2 weeks 1
5 Router, custody, receipts, receipt upgrade 2 weeks 2, 4
6 hermes-web: wizard, compose, traffic, stations, radio, system 4 weeks all
5b Measurement: observation pipeline, rollups, link scoring, watched paths, reports and export 3 weeks 2
5c hermes-vhfd: Direwolf, AX.25 UI framing, fragment transfer over 2 m 2 weeks 2
6b Packaging: APT repository, signed builds, hermes-setup, update pipeline with rollback 2 weeks 1
6d Forms engine, ICS set, ICS-309 generation, final delivery workflow 3 weeks 1, 6
6e Net control board and fleet policy bundles 2 weeks 5b, 6
6c hermes-scand: SDR channelizer, scan decoders, T/R protection validation 2 weeks 2
7 Enclosure: CAD, print, fit, EMC measurement, revision 2 weeks 0
8 Field trial: 4 nodes, 30 days, exercise mode, after-action 4 weeks 6, 7
9 Documentation, image build, A/B update, release 2 weeks 8

Approximately 38 weeks of part-time effort to a releasable system, with a usable two-node demonstration at the end of phase 2, which is roughly six weeks in. Note that phase 5b, the measurement pipeline, delivers standalone value with a single node and could reasonably be pulled forward to run in parallel with phase 2, since it is the part that is useful before the network exists. Phase 2 is the risk-retiring milestone: if reliable fragment transfer over a real HF path at 5 W does not work, everything downstream needs rethinking, and it is worth reaching that point before building anything pretty.


section 17

Test and acceptance

Unit and integration: message canonicalization and signature round trip against fixed vectors, fragmentation and reassembly with injected loss, router scoring against a fixture database, posture engine against a table of band, frequency, and attendance combinations that must be accepted or rejected.

Loopback RF: two nodes connected by attenuated coax and a dummy load, running full sessions at controlled SNR from +20 dB down to -30 dB, producing a measured throughput and success curve per speed. This is the test that produces the real numbers to replace the estimates in section 15.

EMC acceptance: noise floor per band with the Pi off, idle, and loaded, per 6.8. Fails if the delta exceeds 3 dB on any band.

Power acceptance: 72 hour run on battery with logging, comparing measured consumption against the budget in 6.10.

Regulatory acceptance: a scripted audit that replays 30 days of the transmission log and asserts that every transmission was within an authorized posture, was inside a permitted segment or was a response to an interrogation, and that identification occurred within every 10 minute window. This produces a report the operator can keep.

Field acceptance: 30 days, four nodes, at least one of which is deliberately internet-isolated for the whole period, with weekly exercise traffic, measuring end to end delivery rate by precedence and by state.


section 18

Risks

Risk Likelihood Impact Mitigation
Pi-generated RFI desensitizes the receiver High High Shielding, ferrites, measured acceptance test; fall back to an aluminum enclosure for the Pi compartment if the test fails
Clock drift when GPS is lost and internet is gone Medium Critical DS3231 holdover, JS8Call drift estimation, loud UI alarm; a node that cannot trust its clock says so
40 m ACDS segment congestion High Medium Prefer 30 m, transmit budget, listen before transmit
Too few nodes for the network to be useful High High Interoperability with plain JS8Call from day one is the primary mitigation; a HERMES node is useful in a JS8Call-only population
JS8Call API instability or breaking change Medium Medium Support both mainline and Improved forks, version-detect, pin a known good version in the image
Antenna failure goes undetected Medium High SWR qualification sweep, SR polling, antenna fault alarm
Regulatory interpretation of the beacon posture is wrong Low High Every parameter is configurable; seek a second opinion before deployment, see section 20
Operators do not exercise the system High High Scheduled exercise mode with automatic after-action reports; the everyday measurement value of 14.3 is the primary defense, since a node that produces something on an ordinary Tuesday stays powered on
The project stalls or the maintainer stops High High Governance in 21.1: specification and implementation separated, conformance vectors published, two-person key custody, named succession, mirrors
Served agencies decline because the forms and reporting are not what they use Medium High Forms engine and shipped ICS set (11.5), ICS-309 generation (11.7), Winlink bridge (11.6)
The Winlink community objects to a bridge under an individual's account Medium Medium Bridge is explicit opt in by the gateway operator, under their own identity, subject to Winlink's rules, and is not an anonymous relay
A fleet policy coordinator is compromised and misconfigures a district Low Medium Policy schema carries no code, no keys, and no transmit commands; local override wins; every application is logged with a diff and one button reverts
Key management confuses volunteers Medium Medium Trust on first use by default, QR fingerprints, no operator ever needs to see a key
5 W is insufficient on the paths that matter Low High Measured in phase 8; the QRP Labs 50 W amplifier is a documented upgrade path for backbone nodes
JS8Call compound call sign behavior differs from the documented cost, breaking per-instance RF identity Medium High First on-air test in phase 0, across both JS8Call variants, before anything is built on the assumption
A node on a public 44Net or IPv6 address is compromised through the web interface Medium Critical Section 12 baseline, WireGuard-preferred administration, no default credentials, automatic security updates with visible staleness indicator
The APT repository or its signing key is compromised Low Critical Offline signing key for update bundles, published key fingerprint, reproducible builds as a goal, no unauthenticated fallback path
Automatic updates break a node during an activation Medium High Policy interlocks (emergency declaration, priority queue, battery floor, maintenance window), health check with automatic rollback, try-boot A/B slots
SDR scan decoders exhaust the Pi and cause missed decodes on the main radio Medium Medium Measured in phase 8; decoder count is configurable and the main JS8Call instance is given CPU priority
Third party traffic moves under an emergency override that is later judged unjustified Low Medium Declaration requires a typed justification, expires in 12 hours, and is stamped on every message and every log line for the after-action record

section 19

Upgrade paths

  • 50 W power amplifier for backbone relay nodes, with the associated increase in power budget (approximately 8 A on transmit) and a change to the duty cycle policy.
  • VHF or UHF packet or LoRa side transport for the last mile within a served agency campus, implemented as a third transport module behind the same three-verb interface.
  • Winlink gateway as a service address, letting HERMES traffic reach the Winlink system where a node has both a HERMES path and an internet path.
  • Multi-band simultaneous receive using a second cheap SDR dongle for monitoring bands the QMX is not currently on, which would eliminate the band scheduling compromise entirely and is probably the highest-value future addition.
  • Mesh of local nodes over Wi-Fi or AREDN at a single site, sharing one RF-capable node, using the instance suffix mechanism exactly as designed in section 5.

section 20

Decisions taken and items still open

Revision 1.0 of this document ended with eight open questions. All eight have been answered by the operator, and the answers are recorded here with what each one changed, because a design document that hides its decisions behind a finished surface is much harder to review.

# Question Decision What it changed
1 Per-instance RF identity, or instance 0 only on the air Hard requirement. Every instance is its own on-air station Section 5.2 rewritten. Heartbeat interval doubled to 30 minutes, capability beacon to 120 minutes, RF transmit lease replaced by a co-site scheduler
2 Automatic control interpretation Second opinion sought from an ARRL Volunteer Counsel; the middle reading stands in the meantime and stays configurable Section 3.1 unchanged pending the opinion, flagged in the risk table
3 Fan dipole or linked end fed half wave as the default Fan dipole, with the end fed half wave documented as a portable option Section 6.6 rewritten, parts list changed
4 QMX band build 80/60/40/30/20 m confirmed Assumption promoted to a decision throughout
5 Hub ownership Left to the deployer. No project hub, none configured by default. Recommended pattern is a self-hosted hub on the node over 44Net, hardened Sections 10.1, 10.6, 10.7, and the whole of section 13
6 Third party traffic and attended operation Emergency override, narrow, logged, and expiring Section 3.4 extended
7 Group namespace Accept collisions, claim no prefix, with the ~ sigil doing the disambiguation and a rule against sending protocol frames into groups with no HERMES members New section 9.12
8 QMX+ and QDX support Included, through a radio driver abstraction New section 6.4, with the reduced capability tier for the QDX stated plainly

Four requirements were added in this revision: the receive-only SDR (6.9), installation on a stock image from a signed repository (8.7), a patching and update strategy that works on a read-only filesystem and offline (8.8), and a full hardening specification for everything that faces the internet (12).

Added in revision 1.2, from an outside critique

A review asked six pointed questions about what a served agency would actually need and what would make anyone run this for two years before it mattered. All six are now in the document, and one architectural question was answered.

Gap identified Resolution
Three message profiles is not enough for a served agency, and there is no path to Winlink Signed form template engine with the ICS set shipped (11.5), and a !WL bridge with its limits stated honestly (11.6)
No ICS-309 communications log, which is the artifact agencies actually ask for Generated from data already held, one button, plus ICS-214 and an after-action summary (11.7)
The design is node-centric and emergency communications is organizational Net control role with a traffic board and a station board, visibility only and no command authority (8.9)
HF-only is the wrong shape when most traffic travels five miles VHF and UHF as a third transport over AX.25, preferred over HF, with no automatic control constraints on 6 m and shorter (6.11)
Delivery tracking stopped one hop short of a human being Operator-asserted final delivery with method and time, service messages back to the originator, feeding the ICS-309 (11.8)
No way to provision or keep forty nodes aligned, and the obvious fix violates the no-remote-command rule Pull-based signed policy bundles, narrowly scoped, local override wins, no code and no keys in the schema (8.10)
The measurement record was buried as a byproduct when it is the day-one value Promoted to section 12 in full, with collection, derivation, watched paths, aggregation, export, and seven written-out use cases, and reframed in the executive summary

Items still open

  1. The Volunteer Counsel opinion on 97.221. Until it arrives, the beacon posture in 3.1 is the author's reading. Every parameter involved is configurable, so an adverse opinion is a settings change rather than a redesign, but it must be settled before any node beacons unattended.
  2. JS8Call compound call sign behavior, verified on the air, for heartbeat participation, automatic SNR replies, and directed message addressing, across both the mainline release and the Improved fork. This is now load bearing.
  3. Whether @HERM is already in use. Check on the air and against community lists before release, and pick something else without argument if it is taken.
  4. QMX+ enclosure dimensions, to be taken from the mechanical drawing rather than estimated, before the parametric source claims to support it.
  5. A project domain and repository hosting, since the install instructions in 8.7 currently point at a placeholder. This includes deciding who holds the signing keys and where the offline bundle key lives.
  6. SDR decoder count on a Pi 5, measured rather than assumed, along with the effect on decode reliability for the primary radio.
  7. Whether the QDX degraded tier is acceptable to actual users, or whether the loss of SWR qualification makes it a support burden that outweighs the cost saving.

section 21

Governance, review, and license

21.1 Governance and continuity

Emergency communications infrastructure that depends on one enthusiastic person is a liability dressed as a project. Most amateur radio software dies when its author's attention moves, and the ones that survive did something deliberate to make survival possible. This is the proposal for doing that deliberately, from the start, rather than after it becomes urgent.

Separate the specification from the implementation. The HERMES Message Protocol is versioned independently of the software that speaks it, published as its own document, and carries a stability policy: within a major version, a node may add fields and may not change the meaning of existing ones, and any node must ignore fields it does not understand. The reference implementation becomes one implementation rather than the definition. This single decision is what makes everything below possible.

Publish conformance vectors and a test suite. This now exists rather than being proposed. Twenty-three vectors across seven groups (base 32, canonical form, station identifiers, messages and signatures, frames, envelopes and fragmentation, ARQ bitmaps), each carrying the input, the expected output, a sentence saying why it matters, and the section it comes from. python -m hermes.conformance checks an implementation against them; regeneration is deterministic and a test asserts the published file matches the generator, so a diff means a real change rather than noise.

Two independent implementations is the real insurance against project death, and nobody writes a second implementation against a codebase; they write it against a specification with tests. The vectors were chosen partly from cases the first implementation got wrong, which is the most honest source available: base 32 left-aligns a digest and keeps the low bits of an integer, and conflating those destroyed the envelope timestamp and every ARQ bitmap until a test caught it.

One consequence, stated plainly: the protocol is frozen at version 1 in practice. The rule below requires two implementations to agree before a change enters a released major version, and there is one. That is uncomfortable and it is deliberate: a protocol that changes whenever its author thinks of something better cannot be implemented by anybody else, and a project with a single implementation has no insurance at all.

Key custody, held by two people. An offline root signing key, split between two custodians in different physical locations, on hardware tokens, used only to sign an annually rotated release key. The release key signs packages and update bundles. Both fingerprints are published in this document, on the project site, and inside the packages themselves, so that a compromised distribution server cannot quietly become the source of truth. Neither custodian can produce a release alone.

A named co-maintainer from day one, with commit access, release rights, and a copy of the documentation for every process. Not a figurehead: someone who has actually cut a release.

The onboarding is written down and deliberately ends with commit access rather than beginning with it: read the whitepaper and the build record, build a node from the install guide without help (anything they get stuck on is a documentation bug and finding those is worth the exercise alone), cut a release to a test repository alone, take a bundle key custody share, and only then take commit rights. A maintainer who has never cut a release is a name on a page.

This remains the largest single risk to the project, and at the time of writing it is unaddressed. The procedure exists; the person does not. Everything else in this document is engineering that one person can do, and this is the one thing that cannot be.

A written succession clause, published with the license: if no release and no public activity occurs for twelve months, the named successors, or failing that any three participants acting together, may fork, publish under the same name, and assume the signing keys through the documented custody process. Say it in advance and the fork is a handover; say nothing and the fork is a schism.

An institutional home, chosen from three realistic options:

Option Strength Weakness
A club as steward, for example a local ARRL-affiliated club holding the repository, the domain, and one custody share Continuity beyond any individual, a natural test deployment, existing legal personality Clubs have their own politics and their own attention spans
An ARDC grant, given that ARDC funds amateur digital infrastructure and this project sits directly in their interest area, including the 44Net pattern in 10.6 Funding for hardware and for someone's time, plus institutional accountability and reporting discipline Grant obligations, and a dependency on a funder's priorities
A plain organization account with three owners and mirrors on two independent hosts Simplest, no politics, no obligations No legal personality, no funding, and continuity rests on three individuals remaining reachable

The recommendation is the club as steward plus a grant application once a working two-node demonstration exists, with mirrors regardless. The demonstration matters: a grant application for a design document is a harder sell than one for a system that has passed traffic.

Decision making is a benevolent maintainer with a public tracker, plus one rule with teeth: a protocol change requires two implementations to agree before it enters a released major version. That rule slows the protocol down, which is the point.

Security disclosure follows the practice already used on the author's other properties: an RFC 9116 security.txt on the project site, a published contact, acknowledgment within a week, and coordinated disclosure at ninety days.

The measurement archive outlives the software. The propagation and reachability dataset of section 12 is the most durable thing this project will produce, and it should be deposited somewhere that survives the project: a mirror held by the steward, an annual signed export published openly, and a documented schema so that the data is readable by anyone in twenty years without the software that produced it.

Naming. The name is used lightly and given away freely. Anyone may implement the protocol and call the result HERMES, provided it passes the conformance suite. That constraint is the only one worth enforcing, and it is enforced socially rather than legally.

21.2 Asking the people it is meant to serve

The forms and reporting in section 11 exist because a structured review concluded that a served agency would otherwise decline the system. That conclusion was an informed guess by an engineer reading FEMA documentation, which is exactly the wrong way to choose a form set.

No emergency coordinator, ARES officer, or active traffic handler has reviewed this design. That is the second largest risk after the maintainer problem, and it is not fixable by more engineering: a system built entirely around what a thoughtful engineer believes emergency communications needs is a much better starting position than most projects have and is not the same thing as what the people doing the work will accept.

A short review packet exists for exactly this, ending in one question: what would make your group decline this? It names the answer that is expected and currently has no good response, which is that a group with eight years of Winlink training is being asked to learn something with one user. Publishing the question with the expected objection attached is more likely to get an honest answer than asking for feedback in general.

21.3 Review

This document is published for review before anything is built, which is the cheapest moment to be told that something is wrong.

The specific things worth attacking:

  • The regulatory reading in section 3, particularly 3.1. This is the part where being wrong has consequences beyond a wasted weekend.
  • The throughput and capacity arithmetic in section 15. Anyone with real JS8Call traffic-handling experience will have better numbers than these estimates.
  • The protocol in section 9, especially the fragment sizing, the ARQ window, and whether the frame grammar survives contact with real JS8Call frame packing.
  • The claim in 9.9 that interoperability with plain JS8Call is achievable as described, from anyone who has written against the JS8Call API in anger.
  • The security baseline in section 13, from anyone who has run a public service and been surprised.
  • Anything in the hardware sections that will not physically fit or will not work at 5 W.
  • The forms and reporting in 11.5 through 11.8, from any emergency coordinator or traffic handler, since those sections exist because an outside review said the design would otherwise be declined by the people it is meant to serve.
  • The measurement design in section 12, from anyone doing serious propagation work, particularly on what should be collected that is not, and on the aggregation and privacy posture.
  • The governance proposal in 21.1, from anyone who has watched a project like this succeed or fail.

How to send comments. Reply to the operator directly, by email or on the air. Corrections with a citation are worth more than opinions, and disagreement is worth more than approval.

21.4 License

License. This document, its diagrams, its schemas, and its example configurations are released under the MIT License. Use it, republish it, adapt it for your club, translate it, or build a competing system from it. Attribution is appreciated and not required.

Disclaimer. Nothing here is legal advice, and nothing here is affiliated with or endorsed by the ARRL, ARDC, QRP Labs, or the JS8Call project. The regulatory statements are one amateur's reading of the rules, and they should be confirmed independently before any station transmits under automatic control.


section 22

Implementation status

The design in this document has been implemented in full. This section says what that means and, more usefully, what it does not.

22.1 What is proven, what is untested

Layer State What backs the claim
Message object, signatures, canonical form proven Round trip, tamper detection, and cross-parser stability all tested
Store, custody, receipts, router proven Custody timeout, loop refusal, hop limit, and receipt upgrade all tested
Measurement: ingest, rollup, link quality, watched paths proven Runs against a live JS8Call instance today; rollup idempotence tested against a full rebuild
Internet transport and hub proven Verified over TCP between two node processes and a hub process, including the hub-gone case
Web interface, authentication, forms, ICS-309 proven Strict content security policy asserted per page; every mutating route tested for session and token
Packaging, setup, update pipeline proven Interlocks, rollback, and signed offline bundles all tested
Identity backup and rotation proven Round trip, wrong passphrase, forged certificate, and chain-from-unknown all tested
Clock and power health proven Clock parsing tested against chrony output; power against a synthetic sensor. No INA219 has been connected
Delivery estimation and reporting proven Estimates from measured history; actual times computed from message events
Airtime limits and duplicate delivery proven
Exercises proven Marked traffic, per-participant report, findings
Group enforcement and identity conflicts proven Including the operator override and the notification to the other node
Hub operations and federation proven Including the refusal to let a peer rewrite a held key
Conformance vectors and harness proven 23 vectors, deterministic regeneration, checked in CI
Radio control and posture engine partial Complete against a mock radio. No QMX has been connected
HF transport: framing, ARQ, beacons, budget partial Complete against a simulated channel with injected loss. Never on the air
VHF over AX.25 partial Complete against a loopback link. No TNC connected
Receive-only SDR scanning untested Planning and interlocks written; the signal chain is external processes and unexercised
Enclosure untested Parametric source only; dimensions must be confirmed against a real PCB

The largest untested surface is everything below the frame codec. The protocol has been exercised hard against a simulator that models loss as independent per frame. Real HF loss is correlated and arrives in bursts, which an ARQ window handles worse. Nothing in section 15 should be treated as validated until a QMX has been on an antenna.

22.2 Deviations from this document

Three, all deliberate, each recorded at the point in the text where it applies rather than only here:

  1. REST and polling instead of a WebSocket for the hub (10.4).
  2. A wire digest instead of the canonical digest on RF (appendix B).
  3. scrypt instead of argon2id for passwords (13.3), and CSV plus print-ready HTML instead of server-side PDF for ICS-309 (11.7).

22.3 Second implementation pass

Revision 2.1 followed a structured review that asked what was missing beyond the already-known gaps. Fifteen findings came out of it, and thirteen were implemented; the two that were not are the two that are not engineering.

Not implemented, and the more important of the two sets. There is still no second maintainer, and no emergency coordinator or active traffic handler has reviewed the design. These are the largest risks to the project and neither is fixed by writing code.

What has been built is everything that makes them possible and cheap: the conformance vectors and harness that let a second implementer work without reading this implementation's source (21.1), a written release and key custody procedure with a five-step co-maintainer onboarding that ends in them cutting a release alone, and a ten minute review packet for an emergency coordinator that ends in a single question. The procedures exist; the people do not, and no amount of further work changes that.

Implemented. Identity backup and rotation (5.3); clock health surfaced (6.7); power monitoring with the update interlock that depended on it (6.10); time-to-a-person estimated at composition and reported afterwards (11.2, 11.7); airtime ceiling (11.2); duplicate delivery detection (11.8); group namespace enforcement with an operator override (9.12); duplicate identifier detection that tells both nodes (13.5); exercise mode (14.4); hub operator interface and federation (10.5); plus an end-user quick reference and an exercise guide with a worked scenario, both of which were missing and both of which matter more than any of the code for a volunteer who has never seen the system.

Two of these changed behavior that a reader of an earlier revision would not expect, and are worth calling out. A node now holds back its capability beacon until it has heard another HERMES station in the group, which means a genuinely first node in a region transmits nothing until its operator enables it deliberately. And composition refuses an over-long RF message rather than warning about it.

22.4 What the build changed in the design

Three findings were significant enough to change the specification. They are described in 1.1 and folded into 4.1, 9.10, and 15.

22.5 Bugs worth recording

Not a changelog. These four are worth naming because each is a class of mistake that would have been expensive to find on the air, and because a reader implementing this independently will meet them too.

Precedence was silently dropped on receipt. The offer frame carries it, the receiving session discarded it, and every message arriving over the air was recreated as routine. On a congested channel that quietly demotes emergency traffic at exactly the moment it matters, and nothing looks broken. Any second implementation should test that precedence survives the air.

A base 32 helper built for truncating digests destroyed small integers. Left-aligning is right for taking the top 40 bits of a hash and catastrophically wrong for encoding a timestamp or an ARQ bitmap. The on-air symptom would have been fragments that never reassembled, for no visible reason.

A node could not verify its own outbound messages, because nothing had written its own public key into its own roster. It presented as a signature failure and was a missing row.

The plain-English auto-reply refused the first reply to every station, because an unseen call sign defaulted to timestamp zero and every comparison looked like a recent reply. The symptom on the air is not a crash: it is a node that ignores people who call it, which is precisely the behavior the interoperability argument in 9.9 depends on avoiding.

22.6 What to do next, in order

  1. Connect a QMX to an antenna and run the RF transport against a second node. This retires more risk than everything else combined.
  2. Verify JS8Call compound call sign behavior on the air, across the mainline release and the Improved fork, for heartbeat participation, automatic SNR replies, and directed addressing. Per-instance RF identity is a hard requirement and this is the assumption it rests on.
  3. Obtain the Volunteer Counsel opinion on 97.221. Until it arrives, nothing should beacon unattended.
  4. Capture an hour of real JS8Call API traffic and replay it. The decode parser was written defensively against both forks and has never seen the real thing.
  5. Print an enclosure and confirm the seven measurements in appendix E.
  6. Find a second maintainer, and show the design to an emergency coordinator. These sit last in this list only because they are not engineering tasks. They are the two largest risks to the project, no amount of the work above compensates for either, and everything needed to make both cheap is now written down and waiting.

revision history

Revision history

Revision Date Changes
1.0 10 August 2026 Initial design, issued with eight open questions
2.2 12 August 2026 Governance infrastructure. Conformance vectors and harness published (21.1), so a second implementation can be written from the specification rather than from this source, with the consequence that the protocol is frozen at version 1 until a second one exists. Release procedure, two-key custody, succession, and co-maintainer onboarding written down. Review packet for an emergency coordinator, and 21.2 added stating plainly that nobody doing the work has reviewed the design
2.1 12 August 2026 Second implementation pass against a structured gap review. Identity backup and rotation (5.3), clock health surfacing (6.7), power monitoring and the interlock that depended on it (6.10), time-to-a-person estimation and reporting (11.2, 11.7), airtime ceiling (11.2), duplicate delivery detection (11.8), group namespace enforcement with an override (9.12), duplicate identifier detection (13.5), exercise mode made real (14.4), hub operator interface and federation (10.5). Requirements R28 to R37 added; section 22.3 records what was found and what was deliberately not built
2.0 11 August 2026 Revised against a complete reference implementation. Section 22 added: implementation status, the three deviations, the design changes the build forced, and the bugs worth recording. Section 15 replaced with measured throughput and a breakdown of where the time goes. Section 9.10 corrected: compression is a composition aid, not a wire transform, because the body is signed. Section 4.1 extended with the pending transport result. Section 5.2 given the measured compound call sign cost. Sections 10.4, 11.7, 13.3, and appendix B annotated with their deviations
1.2 10 August 2026 Response to an outside critique. Measurement promoted to section 12 as a headline capability with use cases written out and the executive summary reframed. Forms template engine and shipped ICS set, Winlink bridge, ICS-309 generation, and operator-asserted final delivery added to section 11. Net control role (8.9) and pull-based fleet policy (8.10) added. VHF and UHF added as a third transport (6.11) with the routing weights revised. Prior art table added at 1.1. Governance and continuity proposal added at 21.1
1.1 10 August 2026 All eight questions resolved (section 19). Per-instance RF identity made a hard requirement with the beacon rates rebalanced. Fan dipole made the default antenna. QMX+ and QDX support added through a driver abstraction. Receive-only SDR specified as a recommended addition. Stock-image installation from a signed APT repository. Update and patching strategy for read-only and offline nodes. Full security and hardening specification added as section 13. Hub ownership devolved to deployers with a 44Net self-hosting pattern. Group namespace policy decided. Released for public review

appendix a

Database schema

sql: schema
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;

-- Who exists
CREATE TABLE stations (
  id            TEXT PRIMARY KEY,        -- canonical 'K9MEV-1'
  callsign      TEXT NOT NULL,           -- 'K9MEV'
  instance      INTEGER NOT NULL DEFAULT 0 CHECK (instance BETWEEN 0 AND 15),
  pubkey        BLOB,                    -- 32 byte Ed25519
  key_state     TEXT NOT NULL DEFAULT 'unverified',  -- unverified|pinned|attested|revoked
  key_pinned_at INTEGER,
  fingerprint   TEXT,                    -- 8 char base32 of first 40 bits
  display_name  TEXT,
  grid          TEXT,
  lat           REAL, lon REAL,
  caps          TEXT,                    -- JSON: {relay:true,bands:[80,40,30,20],sched:2,gw:["EMAIL"]}
  first_seen    INTEGER NOT NULL,
  last_seen     INTEGER,
  last_seen_rf  INTEGER,
  last_seen_ip  INTEGER,
  is_self       INTEGER NOT NULL DEFAULT 0,
  blocked       INTEGER NOT NULL DEFAULT 0,
  notes         TEXT,
  UNIQUE (callsign, instance)
);
CREATE INDEX ix_stations_callsign ON stations(callsign);

-- Raw hearing log: every decode, HERMES or not
CREATE TABLE observations (
  id         INTEGER PRIMARY KEY,
  ts         INTEGER NOT NULL,           -- unix seconds UTC
  station_id TEXT,                       -- null if unknown/unparseable
  callsign   TEXT NOT NULL,
  band       INTEGER NOT NULL,           -- metres: 80,60,40,30,20
  dial_hz    INTEGER NOT NULL,
  offset_hz  INTEGER NOT NULL,
  snr_db     INTEGER,
  speed      TEXT,                       -- slow|normal|fast
  tdrift     REAL,
  grid       TEXT,
  frame_type TEXT,                       -- HB|CQ|DIRECTED|HERMES-H|HERMES-D|...
  to_us      INTEGER NOT NULL DEFAULT 0,
  raw        TEXT
);
CREATE INDEX ix_obs_ts       ON observations(ts);
CREATE INDEX ix_obs_call_band ON observations(callsign, band, ts);

-- Aggregated link quality, the table the router reads
CREATE TABLE links (
  station_id  TEXT NOT NULL,
  band        INTEGER NOT NULL,
  hour_utc    INTEGER NOT NULL CHECK (hour_utc BETWEEN 0 AND 23),
  month       INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),
  decodes     INTEGER NOT NULL DEFAULT 0,
  snr_mean    REAL,
  snr_best    INTEGER,
  attempts    INTEGER NOT NULL DEFAULT 0,
  successes   INTEGER NOT NULL DEFAULT 0,
  last_ok     INTEGER,
  quality     REAL,                      -- computed score, see 9.6
  PRIMARY KEY (station_id, band, hour_utc, month)
);

-- IP-side reachability
CREATE TABLE reachability (
  station_id     TEXT PRIMARY KEY REFERENCES stations(id),
  direct_ok      INTEGER NOT NULL DEFAULT 0,
  last_direct    INTEGER,
  last_via_hub   INTEGER,
  hub            TEXT,
  endpoints      TEXT,                   -- JSON array
  rtt_ms         INTEGER,
  accepts_inbound INTEGER NOT NULL DEFAULT 0,
  duty_schedule  TEXT                    -- JSON: awake windows
);

-- Traffic
CREATE TABLE messages (
  id          TEXT PRIMARY KEY,          -- 16 char base32
  from_id     TEXT NOT NULL,
  to_ids      TEXT NOT NULL,             -- JSON array
  profile     TEXT NOT NULL,             -- radiogram|message|ics213|status|receipt
  prec        TEXT NOT NULL CHECK (prec IN ('E','P','W','R')),
  hlim        INTEGER NOT NULL DEFAULT 4,
  created     INTEGER NOT NULL,
  expires     INTEGER,
  subject     TEXT,
  body        TEXT NOT NULL,
  meta        TEXT,                      -- JSON, profile specific
  orig_class  TEXT NOT NULL DEFAULT 'licensee',
  sig         BLOB,
  digest40    TEXT NOT NULL,             -- 8 char base32
  canonical   BLOB NOT NULL,             -- RFC 8785 bytes, for re-verification
  state       TEXT NOT NULL,             -- draft|queued|inflight|custody|delivered|verified|expired|failed
  require_verified INTEGER NOT NULL DEFAULT 0,
  inbound     INTEGER NOT NULL DEFAULT 0,
  read_at     INTEGER
);
CREATE INDEX ix_msg_state ON messages(state, prec, created);

CREATE TABLE custody (
  msg_id     TEXT NOT NULL REFERENCES messages(id),
  holder_id  TEXT NOT NULL,
  accepted   INTEGER NOT NULL,
  deadline   INTEGER NOT NULL,
  path       TEXT NOT NULL,              -- JSON array of station ids, loop prevention
  released   INTEGER,
  PRIMARY KEY (msg_id, holder_id, accepted)
);

CREATE TABLE receipts (
  msg_id     TEXT NOT NULL REFERENCES messages(id),
  by_id      TEXT NOT NULL,
  kind       TEXT NOT NULL,              -- hop|delivered
  level      INTEGER NOT NULL,           -- 1..4 per 9.8
  ts         INTEGER NOT NULL,
  transport  TEXT NOT NULL,              -- ip|rf
  digest40   TEXT,
  sig        BLOB,                       -- null for compact RF receipts
  PRIMARY KEY (msg_id, by_id, kind, ts)
);

CREATE TABLE transmissions (
  id        INTEGER PRIMARY KEY,
  ts        INTEGER NOT NULL,
  msg_id    TEXT,
  sid       TEXT,
  peer_id   TEXT,
  band      INTEGER, dial_hz INTEGER, offset_hz INTEGER, speed TEXT,
  frame_type TEXT NOT NULL,
  frag_k    INTEGER, frag_n INTEGER,
  text      TEXT NOT NULL,
  duration_s REAL,
  posture   TEXT NOT NULL,               -- auto_b|auto_c_response|attended
  result    TEXT                         -- sent|acked|timeout|swr_fault
);
CREATE INDEX ix_tx_ts ON transmissions(ts);

-- Radio health
CREATE TABLE band_swr (
  band INTEGER, freq_hz INTEGER, ts INTEGER NOT NULL,
  swr REAL, power_w REAL, usable INTEGER NOT NULL,
  PRIMARY KEY (band, freq_hz, ts)
);
CREATE TABLE band_noise (
  band INTEGER, hour_utc INTEGER, ts INTEGER NOT NULL,
  s_units REAL, pi_state TEXT,           -- off|idle|loaded
  PRIMARY KEY (band, hour_utc, ts)
);
CREATE TABLE tx_budget (
  window_start INTEGER PRIMARY KEY,
  window_len_s INTEGER NOT NULL,
  tx_seconds   REAL NOT NULL DEFAULT 0,
  by_prec      TEXT
);

-- Housekeeping
CREATE TABLE config (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated INTEGER NOT NULL);
CREATE TABLE users (
  name TEXT PRIMARY KEY, pw_hash TEXT NOT NULL, role TEXT NOT NULL,
  totp_secret TEXT, created INTEGER NOT NULL, last_login INTEGER
);
-- Measurement, section 12
CREATE TABLE tx_attempts (
  id         INTEGER PRIMARY KEY,
  ts         INTEGER NOT NULL,
  peer_id    TEXT NOT NULL,
  transport  TEXT NOT NULL,          -- ip | vhf | hf
  band       INTEGER, speed TEXT,
  msg_id     TEXT,
  frames     INTEGER, retries INTEGER,
  elapsed_s  REAL,
  result     TEXT NOT NULL,          -- ok | timeout | rejected | aborted
  snr_reported INTEGER
);
CREATE INDEX ix_txa_peer ON tx_attempts(peer_id, transport, band, ts);

CREATE TABLE spots_of_us (              -- reverse path: who heard us
  id        INTEGER PRIMARY KEY,
  ts        INTEGER NOT NULL,
  by_call   TEXT NOT NULL,
  band      INTEGER NOT NULL,
  snr_db    INTEGER,
  source    TEXT NOT NULL,           -- ack | hb_reply | pskreporter
  grid      TEXT
);

CREATE TABLE path_watch (
  peer_id       TEXT PRIMARY KEY,
  label         TEXT,
  expect_pct    REAL NOT NULL,
  window_days   INTEGER NOT NULL DEFAULT 7,
  expect_bands  TEXT,
  expect_hours  TEXT,
  degraded_snr  INTEGER,
  alert_after_h INTEGER NOT NULL DEFAULT 12,
  state         TEXT NOT NULL DEFAULT 'ok',   -- ok | degraded | failed
  since         INTEGER
);

CREATE TABLE fleet_policy (
  coordinator_id TEXT NOT NULL,
  version        INTEGER NOT NULL,
  applied        INTEGER NOT NULL,
  classes        TEXT NOT NULL,      -- JSON array actually applied
  diff           TEXT,
  PRIMARY KEY (coordinator_id, version)
);

CREATE TABLE deliveries (               -- final delivery to a human, 11.8
  msg_id     TEXT NOT NULL REFERENCES messages(id),
  ts         INTEGER NOT NULL,
  operator   TEXT NOT NULL,
  method     TEXT NOT NULL,          -- phone | in_person | email | hand | third_party
  received_by TEXT,
  outcome    TEXT NOT NULL,          -- served | undeliverable
  detail     TEXT,
  sig        BLOB,
  PRIMARY KEY (msg_id, ts)
);

CREATE TABLE emergency_declarations (
  id         TEXT PRIMARY KEY,
  declared   INTEGER NOT NULL,
  expires    INTEGER NOT NULL,
  by_user    TEXT NOT NULL,
  scope      TEXT NOT NULL,          -- band_plan | third_party | international | all
  justification TEXT NOT NULL,
  revoked    INTEGER
);

CREATE TABLE updates (
  id          INTEGER PRIMARY KEY,
  ts          INTEGER NOT NULL,
  kind        TEXT NOT NULL,         -- os | hermes | bundle
  source      TEXT NOT NULL,
  from_version TEXT, to_version TEXT,
  result      TEXT NOT NULL,         -- applied | deferred | failed | rolled_back
  reason      TEXT,
  health_check TEXT
);

CREATE TABLE audit_log (
  id INTEGER PRIMARY KEY, ts INTEGER NOT NULL, actor TEXT, action TEXT NOT NULL,
  detail TEXT, ip TEXT
);

appendix b

RF frame grammar

Encoding alphabet for binary fields is Crockford base 32: 0123456789ABCDEFGHJKMNPQRSTVWXYZ (no I, L, O, U).

abnf: frame grammar
frame        = sigil type [flags] SP fields
sigil        = "~"
type         = "H" / "Q" / "O" / "A" / "D" / "K" / "R" / "X" / "I" / "S"
flags        = 1*2(ALPHA / DIGIT)

hello        = "~H" SP ver SP bandmask SP "SCH" sched SP "Q" qdepth SP "NET" net SP fp
ver          = 1DIGIT
bandmask     = 4DIGIT              ; 1 = enabled, order 80/40/30/20
sched        = 1DIGIT
qdepth       = 4DIGIT              ; counts E,P,W,R clamped to 9
net          = "0" / "1"
fp           = 5(base32)           ; key fingerprint prefix, 25 bits

query        = "~Q" [SP dest]
offer        = "~O" SP sid SP nfrag SP prec SP dest SP digest
accept       = "~A" SP sid SP start
data         = "~D" SP sid SP k "/" n SP payload SP crc
ack          = "~K" SP sid SP bitmap
receipt      = "~R" SP sid SP kind SP digest      ; kind = "C" custody, "D" delivered
reject       = "~X" SP sid SP reason              ; FULL BUDG DEST UNV EXP
identify     = "~I" SP callsign
status       = "~S" SP qdepth SP net SP 1*(dest SP band)

sid          = 4(base32)
nfrag        = 1*2DIGIT
k            = 1*2DIGIT
n            = 1*2DIGIT
prec         = "E" / "P" / "W" / "R"
dest         = callsign ["/" 1*2DIGIT]
digest       = 8(base32)           ; 40 bits of SHA-256 over the wire envelope
crc          = 2(base32)           ; 10 bit CRC over the fragment payload
bitmap       = 2(base32)           ; 10 bits, one per fragment in window
payload      = 1*40(payload-char)
payload-char = ALPHA / DIGIT / SP / "." / "," / "?" / "/" / "-" / "+"

Worked example: one radiogram, K9MEV to N9ABC, dark state, direct

diagram
1  K9MEV/1 -> @HERM   ~H 1 1110 SCH2 Q0000 NET0 F7K2A
2  N9ABC   -> @HERM   ~H 1 1110 SCH2 Q0000 NET0 R3M8P
3  K9MEV/1 -> N9ABC   ~O 7QK2 08 P N9ABC K4M2XP9T
4  N9ABC   -> K9MEV/1 ~A 7QK2 01
5  K9MEV/1 -> N9ABC   ~D 7QK2 01/08 NR 21 P HXG K9MEV 9 BATAVIA IL 1432Z A 7M
6  K9MEV/1 -> N9ABC   ~D 7QK2 02/08 UG 10 BT JOHN SMITH 412 OAK ST BATAVIA I 3K
   ... fragments 03 through 06 ...
7  N9ABC   -> K9MEV/1 ~K 7QK2 ZW
8  K9MEV/1 -> N9ABC   ~D 7QK2 07/08 ARL SIXTY SIX WITH TRUCK X ALL WELL X BT 9P
9  K9MEV/1 -> N9ABC   ~D 7QK2 08/08 MARK K9MEV AR                          4T
10 N9ABC   -> K9MEV/1 ~K 7QK2 ZZ
11 N9ABC   -> K9MEV/1 ~R 7QK2 D K4M2XP9T
12 K9MEV/1 -> @HERM   ~I K9MEV                      (if 9 minutes elapsed)

Note that K9MEV/1 is a compound call sign, so each of its transmission bursts costs one extra JS8 frame for the call sign, per 5.2. N9ABC, operating a single node, pays nothing. The station identification in frame 12 uses the base call sign, per 3.3.

Deviation from earlier revisions: the digest carried on RF is over the wire envelope, not the canonical JSON form. A receiving station cannot compute the canonical digest: it would need every field of the canonical object, and sending them costs frames a five watt link does not have. The wire digest is 40 bits of SHA-256 over the encoded envelope that actually crossed the air, which proves exactly what the canonical digest proves in practice, namely that these precise bytes arrived intact, and costs nothing extra. The canonical digest still binds the signed object and still travels with the signed receipt upgrade over IP, so nothing is lost at level 4.

Frame 11 moves the message to Delivered (level 3). When either station next has internet, N9ABC transmits the full signed receipt object over IP and K9MEV's UI moves the message to Verified (level 4).


appendix c

REST API summary

diagram
GET    /api/v1/status                      node health, postures, budgets
GET    /api/v1/config                      full configuration
PATCH  /api/v1/config                      update configuration (admin)
POST   /api/v1/setup/{step}                first boot wizard

GET    /api/v1/messages?state=&prec=&q=    list with filters
POST   /api/v1/messages                    compose and queue
GET    /api/v1/messages/{id}               full object plus timeline
DELETE /api/v1/messages/{id}               cancel if not yet in custody elsewhere
POST   /api/v1/messages/{id}/retry         force re-route
GET    /api/v1/messages/{id}/receipts      proof chain
GET    /api/v1/messages/{id}/render.pdf    radiogram or ICS-213 form

GET    /api/v1/stations                    roster
GET    /api/v1/stations/{id}               detail plus heat map data
POST   /api/v1/stations/{id}/verify        pin or accept a key
POST   /api/v1/stations/{id}/block

GET    /api/v1/observations?since=&band=   raw hearing log
GET    /api/v1/links?band=&hour=           aggregated quality
GET    /api/v1/paths                       watched paths and their current state
PUT    /api/v1/paths/{peer}                set or change an expectation
GET    /api/v1/reports/reachability        availability by peer, band, hour, period
GET    /api/v1/reports/noise               noise floor trend with change points
GET    /api/v1/reports/coverage            group coverage gaps (aggregator role)
GET    /api/v1/export/observations.csv     raw decode log
GET    /api/v1/export/links.json           link table
GET    /api/v1/export/log.adi              ADIF for a normal logging program

GET    /api/v1/reports/ics309?from=&to=    communications log, PDF or CSV
GET    /api/v1/reports/ics214              operator activity log
GET    /api/v1/forms                       installed form templates
POST   /api/v1/messages/{id}/deliver       assert final delivery (method, time, recipient)

GET    /api/v1/net/board                   net control traffic board
GET    /api/v1/net/stations                net control station board
POST   /api/v1/net/report                  submit a netstat report (peers)

GET    /api/v1/fleet/policy                current subscription and applied version
POST   /api/v1/fleet/subscribe             pin a coordinator identity
POST   /api/v1/fleet/revert                return to local configuration

GET    /api/v1/radio                       band, swr, noise, budget
POST   /api/v1/radio/band                  manual band change (attended)
POST   /api/v1/radio/sweep                 run SWR qualification
POST   /api/v1/attend                      assert control operator, with timeout
DELETE /api/v1/attend                      release

GET    /api/v1/network                     hub and peer sessions
POST   /api/v1/network/hubs                add or remove a hub

GET    /api/v1/exercise                    exercise status and reports
POST   /api/v1/exercise                    start a scheduled exercise

GET    /api/v1/system/logs?service=
POST   /api/v1/system/backup               signed tarball
POST   /api/v1/system/update
GET    /api/v1/system/compliance?days=30   regulatory audit report
GET    /api/v1/system/updates               update history, current versions, staleness
POST   /api/v1/system/updates/check         check now
POST   /api/v1/system/updates/apply         apply now, subject to the same policy interlocks
POST   /api/v1/emergency                    declare, with scope and justification (admin)
DELETE /api/v1/emergency/{id}               revoke a declaration
GET    /api/v1/scan                         SDR scan status, bands, decoder health

WS     /api/v1/events                      push: decodes, state changes, alarms

appendix e

Enclosure parametric source (skeleton)

openscad
// HERMES enclosure, parametric. Verify the MEASURE block against the
// actual QMX PCB revision before printing. QMX PCB revisions have
// moved connector positions between Rev 1 and Rev 5.

/* [MEASURE - confirm these seven against your hardware] */
qmx_pcb_w      = 92;    // PCB width
qmx_pcb_d      = 60;    // PCB depth
qmx_hole_inset = 3.5;   // mounting hole inset from edge
qmx_lcd_x      = 8;     // LCD window origin from PCB left
qmx_lcd_w      = 62;    // LCD window width
qmx_enc_1_x    = 16;    // left encoder shaft center
qmx_enc_2_x    = 76;    // right encoder shaft center

/* [Shell] */
wall        = 2.4;
chamfer     = 3;
fillet_top  = 2;
box_w       = 175;
box_d       = 125;
box_h       = 62;
partition_x = 100;
partition_t = 3;

/* [Pi] */
pi_w = 85; pi_d = 56; pi_hole = 3.5; pi_standoff_h = 6;
cooler_clearance_h = 25;

/* [Detail] */
insert_d      = 4.2;   // M3 heat set outer diameter
insert_h      = 5.0;
louver_slot   = 1.6;
louver_pitch  = 3.0;
louver_angle  = 45;
lightpipe_d   = 5.0;
plate_w = 100; plate_h = 18; plate_text_h = 8;
callsign = "K9MEV-1";

module shell_bottom() { /* chamfered box, compartments, standoffs, louvers */ }
module shell_top()    { /* lid, fuzzy skin surface, vent, insert bosses */ }
module faceplate()    { /* LCD window, encoder holes, lightpipes, plate rail */ }
module callsign_plate(txt = callsign) { /* dovetail, embossed text */ }

part = "all";   // all | bottom | top | face | plate
if (part == "bottom") shell_bottom();
else if (part == "top") shell_top();
else if (part == "face") faceplate();
else if (part == "plate") callsign_plate();
else { shell_bottom(); translate([0,0,box_h]) shell_top(); }

appendix f

Glossary

Term Meaning
ACDS Automatically controlled digital station, per 47 CFR 97.221
ARQ Automatic repeat request, the retransmission scheme in 9.4
Attended posture A control operator has asserted presence through the UI, with a timeout
Capability beacon The ~H frame advertising what a node can do
Custody Explicit transfer of delivery responsibility between nodes
Digest40 The first 40 bits of SHA-256 over the canonical message form
Hub The small central rendezvous, presence, mailbox, and directory service
Instance The -N suffix distinguishing multiple nodes under one call sign
Posture The legal operating mode: A (unattended beacon), B (unattended response), C (attended)
Receipt upgrade Replacing a compact RF receipt with a full signed receipt when IP returns
Transmit budget The rolling duty cycle limit enforced by hermes-rfd
Co-site scheduler The mechanism preventing two of one operator's own nodes from transmitting into each other
Layout A / Layout B Read-write root, versus read-only root with an overlay and A/B slots
Update bundle A signed offline update package applied from removable media
44Net AMPRNet, the amateur radio IPv4 allocation administered by ARDC
Watched path A peer link with a stated availability expectation and an alarm when it is not met
Policy bundle A signed, narrowly scoped configuration document a node pulls from a coordinator it has pinned
Netstat report The periodic queue, custody, and health summary a node sends to a net control station
Final delivery An operator's assertion that a human being received the message, with method and time
Reverse spot An SNR report from a station that heard us, as distinct from a station we heard

appendix g

References

  • QRP Labs QMX product page, specifications, and CAT programming manual, firmware 1_04_004 and later, qrp-labs.com/qmx.html
  • 47 CFR 97.221, Automatically controlled digital station, eCFR, current as of August 2026
  • 47 CFR 97.113, 97.115, 97.119, 97.303(h), 97.401, 97.403, 97.405
  • JS8Call documentation and TCP API, js8call.com, and the JS8Call-Improved fork
  • RFC 8785, JSON Canonicalization Scheme
  • RFC 8032, Edwards-Curve Digital Signature Algorithm (Ed25519)
  • ARRL Public Service Communications Manual, radiogram format and ARL numbered texts
  • FEMA ICS forms: ICS-205, ICS-213, ICS-213RR, ICS-214, ICS-309
  • Winlink Global Radio Email, winlink.org, for the interoperation discussion in 11.6
  • Reticulum and LXMF, for the architectural comparison in 1.1
  • Direwolf software TNC and the AX.25 UI frame format, for the VHF transport in 6.11
  • PSKReporter, for the reverse spot and community contribution paths in 12.5
  • ARDC 44Net Connect, connect.44net.cloud, for the self-hosted hub pattern in 10.6
  • RFC 9116, A File Format to Aid in Security Vulnerability Disclosure (security.txt)
  • systemd.exec sandboxing directives, for the service isolation baseline in 13.3

Revision 2.2, revised against a reference implementation. Regulatory statements are the author's reading and should be confirmed independently before deployment. All performance figures marked as estimates require measurement in phase 8. MIT licensed, see section 21.