Skip to content

Trust store self-repair, preset sync between phone and computer, group join while waking - #599

Merged
JRpersonal merged 7 commits into
mainfrom
fix/2026-08-13-sweep
Aug 14, 2026
Merged

Trust store self-repair, preset sync between phone and computer, group join while waking#599
JRpersonal merged 7 commits into
mainfrom
fix/2026-08-13-sweep

Conversation

@JRpersonal

Copy link
Copy Markdown
Owner

Sweep of the day's mail and GitHub reports.

A speaker that stopped trusting the internet

An ST20 field bundle had every https station and the Spotify engine dying
seconds apart on the identical x509: certificate signed by unknown authority,
both hosts chaining to a root every healthy box trusts. The bootstrap builds
the box's trust store by copying the firmware's bundle to /tmp, appending STR's
root and bind-mounting the result. It copied BEFORE unmounting an overlay left
by an earlier run, and with that mount active source and destination are the
same inode, so the copy could leave the file empty. What got mounted was STR's
root and nothing else.

Two commits, because the boot-script fix cannot reach the speakers already in
that state: an update replaces the agent binary and nothing else, and the NAND
copy of the boot script is only refreshed from a physical USB stick.

  • 6daccad stops the boot script producing it, on both install paths.
  • df3a351 makes the agent repair it, which is what an update can deliver.
    Every failure path leaves the firmware's own bundle live rather than the
    broken overlay, and if the firmware bundle is empty too the overlay goes
    back, so a box never ends up with less than it had.
  • 8202845 reports what the box actually trusts in the diagnostic, since
    from a bundle this was indistinguishable from a station-side problem.

Verified on an ST10: field state reproduced exactly (one certificate, every
handshake refused), agent restarted, store rebuilt 0 -> 158 public roots, and
both hosts from the reporter's log answered again. A natural boot afterwards
reports both stores healthy and touches neither.

Presets out of step between the phone and the computer

Reported on discussion #597 with a screenshot: tile 4 shows the station it used
to hold and says it is playing, while the line above it plays the one just
assigned from the phone. Both screens read the presets once and kept them.
Both now re-read in the background and redraw only on an actual difference, at
a cadence deliberately slower than the transport poll and against the agent's
own store rather than the Bose firmware.

Also here

Refs #597

🤖 Generated with Claude Code

JRpersonal and others added 7 commits August 13, 2026 19:36
… added to

Zone forming corrects a member's deviceID from a live /info read on the member
itself, because a two-chip chassis announces its wlan0 (SMSC) MAC over mDNS and
that is not the SoundTouch ID the firmware keys /setZone on. When that read did
not answer in time the correction was skipped and the caller's value went into
the group unchanged, so the master enrolled a member nobody answers for: its own
zone reads back one member short, and the speaker wakes up showing "Select a
source".

Two field bundles on the same day show it, one of them twice within half a
minute. On a seven-speaker fleet the correction fired for .26 at 18:38:10 and
did not fire for that same speaker at 18:38:35, when its :8090 was busy waking,
which put that box AND one other into the group under their wlan0 MAC:
requestedSlaves=4, verified=2, missing exactly those two addresses. The Wave in
issue #544 is the same shape, right after its OTA.

So remember, per member IP, what that speaker's firmware last said about itself,
and fall back to that instead of to the caller when it cannot be asked right
now. The cache is only ever written from a firmware answer, so an id supplied by
a caller can never get into it.

Release-Note: fix(app): a speaker that is still waking up now joins the group it was added to

Refs #544

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tart

The bootstrap builds the box's certificate trust store by copying the
firmware's own bundle to /tmp, appending STR's root CA and bind-mounting the
result over the original. It copied BEFORE unmounting an overlay left by an
earlier run, and with that mount still active source and destination are the
same inode: the copy reads and writes one file and can leave it empty. What
then gets mounted is STR's root and nothing else, and the box stops trusting
every public certificate.

That failure is silent and total. An ST20 field bundle from today has every
https station and the Spotify engine dying seconds apart on the identical
"x509: certificate signed by unknown authority", both hosts chaining to
DigiCert Global Root G2, which every healthy box trusts. From the outside it
reads like a station problem.

Unmount first, so the copy always reads the pristine firmware bundle. Install
the overlay only once it demonstrably still carries public roots: a box that
trusts the internet but not us still plays radio and Spotify, and the agent
re-appends its root to the live overlay anyway, while the reverse leaves the
speaker unable to reach anything. Report the outcome through log() so the next
bundle says which of the two happened.

The overlay name is a plain counter now. One hex digit of an md5 collides one
time in sixteen, and a collision made both trust stores share a single file.

Release-Note: fix(speaker): the speaker keeps trusting internet stations and Spotify after a restart

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A box whose overlaid trust store lost the vendor's public roots fails every
outbound TLS handshake with the same terse "certificate signed by unknown
authority", https radio and the Spotify engine alike. From a bundle that error
is indistinguishable from a station-side problem, because nothing recorded what
the box trusts.

