7 Commits
Author SHA1 Message Date
Tobias GesellchenandClaude Opus 5 b8427b0bbe feat(on-device): reach AfterTouch from the LAN without an SSH tunnel
On-device installs were only reachable through an SSH tunnel, and the
docs blamed it on the service binding loopback-only. That was wrong.

Some SoundTouch chassis carry a BCO ("SMSC") Wi-Fi/Bluetooth
co-processor, and inbound LAN traffic reaches the main Linux SoC only
for a fixed set of Bose's own service ports, a list that appears to be
compiled into the co-processor firmware. AfterTouch's :8000 was never
part of that design, so connections never arrive at the SoC at all.
Confirmed on an ST20: a port sweep from a LAN client showed Bose's
:82/:8080/:8090/:8091/:8200/:17000 all answering while :8000 failed,
and tcpdump on the speaker's own eth0 recorded zero packets for it.
Ruled out along the way: iptables (empty), nft/ebtables (absent), the
router, Wi-Fi isolation, and the binding itself (0.0.0.0 is correct).

The init script now redirects one of the relayed ports to AfterTouch,
so http://<speaker-ip>:17008 works with no tunnel. 17008 is Bose's
software-update listener, whose cloud no longer exists. Only external
traffic is matched, so anything on the speaker still reaches :8000 as
before. Auto-enabled only where has-bco reports the co-processor, and
configurable via AFTERTOUCH_LAN_PORT (auto/none/port) in
aftertouch.conf. The rule is re-applied on every start and removed on
stop and uninstall, so it needs no watchdog; unlike prior art it is not
pinned to the LAN IP, so it also survives DHCP changes.

Credit for the REDIRECT technique goes to the STR / SoundTouch Reborn
project, which documented and shipped it first.

Also de-hardcodes the service port, which was baked independently into
the daemon args, the readiness poll and status, and makes install.sh
print the speaker's real address instead of a <your-device-ip>
placeholder it never filled in.

Adds a model support matrix, since the repo had no per-model
compatibility record and this behaviour is entirely chassis-dependent.
Only the verified ST20 row is filled in; everything else is marked
unknown rather than inferred.

Verified on hardware: auto-detection, idempotency across restarts,
teardown and restore, persistence across a full reboot, and LAN access
returning the service's health JSON.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 15:54:00 +02:00
Tobias GesellchenandClaude Sonnet 5 4a1e722069 fix(on-device-install): daemon restart never replaced the running process
Three stacked bugs, found and confirmed on real hardware while
downgrading a speaker: the new binary landed on disk correctly, but
the running service kept reporting the old version indefinitely.

- install.sh called `/etc/init.d/aftertouch start`, not `restart`,
  after installing. start-stop-daemon silently refuses to launch a
  second instance when one is already running, and the init script
  never checked its exit status, so the old process was never
  replaced.

- The init script started the daemon through a `sh -c "exec ... |
  logger"` pipeline, on the assumption that `exec` lets --make-pidfile
  record the daemon's own PID. POSIX forks each side of a pipe into
  its own process, so the wrapper shell (not the daemon) was the one
  actually tracked. `stop` killed the wrapper, which doesn't forward
  SIGTERM to its children, orphaning the real daemon to keep running
  and keep holding :8000 forever.

- Once the wrapper correctly tracked the daemon's own PID, a further
  race surfaced: start-stop-daemon's own "already running?" check
  matched on generic `/bin/sh` identity, so a `restart`'s `start`
  phase could catch the previous wrapper still mid-teardown and
  silently refuse to launch a new one (masked by --quiet, looking
  like a 120s hang).

Fixed by calling `restart` instead of `start` in install.sh, and by
having the wrapper shell record the daemon's real PID itself (via $!)
while keying start-stop-daemon's own check on that same pidfile
instead of process identity.

Verified on hardware: three consecutive restart cycles, each fast,
each with the pidfile matching the live daemon PID and daemon output
flowing through syslog again via logread.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 15:54:00 +02:00
Tobias GesellchenandClaude Sonnet 4.6 73d0d4b176 fix(install): reliable stop, VERSION flag, and on-device walkthrough
- aftertouch init script: stop) now waits up to 15 s for SIGTERM
  to take effect, then escalates to SIGKILL; prevents stale daemon
  processes after '/etc/init.d/aftertouch stop' returns (weissigera's
  workaround was manual 'killall aftertouch-service')

