diff --git a/cmd/soundtouch-cli/cmd_setup.go b/cmd/soundtouch-cli/cmd_setup.go index 0ab10f0..f920e1c 100644 --- a/cmd/soundtouch-cli/cmd_setup.go +++ b/cmd/soundtouch-cli/cmd_setup.go @@ -52,10 +52,12 @@ func setupCommand() *cli.Command { setupRemoteServicesCmd(), setupInstallCACmd(), setupMigrateCmd(), + setupRevertCmd(), setupRebootCmd(), setupVerifyCmd(), setupPlanCmd(), setupPairCmd(), + setupSyncCmd(), }, } } @@ -1013,6 +1015,116 @@ func promptBasicAuth() (string, string, error) { return user, string(pass), nil } +// setupSyncCmd wraps POST /api/setup/sync/{deviceId} — the same operation +// as the web UI's Devices → Sync Data button. It only reads from the +// speaker (presets, recents, sources) into AfterTouch's datastore; it never +// writes anything back to the speaker. Useful for scripting or reproducing +// what Sync does in isolation (see issue #614: Sync's own code cannot wipe +// the speaker's preset table, since it never sends anything back). +func setupSyncCmd() *cli.Command { + return &cli.Command{ + Name: "sync", + Usage: "Pull presets/recents/sources from the speaker into AfterTouch's datastore (same as the web UI's \"Sync Data\" button)", + Before: RequireHost, + Flags: []cli.Flag{ + &cli.StringFlag{Name: "service-url", Required: true, Usage: "AfterTouch base URL"}, + &cli.StringFlag{Name: "auth", Usage: "Basic-auth credentials for AfterTouch as user:pass (omit to be prompted on 401)"}, + }, + Action: func(c *cli.Context) error { + cfg := GetClientConfig(c) + serviceURL := strings.TrimRight(c.String("service-url"), "/") + + if err := validateServiceURL(serviceURL); err != nil { + PrintError(err.Error()) + return err + } + + client, err := CreateSoundTouchClient(cfg) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + deviceInfo, err := client.GetDeviceInfo() + if err != nil { + PrintError(fmt.Sprintf("Failed to get device info from speaker: %v", err)) + return err + } + + if deviceInfo.DeviceID == "" { + err := fmt.Errorf("speaker at %s did not report a DeviceID", cfg.Host) + PrintError(err.Error()) + + return err + } + + PrintDeviceHeader(fmt.Sprintf("Syncing %s into AfterTouch", deviceInfo.DeviceID), cfg.Host, cfg.Port) + + if err := postSetupSync(serviceURL, deviceInfo.DeviceID, c.String("auth")); err != nil { + PrintError(err.Error()) + return err + } + + PrintSuccess(fmt.Sprintf("Synced presets, recents, and sources for %s.", deviceInfo.DeviceID)) + + return nil + }, + } +} + +// postSetupSync POSTs to AfterTouch's /api/setup/sync/{deviceId}, prompting +// for basic-auth credentials on 401 (matches fetchCACert's pattern). +func postSetupSync(serviceURL, deviceID, authFlag string) error { + endpoint := fmt.Sprintf("%s/api/setup/sync/%s", serviceURL, deviceID) + + doRequest := func(user, pass string) (*http.Response, error) { + req, err := http.NewRequest(http.MethodPost, endpoint, nil) + if err != nil { + return nil, err + } + + if user != "" { + req.SetBasicAuth(user, pass) + } + + client := &http.Client{Timeout: 30 * time.Second} + + return client.Do(req) + } + + user, pass := splitAuth(authFlag) + + resp, err := doRequest(user, pass) + if err != nil { + return fmt.Errorf("POST %s: %w", endpoint, err) + } + + if resp.StatusCode == http.StatusUnauthorized { + _ = resp.Body.Close() + + fmt.Printf("%s requires basic auth.\n", endpoint) + + user, pass, err = promptBasicAuth() + if err != nil { + return err + } + + resp, err = doRequest(user, pass) + if err != nil { + return fmt.Errorf("POST %s (with auth): %w", endpoint, err) + } + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("POST %s returned %d: %s", endpoint, resp.StatusCode, strings.TrimSpace(string(body))) + } + + return nil +} + func setupMigrateCmd() *cli.Command { return &cli.Command{ Name: "migrate", @@ -1386,6 +1498,45 @@ func renderMigrationSummary(deviceIP, serviceURL string, s *setup.MigrationSumma } } +// setupRevertCmd wraps setup.Manager.RevertMigration — the same operation +// as the web UI's "Revert to Defaults" button (Migrate tab). Restores +// SoundTouchSdkPrivateCfg.xml, /etc/hosts, and /etc/resolv.conf from their +// .original backups, removes the AfterTouch DNS-hook artifacts, and strips +// just the AfterTouch-labeled cert out of the trust bundle. No --service-url +// needed: everything it touches already lives on the speaker. +// +// Deliberately out of scope (matches the web UI button): SSH/remote_services +// persistence (use `setup remote-services --remove`) and account pairing +// (use `account unpair`) — see #614 self-test notes for the full checklist. +func setupRevertCmd() *cli.Command { + return &cli.Command{ + Name: "revert", + Usage: "Undo a migration: restore SoundTouchSdkPrivateCfg.xml/hosts/resolv.conf from backups and remove the AfterTouch CA cert", + Before: RequireHost, + Action: func(c *cli.Context) error { + cfg := GetClientConfig(c) + m := setup.NewManager("", nil, nil) + + fmt.Printf("Reverting migration on %s...\n", cfg.Host) + + logs, err := m.RevertMigration(cfg.Host) + if logs != "" { + fmt.Print(logs) + } + + if err != nil { + PrintError(err.Error()) + return err + } + + PrintSuccess("Migration reverted. SSH access and account pairing are untouched by this — " + + "see `setup remote-services --remove` and `account unpair` if you want those cleared too.") + + return nil + }, + } +} + func setupRebootCmd() *cli.Command { return &cli.Command{ Name: "reboot", diff --git a/cmd/soundtouch-cli/cmd_setup_test.go b/cmd/soundtouch-cli/cmd_setup_test.go index 28f57dc..d3a5e13 100644 --- a/cmd/soundtouch-cli/cmd_setup_test.go +++ b/cmd/soundtouch-cli/cmd_setup_test.go @@ -3,6 +3,8 @@ package main import ( "bytes" "io" + "net/http" + "net/http/httptest" "os" "strings" "testing" @@ -43,6 +45,46 @@ func captureStdout(t *testing.T, fn func()) string { return buf.String() } +func TestPostSetupSync_PostsToDeviceScopedURL(t *testing.T) { + var gotMethod, gotPath string + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotMethod = r.Method + gotPath = r.URL.Path + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"ok": true}`)) + })) + defer srv.Close() + + if err := postSetupSync(srv.URL, "DEVICEID01", ""); err != nil { + t.Fatalf("postSetupSync: %v", err) + } + + if gotMethod != http.MethodPost { + t.Errorf("expected POST, got %s", gotMethod) + } + + if want := "/api/setup/sync/DEVICEID01"; gotPath != want { + t.Errorf("expected path %q, got %q", want, gotPath) + } +} + +func TestPostSetupSync_PropagatesServerError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "device not found", http.StatusNotFound) + })) + defer srv.Close() + + err := postSetupSync(srv.URL, "DEVICEID01", "") + if err == nil { + t.Fatal("expected an error for a 404 response") + } + + if !strings.Contains(err.Error(), "device not found") { + t.Errorf("expected error to include server body, got %q", err.Error()) + } +} + func TestRenderSourceTable_AlignsColumnsAndDedupsDisplayName(t *testing.T) { items := []models.SourceItem{ // displayName != account → kept as "AUX (AUX IN)" diff --git a/docs/content/docs/guides/CLI-REFERENCE.md b/docs/content/docs/guides/CLI-REFERENCE.md index 095afeb..afd66de 100644 --- a/docs/content/docs/guides/CLI-REFERENCE.md +++ b/docs/content/docs/guides/CLI-REFERENCE.md @@ -592,6 +592,9 @@ soundtouch-cli --host account remove-amazon --user soundtouch-cli --host account remove-deezer --user soundtouch-cli --host account remove-iheart --user soundtouch-cli --host account remove-nas --user [--name ] + +# Unpair the device from its Marge cloud account entirely +soundtouch-cli --host account unpair ``` **Supported Services:** @@ -648,6 +651,11 @@ soundtouch-cli --host 192.0.2.10 account remove \ - Network music libraries (STORED_MUSIC) don't require passwords, only the UPnP server GUID - After adding an account, use `source list` to verify it appears as available - Some services may require additional authentication steps through their mobile apps +- `account unpair` is different from the above: it sends `UnPairDeviceWithAccount` + over the speaker's own local WebSocket to remove its **Marge cloud account** + pairing entirely (`margeAccountUUID`), not a single streaming-service login. + See `setup revert` for the related "undo a migration" operation, which + deliberately does *not* call this — the two are separate steps. ### Bass Control @@ -1183,6 +1191,238 @@ https://github.com/gesellix/Bose-SoundTouch/releases/tag/v1.3.0 - If the running binary isn't a released version (e.g. a dev build), the command reports that and skips the comparison. +### Setup & Migration + +The `setup ` group provisions a speaker end-to-end: enabling +SSH, factory-reset + Wi-Fi re-provisioning, pointing it at AfterTouch, CA +trust, account pairing, reverting, and one-shot data sync. Each subcommand +wraps an existing `pkg/service/setup` helper directly — there's no separate +business logic in the CLI layer. Manual provisioning-loop background: +[docs/analysis/SETUP-WEBSOCKET-EXPERIMENT.md](../analysis/SETUP-WEBSOCKET-EXPERIMENT.md) +and [Device Initial Setup](DEVICE-INITIAL-SETUP.md). + +#### `setup inspect` + +Non-destructive snapshot of the speaker: identity, pairing state, Wi-Fi, +sources, presets, and (with `--telnet`) the runtime URL configuration via +`getpdo`. Good first command to run against an unfamiliar speaker. + +```bash +soundtouch-cli --host setup inspect +soundtouch-cli --host setup inspect --telnet # also reads runtime URLs (slower) +``` + +#### `setup ssh-check` + +Probes whether port 22 is reachable. On failure, prints the `enable-ssh` +suggestion and the USB-stick fallback procedure. + +```bash +soundtouch-cli --host setup ssh-check [--timeout 3s] +``` + +#### `setup enable-ssh` + +Bootstraps SSH on a speaker with no prior access, via the port-17000 +`envswitch` trick (#471) — no USB stick needed. Auto-pairs an unpaired +(factory-reset) device first by default (the injection needs something to +poll), waits for `:22`, and persists the `remote_services` marker so SSH +survives a reboot. + +```bash +soundtouch-cli --host setup enable-ssh +soundtouch-cli --host setup enable-ssh --service-url https://192.0.2.10:8443 +``` + +Flags: +- `--service-url` — optional; only the vehicle for the injection, no live + server required. Set the real URL later via `setup migrate`. +- `--wait` (default `90s`) — how long to wait for `:22` after injection. +- `--full-config` — for stubborn devices (ST Portable, CineMate 520) where + the default injection is accepted but `sshd` never starts: writes all + four config URLs (the #515 sequence) and reboots. +- `--command-delay` — only affects `--full-config`; pause between its 6 + steps. +- `--no-auto-pair` / `--account` — skip or control the automatic pairing + check. +- `--no-reset-urls` — skip restoring clean `boseurls` after SSH is up. +- `--no-persist` — skip persisting `remote_services` (SSH won't survive a + reboot). +- `--authorized-key` — opt-in hardening: install an SSH public key instead + of relying on the empty-password login. +- `--close-17000` — opt-in hardening: firewall off port 17000 from the LAN + (loopback access kept). + +#### `setup remote-services` + +Enables (default) or removes the `remote_services` SSH-enablement marker. + +```bash +soundtouch-cli --host setup remote-services # ensure it's present +soundtouch-cli --host setup remote-services --remove # disable SSH after next reboot +``` + +#### `setup factory-reset` + +Issues `sys factorydefault` over telnet — wipes account, presets, and +Wi-Fi, and reboots the speaker into its own setup-mode AP. Prints the next +steps (`wait-ap`, then `wifi-push`). + +```bash +soundtouch-cli --host setup factory-reset +``` + +> **Heads-up:** just before resetting, the speaker sends +> `DELETE /streaming/account/{id}/device/{id}` to whatever `margeURL` is +> *currently* configured. If that still points at `streaming.bose.com` +> (not AfterTouch), AfterTouch keeps a stale datastore entry — migrate +> first if you want a clean record. + +#### `setup wait-ap` + +Polls the speaker's setup-mode AP (default `192.0.2.1`) until `/info` +responds, after a factory reset. + +```bash +soundtouch-cli setup wait-ap [--ap-host 192.0.2.1] [--interval 2s] [--timeout 5m] +``` + +#### `setup wifi-push` + +POSTs `AddWirelessProfile` to the speaker's setup-mode endpoint — pushes +your home Wi-Fi credentials while connected to the speaker's AP. + +```bash +soundtouch-cli setup wifi-push --ssid="YourHomeSSID" --pass='your-password' +``` + +Flags: `--security` (default `wpa_or_wpa2`), `--ap-host` (default +`192.0.2.1`), `--request-timeout` (default `30s` — the speaker can be slow +to ACK before tearing down AP mode; 10s often races). + +#### `setup wait-online` + +Polls mDNS until a speaker matching `--match` comes online on the home +network — run this after switching back from the speaker's AP. + +```bash +soundtouch-cli setup wait-online --match= +``` + +`--match` is empty by default (first speaker seen); `--interval` (`3s`) and +`--timeout` (`5m`) control the poll. + +#### `setup install-ca` + +Fetches AfterTouch's CA cert from `/api/setup/ca.crt` and injects it into +the speaker's trust store via SSH. + +```bash +soundtouch-cli --host setup install-ca --service-url https://192.0.2.10:8443 +``` + +`--auth` (`user:pass`) supplies basic-auth credentials up front; omit it to +be prompted interactively if the endpoint returns 401. + +#### `setup migrate` + +Applies a migration method to point the speaker at AfterTouch — the CLI +equivalent of the web UI's Migrate tab. + +```bash +soundtouch-cli --host setup migrate --service-url http://192.0.2.10:8000 --method telnet +``` + +`--method` is one of `telnet` (default) | `hosts` | `resolv` | `xml`. +`--proxy-url` sets an optional upstream proxy (only used by `--method=xml`). +`--skip-preflight` skips AfterTouch's settings preflight check (useful when +that endpoint is unreachable). + +#### `setup revert` + +Undoes a migration — the CLI equivalent of the web UI's "Revert to +Defaults" button. Restores `SoundTouchSdkPrivateCfg.xml`, `/etc/hosts`, and +`/etc/resolv.conf` from their `.original` backups, removes the AfterTouch +DNS-hook artifacts, and strips just the AfterTouch-labeled certificate out +of the trust bundle. No `--service-url` needed — everything it touches +already lives on the speaker. + +```bash +soundtouch-cli --host setup revert +``` + +**Out of scope for this command** (matches the web UI button): SSH / +`remote_services` persistence (use `setup remote-services --remove`) and +account pairing (use `account unpair`) are untouched — revert them +separately if you want a fully clean speaker. + +#### `setup reboot` + +Reboots the speaker — useful to force the envswitch parallel-persistence +layer to apply after a migration. + +```bash +soundtouch-cli --host setup reboot [--method telnet|ssh] +``` + +`--method` defaults to `telnet`, which works without SSH on modern +firmware. + +#### `setup verify` + +Read-only status probe across every migration axis (transports, URL +configuration, DNS interception, CA/TLS, pairing) — doubles as a preflight +check before applying changes and a verification step afterward. Exits +non-zero if nothing reports migrated, so it's usable as a CI gate. + +```bash +soundtouch-cli --host setup verify --service-url http://192.0.2.10:8000 +``` + +#### `setup plan` + +Recommends the next setup/migration steps based on `inspect` + `verify` +state — prints a ready-to-run command for each recommended step. + +```bash +soundtouch-cli --host setup plan --service-url http://192.0.2.10:8000 +soundtouch-cli --host setup plan --service-url http://192.0.2.10:8000 --reset # plan a full factory-reset → Wi-Fi → migrate → pair flow +``` + +`--wifi-ssid` overrides the SSID used for the `wifi-push` step in a reset +plan (default: reuse the SSID `inspect` found). `--include-pair` (default +`true`) can be disabled if you'll pair manually. + +#### `setup pair` + +Pairs the speaker with an account via the WebSocket `SETUP` state machine +(`--mode=full`, matching the Bose app's own flow) or a minimal +`setMargeAccount`-only call (`--mode=bare`, the same underlying call the +Health tab's "empty margeAccountUUID" QuickFix uses). + +```bash +soundtouch-cli --host setup pair --mode=full --account=1111111 --service-url http://192.0.2.10:8000 +soundtouch-cli --host setup pair --mode=bare --account=1111111 --service-url http://192.0.2.10:8000 +``` + +`--account` empty generates a fresh 7-digit ID. `--name` sets the speaker +name during pairing (empty keeps current). `--language` defaults to `2` +(English). `--token` defaults to a built-in placeholder matching the Bose +app's token shape. + +#### `setup sync` + +Pulls presets, recents, and sources from the speaker into AfterTouch's +datastore — the CLI equivalent of the web UI's Devices → Sync Data button. +Read-only towards the speaker: it never writes anything back. + +```bash +soundtouch-cli --host setup sync --service-url http://192.0.2.10:8000 +``` + +`--auth` (`user:pass`) supplies basic-auth credentials up front; omit it to +be prompted interactively if the endpoint returns 401. + ## Common Usage Patterns ### Quick Device Setup diff --git a/docs/content/docs/guides/DEVICE-INITIAL-SETUP.md b/docs/content/docs/guides/DEVICE-INITIAL-SETUP.md index 57adc43..c880e55 100644 --- a/docs/content/docs/guides/DEVICE-INITIAL-SETUP.md +++ b/docs/content/docs/guides/DEVICE-INITIAL-SETUP.md @@ -99,18 +99,28 @@ After factory restore the speaker enters setup mode automatically; no power-cycl ## 6. AP Mode Wi-Fi Provisioning via Console -When BLE is unavailable (e.g. when using an Android emulator), use AP mode to push Wi-Fi credentials from the Mac command line. +When BLE is unavailable (e.g. when using an Android emulator), use AP mode to push Wi-Fi credentials from the command line. The HTTP steps below (6.2, 6.3) are OS-agnostic; only the Wi-Fi-network-switching commands (6.1, 6.4) are platform-specific — macOS is shown inline, with Linux and Windows equivalents alongside. -### 6.1 Connect Mac to Speaker AP +### 6.1 Connect your machine to the Speaker AP -After factory reset the speaker broadcasts an SSID like `Bose SoundTouch XXXX`. Connect the Mac to it: +After factory reset the speaker broadcasts an SSID like `Bose SoundTouch XXXX`. Connect to it: ```bash -# List nearby SSIDs — use System Settings → Wi-Fi (the airport command was removed in macOS Sequoia+) -# Connect (replace with actual SSID) +# macOS — list nearby SSIDs via System Settings → Wi-Fi (the `airport` +# command was removed in macOS Sequoia+); connect (replace with actual SSID): networksetup -setairportnetwork en0 "Bose SoundTouch XXXX" ``` +```bash +# Linux (NetworkManager) — one-shot connect, no password (open AP): +nmcli device wifi connect "Bose SoundTouch XXXX" +``` + +```powershell +# Windows — connect via the built-in Wi-Fi menu, or from PowerShell: +netsh wlan connect name="Bose SoundTouch XXXX" +``` + The speaker's web UI gateway is at `192.0.2.1` (verified: ST10 assigns `192.0.2.2` to the client via DHCP). ```bash @@ -143,20 +153,37 @@ Expected response: `/dev/null # only needed for the dns-sd form ``` +Windows has no equivalent built-in mDNS browser; use `soundtouch-cli discover devices` (this repo's own mDNS/UPnP discovery, cross-platform) or check your router's DHCP client list instead. + --- ## Comparison: Initial Setup vs. Migration diff --git a/docs/content/docs/guides/MIGRATION-GUIDE.md b/docs/content/docs/guides/MIGRATION-GUIDE.md index 13c41d5..5a435e8 100644 --- a/docs/content/docs/guides/MIGRATION-GUIDE.md +++ b/docs/content/docs/guides/MIGRATION-GUIDE.md @@ -107,6 +107,8 @@ Open `http://:8000` and go to the **Settings** tab. Set the **Target Domain** to the address your speakers can reach — for example `https://soundtouch.fritz.box` or `http://192.0.2.100:8000`. This must be the host's address on your local network, not `localhost`. +> **On-device install:** this "not `localhost`" rule is for the local-network-host and cloud/VPS scenarios above, where the service runs on a *different* machine than the speaker. If you're running AfterTouch directly on the speaker itself (see the [On-Device Install Walkthrough](ON-DEVICE-INSTALL-WALKTHROUGH.md)), the speaker and the service are the same machine — `http://localhost:8000` is exactly right there, and is the recommended value: it needs no DNS/mDNS to resolve and survives DHCP address changes since it never depends on the LAN address at all. + If you plan to use DNS/DHCP redirect, enable the **DNS Discovery Server** and set the **DNS Bind Address** to `:53`. The upstream DNS should be your router's IP, not the service's own address. > **Tip**: If you change settings and they don't seem to take effect, check `data/settings.json` — settings saved in the UI take precedence over environment variables. @@ -127,6 +129,14 @@ The XML migration writes updated configuration to the speaker's filesystem, whic 4. Power-cycle the speaker (unplug the power cable, wait 10 seconds, reconnect). 5. After boot, root SSH is available with no password: `ssh -oHostKeyAlgorithms=+ssh-rsa root@` +**Or, without a USB stick:** `soundtouch-cli setup enable-ssh` (#471) bootstraps SSH purely over the network, using the speaker's telnet:17000 diagnostic shell (open by default on most firmware) to inject the SSH-enable command: + +```shell +soundtouch-cli --host setup enable-ssh +``` + +It waits for `:22` to come up and persists the change (survives a reboot) by default. Falls back to the USB-stick method above if telnet:17000 is closed or the injection doesn't take on your model. + You only need to do this once per speaker. SSH can remain enabled for future maintenance or be disabled after migration — your choice. **To disable SSH after migration:** diff --git a/docs/content/docs/guides/ON-DEVICE-INSTALL-WALKTHROUGH.md b/docs/content/docs/guides/ON-DEVICE-INSTALL-WALKTHROUGH.md index f915bc7..b0ce938 100644 --- a/docs/content/docs/guides/ON-DEVICE-INSTALL-WALKTHROUGH.md +++ b/docs/content/docs/guides/ON-DEVICE-INSTALL-WALKTHROUGH.md @@ -14,7 +14,9 @@ documenting a successful fresh installation on a SoundTouch 20 Series I. ## Prerequisites -- SSH enabled on the speaker (the usual "Stick with remote_services" procedure). +- SSH enabled on the speaker — either the usual "USB stick with + `remote_services`" procedure, or `soundtouch-cli setup enable-ssh` + (no stick needed, see Step 1). - Your machine can reach the speaker on the LAN. - The speaker's LAN IP address — replace `192.0.2.1` throughout with the actual address shown in your router or `arp -a`. @@ -29,6 +31,23 @@ documenting a successful fresh installation on a SoundTouch 20 Series I. ## Step 1 — Connect to the speaker via SSH +If SSH isn't enabled yet, you don't need a USB stick: `soundtouch-cli` can +bootstrap it purely over the network (#471), using the speaker's +telnet:17000 diagnostic shell (open by default on most firmware) to inject +the SSH-enable command: + +```bash +soundtouch-cli --host 192.0.2.1 setup enable-ssh +``` + +This waits for `:22` to come up and persists it (survives a reboot) by +default. The USB-stick method (format FAT32, create an empty +`remote_services` file in its root, insert, power-cycle) still works as a +fallback if telnet:17000 is closed or the injection doesn't take on your +model. + +Either way, connect the same way: + ```bash ssh -oHostKeyAlgorithms=+ssh-rsa root@192.0.2.1 ``` @@ -137,7 +156,42 @@ browser. --- -## Step 6 — Run the Health QuickFix for empty `margeAccountUUID` +## Step 6 — Migrate (point the speaker at itself) + +The speaker isn't pointed at the AfterTouch instance you just installed yet +— this step does that. On-device, the speaker and the AfterTouch instance +are the same machine, so **loopback is the correct and recommended Target +Domain value**: `http://localhost:8000`. This is the one case where the +general migration guide's "must not be `localhost`" warning does not +apply — that warning is about the external-host/cloud scenarios, where +`localhost` would resolve on the wrong machine (the service host, not the +speaker). Here there is no wrong machine to resolve on. + +**Via the Admin UI:** + +1. Go to **Settings**, set **Target Domain** to `http://localhost:8000`. +2. Go to **Devices**, find your speaker (it self-discovers on its own LAN + IP), click **Migrate**. +3. Accept the suggested plan and let it apply. +4. Reboot to apply the change: + ```bash + sync + reboot + ``` + +**Or via the CLI** (equivalent, no browser needed — grab `soundtouch-cli` +from Step 9 below first if you want this path): + +```bash +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 setup migrate \ + --service-url http://localhost:8000 --method telnet +sync +reboot +``` + +--- + +## Step 7 — Run the Health QuickFix for empty `margeAccountUUID` In the AfterTouch UI: @@ -148,6 +202,14 @@ In the AfterTouch UI: 4. Click the **QuickFix** button (labelled "Fix", "Pair account", or "Apply QuickFix" depending on the version) and confirm. +Or via the CLI (same underlying pairing call, `--mode=bare` matches what +the QuickFix does — see Step 9 to grab `soundtouch-cli` first): + +```bash +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 setup pair \ + --mode=bare --account=1111111 --service-url http://localhost:8000 +``` + Then reboot again to let the pairing take effect: ```bash @@ -157,7 +219,7 @@ reboot --- -## Step 7 — Verify pairing and sources +## Step 8 — Verify pairing and sources After the reboot reconnect via SSH and check: @@ -171,32 +233,37 @@ wget -qO- http://localhost:8090/info | grep margeAccountUUID wget -qO- http://localhost:8090/sources ``` -If `margeAccountUUID` is still empty, re-run the Health QuickFix (Step 6) +If `margeAccountUUID` is still empty, re-run the Health QuickFix (Step 7) and reboot again. --- -## Step 8 — Download soundtouch-cli (optional, for preset setup) +## Step 9 — Download soundtouch-cli (optional, for preset setup) If you want to program preset buttons from the command line, download the -CLI binary to the speaker's `/tmp` (tmpfs, so it survives only until the -next reboot — which is fine for a one-time setup run): +CLI binary to `/mnt/nv/aftertouch` (the same persistent partition +AfterTouch itself lives on) rather than `/tmp`: `/tmp` is tmpfs and gets +wiped on every reboot, and if you used the CLI alternatives in Steps 6/7 +above, it needs to survive those steps' reboots too, not just the final +one: ```bash -cd /tmp +cd /mnt/nv/aftertouch curl -L --fail -o soundtouch-cli \ https://github.com/gesellix/Bose-SoundTouch/releases/download/v0.123.0/soundtouch-cli-v0.123.0-linux-armv7 chmod +x soundtouch-cli -/tmp/soundtouch-cli --version +/mnt/nv/aftertouch/soundtouch-cli --version ``` -Replace `v0.123.0` with the version you installed. +Replace `v0.123.0` with the version you installed. If you want the CLI +alternatives in Steps 6/7, download it here first, before doing those +steps — it'll be in place and already persistent either way. --- -## Step 9 — Store custom radio streams to preset buttons +## Step 10 — Store custom radio streams to preset buttons Each station must be playing before it can be saved. The `sleep 5` gives the speaker time to buffer and confirm the stream before storing. @@ -206,52 +273,52 @@ the speaker time to buffer and confirm the stream before storing. ```bash # Preset 1 — Hitradio OE3 -/tmp/soundtouch-cli --host 127.0.0.1 source custom-radio \ +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 source custom-radio \ --url "http://orf-live.ors-shoutcast.at/oe3-q2a" \ --name "Hitradio OE3" \ --service-url "http://localhost:8000" sleep 5 -/tmp/soundtouch-cli --host 127.0.0.1 preset store-current --slot 1 +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 preset store-current --slot 1 # Preset 2 — Lounge FM -/tmp/soundtouch-cli --host 127.0.0.1 source custom-radio \ +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 source custom-radio \ --url "http://188.138.9.183/digital.mp3" \ --name "Lounge FM" \ --service-url "http://localhost:8000" sleep 5 -/tmp/soundtouch-cli --host 127.0.0.1 preset store-current --slot 2 +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 preset store-current --slot 2 # Preset 3 — Country Nonstop -/tmp/soundtouch-cli --host 127.0.0.1 source custom-radio \ +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 source custom-radio \ --url "https://stream.laut.fm/country-nonstop" \ --name "Country Nonstop" \ --service-url "http://localhost:8000" sleep 5 -/tmp/soundtouch-cli --host 127.0.0.1 preset store-current --slot 3 +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 preset store-current --slot 3 # Preset 4 — Radio Piterpan -/tmp/soundtouch-cli --host 127.0.0.1 source custom-radio \ +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 source custom-radio \ --url "https://klasse1.fluidstream.eu/piterpan.mp3?FLID=8" \ --name "Radio Piterpan" \ --service-url "http://localhost:8000" sleep 5 -/tmp/soundtouch-cli --host 127.0.0.1 preset store-current --slot 4 +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 preset store-current --slot 4 # Preset 5 — kronehit -/tmp/soundtouch-cli --host 127.0.0.1 source custom-radio \ +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 source custom-radio \ --url "https://secureonair.krone.at/kronehit-hp.mp3" \ --name "kronehit" \ --service-url "http://localhost:8000" sleep 5 -/tmp/soundtouch-cli --host 127.0.0.1 preset store-current --slot 5 +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 preset store-current --slot 5 # Preset 6 — Radio Niederösterreich -/tmp/soundtouch-cli --host 127.0.0.1 source custom-radio \ +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 source custom-radio \ --url "http://orf-live.ors-shoutcast.at/noe-q2a" \ --name "Radio Niederoesterreich" \ --service-url "http://localhost:8000" sleep 5 -/tmp/soundtouch-cli --host 127.0.0.1 preset store-current --slot 6 +/mnt/nv/aftertouch/soundtouch-cli --host 127.0.0.1 preset store-current --slot 6 ``` These are the stations from weissigera's setup (Austrian public and @@ -260,7 +327,7 @@ pattern is the same regardless of station. --- -## Step 10 — Verify presets and final reboot +## Step 11 — Verify presets and final reboot ```bash wget -qO- http://localhost:8090/presets @@ -286,7 +353,7 @@ should start playing the corresponding stream. | SSH "no matching host key type" | Add `-oHostKeyAlgorithms=+ssh-rsa` | | Port 8000 not reachable from LAN | Use the SSH tunnel (Step 5) | | `margeAccountUUID` still empty after reboot | Re-run Health QuickFix, reboot again | -| Radio source error 1005 | `margeAccountUUID` is empty — complete Step 6 first | +| Radio source error 1005 | `margeAccountUUID` is empty — complete Step 7 first | | `http://localhost:8000` not responding after install | `logread \| grep aftertouch \| tail -20` | | No space left on device during install | Run the cleanup in Step 2; check `df -h /mnt/nv` | @@ -322,6 +389,39 @@ cp /mnt/nv/aftertouch/aftertouch-service..backup \ /etc/init.d/aftertouch restart ``` +**Testing a pre-release build (from `main`, not yet tagged):** `install.sh` +only ever downloads from GitHub Releases, so there's no one-line installer +for an unreleased commit. Cross-compile and swap the binary manually +instead — this is a direct extension of the rollback procedure above: + +```bash +# On your own machine, from a checkout of the branch/commit you want: +make build-linux-armv7 # builds build/soundtouch-service-linux-armv7, + # build/soundtouch-cli-linux-armv7, and + # build/soundtouch-backup-linux-armv7 + +scp build/soundtouch-service-linux-armv7 root@192.0.2.1:/mnt/nv/aftertouch/aftertouch-service.new +ssh -oHostKeyAlgorithms=+ssh-rsa root@192.0.2.1 + +rw +/etc/init.d/aftertouch stop +cp /mnt/nv/aftertouch/aftertouch-service /mnt/nv/aftertouch/aftertouch-service.pre-test.backup +mv /mnt/nv/aftertouch/aftertouch-service.new /mnt/nv/aftertouch/aftertouch-service +chmod +x /mnt/nv/aftertouch/aftertouch-service +/etc/init.d/aftertouch start +``` + +If you're testing an unreleased `soundtouch-cli` change (not just the +service), swap that binary too — same idea, and it lands in the same +`/mnt/nv/aftertouch` directory Step 9 above uses: + +```bash +scp build/soundtouch-cli-linux-armv7 root@192.0.2.1:/mnt/nv/aftertouch/soundtouch-cli +ssh -oHostKeyAlgorithms=+ssh-rsa root@192.0.2.1 chmod +x /mnt/nv/aftertouch/soundtouch-cli +``` + +Roll back the same way as above, using the `.pre-test.backup` file. + --- ## Service management