Replies: 9 comments 7 replies
-
|
@Zimbo88 — seriously, why aren't you a contributor yet? This is excellent work. Let me address a few things point by point.
|
Beta Was this translation helpful? Give feedback.
-
|
First of all: thank you for the kind words 🙂 Honestly, I’ve only been digging into all of this for about one week now. I suddenly had way too much forced free time, and I used that time obsessively researching the SoundTouch ecosystem because the whole situation with the Bose shutdown simply frustrated me a lot. Starting Monday my normal work life returns, and realistically I probably won’t have the headspace to contribute to the project in the way I personally would consider “proper” long-term contribution work. But I still want to share everything I discover so the project and community can benefit from it. About the reverse engineering: So my process was basically:
The USB-less/telnet approach was simply my attempt to solve my own problem:
That’s why I published it — not because I think it’s universally perfect, but because maybe parts of it help you or someone else move OCT further forward. Regarding OCT 1.2.6: I know this sounds weird, but once I start following an idea I usually continue until I hit a complete dead end. Only then do I step back and try another approach. Otherwise I lose my mental model of what exactly caused which effect. So my successful flow was basically:
…and suddenly radio playback worked perfectly. After probably 40 failed attempts or more 😄 The important breakthrough for me was realizing: Playback, source handling, persistence, stream handling — most of it already exists internally. What seems to be missing after the Bose cloud shutdown is mainly:
I currently only own ST10, ST20 and ST30 units. And yes — I completely understand your priorities. I probably added a bit more chaos over the last few days too… sorry about that. About Bose documentation: The really interesting parts were clearly never publicly documented. The persistence system alone is kind of insane. We can basically create fantasy streaming providers, custom source structures, different playback identities, arbitrary naming schemes, and many more things. The deeper I looked, the more absurd it became how much capability was already hidden inside the firmware while the official APIs exposed almost none of it. Ideas that I really would like to see implemented properly, I will definitely prepare and submit as proper feature requests — as clearly and structured as possible, so your implementation effort stays as small as possible. I’ll probably try to organize and write those down before Monday. What especially interests me are:
Because I discovered a few very interesting things regarding presets as well. But I’d rather document that separately and properly instead of throwing half-chaotic information into this thread 😄 Anyway: And if any of my findings help move OCT forward, I’m happy 🙂 |
Beta Was this translation helpful? Give feedback.
-
|
Thanks a lot for sharing your results and learnings. I'll try to include missing parts in my cli tool, maybe it helps to fix issues like this: gesellix/Bose-SoundTouch#195 So, the most obivous question: have you experienced broken AUX while LOCAL_INTERNET would work? |
Beta Was this translation helpful? Give feedback.
-
|
@Zimbo88 Thank you for your great work! Thanks and best regards, |
Beta Was this translation helpful? Give feedback.
-
|
Hi @TomS1985! Great question. We don't have a confirmed way to enable SSH purely through telnet commands on port 17000 yet -- that's still an open research area. However, it looks like you've already found a working path! Your success with the ETAP cable approach in #177 is exactly the kind of solution others with the Wireless Adapter could use. If anyone in the community discovers a way to enable SSH via telnet commands alone, that would be a great addition to the knowledge base. For now, the ETAP cable method is the confirmed approach for the SoundTouch Wireless Adapter. |
Beta Was this translation helpful? Give feedback.
-
Updated USB-less provisioning script proposal — v2.0Based on later runtime testing, the older USB-less helper should probably not be treated as final. This v2.0 proposal is more conservative and focuses on stability. It does not claim to enable SSH purely through port At the moment, enabling SSH only through the SoundTouch CLI on port The goal is not just to make the device visible in OCT. Important changes compared to the older script
This is still a research/provisioning helper and not a production-grade installer. Use at your own risk. #!/usr/bin/env bash
#
# OpenCloudTouch USB-less SoundTouch provisioning helper v2.0
#
# Purpose:
# Prepare a Bose SoundTouch device for local OpenCloudTouch usage
# without USB provisioning, where HTTP API and/or port 17000 CLI access
# is available.
#
# Important:
# This script does NOT reliably enable SSH through telnet/CLI.
# Pure SSH enablement through port 17000 is still an open research topic.
#
# If SSH is already available, this script uses it for:
# - persistence backups
# - hosts redirection
# - Sources.xml initialization
# - optional remote_services flag
#
# Focus:
# - stable local runtime preparation
# - conservative persistence changes
# - backups before modification
# - BMX/Marge redirect
# - LOCAL_INTERNET_RADIO initialization
# - optional Orion runtime playback test
#
# Requirements:
# - OpenCloudTouch running locally
# - SoundTouch reachable on the network
# - port 8090 reachable
# - port 17000 CLI reachable recommended
# - SSH reachable recommended, but not mandatory
#
# Usage:
# chmod +x oct_soundtouch_usbless_prepare_v2.sh
# ./oct_soundtouch_usbless_prepare_v2.sh DEVICE_IP OCT_IP [PORT]
#
# Example:
# ./oct_soundtouch_usbless_prepare_v2.sh 192.168.178.112 192.168.178.200 7777
#
set -euo pipefail
DEVICE_IP="${1:-}"
OCT_IP="${2:-}"
PORT="${3:-7777}"
if [ -z "${DEVICE_IP}" ] || [ -z "${OCT_IP}" ]; then
echo "Usage: $0 DEVICE_IP OCT_IP [PORT]"
exit 1
fi
ACCOUNT_ID="octlocal$(date +%s)"
OCT_URL="http://${OCT_IP}:${PORT}"
BMX_REGISTRY_URL="http://content.api.bose.io:${PORT}/bmx/registry/v1/services"
MARGE_URL="${OCT_URL}"
UPDATE_URL="${OCT_URL}/updates/soundtouch"
STAMP="$(date +%Y%m%d-%H%M%S)"
SSH_OPTS=(
-o HostKeyAlgorithms=+ssh-rsa
-o PubkeyAcceptedAlgorithms=+ssh-rsa
-o StrictHostKeyChecking=accept-new
-o ConnectTimeout=8
)
have_cmd() {
command -v "$1" >/dev/null 2>&1
}
need_cmd() {
if ! have_cmd "$1"; then
echo "Missing required command: $1"
exit 1
fi
}
check_port_raw() {
local host="$1"
local port="$2"
nc -z -w 3 "$host" "$port" >/dev/null 2>&1
}
cli() {
local cmd="$1"
if [ "${CLI_OK}" != "1" ]; then
echo "CLI unavailable, skipping: ${cmd}"
return 0
fi
printf '%s\r\n' "${cmd}" | nc -w 5 "${DEVICE_IP}" 17000 || true
}
ssh_run() {
if [ "${SSH_OK}" != "1" ]; then
echo "SSH unavailable, skipping SSH action"
return 0
fi
ssh "${SSH_OPTS[@]}" root@"${DEVICE_IP}" "$@" || true
}
http_get() {
local path="$1"
curl -s --max-time 10 "http://${DEVICE_IP}:8090/${path}" || true
}
http_post_xml() {
local path="$1"
local body="$2"
curl -s --max-time 15 \
-X POST "http://${DEVICE_IP}:8090/${path}" \
-H "Content-Type: application/xml" \
--data-binary "${body}" || true
}
section() {
echo
echo "===================================================================="
echo "$1"
echo "===================================================================="
}
need_cmd nc
need_cmd curl
need_cmd base64
need_cmd python3
section "OpenCloudTouch USB-less provisioning v2.0"
echo "Device IP: ${DEVICE_IP}"
echo "OCT IP: ${OCT_IP}"
echo "OCT URL: ${OCT_URL}"
echo "BMX registry: ${BMX_REGISTRY_URL}"
echo "Marge URL: ${MARGE_URL}"
echo "Update URL: ${UPDATE_URL}"
echo "Account ID: ${ACCOUNT_ID}"
echo "Timestamp: ${STAMP}"
section "1. Connectivity check"
HTTP_OK=0
CLI_OK=0
SSH_OK=0
if check_port_raw "${DEVICE_IP}" 8090; then
HTTP_OK=1
fi
if check_port_raw "${DEVICE_IP}" 17000; then
CLI_OK=1
fi
if check_port_raw "${DEVICE_IP}" 22; then
SSH_OK=1
fi
echo "HTTP 8090: ${HTTP_OK}"
echo "CLI 17000: ${CLI_OK}"
echo "SSH 22: ${SSH_OK}"
if [ "${HTTP_OK}" != "1" ]; then
echo "Device HTTP API is not reachable. Aborting."
exit 1
fi
if [ "${SSH_OK}" != "1" ]; then
echo
echo "NOTE:"
echo "SSH is not reachable."
echo "This script will continue with HTTP/CLI-only steps."
echo "Persistence file modification, hosts override and Sources.xml repair require SSH."
echo "Pure SSH enablement through port 17000 is not implemented here."
fi
section "2. Current device info"
http_get info
echo
section "3. Backup persistence files if SSH is available"
if [ "${SSH_OK}" = "1" ]; then
ssh "${SSH_OPTS[@]}" root@"${DEVICE_IP}" <<EOF || true
set -e
mkdir -p /mnt/nv/BoseApp-Persistence/1
mkdir -p /mnt/nv/oct-usbless-backups/${STAMP}
for f in \
/mnt/nv/BoseApp-Persistence/1/Sources.xml \
/mnt/nv/BoseApp-Persistence/1/Presets.xml \
/mnt/nv/BoseApp-Persistence/1/Recents.xml \
/mnt/nv/BoseApp-Persistence/1/LegacyProduct.dat \
/mnt/nv/SystemConfigurationDB.xml \
/opt/Bose/etc/SoundTouchSdkPrivateCfg.xml \
/etc/hosts
do
if [ -e "\$f" ]; then
cp "\$f" "/mnt/nv/oct-usbless-backups/${STAMP}/\$(basename "\$f").bak" || true
fi
done
sync
EOF
else
echo "SSH unavailable, backup step skipped."
fi
section "4. Configure runtime URLs through port 17000 CLI if available"
cli "sys configuration bmxRegistryUrl ${BMX_REGISTRY_URL}"
cli "sys configuration margeServerUrl ${MARGE_URL}"
cli "sys configuration statsServerUrl ${OCT_URL}/stats"
cli "envswitch AccountId set ${ACCOUNT_ID}"
cli "envswitch boseurls set ${OCT_URL} ${UPDATE_URL}"
section "5. Apply local hosts redirect if SSH is available"
if [ "${SSH_OK}" = "1" ]; then
ssh "${SSH_OPTS[@]}" root@"${DEVICE_IP}" <<EOF || true
set -e
cp /etc/hosts "/mnt/nv/oct-usbless-backups/${STAMP}/hosts.before-oct.bak" 2>/dev/null || true
grep -v "content.api.bose.io" /etc/hosts > /tmp/hosts.oct || true
grep -v "bmx.bose.com" /tmp/hosts.oct > /tmp/hosts.oct2 || true
grep -v "streaming.bose.com" /tmp/hosts.oct2 > /tmp/hosts.oct3 || true
cat /tmp/hosts.oct3 > /etc/hosts
cat >> /etc/hosts <<HOSTS
${OCT_IP} content.api.bose.io
${OCT_IP} bmx.bose.com
${OCT_IP} streaming.bose.com
HOSTS
sync
EOF
else
echo "SSH unavailable, hosts redirect skipped."
fi
section "6. Initialize conservative Sources.xml if SSH is available"
if [ "${SSH_OK}" = "1" ]; then
ssh "${SSH_OPTS[@]}" root@"${DEVICE_IP}" <<'EOF' || true
set -e
mkdir -p /mnt/nv/BoseApp-Persistence/1
if [ -e /mnt/nv/BoseApp-Persistence/1/Sources.xml ]; then
cp /mnt/nv/BoseApp-Persistence/1/Sources.xml \
/mnt/nv/BoseApp-Persistence/1/Sources.xml.pre-oct-usbless.bak || true
fi
cat > /mnt/nv/BoseApp-Persistence/1/Sources.xml <<XML
<?xml version="1.0" encoding="UTF-8" ?>
<sources>
<source displayName="AUX IN" secret="" secretType="">
<sourceKey type="AUX" account="AUX" />
</source>
<source displayName="Local Internet Radio" secret="" secretType="">
<sourceKey type="LOCAL_INTERNET_RADIO" account="" />
</source>
<source displayName="TuneIn" secret="" secretType="">
<sourceKey type="TUNEIN" account="" />
</source>
</sources>
XML
sync
EOF
else
echo "SSH unavailable, Sources.xml initialization skipped."
fi
section "7. Optional remote_services flag if SSH is available"
if [ "${SSH_OK}" = "1" ]; then
ssh_run "touch /mnt/nv/remote_services 2>/dev/null || true; sync"
else
echo "SSH unavailable, remote_services step skipped."
fi
section "8. Trigger setMargeAccount pairing"
PAIR_XML="<PairDeviceWithAccount>
<accountId>${ACCOUNT_ID}</accountId>
<userAuthToken>Bearer local-oct-token</userAuthToken>
<boseServer>${MARGE_URL}</boseServer>
<updateServer>${UPDATE_URL}</updateServer>
<accountEmail>local@opencloudtouch.invalid</accountEmail>
</PairDeviceWithAccount>"
curl -s --max-time 15 \
-X POST "http://${DEVICE_IP}:8090/setMargeAccount" \
-H "Content-Type: application/vnd.bose.streaming-v1.2+xml" \
--data-binary "${PAIR_XML}" || true
echo
section "9. Reboot device"
cli "sys reboot"
echo "Waiting for device reboot..."
sleep 90
section "10. Verify /info"
INFO_XML="$(http_get info)"
echo "${INFO_XML}"
echo
if echo "${INFO_XML}" | grep -q "<margeAccountUUID>"; then
echo "margeAccountUUID present."
else
echo "WARNING: margeAccountUUID not detected in /info."
fi
section "11. Verify /sources"
SOURCES_XML="$(http_get sources)"
echo "${SOURCES_XML}"
echo
if echo "${SOURCES_XML}" | grep -q 'source="LOCAL_INTERNET_RADIO".*status="READY"'; then
echo "LOCAL_INTERNET_RADIO is READY."
else
echo "WARNING: LOCAL_INTERNET_RADIO not confirmed READY."
fi
if echo "${SOURCES_XML}" | grep -q 'source="TUNEIN".*status="READY"'; then
echo "TUNEIN is READY."
else
echo "WARNING: TUNEIN not confirmed READY."
fi
section "12. Verify /presets"
http_get presets
echo
section "13. Optional Orion runtime playback test"
read -r -p "Run Orion LOCAL_INTERNET_RADIO playback test now? [y/N] " RUN_TEST
if [ "${RUN_TEST}" = "y" ] || [ "${RUN_TEST}" = "Y" ]; then
TEST_NAME="OCT Orion Test"
TEST_STREAM="http://onair.krone.at/kronehit.mp3"
TEST_IMAGE="https://i.imgur.com/Z3rkNFc.png"
DATA="$(python3 - <<PY
import base64, json, urllib.parse
obj = {
"streamUrl": "${TEST_STREAM}",
"name": "${TEST_NAME}",
"imageUrl": "${TEST_IMAGE}",
}
raw = json.dumps(obj, separators=(",", ":")).encode()
print(urllib.parse.quote(base64.b64encode(raw).decode()))
PY
)"
LOCATION="http://content.api.bose.io:${PORT}/core02/svc-bmx-adapter-orion/prod/orion/station?data=${DATA}"
CI="<ContentItem source=\"LOCAL_INTERNET_RADIO\" type=\"stationurl\" location=\"${LOCATION}\" sourceAccount=\"\" isPresetable=\"true\"><itemName>${TEST_NAME}</itemName><containerArt>${TEST_IMAGE}</containerArt></ContentItem>"
echo "Sending /select test ContentItem..."
http_post_xml select "${CI}"
echo
echo "Waiting for playback..."
sleep 8
NOW_PLAYING="$(http_get now_playing)"
echo "${NOW_PLAYING}"
echo
if echo "${NOW_PLAYING}" | grep -q 'source="LOCAL_INTERNET_RADIO"' \
&& echo "${NOW_PLAYING}" | grep -q '<playStatus>PLAY_STATE</playStatus>'; then
echo "Orion runtime test succeeded: LOCAL_INTERNET_RADIO is playing."
else
echo "WARNING: Orion runtime test did not confirm PLAY_STATE."
fi
else
echo "Skipping Orion runtime playback test."
fi
section "14. Final summary"
echo "Device: ${DEVICE_IP}"
echo "OCT URL: ${OCT_URL}"
echo "Account ID: ${ACCOUNT_ID}"
echo "Backup stamp: ${STAMP}"
echo
echo "Provisioning finished."
echo
echo "Recommended next checks:"
echo " curl -s http://${DEVICE_IP}:8090/info"
echo " curl -s http://${DEVICE_IP}:8090/sources"
echo " curl -s http://${DEVICE_IP}:8090/now_playing"
echo
echo "If playback works, the runtime state should be saved by OCT or an external"
echo "runtime persistence helper so it can be restored after reboot/standby." |
Beta Was this translation helpful? Give feedback.
-
|
Notes about this v2.0 proposal This script intentionally does not try to solve every possible SoundTouch model variation. It is meant as a conservative provisioning helper for devices where: the HTTP API is reachable, The most important practical additions are: proper backups, The Orion test is important because it validates the actual runtime path: LOCAL_INTERNET_RADIO This is more useful than only checking whether the device appears in OCT. |
Beta Was this translation helpful? Give feedback.
-
|
There seems to be some really good news: gesellix/Bose-SoundTouch#471 |
Beta Was this translation helpful? Give feedback.
-
|
Das wäre ja mega wenn das in den Setup Assistent integriert werden könnte für alle anderen Gerätetypen bei denen der USB Stick nicht funktioniert |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
What would you like to discuss?
USB-less provisioning for Bose SoundTouch devices (OpenCloudTouch 1.2.6)
Hi everyone,
after many hours of testing, packet captures, firmware analysis and trial-and-error, I finally managed to get OpenCloudTouch working on SoundTouch devices without using the original Bose cloud and without requiring USB provisioning.
This is especially useful for:
Motivation
The motivation behind this was simple:
I apparently own a defective device — or I simply never figured out which button combinations Bose expected us to press to gain access to the hidden provisioning functionality.
Since Bose shut down the SoundTouch cloud services, many devices became partially unusable after factory reset.
The biggest problem:
After reset, the firmware no longer initializes the internet radio sources properly.
That means:
I started analyzing:
and eventually discovered:
The firmware already contains almost everything internally.
It simply waits for the correct configuration and persistence state.
Important discovery
The SoundTouch firmware is surprisingly open internally.
It reacts to:
Meaning:
The firmware itself is not "locked down" in the traditional sense.
It already supports:
The missing part after Bose cloud shutdown is mostly:
Main discovery
The critical file is:
Factory reset devices often only contain:
Which explains why internet radio disappears completely.
The fix is:
After reboot the device automatically exposes:
and playback starts working again.
USB-less provisioning script
The attached script:
does the following:
connects to the SoundTouch CLI (port 17000)
points the device to OpenCloudTouch
sets local account IDs
configures BMX registry URLs
creates a minimal Sources.xml
optionally creates:
reboots the device
validates source initialization
Full script
Why this matters
This means:
A large amount of SoundTouch devices can likely be revived without:
Especially:
can potentially be restored entirely over network.
About
/mnt/nv/remote_servicesOne particularly interesting discovery:
appears to influence persistent remote access behavior.
I cannot fully confirm the exact internal firmware logic yet, but based on testing it may enable or preserve SSH-related functionality on some firmware variants.
This still needs further investigation.
Known issue: error 1038
This error still appears during provisioning:
[6/10] Triggering streaming account sync... Using Bose PairDeviceWithAccount XML format. <?xml version="1.0" encoding="UTF-8" ?> <errors deviceID="506583DE4803"> <error value="1038" name="SET_MARGE_ACCOUNT_ERROR" severity="Unknown"> 1038 </error> </errors>I currently do not know how to fully eliminate this error.
However:
everything is still written correctly afterwards.
The important part:
gets persisted correctly and:
appears after reboot.
Playback works despite the error.
So while this is not perfect, it is stable enough to actually restore functionality.
Important note about backups
One downside of this approach:
there is no full backup/restore mechanism.
However, realistically speaking:
devices that already lost cloud functionality after factory reset are effectively unusable anyway.
So in practice the choice often becomes:
or
Everyone should decide for themselves whether they are comfortable modifying persistence data on unsupported hardware.
My conclusion
At this point I believe:
The Bose firmware was never truly "closed".
Most functionality was already present locally.
The cloud mainly handled:
The actual playback pipeline always existed inside the firmware itself.
This provisioning approach simply restores the missing initialization state.
Tested successfully on
Playback confirmed working after factory reset.
Future ideas
Potential future improvements:
Huge thanks to everyone working on OpenCloudTouch and preserving SoundTouch devices.
Without this project none of this would have been possible.
Beta Was this translation helpful? Give feedback.
All reactions