- install.sh: add --version / -v CLI flag so the version to install
  can be passed as a command-line argument in addition to the VERSION
  env var; document the trade-off of the hard-coded default in a
  comment; update scripts/on-device-install/README.md with concrete
  usage examples for env-override, CLI flag, and rollback tip

- docs/guides/ON-DEVICE-INSTALL-WALKTHROUGH.md: 10-step runbook
  derived from weissigera's field-tested procedure (issue #329
  comment #4521280831): SSH connection, storage cleanup, install via
  install.sh, reboot, SSH tunnel, Health QuickFix, pairing
  verification, soundtouch-cli download, custom-radio preset setup,
  and final verification; troubleshooting table at the end

Closes #329 (remaining two tasks)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:31:16 +02:00
Tobias GesellchenandClaude Opus 4.7 064fe80e18 fix(on-device-install): persistent install path + syslog-based logging
Bundles the install-time hygiene work for issues #268 and #250.

# Install location — #268

Stock SoundTouch rootfs has only a few MB free (~4 MB on the ST20
the reporter captured); the AfterTouch binary is ~12 MB. The previous
flow downloaded into tmpfs (/media/aftertouch) and then `mv`'d the
binary into /opt/aftertouch on rootfs — which fails with
"No space left on device" on any speaker with the standard layout.

install.sh now installs to /mnt/nv/aftertouch by default (the
persistent partition, ~30 MB free on the same captures) and points
/opt/aftertouch at it via a symlink so the init script's hardcoded
DAEMON path keeps working unchanged. Power users can override with
INSTALL_DIR=/some/other/path. The interactive prompt from the
community patch in #268's thread is dropped — STDIN is the curl
pipe under the documented `curl | sh` invocation, so a read prompt
would hang or read garbage.

uninstall.sh is updated to resolve the symlink and remove the
target before unlinking, so the 12 MB binary doesn't get orphaned
on /mnt/nv when users uninstall.

# Logging — #250

Issue #250 surfaced a "running but unreachable" state: the install
script reported AfterTouch as running, the init script's status
agreed, but `curl :8000` returned connection-refused. start-stop-
daemon's --background detaches stdout/stderr, so any panic the
daemon emitted before dying went to /dev/null with no diagnostic
trail.

The fix is to route the daemon's stdout/stderr through `logger -t
aftertouch` so output lands in BusyBox syslog — a bounded in-memory
ring buffer that never grows on disk (writing to a file in /mnt/nv
would have eaten the volume over months). Diagnostic flow is now:

    logread        | grep aftertouch | tail -20
    logread -f     | grep aftertouch     # live tail

Matches the recipe already documented in TROUBLESHOOTING.md for the
speaker's own logs (Curl 7 section).

Tightening on top of the syslog change:

  - The init script's `status` case now also curls localhost:8000
    when the PID is alive — distinguishes "PID alive, listener up"
    from "PID alive, listener silently died" (which is what fooled
    everyone on #250). A bare PID-liveness check returned "running"
    in both cases.

  - install.sh's post-install verification now does its own 10s
    curl probe after the init script returns; on failure it tails
    the aftertouch syslog so the user sees the actual error rather
    than the install script claiming success.

  - `exec` is added inside the start-stop-daemon's shell wrapper so
    --make-pidfile records the daemon's own PID (not the shell's),
    which keeps `stop` semantics correct.

README updated to document the install location, INSTALL_DIR
override, and the syslog tag.

No automated tests — these are shell scripts the install pipeline
runs once on the device. All three scripts pass `bash -n` /
`sh -n` syntax checks. Real validation is end-user retest, gated on
the next release.

Refs #268, refs #250.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:11:02 +02:00
Tim VahlbrockandTobias Gesellchen bc8213f0a1 change default discovery interval 2026-05-10 12:58:00 +02:00
Tim VahlbrockandTobias Gesellchen e008bb6a2b Apply suggestions from code review
Co-authored-by: Tobias Gesellchen <tobias@gesellix.de>
2026-05-10 12:58:00 +02:00
Tim VahlbrockandTobias Gesellchen 370b587fcf feat: Provide scripts and documentation for on-device install 2026-05-10 12:58:00 +02:00