Repository navigation
DCC EX Adapter
Status: driver implemented (2026-10-29) and tested against DCC-EX v5.6.6, first on an ESP32 with an OLED and no motor shield, then on the 2021 board with the node board definition (tracks
A MAIN,B PROG, no current sense) on USB power, including its stop and go buttons. Protocol facts come from the public DCC-EX command reference and the EX-CommandStation source (GPLv3). Hive-Rail implements the text protocol and copies no DCC-EX code.
Contents
- Role
- Connection
- Commands and replies
- Short circuits
- Fast clock
- Recorded transcripts
- Not yet implemented
- Open questions
DCC-EX is the expected command station in most clusters (decision R8 in RocNet on the Mesh). The node drives it directly: a node started with --dccex follows the real command station's power state instead of the simulated one, and passes the Hive-Rail fast clock to it.
flowchart LR
MESH["Mesh<br/>RocNet over Zenoh"] <--> NODE["Hive-Rail node"]
RR["Rocrail / JMRI"] <-->|"LocoNet over TCP"| NODE
NODE <-->|"USB serial 115200<br/>or WiFi TCP 2560"| DCCEX["DCC-EX<br/>EX-CommandStation"]
DCCEX -->|"DCC"| TRACK["District track"]
hive-rail --mode node --session bench --id MacBook --rocnet-address 101 --dccex /dev/cu.usbserial-0001 # macOS
hive-rail --mode node --session bench --id RaspberryPi --rocnet-address 105 --dccex /dev/ttyUSB0 # Linux
hive-rail.exe --mode node --session bench --id Windows --rocnet-address 102 --dccex COM3 # Windows
hive-rail --mode node --session bench --id MacBook --rocnet-address 101 --dccex 192.168.4.1:2560 # WiFiThe same from a config file, with the options in examples/config/dccex-serial.toml or examples/config/dccex-wifi.toml (see the node file):
hive-rail --config examples/config/dccex-serial.tomlGive every node its own --rocnet-address; the default (100) is only safe for a single node.
- A target with a colon is TCP; anything else is a serial port at 115200 baud, 8N1.
-
Opening the serial port resets an ESP32 (and most Arduinos). DCC-EX boots with track power off and reports
<p0>; the node then shows the district asOFF. - After connecting, the node sends
<s>and<=>to read the version, power state and track modes. - The driver reconnects every 2 s. While DCC-EX is unreachable the district is
UNKNOWN(RocNet status: power off), and every node shows it. - The version from
<s>is logged and shown in the dashboard footer (Command station: DCC-EX V-5.6.6 / ESP32 / … on /dev/…). -
Without a motor shield DCC-EX has no tracks:
<1>and<0>change nothing, so it sends no<p1>/<p0>and its display keepsPWR Off. The node then shows the requested state. A motor shield type in DCC-EX'sconfig.hgives it tracks to switch, even with no shield attached. - An ESP32 also runs its own WiFi access point (
DCCEX_xxxxxx,192.168.4.1) with a command server on port 2560.
| Purpose | Node sends | DCC-EX replies |
|---|---|---|
| Status | <s> |
<iDCC-EX V-5.6.6 / ESP32 / No shield at all G-…> and <p0> / <p1>
|
| Power on / off |
<1 MAIN> / <0>
|
<p1 A> / <p0 A> per track, <p1 MAIN>, and an overall <p1>/<p0> only when all tracks agree. No reply when no tracks are configured
|
| Track modes | <=> |
<= A MAIN>, <= B PROG>
|
| District emergency stop | <!> |
none; DCC-EX sends a broadcast emergency-stop packet. Track power stays on |
| Fast clock | <JC minutes rate> |
<jC minutes rate> |
DCC-EX broadcasts <p…> on every power change, also when a button, a WiThrottle or another client switched it.
-
The district's power is the power of its MAIN tracks. The node learns the track modes (
<= A MAIN>) and follows the per-track<p1 A>/<p0 A>of the MAIN tracks. A MAIN-only power on (EX-RAILPOWERON,<1 MAIN>) sends no overall<p1>, because the programming track stays off; following only the overall state missed it. - Without tracks (no motor shield) the overall
<p0>/<p1>is used. -
A power change the node did not request (a button, WiThrottle, another client) is reported to every node and sent to the local LocoNet clients as
OPC_GPON/OPC_GPOFF, so Rocrail follows. - A
<p1>during an emergency stop does not end the stop.
Read-only information (tested on the ESP32 without motor shield, where every list is empty):
| Command | Reply | Meaning |
|---|---|---|
<JI> |
<jI mA mA …> |
Current per track in mA; -1 = that track is in overload |
<JG> |
<jG mA mA …> |
Trip limit per track in mA |
<=> |
<= A MAIN> per track |
Track modes; no reply without tracks |
<#> |
<# 120> |
Number of loco slots |
<JR>, <JT>, <JA>
|
<jR …>, <jT …>, <jA …>
|
Roster, turnouts, routes and automations |
<Q> |
<Q id state> per sensor |
Sensor states; DCC-EX also sends <Q id>/<q id> when a sensor changes |
DCC-EX has no voltage or temperature reading; it only measures current.
Never send
<c>. The legacy JMRI current command crashed DCC-EX v5.6.6 on an ESP32 without motor shield (Guru Meditation Error, reboot): it reads track A without checking that it exists. Use<JI>.
Other frames:
-
<* … *>diagnostics may span several lines and contain>; only*>ends them. -
<@ row col "text">is text for the command station's own display; the node does not log it.
How power requests map:
| Request (dashboard, LocoNet, global stop) | DCC-EX |
|---|---|
Power on (1, OPC_GPON) |
<1 MAIN>: MAIN only, so a local programming track stays independent |
Power off (0, OPC_GPOFF) |
<0> |
Emergency stop (h, OPC_IDLE, global E) |
<!> |
The node drives the locos of its district on DCC-EX from the loco register: every loco that is refreshed (in use, or common after a release) with this node as its district, or, without a district, last driven here. Only what changed is sent.
| Register | DCC-EX command | DCC-EX reply |
|---|---|---|
| Speed and direction (LocoNet 0 stop, 1 emergency stop, 2 to 127) |
<t cab speed dir>: speed 0 stop, -1 emergency stop, 1 to 126; dir 1 forward |
<l cab reg speedByte functionMap> |
| F0 to F8 (DIRF and SND bytes) | <F cab function 0|1> |
<l …> |
| Loco freed | <- cab> |
none |
- DCC-EX keeps the locos it drives in its LocoSlot table and refreshes them on the track: 120 on an ESP32, 50 on a Mega, 8 on an Uno or Nano (
<#>reports it).<t>or<F>adds a loco. -
<- cab>emergency-stops the loco before it takes it out of the table (DCC::forgetLoco). A loco that leaves the district therefore stays in the table and keeps following the register, so both districts send the same commands while the train may stand across the border. It is taken out once it is freed; the handoff (roadmap 5.x) will take it out when the train has cleared the transition zone. - DCC-EX forgets every loco when it starts. The node sends every loco it drives again when DCC-EX reports its version.
- Recorded with the transfer test tool:
<t 4444 60 1>and<F 4444 0 1>were answered with<l>broadcasts, logged asloco 4444 speed 60, forward, F0; then<t 4444 90 0>asloco 4444 speed 90, reverse, F0.
Where the node is the command station on its LocoNet (--loconet-connect-slots node, --loconet-tcp-slots node), a switch command (OPC_SW_REQ) taken with track power on is also sent to DCC-EX as <a turnout 1|0> (closed is 1), so the turnout really switches (LocoNet Adapter §4.3). Not yet tested with a real accessory decoder (L-Q11): whether 1 is closed on every decoder is still to be checked.
From the EX-CommandStation motor driver source: an overload is not reported as <p0>. DCC-EX cuts the affected track itself and writes a diagnostic:
<* TRACK A POWER OVERLOAD 3200mA (max 2000mA) detected after 100ms. Pause 40ms *>
<* TRACK A FAULT PIN detected after 2ms. Pause 80ms) *>
It retries on its own: the pause starts at 40 ms and doubles up to 10 s. When the current is normal again:
<* TRACK A NORMAL (after 20ms/45ms) 120mA *>
The node maps these as:
| DCC-EX | District state | Also |
|---|---|---|
POWER OVERLOAD or FAULT PIN
|
SHORT |
status event to every node, OPC_GPOFF to local LocoNet clients |
NORMAL after a short |
ON |
DCC-EX restored power itself |
<p0> not requested |
OFF |
reported as a change by DCC-EX |
This refines Architecture §7.2: for DCC-EX 5.6 the short token is known, and "unexpected power loss" alone would miss it. The overload messages are taken from the source and not yet recorded from real hardware; that needs a motor shield (roadmap 0.2).
DCC-EX does not run a clock. <JC minutes rate> only stores the time, shows it on the OLED (Time 06:15 (4)), tells WiThrottle clients and EX-RAIL, and replies <jC minutes rate>. The node sends <JC> on every fast minute and every clock change of the Hive-Rail clock (RocNet on the Mesh §7), with rate 0 while paused.
Recorded on 2026-10-29 from DCC-EX v5.6.6 (G-master-202609231425Z) on an ESP32 with an SSD1306 OLED and no motor shield. The boot output is the replay test in src/dccex.rs.
<* License GPLv3 fsf.org (c) dcc-ex.com *>
<* Scanning I2C bus for devices...
I2CManager: Using Wire library
Device found at 0x3C, OLED Display or HMC583L Magnetometer?
*>
<* 128x64 OLED display configured on I2C:0x3c *>
<* Wifi in AP mode *>
<* Server will be started on port 2560 *>
<iDCC-EX V-5.6.6 / ESP32 / No shield at all G-master-202609231425Z>
<p0>
> <1> (no reply: no tracks without a motor shield)
> <JC 375 4>
<* New DCC queue slot type=0 length=5 loco=0 q1=0 q2=0 created=0 *>
<@ 0 6 "Time 06:15 (4)">
<jC 375 4>
> <!>
<* New DCC queue slot type=6 length=3 loco=0 q1=0 q2=1 created=1 *>
- Functions above F8.
- Accessories other than LocoNet switch commands: DCC-EX's own turnout objects (
<T>) and their replies are not used. - Locos changed on DCC-EX itself (a WiThrottle, EX-RAIL): their
<l>broadcasts are logged, not yet taken into the register. - Current readings (
<JI>,<JG>) once a second for the dashboard, for exampleA MAIN 450/2000 mA. - Track manager configuration (
<=>); the node uses DCC-EX's own configuration. - Programming track: always local, never on the mesh.
| # | Question | How it will be answered |
|---|---|---|
| D-Q1 | Do the overload messages on real hardware match the source, per shield? | Record a real trip with a motor shield (roadmap 0.2) |
| D-Q2 | Should the node switch only MAIN (<1 MAIN>) so a local programming track stays independent? |
Decided (2026-10-29): yes. Power on is <1 MAIN>, power off <0> (all tracks) |
| D-Q3 | Does resetting the ESP32 on connect cause problems at a meeting (power drops when the node restarts)? | Test with a real district; consider disabling DTR reset |