Bose SoundTouch Toolkit

Documentation for controlling and preserving Bose SoundTouch devices

View the Project on GitHub gesellix/Bose-SoundTouch

Experiment: Does bare setMargeAccount work outside the SETUP bracket?

Why we are doing this

Our captured pairing flow (docs/reference/DEVICE-PAIRING-FLOW.md) shows the official Bose app always sends setMargeAccount inside a SETUP_STARTSETUP_ENTERSETUP_LEAVE state-machine bracket over WebSocket. The question this experiment answers:

If we open a WebSocket to a factory-reset speaker and send only setMargeAccount — no surrounding setupState messages — does the device honor it and write its persistence files (SystemConfigurationDB.xml, Sources.xml) cleanly?

The answer determines the shape of PairAccount:

Preconditions

Step 0 — Baseline

DEVICE=192.168.x.x
curl -s http://$DEVICE:8090/info     | xmllint --format -
curl -s http://$DEVICE:8090/sources  | xmllint --format -
curl -s http://$DEVICE:8090/presets  | xmllint --format -

Record:

Step 1 — Send bare setMargeAccount over WebSocket

Build the CLI once:

make build

Then run the bare path against the speaker:

DEVICE=192.168.x.x
./build/soundtouch-cli setup pair --host=$DEVICE --account=1234567 --mode=bare

What it does:

  1. Reads /info to discover deviceID, logs the pre-state.
  2. Opens a WebSocket to $DEVICE:8080 with the gabbo subprotocol.
  3. Sends exactly one frame — the setMargeAccount envelope — without any preceding SETUP_START/SETUP_ENTER.
  4. Reads frames for up to --step-timeout=8s (configurable), looking for an ack referencing our requestID.
  5. Closes the WebSocket, waits 2 s, re-reads /info, prints whether margeAccountUUID now equals our supplied ID.

The exact frame sent (built by setup.SetupSession.SetMargeAccount):

<msg><header deviceID="DEVICE_ID" url="setMargeAccount" method="POST"><request requestID="1"/></header><body>
  <PairDeviceWithAccount>
    <accountId>1234567</accountId>
    <userAuthToken>Bearer aftertouch</userAuthToken>
  </PairDeviceWithAccount>
</body></msg>

Outcomes the CLI will surface:

Step 2 — Record outcome

After step 1 (regardless of which branch happened):

sleep 2
curl -s http://$DEVICE:8090/info | grep margeAccountUUID
Observed result Verdict
<margeAccountUUID>1234567</margeAccountUUID> appears YES — Option 1 wins
<margeAccountUUID></margeAccountUUID> still empty, no error frame received Refused silently → NO
Error frame returned (e.g. <error name="UNSUPPORTED_STATE"/>) Refused explicitly → NO
Device drops the WebSocket connection without replying Refused → NO

If verdict is YES, also verify the device wrote persistence cleanly. Reboot the device, then:

ssh root@$DEVICE 'cat /mnt/nv/BoseApp-Persistence/1/SystemConfigurationDB.xml'
ssh root@$DEVICE 'cat /mnt/nv/BoseApp-Persistence/1/Sources.xml'
curl -s http://$DEVICE:8090/info | grep margeAccountUUID

The UUID must still be present after reboot, and SystemConfigurationDB.xml must contain <AccountUUID>1234567</AccountUUID>. If it survives reboot, YES is confirmed.

Step 3 — Control: full state machine

Factory-reset the same speaker again and run the full state machine — the same CLI, --mode=full:

./build/soundtouch-cli setup pair --host=$DEVICE --account=1234567 --mode=full

This drives setup.Manager.ExecuteInitPlan with SkipURLRewrite=true, which runs:

SETUP_START
SETUP_IDENTIFY_DEVICE_ENTER
language sysLanguage=2
SETUP_ENTER
SETUP_IDENTIFY_DEVICE_LEAVE
setMargeAccount …
SETUP_LEAVE
pushCustomerSupportInfoToMarge

