Push the DCC link view, and stop commanding track power at boot - #181
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Three changes to the command-station link, all out of one bench session on the live layout.
DCC_LINKevent onLayoutEvent/ServerMessage, emitted fromLayoutService.applyDccLinkEffectswhenevereffects.healthChanged, with a matching case in theuseLayoutSocketreducer.handleDccConnectionChangeprobes with<s>where it used to send
<1>.formatTrackPoweremits<1 MAIN>/<0 MAIN>.Why 1 — the badge was frozen (#179)
DccLinkViewreached the browser only inside the openingSTATE_SNAPSHOT. Nothing pushed itafterwards, so the track-power badge sat at whatever was true when the page loaded. From the
bench journal, 2026-08-25:
<0>→<p0 MAIN><0>→<p0 MAIN><1>→<p1 MAIN>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.
ControlVieweven carried a comment saying "the socket delivers the samefact a moment later", describing a mechanism that was never built.
The whole view, replaced wholesale, not a power field:
responsive, the latchedDccLinkFaultand the station identity were equally frozen, so a link fault raised after pageload was invisible too.
The POST reply is deliberately still ignored. It is tempting to apply
POST .../dcc-link/power's body for instant feedback, butsetTrackPowerwrites<1 MAIN>then
<s>and both resolve when the write flushes — the<p1 MAIN>arrives a round triplater. 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.
noteIdentitysethealthChangedunconditionally, so the 5 s
<s>reply flagged a health change forever. Harmless while nothingwatched 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).observedAtmovingis 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 acold 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.shrestartsthe 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 reconnectactually raises — what the station's power state is — and that was always where
mainPowerOncame from, since #149's<1>was followed by an<s>for exactly this reason.The layout comes up dark,
mainPowerOnisfalse, and #149's gating refuses routes andautomation until an operator presses On. That gating is the half of #149 that was doing the
safety work.
setTrackPoweris untouched in every other respect: still in the service rather than theadapter, 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>isDCCEX_TRACK_ALL, and PicoDCC implements it as both tracks. The programmingtrack 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.cppmatchesMAIN/PROG) andanswers
<p1 MAIN>alone, so D8's positional correlation is unaffected — tidier, in fact, onereply per command instead of two.
progPowerOnstays observed through the<p? PROG>inevery
<s>reply; what has gone is the orchestrator ever writing it.SimulatedDccAdaptermirrors this.
Deliberately not done
over a fresher pushed one.
progPowerOncontrol. Do not command track power at start-up, and command only the main track #180 removes the accidental write; it does not add a deliberateone. That belongs with the service-mode feature.
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 darkacross 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;
progPowerOnunmoved),case 4 added (every move of the view is pushed, and a repeat of a held state pushes nothing).
useLayoutSocket.test.ts—DCC_LINKreplaces the whole view,responsiveandrestartCountincluded.control-view.spec.ts— the badge does not follow the POST body (the mock deliberatelyanswers "still on") and does follow a pushed
DCC_LINKframe. Needed a newpushServerMessagehelper: the e2e mock socket could only set the opening snapshot, which isprecisely 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— assertsMAINis named andPROGnever appears.Docs
docs/dcc-link.md— D10 rewritten (never restore except on an operator's ask; main trackonly), 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.mdM17,docs/current-state.md,README.mdandCLAUDE.md's index, Traps and Open limits all follow.Closes #179
Closes #180