Skip to content

Transition Zone Specification

Timo Sariwating edited this page Oct 31, 2026 · 4 revisions

Status: design draft. This page describes the software side of moving a train between two clusters with separate command stations. It refines profile I of Architecture §5. All timings are starting values.

Contents

  1. Catch and takeover
  2. Roles
  3. Layout of the transition zone
  4. Handshake
  5. State machine
  6. Latching relay driver
  7. Crash recovery
  8. 2-rail and 3-rail selection
  9. Profile fields
  10. Current budget admission (optional)
  11. Open questions

1. Catch and takeover

The command stations of two clusters are not synchronised, so a train must never bridge track powered by both. Hive-Rail uses an asymmetric catch and takeover:

  • The receiving module hosts all switching hardware and controls the transition.
  • The sending module is unchanged. It only exports its track feed to the receiving module.
  • The first module of the receiving cluster is a switched buffer block. It is fed by the sending cluster while the train enters, and switched to the receiving cluster once the train is fully inside.
  • No distributed consensus is needed: the receiving node is the arbiter for its transition zone.

2. Roles

Role Node Responsibility
Source Sending cluster (A) Proposes the handoff, keeps driving the loco until the takeover, then drops it
Destination and arbiter Receiving cluster (B) Configures the track, pre-loads the loco, watches the sensor, switches the relay, takes over the loco

3. Layout of the transition zone

flowchart LR
    A["Main body A<br/>always fed by A"] ---|"module border"| SW["Switched buffer block<br/>first module of B<br/>fed by A or B"]
    SW ---|"internal joint"| B["Main body B<br/>always fed by B"]
    IR(["IR break-beam sensor<br/>at the entrance of the buffer block"]) -.-> SW
    R{{"Dual-coil latching relay<br/>RESET: feed from A<br/>SET: feed from B"}} -.-> SW
Loading
  • Length rule: a train must be shorter than the buffer block.
  • Rail cuts: the buffer block is cut in both rails at both ends, because it is fed by one district or the other and two boosters never power the same rail (Architecture §5.1).
  • Double track: with right-hand running each track has one direction, so each district hosts the buffer block, relay and IR beam for its incoming track: at a border between districts A and B, the track towards B has its buffer block on B's first module, the track towards A on A's last module. On single track one buffer block serves both directions. See Module and Layout Files §3.9.
  • Long trains: several modules can be chained into one buffer block, for example 2 × 1200 mm. The software only needs the total length in the profile.

4. Handshake

sequenceDiagram
    autonumber
    participant A as Node A<br/>source
    participant M as Zenoh mesh
    participant B as Node B<br/>destination and arbiter
    participant HW as B hardware<br/>relay, sensor, DCC-EX
    A->>M: propose loco, speed, direction,<br/>functions, wheel standard
    M->>B: propose
    B->>HW: set 2-rail or 3-rail selection
    B->>HW: acquire loco and pre-load<br/>speed, direction, functions
    B->>HW: RESET pulse, block fed by A
    B->>M: ready
    M->>A: ready
    Note over A: boundary signal may show proceed
    Note over A,HW: train crosses the module border on A's feed
    HW-->>B: IR beam broken: the front enters
    HW-->>B: IR beam restored and stays clear for beam_clear_ms:<br/>the last car has passed the entrance
    B->>B: check: A's edge block is clear
    B->>HW: SET pulse, block fed by B
    B->>M: takeover complete, district = B
    M->>A: takeover complete
    A->>A: purge loco from A's command station
Loading

B pulses RESET before sending ready, so ready always means the buffer block is fed by A.

Which train proposes. The proposal names the loco, so A must know which train is in its edge block. A's train tracking normally knows it; otherwise the loco register, and with more than one candidate the operator (Architecture §10.3). A train driven by hand without a route uses the same path: the edge block sensor starts the handoff, and until B grants it the boundary signal stays at Stop. A train that enters the zone without a grant is fault F6: it is stopped and the zone stays unpowered. After the takeover, B's tracking continues the train in its own block graph with the identity it came with.

5. State machine

The destination node runs one state machine per transition zone. It plugs into the seam state machine of Architecture §5.2.

