Documentation for controlling and preserving Bose SoundTouch devices
This document captures the use cases, community findings, and feasibility analysis
for adding a Telnet/port 17000 migration path to soundtouch-service as a
peer of the existing XML and DNS-based methods. The /etc/hosts method stays
deprecated and is intentionally kept off the visible UI options.
Sources — community discussion synthesised from gesellix/Bose-SoundTouch#221, gesellix/Bose-SoundTouch#236, scheilch/opencloudtouch#167, deborahgu/soundcork#228, deborahgu/soundcork#141, the post-EOS walkthrough PDF in
docs/, Bose SoundTouch Telnet Probing thread, and flarn2006’s blog post on hacking SoundTouch.
The two currently shipped methods both have hard preconditions that block real users:
| Method | Preconditions | Failure modes seen in the wild |
|---|---|---|
XML (SoundTouchSdkPrivateCfg.xml) |
SSH/root access — needs remote_services USB unlock first |
Some firmware revisions (e.g. SA-5, ST520, latest ST Portable) refuse the USB unlock entirely; remote_services on was removed from the telnet command set in firmware 7.x and later. |
DNS (resolv.conf priority hook) |
SSH/root access; service must own port 53 on the LAN gateway | Won’t fit users behind ISP routers they can’t reconfigure; still requires the device to be SSH-reachable to write the hook. |
The community has demonstrated a third path that needs no SSH at all: the device’s built-in diagnostic Telnet shell on TCP port 17000 accepts configuration commands that change exactly the same fields the XML method would.
| Reporter | Hardware | Outcome |
|---|---|---|
foob61451 (#221) |
ST 10, ST 20 (non-rooted) | All four URLs persisted via sys configuration …; envswitch boseurls set … survived sys reboot. |
bveenker (#221) |
Wave III | URLs accepted; presets work after pairing via /setMargeAccount (see §3). |
stephan48 (#221) |
Wave IV | Telnet:1700 + USB stick remote_services did not work; port 17000 telnet worked for all four URLs. |
mcdona1d (#141) |
ST 20, ST 300 | Confirmed working with sys configuration … + envswitch … + sys reboot. |
TJGigs (#228) |
ST 20 ×2, ST 10 | Wraps telnet:17000 into an admin “Smart Inject” tool; uses sys reboot over telnet to nudge devices. |
So the method is plausible across at least ST 10/20/300 and Wave III/IV on the most common firmware that survived the EOS cut, without the USB unlock dance that newer firmware refuses.
For a broader catalogue of every telnet command the community has documented across firmware eras (the
key,network,sys,envswitch,getpdo,scm,ws,swupdate, and shell-unlock families), see TELNET-COMMAND-REFERENCE.md. This section only lists the subset our migration actually drives.
The sequence we send for soundtouch-service (community-validated in #221, #141):
sys configuration bmxRegistryUrl http://<service-host>:8000/bmx/registry/v1/services
sys configuration statsServerUrl http://<service-host>:8000
sys configuration margeServerUrl http://<service-host>:8000
sys configuration swUpdateUrl http://<service-host>:8000/updates/soundtouch
envswitch boseurls set http://<service-host>:8000 http://<service-host>:8000/updates/soundtouch
getpdo CurrentSystemConfiguration
sys reboot is not part of this sequence. The migration flow only writes
configuration — the reboot is user-initiated via the existing reboot button in
the web UI, mirroring what XML/DNS migration already does. See §6.2 for how
that button gains a ?method=ssh|telnet selector.
Three important details from the discussion:
sys configuration alone is not enough. stephan48 reported that
without the envswitch boseurls set … line his typo in bmxRegistryUrl was
silently restored on reboot — i.e. there is a parallel “envswitch” persistence
layer that wins on next boot if you don’t also write to it. We must always
issue both.soundtouch-service. We mount the marge
endpoints at the root of port 8000, matching what the existing XML
migration writes (Manager.migrateViaXML in pkg/service/setup/setup.go
sets MargeServerUrl: targetURL without any suffix). Some community
recipes appended /marge because they were targeting
deborahgu/soundcork, which
routes marge under that sub-path. For our service: bare URL. For users
redirecting to soundcork: append /marge to both margeServerUrl and
the first argument of envswitch boseurls set.OK
response before sending the next one (foob61451’s explicit warning).envswitch accountid set <numeric-id> was reported by bveenker (#221) as an
in-band equivalent to the HTTP /setMargeAccount call, useful when the
/setMargeAccount endpoint is missing on the firmware (see §3).
<deviceIP>:17000 answers (no auth) on devices we care
about.getpdo CurrentSystemConfiguration —
prints the URLs after the changes have been applied so we can verify before
rebooting.sys reboot is the trigger that re-reads both layers.resolv.conf
redirection collides with the device’s TLS validation unless our root CA is
trusted on the device), telnet alone won’t cover it. This is fine for our
default flow, which uses plain http:// URLs to the service’s port 8000.Sources.xml (third-party
account credentials) — that still requires SSH, but for a migration we don’t
actually need it./setMargeAccount problem (issue #236, #228)A factory-reset speaker has an empty <margeAccountUUID/> in :8090/info. The
marge endpoints fail with 502 / unhandled until that field is populated, which
is why several users (#221, #236) saw everything except AUX broken after
migration:
POST http://<deviceIP>:8090/setMargeAccount
Content-Type: application/xml
<PairDeviceWithAccount>
<accountId>1234567</accountId>
<userAuthToken>soundcorkdoesntcare</userAuthToken>
</PairDeviceWithAccount>
The values are not validated by the local service, so any numeric accountId
will work — soundcork’s runbook (#228) literally calls the token
soundcorkdoesntcare to make the point.
There are three independent failure modes observed:
| Symptom | Cause | Detection |
|---|---|---|
| Endpoint returns 404 / “not implemented” | Newer firmware (e.g. some BST20 Portable, latest ST Portable) drops the endpoint entirely. | GET /supportedURLs does not list /setMargeAccount in <URL location="…"/>. |
| Endpoint hangs (no response / socket stays open) | “Broken state” the user explicitly called out — endpoint advertised, but handler is wedged. | Caller has to time out; we currently have no timeout, so the request appears to hang the migration UI indefinitely. |
POST /marge/streaming/support/power_on → 502 unhandled (#236) |
Device keeps polling marge after migration but no margeAccountUUID was ever assigned, so all subsequent calls fail. |
:8090/info shows <margeAccountUUID/> empty after reboot. |
Per the user’s brief, the migration logic must:
GET http://<deviceIP>:8090/supportedURLs and check whether
/setMargeAccount is in the list before trying to POST it.envswitch accountid set <id> over the same pkg/telnet connection used
for the URL flip. Reboot stays a user-initiated action (§6.2).<id> comes fromThe device’s current account ID is already discoverable through endpoints we control:
GET :8090/info returns <margeAccountUUID>…</margeAccountUUID>. If it
is non-empty the device is already paired — reuse that ID, do not
reassign. Our local marge accepts any ID, so the existing one is fine.DataStore.ListAccounts() so a user can re-attach a fresh device to an
account that already has presets/recents/sources.envswitch accountid get is plausible by
symmetry with envswitch accountid set (#221) but is not yet confirmed
across firmwares. We will probe it during preflight; if it returns a value
we cross-check it against :8090/info and warn on mismatch.This means the user is never forced to invent a number — the common path is “the device already has an ID, reuse it” — and the manual/randomize controls only show up when the device is genuinely fresh.
The diagnostic shell is gated by firmware build and product family. Anecdotally:
local_services on) but
no remote_services on and no SSH on FW 9.0.43.23466 (#141).Because of this, we cannot assume port 17000 is reachable. The migration flow must:
<deviceIP>:17000, with a tight timeout
(≤2s). A successful TCP handshake is necessary but not sufficient — some
hardened firmware closes the port immediately.getpdo CurrentSystemConfiguration
and look for any non-empty response. If the device replies “Command not
found” we abort and suggest XML or DNS instead.This is a feasibility check only; no code is written yet.
“Telnet” on port 17000 is effectively a line-oriented plain-TCP shell. The
device prints a small prompt (-> in the SA-5 captures from #141) and reads
newline-terminated commands. There is no real Telnet option negotiation
(no IAC/DO/WILL exchanges visible in the wild captures), so we don’t
need golang.org/x/crypto/ssh-class machinery.
A minimal client is just net.DialTimeout("tcp", host+":17000", 2*time.Second) +
bufio.Scanner + time.Time-based deadlines on Conn. No third-party Telnet
library is needed; github.com/reiver/go-telnet would be overkill and adds
maintenance surface for no benefit. This matches the project’s KISS principle
in docs/CLAUDE.md §3.
net.Dial over TCP works identically on Windows, macOS, Linux and (with
limitations on listening) WASM. WASM-side: soundtouch-service runs server-side
anyway, so this only matters for soundtouch-cli, where TCP dial works in any
target other than browser-WASM — an acceptable carve-out documented separately.
Each migration is a single goroutine driving one device. The client must:
/setMargeAccount requirement);OK response so we don’t
half-write configuration;We can test without a real speaker by spinning up a net.Listen("tcp", "127.0.0.1:0")
in the test, scripting it to consume our commands and emit canned OK/error
responses. That gives us deterministic coverage for:
envswitch … → fallback path exercised,The repo already follows the “real device responses preferred, mock servers
otherwise” rule (see docs/CLAUDE.md §1, §8). The tests above are the mock-server
half of that pattern.
The protocol client is a standalone package, not buried inside
pkg/service/setup, so it can be reused from CLI tools, future setup wizards,
and tests without dragging the migration manager in:
pkg/telnet/ # NEW reusable package
client.go # Dial / SendCommand / Probe / Close
client_test.go # mock-server tests against a net.Listen
pkg/service/setup/
telnet_migration.go # NEW thin wrapper that imports pkg/telnet
# and runs the URL config sequence
marge_pairing.go # NEW /setMargeAccount probe + post + telnet
# `envswitch accountid set` fallback
setup.go # add MigrationMethodTelnet const + case
UI plumbing is pkg/service/handlers/web/index.html (option list) and
pkg/service/handlers/web/js/script.js (toggleMigrationMethod()). The
deprecated hosts option is already hidden from the dropdown when we ship
this; we just add a telnet option next to xml/resolv.
Feasible and small. Estimated scope: ~200 lines of client code in
pkg/telnet, ~300 lines of tests, plus a MigrationMethodTelnet branch in
Manager.MigrateSpeaker, plus the preflight probe described in §4 and the
/setMargeAccount guarding described in §3.
:8090/info first; if margeAccountUUID is non-empty it is reused.
Otherwise the UI offers (a) pick from DataStore.ListAccounts(),
(b) manual entry validated as 7 numeric digits, (c) a “Generate” button
that randomizes a 7-digit number and re-rolls on collision.sys reboot itself. Reboot stays user-initiated via the existing
reboot button in the web UI, the same way XML/DNS migration already works.
That button’s endpoint (POST /setup/reboot/{deviceId},
Manager.Reboot(deviceIP)) gains an optional ?method=ssh|telnet query
parameter; default stays ssh so existing behavior is preserved. The
button itself uses a plain confirm() dialog before firing.See §9 for the as-shipped state. Section 7 below records the original forecast; the wizard grew larger during implementation and §9 documents what actually landed.
pkg/telnet — sibling of pkg/ssh, line-oriented
TCP client with Dial, SendCommand, Probe, Close, all deadline-driven.
No external dependencies, usable from CLI, service, and tests.MigrationMethodTelnet = "telnet" constant in pkg/service/setup/setup.go
plus a migrateViaTelnet branch in Manager.MigrateSpeaker.pkg/service/setup/telnet_migration.go orchestrating the URL
configuration sequence (§2.1) on top of pkg/telnet. Configuration only —
no sys reboot here.pkg/service/setup/marge_pairing.go with PairAccount(deviceIP, id):
probes /supportedURLs, time-bounded POST /setMargeAccount, falls back to
telnet envswitch accountid set <id> on missing/wedged endpoint.Manager.Reboot and HandleRebootDevice gain a method selector —
signature changes to Reboot(deviceIP string, method RebootMethod) (string, error)
with RebootMethodSSH (default, today’s behavior) and RebootMethodTelnet
(sends sys reboot over a fresh pkg/telnet connection). Handler reads
?method=ssh|telnet from the query string.MigrationSummary gains TelnetReachable, TelnetBanner,
TelnetCommandsAccepted, SetMargeAccountSupported, CurrentAccountID,
KnownAccountIDs so the UI can show preflight outcomes and offer reuse.web/index.html dropdown gets a telnet option (greyed out when
preflight fails) and a new pane for picking/entering/randomizing a 7-digit
account ID when :8090/info reports an empty margeAccountUUID. The
existing reboot button gets a method selector (radio or dropdown) wired to
the new query param, with confirm() before firing. The legacy hosts
option stays out of the dropdown (deprecated).What follows is the current best read on which devices our migrateViaTelnet
flow handles end-to-end, derived from the same six sources catalogued in
TELNET-COMMAND-REFERENCE.md plus the issue
threads cited above. This is migration-outcome perspective; for per-command
availability see the reference doc.
All on the firmware-27.0.6 family, which is what survived through Bose’s end-of-service cut. Multi-reporter agreement on every row.
| Device | Reporter(s) | Source | Confirmed |
|---|---|---|---|
| ST 10 | foob61451, TJGigs | #221, #228 | All four URLs persist; envswitch boseurls set survives sys reboot |
| ST 20 | foob61451, mcdona1d, TJGigs | #221, #141, #228 | Same; multiple independent reports |
| ST 300 | mcdona1d | #141 | sys configuration + envswitch + sys reboot round-trip |
| Wave III | bveenker | #221 | URLs accepted; presets work after pairing fallback (§3) |
| Wave IV | stephan48 | #221 | Port-17000 path was the only one that worked — USB-stick unlock failed |
The exact sequence each reporter ran by hand is the sequence our migration sends (§2.1). So the migration’s happy path is exercised against five hardware variants in independent captures.
Migration of the URLs themselves works on these models, but
POST /setMargeAccount is missing or wedged on the firmware build, so
pairing has to go through the telnet envswitch accountid set <id> path
that setup.PairAccount already implements.
| Device | Reporter | Source | Why fallback is needed |
|---|---|---|---|
| ST Portable / FW 27.0.6 | jmosen | #236 | After migration: POST /marge/streaming/support/power_on → 502; <margeAccountUUID/> empty. Time-bounded HTTP path fails; envswitch fallback succeeds. |
| BST20 Portable (factory reset) | ubittner | scheilch/opencloudtouch#167 | <margeAccountUUID/> empty; /setMargeAccount not in /supportedURLs. HTTP path skipped entirely; only the telnet fallback works. |
Our preflight + abort-on-first-rejection design (TestMigrateViaTelnet_CommandNotFoundAborts)
means none of these scenarios leave a device half-configured. The user is
told what failed and pointed to the XML or DNS method.
| Device | Source | Likely cause |
|---|---|---|
| SA-5 (sound amplifier) on FW 9.0.43.x | soundcork#141 | FW 9.x has a different shell generation: -> prompt, local_services on, scm uboot_ver. sys configuration and envswitch are not documented as working there. Migration fails on command #1. |
| Recent ST Portable (post-27.0.6.46330) | #236 (indirect) | /setMargeAccount removal points to broader command-set shrinkage. If envswitch accountid set is also gone, both migration and pairing fallback fail; user is told to pair via the official Bose app before EOS, or use XML over SSH. |
| Device | Why unknown | What we’d want to confirm |
|---|---|---|
ST 30 (mojo) |
No concrete capture in any of the six sources | Almost certainly works — same FW family as ST 10/20/300 — but unverified |
| ST 520 / Home Cinema | USB-unlock reports failing (#141), no port-17000 capture either way | Whether sys configuration and envswitch are exposed at all |
| Wave Music System I/II | flarn2006-era hardware, not seen in 27.x reports |
Whether port 17000 is even open on those models |
S5 (the r/bose telnet-probing thread) lists only key, net, sys,
getpdo as command roots that don’t return “Command not found” on its
ST 10 / FW 27.0.6 — which would seem to rule out envswitch. But foob61451
on the same hardware/firmware ran envswitch boseurls set successfully
(#221).
The most plausible reading is that S5 is a non-exhaustive probe, not a
negative claim: the author writes “I’ve made some educated guesses and come
up with the following valid commands” and never says they tested
envswitch. We do not down-weight envswitch availability on the strength
of S5 alone — but if a real-device run ever shows envswitch rejected on
an ST 10, our preflight catches it, the migration aborts on the first
non-OK response, and the user gets a clear error rather than partial state.
What migrateViaTelnet does in each failure mode (verified by
pkg/telnet and pkg/service/setup unit tests):
| Failure | Outcome | Test |
|---|---|---|
| Port 17000 closed / TCP unreachable | Dial errors before any command is sent; UI shows the error; nothing persisted |
TestMigrateViaTelnet_DialFailureReturnsError |
sys configuration rejected (cmd #1) |
Sequence aborts; verification not sent; rest of commands not attempted | TestMigrateViaTelnet_CommandNotFoundAborts (envswitch variant — generalises) |
envswitch boseurls set rejected |
Sequence aborts; runtime-only sys configuration state reverts on reboot — no permanent damage |
TestMigrateViaTelnet_CommandNotFoundAborts |
| Verification mismatch (URLs not echoed back) | Loud “verification failed” error; live state may persist until reboot but UI never claims success | TestMigrateViaTelnet_VerifyMismatchFails |
/setMargeAccount 502 / hang |
5s connect + 12s total budget enforced; falls through to telnet envswitch accountid set |
TestPairAccount_FallsBackWhenHTTPReturnsServerError |
/setMargeAccount missing in /supportedURLs |
HTTP path skipped; goes straight to telnet envswitch accountid set |
TestPairAccount_FallsBackWhenSetMargeAccountMissing |
| Both pairing paths unavailable | Structured error: “use the official Bose app before EOS, or open SSH and use the XML method” | TestPairAccount_NoTelnetAndHTTPMissingReturnsClearError, TestPairAccount_TelnetCommandNotFoundReportsBothPaths |
The most useful next verification step is touching a real ST 30 and ST 520
— those are the two “expected to work” models with zero concrete captures.
Beyond that, every behaviour the doc predicts is exercised by the unit
tests in pkg/telnet and pkg/service/setup.
§7 forecast the surface area roughly; the wizard ended up larger. This section is the present-day map of the migration tab and the supporting backend pieces — kept appended rather than rewritten in place so the feasibility analysis above stays a faithful design record.
MigrationSummary now exposes the four mechanism-specific booleans
that checkIsMigrated writes individually:
XMLMigrated — parsed SoundTouchSdkPrivateCfg.xml’s URLs point at us.HostsMigrated — /etc/hosts carries Bose-domain redirects (the
deprecated method, kept detectable for legacy speakers).ResolvMigrated — the /etc/resolv.conf priority-nameserver hook
is in place (with CA trusted).TelnetMigrated — getpdo CurrentSystemConfiguration reports the
service hostname.IsMigrated is the OR. Plus IsPaired from the live
:8090/info.margeAccountUUID value.
The frontend opens with a state card that surfaces three orthogonal axes derived from these flags:
| Axis | Verdict semantics |
|---|---|
| URL Configuration | URL flip active → ✅; original Bose URLs + DNS hook active → ✅ (intercepted); original + no DNS → ❌ (not intercepted). |
| DNS Interception | None / resolv.conf hook / /etc/hosts (with deprecated badge). |
| CA / TLS | Local root CA installed yes/no. |
Plus a Preconditions row: remote_services persistence, account
pairing state, XML config backup presence. Action affordances
(Trust CA Now, Download CA cert) live inline next to their verdicts.
Replaces the XML method’s self/proxied/original dropdowns and the
duplicate URL inputs that used to live inside the telnet method pane:
Save as default (POSTs to
/setup/settings, preserving the *** secret-unchanged convention).validatePlanURLs), a Soundcork-mode checkbox that flips /marge
on margeServerUrl, and a Reset to defaults button.readPlanPairTarget) queues a pair step at
Apply when the input differs from the current account_id.IsMigrated is already true.The per-field URLs feed both XML and Telnet migrations via the
marge_url / stats_url / sw_update_url / bmx_url option family
(see §9.6). Live preview rewrites #planned-config purely client-side
on every keystroke — optimistic; the backend’s perspective gates the
write via §9.4’s pre-flight.
The <details> “Customize this migration” section replaces the old
migration-method dropdown with three independent radio groups:
/etc/resolv.conf hook.Each option carries a per-axis availability hint
((SSH unreachable), (already trusted), etc.) so users see why
an option is disabled. applyCustomPlan orchestrates the chosen
combination as a sequence of existing backend calls
(/setup/migrate?method=… for each flip/resolv step plus
/setup/trust-ca for standalone CA install, and the queued pair
step from §9.2). Resolv already bundles a CA install, so a redundant
standalone CA step is skipped. First failure aborts the rest.
Both Apply paths run a visible pre-flight panel before any backend
operation touches the speaker. Each check renders inline with the
🕐 / ⟳ / ✅ / ❌ / — idiom. On all-green the panel holds for ~700ms so
the success state registers, then auto-proceeds. On any failure the
panel surfaces Proceed Anyway / Cancel buttons; default is to
abort.
Checks:
| Check | When | Backend route |
|---|---|---|
| Backend summary re-check | always | GET /setup/summary |
| HTTPS connection from device | ssh_success && server_https_url |
POST /setup/test-connection |
| Reachability check (passive observer) | telnet_reachable && is_migrated (see §9.8) |
POST /setup/peer-probe |
| Round-trip skip explainer | telnet_reachable && !is_migrated — runs after reboot |
none (UI-side skip row) |
| DNS redirection from device | methods.includes("resolv") && ssh_success |
POST /setup/test-dns |
The HTTPS check uses use_explicit_ca=true so it exercises the trust
path even when CA install is part of the plan (i.e. forward-looking).
The reachability skip row is explicit (“neither SSH nor Telnet:17000
is reachable”) rather than silently dropped, per the user’s
“feedback always visible” requirement.
REMOVED — see §9.8. Empirical testing showed the swUpdate daemon caches its target URL at boot and ignores live config writes, so the active flip described below could never reach the running daemon. The section is retained as a historical record of what was tried; the running code uses the passive observer in §9.8.
The reachability gap §7 left open for USB-unlock-refusing speakers is
closed by Manager.RunTelnetRoundTripProbe
(pkg/service/setup/telnet_probe.go). Sequence:
getpdo CurrentSystemConfiguration to capture the
speaker’s current swUpdateUrl.probeRegistry (sibling field on
handlers.Server).sys configuration swUpdateUrl <targetURL>/probe/<token>
— runtime layer only, deliberately not envswitch boseurls set
…. The persistence layer keeps the original URL, so a reboot
heals the device naturally if our restore step fails.HTTP GET <deviceIP>:8090/swUpdateCheck — the cleanest
:8090 endpoint that triggers exactly one outbound to the
configured swUpdateUrl. Read-only on the cloud side
(doesn’t initiate an update); independent of margeAccountUUID
so it works on factory-reset speakers.telnetProbeTimeout (6s).sys configuration swUpdateUrl <originalURL> — deferred
restore so it runs even on the failure path.The new /probe/{token}[/*] catch-all on the root router signals the
matching channel when the speaker’s outbound lands. The response is
a minimal <swUpdateIndex/> so the speaker’s swUpdateCheck
doesn’t choke on a missing structure. The /* sub-path is
registered because some firmware appends a path component to the
configured swUpdateUrl.
| Addition | Where | Why |
|---|---|---|
applyURLOverrides(cfg, options) |
pkg/service/setup/setup.go |
Per-field literal marge_url / stats_url / sw_update_url / bmx_url overrides win over applyProxyOptions. Honored by both GetMigrationSummary and migrateViaXML. |
telnetURLsFromOptions(targetURL, options) |
pkg/service/setup/telnet_migration.go |
Same option family as above, plus envswitch arg derivation rule (arg1 = final Marge verbatim; the soundcork-suffix case drops out). |
Per-axis booleans + IsPaired + Warnings |
MigrationSummary |
Surfaces partial-state cells and SSH-XML ⇄ telnet-getpdo cross-check disagreements. |
parseGetpdoConfig |
pkg/service/setup/preflight_crosscheck.go |
Parses the Protobuf-text-like nested-block reply (key { text: "..." }) FW 27.0.6 actually sends, plus the legacy key=value shape as a tolerance path. |
peerObserver + RunPeerReachabilityProbe + /setup/peer-probe |
pkg/service/handlers / pkg/service/setup |
§9.8. Replaces the removed probeRegistry + RunTelnetRoundTripProbe + /setup/telnet-probe from §9.5. |
migrationOptionKeys allow-list |
pkg/service/handlers/migration_options.go |
Unknown query keys never reach the manager. Both XML mode keys and *_url keys are recognised. |
| Telnet client default timeouts: dial 4s, read 7s, write 3s, idle 600ms | pkg/telnet/telnet.go |
Bumped from the original 2s/5s/2s/400ms after observing transient i/o-timeout flakes on healthy speakers that recovered on retry. |
:8090/pushCustomerSupportInfoToMarge — flagged as a potential
“ask the device about itself” probe that could feed a richer
device-info pane (firmware build dates, hardware revisions). Not
implemented.The §9.5 round-trip probe was retired after empirical testing on a
fully-migrated speaker (FW 27.0.6) revealed that the swUpdate
daemon caches its target URL at boot and ignores live config
writes. The diagnostic sequence:
:8090/swUpdateCheck to trigger fan-out./updates/soundtouch (the previous swUpdateUrl value, current
at the last daemon boot) and /streaming/software/update/account/<id>
(a separate Bose URL the daemon hits, routed to this service by DNS
interception). The probe URL was never dialed.This falsifies the original NEXT.md hypothesis that the persistence layer would override the runtime layer for the daemon’s fan-out, and points instead at daemon-level URL caching. Two consequences:
The honest replacement is a passive observer (see
pkg/service/setup/peer_probe.go):
handlers.peerObserver, wired via PeerObserverMiddleware).:8090/swUpdateCheck to make the daemon fan out something
sooner than its ~5min timer.Endpoint: POST /setup/peer-probe/{deviceId}. No device-state
mutation; safe to re-run. Returns {ok, result: {reached,
observed_path, elapsed_ms}, error} with the same UI keying as the
old probe (result.reached).
The web UI’s pre-flight orchestrator (runApplyPreflight in
script.js) branches on summary.is_migrated:
| Migration state | Reachability row |
|---|---|
Migrated (is_migrated=true) |
“Reachability check (passive observer)” — calls POST /setup/peer-probe/{deviceId}. |
| Not migrated (incl. partial) | Skip row “Round-trip validation runs after Apply + reboot” with the rationale “daemon caches swUpdateUrl at boot”. |
Per-axis booleans (xml_migrated, hosts_migrated, resolv_migrated,
telnet_migrated) remain visible in the State card, so the user can
see which parts of the migration are already in place even when the
overall flag is false. The skip row does not attempt the active probe
on unmigrated speakers — the canonical telnet flow is:
Apply telnet config → user-initiated reboot → re-run pre-flight on
the now-migrated speaker → passive observer confirms fan-out.
Removed (or scheduled for removal in a follow-up commit) at the time of §9.8 landing:
pkg/service/setup/telnet_probe.go — RunTelnetRoundTripProbe,
ProbeRegistrar, TelnetProbeResult, generateProbeToken.pkg/service/handlers/handlers_telnet_probe.go — HandleTelnetProbe,
HandleProbeInbound, telnetProbeTimeout, telnetProbeResponse.pkg/service/handlers/probe_registry.go — probeRegistry + tests.Server.probes field./probe/{token}, /probe/{token}/*, /setup/telnet-probe/{deviceId}.target_url query-param plumbing on the deprecated endpoint.script.js — checkTelnetRoundTrip (orchestrator call site removed
in the commit that added the branch; function itself removed later).isCommandNotFound and parseGetpdoConfig stay — they are also used
by the migration writer (telnet_migration.go), preflight reader
(telnet_preflight.go), pairing path (marge_pairing.go), and
cross-check (preflight_crosscheck.go).