Repository navigation
RocNet on the Mesh
Status: design draft, and the core protocol decision of Hive-Rail. RocNet is the protocol Hive-Rail speaks over Zenoh on the mesh. Based on the public RocNet protocol page on the Rocrail wiki, which describes RocNet as a binary open source protocol. Hive-Rail implements it from that page and does not copy Rocrail's C header file.
Contents
- Decision
- Layers
- RocNet frame
- RocNet groups used on the mesh
- Zenoh keys and routing
- District power, emergency stop and short circuits
- Fast clock
- Loco commands
- Points, outputs, sensors and module actions
- Edge adapters
- Node dashboard and CLI log
- Test setups
- Design decisions
- Open questions
Implementation status (2026-10-29):
| Part | State |
|---|---|
| District power on/off/halt, global emergency stop, short reporting (6) | Done, simulated command station; tested with Rocrail over LocoNet TCP |
Fast clock on rn/ck, clock master sync, LocoNet slot 123 and DCC-EX <JC> output (7) |
Done; tested between two nodes and with DCC-EX. Rocrail does not follow the LocoNet clock (§7) |
| Dashboard and decoded RocNet/LocoNet log (11) | Partly done |
| LocoNet over TCP (text and binary clients), locos per node in the dashboard | Done, see LocoNet Adapter. Rocrail's slot server stays on; the node translates its slots |
| Four-module dual-track simulation with power and short faults | Done: scripts/simulate-dual-track.sh
|
| DCC-EX driver: power on MAIN, emergency stop, overload as short, fast clock, unrequested power changes to LocoNet | Done, tested on the 2021 board with DCC-EX 5.6.6, see DCC-EX Adapter |
| Node board firmware: DCC-EX add-on, buttons, power LED | Step 1 done; LocoNet buffer next |
| Loco register (by address) with layout-wide throttle numbers, slot translation per LocoNet connection, dispatcher screen (Zugleiter), sync check | Done, see FRED §14 |
| RocNet UDP adapter, loco commands, points and sensors | Not started |
- RocNet is the mesh language. Everything RocNet defines (track power, halt, loco speed and functions, fast clock, sensors, outputs, switches, programming, identify) travels over Zenoh as RocNet frames.
- Hive-Rail keeps its own small namespace for what RocNet does not define: heartbeats, bindings, handoffs, district ownership, short-circuit state, and later the FRED throttle slots. RocNet groups are never given meanings the RocNet page does not define.
- Everything else is an edge adapter. DCC-EX, LocoNet centrals, Rocrail and RocNet hardware connect to a node through an adapter that translates to and from RocNet.
- FRED and Fredi throttles are deferred. Their design is kept in FRED Throttles and the Distributed Master and fits on top of this page without changes to the mesh.
Why RocNet:
| RocNet | LocoNet | |
|---|---|---|
| Loco addressing | By address, no slots | By slot, needs a slot master |
| Built for | Peer-to-peer network, no master | One central command station |
| Transport | UDP, TCP, MQTT, serial | Its own 16.6 kbit/s bus |
| Fast clock, sensors with RFID, identify, displays | Yes | Partly |
| Rocrail | Native | Supported |
| License | Open source protocol | Personal Use Edition only |
flowchart TB
subgraph edge["Edge adapters on each node"]
DCCEX["DCC-EX<br/>serial text commands"]
LN["LocoNet central and<br/>module LocoNet devices"]
RNU["Rocrail and RocNet hardware<br/>RocNet UDP"]
FRED["FRED / Fredi throttle bus<br/>deferred"]
end
subgraph node["Hive-Rail node"]
CORE["Node core<br/>routing, power, clock, safety"]
end
subgraph mesh["Zenoh mesh"]
RN["RocNet frames<br/>hive/session/rn/..."]
HR["Hive-Rail coordination<br/>hive/session/node/..., seam, loco district"]
end
DCCEX <--> CORE
LN <--> CORE
RNU <--> CORE
FRED -.-> CORE
CORE <--> RN
CORE <--> HR
From the RocNet protocol page:
| Byte | Field | Notes |
|---|---|---|
| 0 | Net ID | 0 = all networks. Hive-Rail uses 0 |
| 1, 2 | Recipient ID high, low | 0 = broadcast. For mobile frames the loco address |
| 3, 4 | Sender ID high, low | 1 = Rocrail server |
| 5 | Group | See 4 |
| 6 | Code | Bits 6 and 5: type (0 request, 1 event, 2 reply). Bits 4 to 0: action |
| 7 | Length | Number of data bytes, 0 to 255 |
| 8 … | Data | Action dependent |
- Reserved IDs: 0 broadcast, 1 Rocrail server (a second server uses 65001 or higher), 65535 initial ID of a new node.
-
Hive-Rail RocNet IDs: every node has a fixed RocNet ID in its profile (
rocnet_id), from a range kept free of Rocrail's own IDs. Hive-Rail never relies on Rocrail to assign IDs. - Events versus requests: a request asks for an action; an event or reply only reports a state and must not trigger I/O. The node core uses this to tell commands from state reports.
flowchart LR
H["8-byte header<br/>net, recipient, sender,<br/>group, code, length"] --> D["data<br/>0 to 255 bytes"]
C["code byte"] --> T{"bits 6 and 5"}
T -->|0| REQ["request: act on it"]
T -->|1| EV["event: state report"]
T -->|2| REP["reply: answer to a request"]
| Group | Name | Used for | Phase |
|---|---|---|---|
| 1 | Command station | Track power, halt, status query, loco velocity and functions by address, switch, output | 4 |
| 7 | Clock | Fast clock set and sync | 4 |
| 8 | Sensor | Occupancy and RFID reports | 4 |
| 9 | Output | Module actions such as windmill and lights | 4 |
| 3 | Stationary | Identify, query, port reports of RocNet hardware | Later |
| 2 | Mobile | Commands addressed to a loco or WLAN decoder directly | Later |
| 4, 5 | Programming | Always local to a node, never routed across the mesh | Later |
| 0 | Host | Shutdown: not allowed across the mesh by default | Later |
| 6, 10, 11, 12 | GP data, input, sound, display | Not used. Input is unused in RocNet itself | — |
Command station actions (group 1):
| Action | Name | Data | Reply |
|---|---|---|---|
| 1 | query | none | status |
| 2 | track power | 0 = off, 1 = on | status |
| 3 | halt | none | status |
| 6 | velocity | addrH, addrL, speed 0 to 127, direction, lights, protocol, speed steps | |
| 7 | function | addrH, addrL, F1–F8, F9–F16, F17–F24, protocol, changed function, F25–F32 | |
| 9 | switch | command, type, -, address | |
| 10 | output | command, type, value, address |
Status byte: bit 0 = power, bit 1 = idle, bit 2 = programming track busy.
Payload: the binary RocNet frame (8-bit format), unchanged. Zenoh attachment: Hive-Rail origin node, origin interface (mesh, dccex, loconet, rocnet-udp) and hop count, used to prevent loops when an adapter bridges a local RocNet network.
The key says who needs the frame, so each node subscribes only to what applies to it:
| Key | Content | Subscribed by |
|---|---|---|
hive/{session}/rn/cs/all |
Group 1 halt for the global emergency stop, and track power on (action 2, data 1) for the global go that ends it. Track power off on this key is ignored: power is per district (6) | Every node, the dispatcher |
hive/{session}/rn/cs/node/{id} |
Group 1 status events and replies from that node's command station | Every node (dashboard) |
hive/{session}/rn/loco/{address} |
Group 1 velocity and function for one loco | The district owner, and the destination during a handoff |
hive/{session}/rn/loco/{address}/event |
Velocity and function events: state actually applied | Nodes with a throttle or Rocrail on that loco |
hive/{session}/rn/ck |
Group 7 clock set and sync | Every node |
hive/{session}/rn/node/{id}/sr |
Group 8 sensor reports from that node | Nodes that need them, Rocrail adapters |
hive/{session}/rn/node/{id}/dev |
Group 1 switch and output, group 9 output requests for that node's devices | That node |
Hive-Rail coordination keys stay as listed in Architecture §2.3, plus hive/{session}/node/{id}/power (6).
Routing rule: the mesh carries everything, but each node only puts on its own track and local equipment what concerns its own modules.
flowchart TD
IN["RocNet frame from a local adapter"] --> G{"Group and action"}
G -->|"1: track power, halt"| PWR["Apply to this district only<br/>publish status on rn/cs/node/id"]
G -->|"1: global emergency stop"| ALL["Publish halt on rn/cs/all<br/>every district halts"]
G -->|"1: velocity, function"| OWN{"Loco in this<br/>node's district?"}
OWN -->|yes| LOC["Apply to local command station"]
OWN -->|no| LOCO["Publish rn/loco/address<br/>district owner applies it"]
G -->|"1: switch, output / 9"| DEV{"Device on this node?"}
DEV -->|yes| LDEV["Apply locally"]
DEV -->|no| RDEV["Publish rn/node/id/dev"]
G -->|"7: clock"| CK["Publish rn/ck"]
G -->|"8: sensor"| SR["Local sensor model,<br/>publish rn/node/id/sr"]
G -->|"4, 5: programming"| PRG["Local only"]
First feature to implement.
Every district has its own power. A large layout with ten command stations keeps running when one district is switched off, halted or shorted. Taking the whole layout down, the classic FREMO way with one central command station, is exactly what Hive-Rail is meant to improve on. There is no layout-wide power on or off; the only layout-wide action is the explicit global emergency stop below.
- Requested power is a RocNet group 1 request to one node: track power on (action 2, data 1), off (action 2, data 0) or halt (action 3). It comes from the node's own dashboard, from Rocrail or a LocoNet central connected to that node (GPON, GPOFF, IDLE), or later from Rocrail over RocNet addressed to that node's RocNet ID. It switches only that node's command station and is never forwarded to other nodes.
-
Global emergency stop is the one exception: an operator's explicit emergency stop for every district, sent as a group 1 halt on
rn/cs/all. Each node puts its district in emergency stop if it is on (DCC idle: trains stop, track power stays on) and echoesOPC_IDLEto its LocoNet clients; a district that is off stays off. Power off is never global. A normalhor a LocoNetOPC_IDLEfrom Rocrail stops only the local district. -
Global go ends a global stop (implemented 2026-10-31): a group 1 track power on request on
rn/cs/all. Every district in emergency stop goes back on; a district that is off stays off, and one with a short stays in short until it is cleared locally. A district can still resume on its own with its own power on. -
Keys: global stop
E, global goG, on a node's dashboard and on the dispatcher (which sends them as RocNet sender 0, with its id as the attachment). On the Node-Lite M5StickC: front button double press and triple press (Node-Lite on the M5StickC). -
Global state: the dispatcher's header and each node's dashboard have a
GLOBALline:normal, orSTOP by <who> at <time>from a global stop until the global go, or until no district is in emergency stop any more. A plain node logsGlobal state: …when it changes. Each district's own power stays in the lists. -
Actual power of each district is reported as a group 1 status event on
rn/cs/node/{id}(bit 0 power, bit 1 idle). - Every node sees every district: the status events and heartbeats show each district's power in every node's neighbour table, so operators and Rocrail know where trains can run.
-
Short circuits: RocNet has no short-circuit message, so the reason travels in Hive-Rail's own key
hive/{session}/node/{id}/powerwith stateShort, and in the heartbeat. Rocrail still sees "power off" for that district through the status event. - A short trips only the affected district, as boosters do today. The neighbours set their boundary signals to Stop, so no train enters the dead district, and keep running.
| State | RocNet status event | Hive-Rail node power |
|---|---|---|
| On | power = 1 | On |
| Off on request | power = 0 | Off |
| Halted | power = 1, idle = 1 | EmergencyStop |
| Short circuit | power = 0 | Short |
| No contact with command station | none | Unknown |
sequenceDiagram
autonumber
participant RR as Rocrail
participant NW as Node windows
participant M as Mesh
participant NM as Node mac
participant CS as DCC-EX on mac
RR->>NW: track power off
NW->>M: rn/cs/node/windows, status event power 0
M->>NM: windows district Off, mac keeps running
Note over NW,NM: global emergency stop pressed on windows
NW->>M: rn/cs/all, halt
M->>NM: halt
NM->>CS: emergency stop all locos
NM->>M: rn/cs/node/mac, status event power 1, idle 1
Note over NM,CS: later, a short on mac
CS-->>NM: power lost, not requested
NM->>M: rn/cs/node/mac, status event power 0
NM->>M: node/mac/power = Short, Priority 0
M->>NW: Short on mac
NW->>RR: RocNet status event power 0
stateDiagram-v2
[*] --> Unknown
Unknown --> Off: command station connected
Off --> On: track power on
On --> Off: track power off
On --> EmergencyStop: halt
EmergencyStop --> On: track power on
EmergencyStop --> Off: track power off
On --> Short: power lost without request
Short --> Off: short cleared
Short --> On: track power on after clearing
On --> Unknown: command station lost
Off --> Unknown: command station lost
How each command station reports a short:
| Command station | Detection |
|---|---|
| DCC-EX | Power-off report the node did not request (Architecture §7.2) |
| LocoNet central |
OPC_GPOFF or the TRK power bit dropping without a request (LocoNet Adapter) |
RocNet group 7 carries the clock: action 1 set, action 2 sync, with data year (2 bytes), month, day, hour, minutes, temperature, divider and brightness.
Hive-Rail commands the fast clock. Rocrail, centrals and throttles follow it; they never set it.
-
Set: an operator sets the clock (run or pause, rate, time) on any node's dashboard. The node sends a
setonrn/ckand every node applies it. -
Every node counts locally from the last
setorsyncat the divider rate, so the clock keeps running when a node loses contact. -
Clock master: the online node with the lowest node ID sends a
syncon every fast minute, which keeps all nodes in phase. If it leaves, the next lowest takes over with no jump, because every node already has the same state. A node that sees a new peer come online sends asyncso the newcomer has the clock at once. - Divider is used as the rate (1 to 60). A divider of 0 means paused, matching LocoNet's rate 0; RocNet does not define a pause.
-
Towards local equipment: every node writes LocoNet slot 123 (
OPC_WR_SL_DATA, setter ID7F 7E) to its own LocoNet clients on each fast minute and on every change, so Rocrail, the central and FREDs show the same time (LocoNet Adapter). Later:syncon a local RocNet network. - Clock writes from LocoNet are ignored and reported once per client: Rocrail or JMRI must have their own clock sync to LocoNet turned off.
-
DCC-EX gets
<JC minutes rate>on every fast minute and shows it on its display (DCC-EX Adapter).
Tested with Rocrail (2026-10-28), LocoNet over TCP with Rocrail's slot server on:
- With its LocoNet clock sync on, Rocrail reads slot 123 every real minute, ignores what it reads and writes its own time. Rocrail's slot server does store Hive-Rail's writes in between.
- After switching Rocrail's clock sync off, the writes stopped: Hive-Rail is then the only clock on LocoNet, so throttles and centrals see a steady time.
- Rocrail's own clock display does not follow the LocoNet clock; it keeps its internal time. Showing the Hive-Rail clock in Rocrail is left to the RocNet UDP adapter (group 7), still to be tested.
flowchart LR
OP["Operator on any node<br/>c, + -, [ ]"] -->|"rn/ck set"| ALL["Every node stores<br/>time and divider"]
SRC["Clock master<br/>lowest online node ID"] -->|"rn/ck sync each fast minute"| ALL
ALL --> DASH["Dashboard header"]
ALL --> LNC["LocoNet central slot 123"]
ALL --> RNL["Local RocNet sync"]
Loco commands use command station velocity (action 6) and function (action 7), which carry the loco address in the data. They are published on rn/loco/{address}. Only the district owner subscribes, so only the command station that powers the loco applies them. During a handoff the destination subscribes too (Transition Zone Specification).
DCC-EX mapping:
| RocNet | DCC-EX |
|---|---|
| Track power on / off |
<1> / <0>
|
| Halt |
<!> emergency stop all locos |
| Velocity: address, speed 0 to 127, direction |
<t address speed direction>, speed scaled to 0 to 126 |
| Function F1 to F32, lights (F0) | <F address function state> |
| Switch / output | <a address subaddress state> |
| Loco leaves the district | <- address> |
- Devices are named in the node's profile and addressed per node. Accessory addresses only mean something inside their own cluster, because every owner numbers from 1.
- Requests for a node's own devices never leave that node.
- A request for another node's device goes to
rn/node/{id}/dev, with the address from that node's published device list. - Sensor reports (group 8) go to
rn/node/{id}/srand only to nodes that need them, such as boundary sensors for a handoff. -
Sensor reports as sent (implemented 2026-11-01): a node reports its own sensors as a RocNet node does, with itself as the sender: group 8, action 1 (report), an event; data bytes 0 and 1 unused (0), byte 2 the state (1 occupied, 0 free), byte 3 the port. This is how Rocrail's own RocNet node sends it and how Rocrail reads it (feedback bus = the sending node, address = the port), so a port is 0 to 255 per node; a sensor with a higher address is not reported on RocNet (the node logs it). The source is the node's detector: a LocoNet
OPC_INPUT_REPon its bus, or the layout simulator standing in for the detector (hive/{session}/sim/{id}/sensor, simulated hardware, see Architecture §2.2). The node also reports the sensor on its LocoNet and on panel LEDs (--show-sensors).
| Adapter | Connects | Translates |
|---|---|---|
| DCC-EX | USB serial or WiFi TCP | RocNet group 1 to DCC-EX text commands; power reports to status events and Short; fast clock to <JC>
|
| LocoNet | LocoNet central over USB or TCP, module LocoNet devices | LocoNet power, clock, loco, switch and sensor messages to and from RocNet |
| RocNet UDP | Rocrail and RocNet hardware on the local LAN (224.0.0.1:4321) |
RocNet frames unchanged, with loop prevention |
| FRED throttle bus | FRED, Fredi | Deferred (FRED Throttles) |
RocNet UDP and loops. Rocrail's multicast group 224.0.0.1:4321 reaches every computer on the LAN. Only one node per LAN segment runs the RocNet UDP adapter (--rocnet-udp); the others do not join the group. Frames the adapter receives from the mesh are marked as mesh-originated so they are never sent back. Hive-Rail's own discovery stays on Zenoh scouting (224.0.0.224:7446).
┌ HIVE-RAIL ─ node mac (RocNet 101) ─ session bench ──── FAST CLOCK 06:42 x4 ┐
│ DISTRICT ON LAST REQUEST ON (power on from mac LocoNet, 10:02:13) │
├ Nodes ───────────────────────────────────────────────────────────────────────┤
│ Node Heartbeat Power Command station Locos Last event │
│ mac local ON DCC-EX (sim) 2 - │
│ windows ONLINE 80ms SHORT DCC-EX (sim) 1 short 10:04:51 │
├ Events ──────────────────────────────────────────────────────────────────────┤
│ 10:04:51 windows SHORT district power lost without request │
│ 10:02:13 windows POWER track power on (Rocrail) │
├ RocNet ──────────────────────────────────────────────────────────────────────┤
│ [ln ] 83 7C GPON power on (Rocrail) │
│ [mesh] 00 00 00 00 65 01 21 01 01 cs query event status: power │
└ q quit Tab switch pane Up/Down scroll End follow ────────────────────────┘
- Header: node and its RocNet ID, session, fast clock, this district's power and the last power request with who sent it.
-
Nodes: one row per node with its power state.
SHORTandUnknownin red. - Events: human-readable events only: power, shorts, nodes joining and leaving, conflicts.
-
RocNet / LocoNet: raw bytes with a decoded line, tagged with where they came from (
[mesh],[udp ],[ln ],[DCC-EX]).--plainprints the same lines as a log.
flowchart LR
subgraph mac["MacBook"]
HM["Hive-Rail node<br/>simulated DCC-EX"]
end
subgraph win["Windows PC"]
HW["Hive-Rail node<br/>simulated DCC-EX"]
RR["Rocrail<br/>virtual command station"]
RR <-->|"LocoNet over TCP<br/>port 1234"| HW
end
HM <-->|"Zenoh mesh, RocNet frames"| HW
Built in this order:
Rocrail connects over LocoNet over TCP for now (--loconet-tcp, port 1234); the RocNet UDP adapter comes later.
-
Done. District power and short reporting: Rocrail switches track power on and off for the district of the node it is connected to, and the other node shows the change without switching; a simulated short on one node shows as
SHORTon both dashboards and as power off in Rocrail. - Done in software, Rocrail still to test. Fast clock: Hive-Rail's clock, started on either node, appears in both dashboard headers and in Rocrail through LocoNet slot 123. Rocrail's own clock sync to LocoNet is turned off.
-
Clear CLI view: decoded RocNet log, event pane,
--plainoutput. - Loco commands: Rocrail drives a loco whose district is on the MacBook.
- Node restart and reconnection.
Rocrail over RocNet UDP replaces LocoNet over TCP once the RocNet UDP adapter exists (4.5); both remain supported.
An Intellibox I (65000) and an IB-Switch on one node through the LocoNet Adapter. Without FREDs, the module LocoNet can stay on the Intellibox as it is today; the node is one more LocoNet client. Run on 30 October 2026 through a GCA101 (LocoNet over UDP) instead of the Intellibox's RS232 port: district power both ways, taking and releasing locos on the Intellibox's slot server, and the IB-Switch's turnout questions (LocoNet Adapter §4.2). The RS232 port is still to be tested.
| # | Decision |
|---|---|
| R1 | RocNet is the protocol on the mesh, carried as binary frames in Zenoh payloads |
| R2 | Hive-Rail coordination (heartbeat, bindings, handoff, district ownership, short state, later throttle slots) stays in Hive-Rail's own namespace. RocNet groups are never given undefined meanings |
| R3 | Zenoh keys route each frame only to the nodes it applies to |
| R4 | Every node has a fixed RocNet ID from its profile; Hive-Rail never depends on ID assignment by Rocrail |
| R5 | A short trips only its own district and is reported to every node |
| R10 | Power is per district: on, off and emergency stop switch only the district of the node that receives them, so the other districts keep running. The only layout-wide actions are the explicit global emergency stop (halt on rn/cs/all) and the global go that ends it (power on on rn/cs/all, which only resumes districts in emergency stop) |
| R11 | Hive-Rail commands the fast clock. The lowest online node ID syncs it each fast minute; clock writes from LocoNet clients are ignored |
| R6 | Programming (groups 4 and 5) and host shutdown (group 0) never cross the mesh by default |
| R7 | Only one node per LAN segment bridges RocNet UDP |
| R8 | DCC-EX is the primary local command station; LocoNet centrals connect through the LocoNet adapter |
| R9 | FRED and Fredi support is deferred and must fit on top of this design without changing it |
| # | Question | How it will be answered |
|---|---|---|
| R-Q1 | Which RocNet ID range should Hive-Rail nodes use so they never collide with Rocrail or RocNet hardware? | Proposal: from the profile, 100 to 999. Check against Rocrail's defaults |
| R-Q2 | What does the clock divider mean exactly: the fast clock rate? |
Hive-Rail uses it as the rate. Confirm with Rocrail's RocNet clock frames when the RocNet UDP adapter exists |
| R-Q3 | How does Rocrail expect speed 0 to 127 to map to 128-step DCC, and how is a per-loco emergency stop sent? | Record Rocrail's velocity frames in stage 1 |
| R-Q4 | Does Rocrail send track power and halt as group 1 requests to recipient 0, and does it react to status events from several command stations? | Stage 1 |
| R-Q5 | Should a short also be sent as a RocNet stationary errorreport (group 3, action 13) so Rocrail can show it? Error classes are not defined on the RocNet page |
Check Rocrail's handling in stage 1 |
| R-Q6 | Can Rocrail use RocNet over TCP instead of UDP multicast, to avoid the one-bridge-per-LAN rule? | Check Rocrail's RocNet settings |