Skip to content

Push the DCC link view, and stop commanding track power at boot - #181

Merged
bazauto merged 1 commit into
mainfrom
fix/dcc-link-push-and-power-defaults
Aug 26, 2026
Merged

bazauto merged 1 commit into
mainfrom
fix/dcc-link-push-and-power-defaults

Conversation

@bazauto

@bazauto bazauto commented Aug 26, 2026

Copy link
Copy Markdown
Owner

What

Three changes to the command-station link, all out of one bench session on the live layout.

  1. The DCC link view is pushed to the browser. A new DCC_LINK event on LayoutEvent /
    ServerMessage, emitted from LayoutService.applyDccLinkEffects whenever
    effects.healthChanged, with a matching case in the useLayoutSocket reducer.
  2. Nothing commands track power at start-up. handleDccConnectionChange probes with <s>
    where it used to send <1>.
  3. Only the main track is ever commanded. formatTrackPower emits <1 MAIN> / <0 MAIN>.

Why 1 — the badge was frozen (#179)

DccLinkView reached the browser only inside the opening STATE_SNAPSHOT. Nothing pushed it
afterwards, so the track-power badge sat at whatever was true when the page loaded. From the
bench journal, 2026-08-25:

Time Operator Wire What the badge said
09:06:19 pressed Off <0> → <p0 MAIN> still "on"
09:06:41 pressed Off again <0> → <p0 MAIN> still "on"
09:06:52 pressed On <1> → <p1 MAIN> "off" — over live rails
09:07:01 … 09:43:04 pressed On five more times acknowledged every time "off"

Every command went out and every one was answered; the backend was right throughout. The badge
only ever moved when the socket reconnected and re-snapshotted, which is what made it look
intermittently correct.

The snapshot-only posture was #148's, argued as "state, not a transition" — sound while the
only thing that moved the view was the station dying, and wrong the moment #149 gave an
operator a button. ControlView even carried a comment saying "the socket delivers the same
fact a moment later"
, describing a mechanism that was never built.

The whole view, replaced wholesale, not a power field: responsive, the latched
DccLinkFault and the station identity were equally frozen, so a link fault raised after page
load was invisible too.

The POST reply is deliberately still ignored. It is tempting to apply
POST .../dcc-link/power's body for instant feedback, but setTrackPower writes <1 MAIN>
then <s> and both resolve when the write flushes — the <p1 MAIN> arrives a round trip
later. That body is the link as it stood before the station answered, which is D12 applied to
our own API. The route's doc comment claimed otherwise and has been corrected.

One supporting change makes the push affordable. noteIdentity set healthChanged
unconditionally, so the 5 s <s> reply flagged a health change forever. Harmless while nothing
watched the flag; a broadcast to every browser on a five-second tick once something did. It now
flags only a changed identity, or a restart (which moves restartCount). observedAt moving
is not a health change. This also stops Safe-Stop being re-evaluated every five seconds for as
long as the layout is up.

Why 2 — a deploy should not energise the layout (#180)

#149 sent <1> when the serial link came up, because PicoDCC's tracks boot unpowered and a
cold start otherwise ran throttle commands into dead rails while reporting healthy. Sound as
far as it went — but the link comes up on every process start, and deploy/deploy.sh restarts
the unit, so in service it meant the rails went live because somebody pushed a build.

D10's own argument against automatic restoration applies to a service restart as much as to a
fault: a decoder that loses the DCC signal falls back to DC, and DC on a powered main track is
full speed.
"A cold start with an operator present" described the bench, not a deployed
system.

Connect now sends <s>. It costs the same round trip and answers the question a reconnect
actually raises — what the station's power state is — and that was always where
mainPowerOn came from, since #149's <1> was followed by an <s> for exactly this reason.
The layout comes up dark, mainPowerOn is false, and #149's gating refuses routes and
automation until an operator presses On. That gating is the half of #149 that was doing the
safety work.

setTrackPower is untouched in every other respect: still in the service rather than the
adapter, still recording the command before writing it (D8), still probing afterwards.

Why 3 — the programming track is not ours to switch (#180)

A bare <1> is DCCEX_TRACK_ALL, and PicoDCC implements it as both tracks. The programming
track belongs to a service-mode process that does not exist here yet, and when it does it will
want to own its own power rather than find it switched underneath by an operator turning the
running lines on.

PicoDCC already parses the track argument (pico_dccexpacket.cpp matches MAIN / PROG) and
answers <p1 MAIN> alone, so D8's positional correlation is unaffected — tidier, in fact, one
reply per command instead of two. progPowerOn stays observed through the <p? PROG> in
every <s> reply; what has gone is the orchestrator ever writing it. SimulatedDccAdapter
mirrors this.

Deliberately not done

Tests

npm test — 1500 backend across 84 files, 328 frontend across 27 files, all passing.
npm run test:e2e — 71 passing.
npm run lint — clean.

New and rewritten coverage:

  • tests/scenario/track-power.scenario.test.ts — case 1 rewritten (a dark station stays dark
    across a restart, and the route is refused), case 2 added (connect observes, so the state is
    known before the first route is judged), case 3 added (MAIN only; progPowerOn unmoved),
    case 4 added (every move of the view is pushed, and a repeat of a held state pushes nothing).
  • useLayoutSocket.test.ts — DCC_LINK replaces the whole view, responsive and
    restartCount included.
  • control-view.spec.ts — the badge does not follow the POST body (the mock deliberately
    answers "still on") and does follow a pushed DCC_LINK frame. Needed a new
    pushServerMessage helper: the e2e mock socket could only set the opening snapshot, which is
    precisely why this suite could not see Track power (and the whole DCC link view) never updates in the UI after page load #179.
  • dccWireFormat.test.ts — asserts MAIN is named and PROG never appears.

Docs

docs/dcc-link.md — D10 rewritten (never restore except on an operator's ask; main track
only), D14 rewritten from "<1> is sent by the service on connect" to "connect observes power,
it does not assert it", D17 added for the push. docs/liveness.md M17, docs/current-state.md,
README.md and CLAUDE.md's index, Traps and Open limits all follow.

Closes #179
Closes #180

The track-power badge never updated after page load: DccLinkView reached the
browser only in the opening STATE_SNAPSHOT, so an operator switched power off
and went on being shown "on", then switched it back on and was shown "off" over
live rails for half an hour. A DCC_LINK event now carries the whole view
whenever it moves — responsive, the latched fault and the identity were equally
frozen. (#179)

Connect no longer sends <1>. The link comes up on every process start and a
deploy restarts the unit, so #149's power-on-at-connect meant the layout came to
life because somebody pushed a build. It probes with <s> instead: the state is
known rather than assumed, and #149's gating refuses routes until an operator
presses On. (#180)

The power command is <1 MAIN> / <0 MAIN>. A bare <1> is DCCEX_TRACK_ALL and
PicoDCC implements it as both tracks; the programming track belongs to a
service-mode process that does not exist yet. progPowerOn stays observed. (#180)

Closes #179
Closes #180

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bazauto
bazauto merged commit 8b2eb03 into main Aug 26, 2026
1 check passed
@bazauto
bazauto deleted the fix/dcc-link-push-and-power-defaults branch August 26, 2026 16:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant