Skip to content

RocNet on the Mesh

Timo Sariwating edited this page Nov 2, 2026 · 12 revisions

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

  1. Decision
  2. Layers
  3. RocNet frame
  4. RocNet groups used on the mesh
  5. Zenoh keys and routing
  6. District power, emergency stop and short circuits
  7. Fast clock
  8. Loco commands
  9. Points, outputs, sensors and module actions
  10. Edge adapters
  11. Node dashboard and CLI log
  12. Test setups
  13. Design decisions
  14. 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

1. Decision

  • 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

2. Layers

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
Loading

3. RocNet frame

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"]
Loading

4. RocNet groups used on the mesh

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.

5. Zenoh keys and routing

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"]
Loading

6. District power, emergency stop and short circuits

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 echoes OPC_IDLE to its LocoNet clients; a district that is off stays off. Power off is never global. A normal h or a LocoNet OPC_IDLE from 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 go G, 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 GLOBAL line: normal, or STOP 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 logs Global 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}/power with state Short, 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
Loading
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
Loading

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)

7. Fast clock

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 set on rn/ck and every node applies it.
  • Every node counts locally from the last set or sync at 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 sync on 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 a sync so 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 ID 7F 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: sync on 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"]
Loading

8. Loco commands

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>

9. Points, outputs, sensors and module actions

  • 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}/sr and 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_REP on 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).

10. Edge adapters

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).

11. Node dashboard and CLI log

┌ 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. SHORT and Unknown in 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]). --plain prints the same lines as a log.

12. Test setups

12.1 Stage 1: software only

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
Loading

Built in this order:

Rocrail connects over LocoNet over TCP for now (--loconet-tcp, port 1234); the RocNet UDP adapter comes later.

  1. 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 SHORT on both dashboards and as power off in Rocrail.
  2. 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.
  3. Clear CLI view: decoded RocNet log, event pane, --plain output.
  4. Loco commands: Rocrail drives a loco whose district is on the MacBook.
  5. Node restart and reconnection.

Rocrail over RocNet UDP replaces LocoNet over TCP once the RocNet UDP adapter exists (4.5); both remain supported.

12.2 Stage 2: first LocoNet hardware

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.

13. Design decisions

# 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

14. Open questions

# 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

Clone this wiki locally