The CLI logs every step with status. Confirm /info, persistence, and reboot-survival checks pass. If the bare path failed but the full path succeeds, the SETUP bracket is load-bearing — a follow-up bisect (e.g. SETUP_START + setMargeAccount + SETUP_LEAVE only) tells us which surrounding messages the firmware actually requires.

Full reset-and-rebuild loop

Once the bare/full question is decided, the loop for repeated experiments is:

# 0. Speaker is currently on home Wi-Fi at $DEVICE.
#    Capture deviceID-suffix + current SSID first so wait-online and
#    wifi-push have the right inputs.
./build/soundtouch-cli setup inspect --host=$DEVICE
./build/soundtouch-cli setup factory-reset --host=$DEVICE

# 1. Manually switch this host to the speaker's AP (Bose SoundTouch XXXX).
#    macOS: networksetup -setairportnetwork en0 "Bose SoundTouch XXXX"

./build/soundtouch-cli setup wait-ap
./build/soundtouch-cli setup wifi-push --ssid="$HOME_SSID" --pass="$HOME_PASS"

# 2. Manually switch this host back to home Wi-Fi.

./build/soundtouch-cli setup wait-online --match=DE4803          # deviceID suffix from /info before reset
# (note the new IP from the "Speaker discovered" line)

NEW_IP=192.168.x.y
./build/soundtouch-cli setup migrate --host=$NEW_IP --service-url=http://aftertouch.local:8000   # default --method=telnet

# Optional, if you want the DNS-redirect path instead of (or alongside) telnet envswitch:
#   1. ./build/soundtouch-cli setup ssh-check --host=$NEW_IP            # USB-stick procedure if 22 is closed
#   2. ./build/soundtouch-cli setup install-ca --host=$NEW_IP --service-url=http://aftertouch.local:8000
#   3. ./build/soundtouch-cli setup migrate --host=$NEW_IP --service-url=http://aftertouch.local:8000 --method=resolv
./build/soundtouch-cli setup pair --host=$NEW_IP --mode=bare   # or --mode=full

The two manual lines are user-side Wi-Fi switches that can’t be automated portably. The wait-ap and wait-online subcommands poll for the corresponding network state, so timing them is hands-off.

Recording the result

Append to this file under ## Results:

- Date: YYYY-MM-DD
- Firmware: 27.x.x
- Model: ST10 / ST20 / ST30 / ST300
- Bare setMargeAccount accepted: yes/no
- Persistence written: yes/no
- Survives reboot: yes/no
- Notes: ...

One row per device tested. Once two devices on different firmware confirm the same verdict, we treat it as decided.

Results

Implication for the codebase

Appendix — SystemConfigurationDB.xml comparison

Post-experiment we compared the device-written /mnt/nv/BoseApp-Persistence/1/SystemConfigurationDB.xml from the bare-paired speaker against two SSH backups taken from speakers originally paired by the official Bose app (account 3230304, devices A_Sound_Machine and Sound_Machinechen). The diff is much smaller than expected — only two fields differ, and neither is set by the pairing protocol itself:

Field Bare-paired (1111111) Real-Bose-paired (3230304) Set by
DeviceName Bose SoundTouch 536A98 (factory default) Sound Machinechen name WS message — only sent in --mode=full
AccountAssociatedEMail empty empty Never populated, even by real Bose
AccountUUID 1111111 3230304 setMargeAccount — both paths set it
Locale empty empty Never populated, even by real Bose
acctMode global global Firmware-default; no protocol path observed to change it
isMultiDeviceAccount false true Derived from the cloud’s /streaming/account/{id}/full response — count of <devices> > 1 flips it true
margeAuthServerToken empty empty Never populated, even by real Bose
Password (encrypted blob) (encrypted blob) Device-local key; expected to differ

Three of the seven informational fields are empty even after a real-Bose pairing — the firmware simply doesn’t populate AccountAssociatedEMail, Locale, or margeAuthServerToken from the pairing flow. So bare pairing isn’t missing any field that real pairing fills.

The two genuinely different fields:

So the experiment’s YES verdict stands unqualified: bare setMargeAccount produces a SystemConfigurationDB.xml functionally equivalent to one written by the official pairing flow.