Protocol specifiction
Scope: Defines a Homie 5.0 device topology for model-railway control: vehicles, command stations, turnouts, signals, and feedback sensors.
Base topic: traintastic/5/ QoS: 1 for all $state, property values, and /set commands. Retained: yes, for every attribute and property value topic, so a client always sees last-known state on subscribe.
Out of scope: multiple unit operations, some things which unclear for me
Expected to hear: review and proposals
Design decision: two device tiers
Not every object in this domain has the same relationship to a controller, and the device topology should reflect that rather than flattening everything to “one object, one device.”
Tier 1 — complex, independently-owned entities. A vehicle and a command station are each managed as a single entity with their own identity, lifecycle, and non-trivial internal state (unit + functions for a vehicle; power + programming for a command station). Each gets its own top-level Homie device:
traintastic/5/vehicle-<id>/...
traintastic/5/cs-<id>/... (the command station itself, id=1)
Tier 2 — simple accessory objects managed collectively by one controller. Turnouts, signals, and feedback sensors are typically all driven or read by a single physical accessory/feedback controller (an accessory decoder bus master, a detector bus interface). Modeling each one as its own top-level Homie device would multiply discovery overhead — $state, LWT, and full device metadata — far out of proportion to how simple each individual object is. Instead, the controller’s domain is represented as one zone device, with each accessory object as a node of that device:
traintastic/5/zone-<id>/turnout-<n>/...
traintastic/5/zone-<id>/signal-<n>/...
traintastic/5/zone-<id>/feedback-<n>/...
This keeps the topology proportional to complexity: a vehicle’s speed/direction/function set genuinely warrants full device status, while a turnout’s single settable value does not.
Vehicle device
Relevant properties: name, address, protocol, speed-step mode, speed, direction, per-function state, active/emergency-stop flag.
traintastic/5/vehicle-<id>/$homie → 5.0
traintastic/5/vehicle-<id>/$name
traintastic/5/vehicle-<id>/$state → init | ready | lost
traintastic/5/vehicle-<id>/$nodes → unit,functions
traintastic/5/vehicle-<id>/unit/$name → "Unit control"
traintastic/5/vehicle-<id>/unit/$properties → speed,direction,speed-steps,active
traintastic/5/vehicle-<id>/unit/speed
$datatype integer
$format 0:127 # normalized to a 128-step range regardless of configured speed-step mode
$settable true
$retained true
traintastic/5/vehicle-<id>/unit/direction
$datatype enum
$format forward,reverse
$settable true
$retained true
traintastic/5/vehicle-<id>/unit/speed-steps
$datatype enum
$format 14,28,128
$settable false # read-only mirror; changing this is a decoder config action, not a runtime command
traintastic/5/vehicle-<id>/unit/active
$datatype boolean
$settable true
# false→true triggers vehicle-scoped emergency stop
traintastic/5/vehicle-<id>/functions/$name → "Function outputs"
traintastic/5/vehicle-<id>/functions/$properties → f-0,f-1,...,f-28 (only the range this vehicle reports)
traintastic/5/vehicle-<id>/functions/f-<n>
$datatype boolean
$settable true
$retained true
Speed, direction, and functions are independently settable properties, deliberately not bundled into one combined value, since the underlying decoder-level protocols on the market vary in whether they bundle these — a Homie device implementing this specification exposes them decomposed regardless of that variation.
Zone device
A zone device represents everything managed by one accessory/feedback controller: turnouts, signals, and feedback sensors, exposed as nodes.
traintastic/5/zone-<id>/$homie → 5.0
traintastic/5/zone-<id>/$name
traintastic/5/zone-<id>/$state → init | ready | lost
traintastic/5/zone-<id>/$nodes → turnout-1,turnout-2,signal-1,feedback-1,feedback-2,...
$state here applies to the whole zone controller: if the physical controller goes offline, every node under this device loses currency together, which is exactly why these objects are grouped under one device rather than given independent device-level state each.
Degenerate case — a zone with a single node. A controller that happens to manage only one turnout, one signal, or one feedback input is not a different device kind — it is the same zone device with $nodes containing exactly one entry (e.g. $nodes → turnout-1). Nothing about the topic shape, $state semantics, or node structure changes; a one-node zone and a fifty-node zone are the same specification applied to different controller sizes. This also means a controller’s node count is free to grow over time (new turnouts added to an existing zone) without any structural change to how the zone device itself is addressed or discovered.
Turnout node — simple (two routes)
traintastic/5/zone-<id>/turnout-<n>/$type → turnout
traintastic/5/zone-<id>/turnout-<n>/$properties → position
traintastic/5/zone-<id>/turnout-<n>/position
$datatype enum
$format straight,thrown
$settable true
$retained true
Turnout node — three-way
traintastic/5/zone-<id>/turnout-<n>/$type → turnout-3way
traintastic/5/zone-<id>/turnout-<n>/$properties → route
traintastic/5/zone-<id>/turnout-<n>/route
$datatype enum
$format left,straight,right
$settable true
$retained true
Turnout node — double slip / single slip
traintastic/5/zone-<id>/turnout-<n>/$type → slip-double (or slip-single)
traintastic/5/zone-<id>/turnout-<n>/$properties → route
traintastic/5/zone-<id>/turnout-<n>/route
$datatype enum
$format through-a,through-b,cross-a,cross-b # single slip: drop one value
$settable true
$retained true
State-transition requirement (applies to all turnout node types): a node MUST NOT publish a new retained position/route value until the commanded position is fully set. If the transition is not atomic on the underlying hardware, the property MUST hold its prior retained value for the duration — a subscriber must never observe a value that does not correspond to an actual, settled track position.
Diagnostics (optional): a node MAY expose a read-only sub-outputs property with implementation-specific detail for troubleshooting a route that fails to settle. It carries no normative meaning — position/route alone is authoritative.
Signal node
Signal state is a normalized aspect, not the raw index used by any particular underlying accessory protocol, since aspect indexing is symbol/type-dependent and varies across regional signaling systems.
traintastic/5/zone-<id>/signal-<n>/$type → signal
traintastic/5/zone-<id>/signal-<n>/$properties → indication
traintastic/5/zone-<id>/signal-<n>/indication
$datatype enum
$format stop,proceed,proceed-restricted,expect-stop
$settable true
$retained true
Feedback node
Represents a single occupancy/detection input.
traintastic/5/zone-<id>/feedback-<n>/$type → feedback
traintastic/5/zone-<id>/feedback-<n>/$properties → occupied,vehicle-id
traintastic/5/zone-<id>/feedback-<n>/occupied
$datatype boolean
$settable false # sensors are inputs only, never settable
$retained true
traintastic/5/zone-<id>/feedback-<n>/vehicle-id
$datatype integer
$settable false
$retained true
# optional — only present if the underlying detector hardware reports per-input vehicle addresses
Lifecycle mapping ($state + LWT)
| Situation | Homie mechanism |
|---|---|
| Device (vehicle, command station, or zone) initializing | $state → init while awaiting first confirmed values |
| First confirmed values available | $state → ready; all property values published retained |
| A property changes | Publish only the changed property topic (retained), not a full device re-publish |
| Zone’s physical controller goes offline | Zone $state → lost; this applies to the zone as a whole, not per-node — individual turnout/signal/feedback nodes have no $state of their own |
| A vehicle or command station goes offline | Its own $state → lost, independent of any zone |
| Loss of currency generally | Retained property values are NOT cleared on lost — last-known-state stays visible until the device or zone returns to ready |
| MQTT client disconnect | Standard Homie LWT on traintastic/5/<device-id>/$state → lost, set at connect time |
Fast clock
Not addressed by any object type above. If a fast-clock node is wanted at all, it is a distinct device with its own identity (traintastic/5/clock-1/...), not an attribute of a vehicle, command station, or zone.
Programming-track CV read/write
Stateful, transactional, and slow (multiple retries typical) — a poor fit for the retained-property model used elsewhere in this specification. Modeled as a request/response pair on the command station device, using a dedicated non-retained command topic (traintastic/5/cs-1/programming/cv-request/set → result published to a non-retained cv-result topic) rather than forcing it into the property pattern.
Summary table
| Object | Homie device | Node(s) | Key properties |
|---|---|---|---|
| Vehicle | vehicle-<id> |
unit, functions | speed, direction, speed-steps, active, f-0..f-28 |
| Command station | cs-1 |
power, programming | track-power, cv-request/cv-result |
| Zone (accessory/feedback controller) | zone-<id> |
turnout-, signal-, feedback- | — |
| — Turnout node, simple | (within zone) | turnout-<n> |
position |
| — Turnout node, three-way / slip | (within zone) | turnout-<n> |
route |
| — Signal node | (within zone) | signal-<n> |
indication |
| — Feedback node | (within zone) | feedback-<n> |
occupied, vehicle-id |