stateDiagram-v2
    [*] --> Idle
    Idle --> Configuring: propose received
    Configuring --> Ready: selection set, loco pre-loaded,<br/>RESET pulsed, ready sent
    Configuring --> Idle: buffer block occupied<br/>or wheel standard conflict, deny
    Ready --> Clearing: IR beam broken
    Ready --> Idle: no entry in t_enter, abort
    Clearing --> Switched: beam restored for beam_clear_ms<br/>and source edge block clear, SET pulsed
    Switched --> Committed: takeover complete sent
    Committed --> Idle: source purged the loco
    Clearing --> Fault: neighbour lost, or t_pass elapsed
    Switched --> Fault: neighbour lost or timeout
    Fault --> Idle: operator clears at node B
Loading

The relay stays on B's feed after the train has passed. The next propose resets it to A, and only while the buffer block is empty.

Timer Start value Purpose
relay_pulse_ms 50 ms Length of a SET or RESET pulse
beam_clear_ms 300 ms How long the beam must stay restored before the train counts as fully inside; a gap between two cars restores it only briefly
t_pass 60 s From the beam break until the beam is restored; longer means a train stopped across the entrance, a fault
t_propose 200 ms Time for ready
t_enter 10 s From ready to the beam break

6. Latching relay driver

The relay is a dual-coil latching DPDT relay. It holds its last position mechanically, so a node crash or power loss mid-transfer does not change the track feed.

Pin Default Pulse effect
SET GPIO 27 Buffer block fed by B
RESET GPIO 22 Buffer block fed by A
IR sensor input GPIO 17 Falling edge when the beam is broken

Pins are configured in the profile; the GPIO numbers above are the reference wiring.

Driver rules:

  1. Never pulse both coils at once. The hardware actor serialises all relay pulses.
  2. Pins are inactive from the moment they are claimed. No pulse can happen during start-up.
  3. Pulses are timed, not busy-waited. A 50 ms pulse does not block other work.
  4. Pulses are idempotent. Repeating a SET or RESET is harmless, so a failed pulse can be retried.
  5. The relay position cannot be read back. The node saves the last commanded position (SET or RESET) and the zone state with an atomic write (Architecture §8.2) whenever it pulses.
  6. GPIO access is portable. The driver uses the Linux GPIO character device (gpiocdev crate) so it runs on both Orange Pi and Raspberry Pi. The beam break is read as a kernel edge event.
flowchart LR
    HS["Handoff state machine"] -->|"set or reset"| HA["HardwareActor<br/>relay queue"]
    HA --> P["Pulse 50 ms<br/>one coil at a time"]
    P --> ST["Save last position<br/>atomic write"]
    IR["IR edge event<br/>gpiocdev"] --> HS
Loading

7. Crash recovery

The specification requires that a node starting after an ungraceful reboot sends a RESET pulse in its setup phase, so the buffer block is back on the passive feed from A before any layout operation resumes.

flowchart TD
    S["Node starts"] --> I["Claim GPIO pins inactive"]
    I --> L["Load saved relay position<br/>and zone state"]
    L --> R["RESET pulse<br/>buffer block fed by A"]
    R --> SIG["Boundary signals at Stop"]
    SIG --> J["Join mesh, publish state"]
    J --> OK["Zone Idle, ready for propose"]
Loading

Open question T1 asks whether this reset should depend on the saved state, so that a train taken over by B just before the crash is not put back on A's feed.

8. 2-rail and 3-rail selection

propose carries the loco's wheel standard (2R or 3R). Node B sets the module's selection relay before it sends ready.

Interlocking rule: a 2-rail train and a 3-rail train never occupy the same block at the same time. While the buffer block is occupied by a train of one standard, a proposal for the other standard is denied and its boundary signal stays at Stop.

flowchart TD
    P["propose with wheel standard"] --> O{"Buffer block empty?"}
    O -->|no| SAME{"Same standard as<br/>the train in the block?"}
    SAME -->|no| DENY["deny, signal stays Stop"]
    SAME -->|yes| WAIT["wait until the block is empty"]
    O -->|yes| SET["set selection relay"] --> READY["continue handshake"]
Loading

The wheel standard is declared by the operator, like the loco identity at a seam (Architecture §5.5).

9. Profile fields

A port with a transition zone describes the receiving side's hardware:

{
  "port_id": "A",
  "label": "West Main",
  "boundary": {
    "sync": "independent",
    "transition_zone": {
      "buffer_length_mm": 1200,
      "ir_sensor_gpio": 17,
      "relay_set_gpio": 27,
      "relay_reset_gpio": 22,
      "relay_pulse_ms": 50,
      "beam_clear_ms": 300,
      "wheel_standards": ["2R", "3R"],
      "selection_relay_gpio": 23
    },
    "signal": 21
  }
}

Validation rules:

  • SET, RESET, sensor and selection pins are all different.
  • relay_pulse_ms and beam_clear_ms are greater than zero.
  • selection_relay_gpio is required when wheel_standards lists both standards.
  • Only the receiving side of a seam declares transition_zone.

10. Current budget admission (optional)

A district near its current limit should not take another train. With admission on, the destination node B answers a proposal with ready only when it has the current to spare; otherwise it queues the proposal, and A's boundary signal stays at Stop until B grants it. The train waits at the border instead of tripping B's district.

Only districts that measure their own current take part. The feature is per district and optional: a district whose command station reports its track current (DCC-EX with current-sensing motor drivers) can switch it on. A district without current monitoring (boosters only, or a command station that does not report current) does not take part: trains enter it as in sections 4 and 5. Hive-Rail does not estimate currents it cannot measure.

Measuring.

  • Current: DCC-EX's <JI> gives each track's current in mA; the node reads it about once a second over its link and smooths it over a few seconds (sound decoders, motor load and starting trains make DCC current jumpy). Only the tracks that feed the layout count (MAIN), not the programming track.
  • Budget: each track's trip limit (<JG>) times a headroom factor set for the district, for example 80 %: a district never plans to run up to its trip.
  • The train's draw: what B needs to know before the train is in. B measures it when a train comes in, as the rise in its current, and keeps it with the train's record, which travels with the train. Until a train has been measured once, B admits it when the district is below the budget by a margin set for the district (for example 0.5 A).
  • Current and budget are in the node's heartbeat, so the dispatcher shows 1.8 / 4.0 A per district, coloured near the budget.

Deciding. B admits a train when current now + the train's draw ≤ budget. A held train is released only when the current is clearly below that (hysteresis), so the boundary signal does not flicker as the current wobbles. While B holds a train, the dispatcher shows why: IC 2001 held at the border: Windows at 3.6 of 4.0 A.

The queue. B may have trains waiting at several borders. It admits them in one of two orders, set per meeting:

Order Rule
FIFO (default) First proposal first
Priority by train type For example express, then regional, then freight, then shunting moves; the types and their order come from the meeting's timetable. With aging: a waiting train gains priority over time, so a freight train is not held forever behind a stream of expresses

Safety. Admission is about capacity, not safety: the short-circuit trip and the transfer zone keep the layout safe without it. If B loses its current reading (the link to DCC-EX is down, or DCC-EX stops reporting), B admits as without the feature and warns the dispatcher; a district can be set to hold instead.

Build order. (1) The node reads <JI> and <JG> and puts current and budget in its heartbeat; the dispatcher shows them; the simulated command station simulates a current. (2) The layout simulator: trains draw current, districts have budgets, border signals hold trains with the FIFO or priority queue, and the web view shows each district's current and the held trains. (3) The rule in the node's real handover, once train tracking (Phase T) and the transfer zone (Phase 5) are built.

11. Open questions

# Question Notes
T1 Should the start-up RESET be unconditional? If B crashes right after a takeover, an unconditional RESET puts a train already handed to B back on A's feed. Option: reset only when the saved state and the neighbour both say the block is empty; otherwise hold the relay, keep signals at Stop and wait for the operator
T2 How does a train travelling from B to A use the zone? The handshake above covers A to B. The reverse direction needs its own sequence: SET while the block is empty, RESET once the train is fully inside
T3 How is train_clearance_delay_ms chosen? Replaced: the zone switches when the beam at the entrance is restored (the last car has passed) and the source edge block is clear, so no delay depends on speed or train length. Open: does a 300 ms beam_clear_ms cover the gaps between cars at crawling speed? Bench test
T4 IR sensor reliability under exhibition lighting Bench test
T5 Current admission: how fast must the current reading be, and how much smoothing, so a train is held in time but not by a moment's peak? Simulator first, then a DCC-EX district under load
T6 Current admission: which train types and order, and how fast does aging raise a waiting train's priority? Decide with FREMO timetable planners

Clone this wiki locally