tls_trust reports, per trust store path, its size, how many certificates are in
it, whether STR's own root made it in, and a public_roots_missing verdict. A
healthy box shows a three-digit count; one certificate means the overlay holds
our root and nothing else, which is the broken state above. dns_status already
covers the neighbouring fault class where the box cannot resolve at all; this is
the one where it resolves and connects fine and then refuses what it gets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A slot recall was the only play path in the app that logged nothing on any
exit. When one failed there was nothing to read on either side: the speaker's
log has no incoming request, because the app never got that far, and the app's
log was silent too. A bundle taken right after a failing preset press was
therefore unreadable (#582: two tiles showing "speaker is still starting" over
an idle speaker log).

PlaySlot now logs accepted, rejected and failed, and the readiness probe logs
its give-up with the ports it tried, how long it waited, and the last error or
body it saw.

Same file, unrelated: probeSTR overwrote a good deviceID with an empty one from
/info unconditionally, while every neighbouring field was guarded against
exactly that. A box with an empty deviceID drops out of every zone operation,
since members are addressed by it.

Refs #582

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…self

The previous commit stops the boot script from ever building a trust store
that holds STR's root and nothing else. It cannot help the speakers that are
already in that state: an over-the-air update replaces the agent binary and
nothing else, and the NAND copy of the boot script is only ever refreshed
from a physical USB stick. Those boxes would re-break at every boot for good,
however many updates they took, and the owner has no way to tell because the
symptom is every https station and Spotify failing at once, which reads like
a station problem.

So the agent repairs it. A minute after start, past the window in which the
boot script mounts its own overlay, it counts the certificates each trust
store actually serves. A store that carries public roots is left untouched,
which is every healthy box and costs two file reads. A store that carries
none gets the overlay taken off, which is what reveals the pristine firmware
bundle, rebuilt from it with our root appended, and mounted back.

Every failure path leaves the firmware's own bundle live rather than the
broken overlay: a speaker that trusts the internet but not STR still plays
radio and Spotify and gets our root re-appended anyway, while the reverse
leaves it unable to reach anything. If the firmware bundle turns out to be
empty too, the overlay goes back, so the box never ends up with less than it
had. The check is two bounded attempts, not a poll, because this corruption
is created at boot and never mid-run.

Verified on an ST10: the field state reproduced exactly (one certificate,
every handshake refused), agent restarted, store rebuilt 0 -> 158 public
roots, and both hosts from the reporter's log answered again -
apresolve.spotify.com and the Deutschlandfunk stream. A natural boot after
that reports both stores healthy and touches neither.

/api/debug/state now reports the repair next to the trust store snapshot,
because afterwards a repaired box and a box that was never broken read
identically.

Release-Note: fix(speaker): a speaker that stopped trusting internet stations and Spotify now repairs itself on the next update

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rc.local logged the NAND override decision in German. Every other
developer-facing line in this repository is English, and this one shows up in
each diagnostic bundle's boot_log, where it is read by people who do not speak
it. Nothing parses these strings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the other way round

Both screens read the presets once and then kept them. A key reassigned from
the phone remote left the desktop tile showing the station it used to hold,
next to a status line already naming the new one, and the same happened in
reverse. Reported with a screenshot on discussion #597: tile 4 reads
"181.FM - Classical Guitar" and says it is playing, while the line above it
plays "Radio Art - Guitar Jazz".

Both now re-read the list in the background and redraw only when it actually
differs, so the common case costs one comparison. The cadence is 15 s and
deliberately slower than the transport poll: this only has to catch a person
pressing save somewhere else. The read goes to the agent's own preset store
rather than the Bose firmware, so it adds nothing to the :8090 load that the
status poll is rationed for.

Two things fall out of re-reading in the background that did not matter when
the list was read once. A read that fails or times out must not blank a grid
that is already showing presets, so on the phone a null answer now keeps what
is on screen, the way the desktop already guarded its own empty result. And
the tapped tile keeps its highlight across a redraw, since losing the marker
mid-song would read as the speaker forgetting what it is playing.

Verified against the page the speaker actually serves: first load builds the
grid, an unchanged re-read leaves it alone, a station renamed elsewhere
reaches the tile while the playing marker survives, a failed read changes
nothing, and a genuinely empty speaker still draws six empty tiles.

Release-Note: fix(app): a preset saved on the phone now appears on the computer straight away, and the other way round

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JRpersonal JRpersonal added bug Something isn't working go Go-related changes frontend Desktop app frontend (Wails Vite/JS/CSS) changes labels Aug 13, 2026
continue
}
s.logger.Warn("zone: member did not answer its firmware /info, using the deviceID it reported earlier instead of the caller's",
"ip", slaves[i].IP, "supplied", slaves[i].DeviceID, "cached", cached, "err", err)
continue
}
s.logger.Warn("zone: member did not answer its firmware /info, using the deviceID it reported earlier instead of the caller's",
"ip", slaves[i].IP, "supplied", slaves[i].DeviceID, "cached", cached, "err", err)
@JRpersonal
JRpersonal merged commit 8cd9b97 into main Aug 14, 2026
12 checks passed
@JRpersonal
JRpersonal deleted the fix/2026-08-13-sweep branch August 14, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working frontend Desktop app frontend (Wails Vite/JS/CSS) changes go Go-related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants