mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
Add `soundtouch-cli setup` subcommand group covering the full reset →
re-provision → pair lifecycle as a scriptable alternative to the web UI:
inspect, verify, plan, factory-reset, wait-ap, wifi-push, wait-online,
ssh-check, install-ca, migrate, reboot, pair (bare | full state machine)
Supporting library code lives in pkg/service/setup: factory_reset.go,
wifi_provision.go, inspect.go, init_plan.go, setup_session.go.
Confirmed against ST10 firmware 27.0.6 that bare setMargeAccount over
WebSocket — no SETUP_START/SETUP_ENTER/SETUP_LEAVE bracket — is
sufficient to pair a factory-reset speaker; the firmware materializes
SystemConfigurationDB.xml and Sources.xml itself and the pairing
survives reboot. Result and field-by-field SystemConfigurationDB
comparison documented in docs/analysis/SETUP-WEBSOCKET-EXPERIMENT.md.
Captures the device's pre-reset DELETE-to-marge plus its LAN peer
notification flow in docs/analysis/FACTORY-RESET-PROTOCOL.md.
Perf: batch GetMigrationSummary's SSH probes into one Run() call via
ssh_probe.go / ssh_probe_apply.go — was ~8 sequential dials at
500-1000 ms each on FW 27 crypto, now one round-trip. Same data shape,
same MigrationSummary fields populated.
Fixes /clockTime and /clockDisplay wire formats — firmware 27 rejects
the legacy flat XML ("Error parsing request"). ClockTimeRequest now
uses utcTime attribute; ClockDisplayRequest emits the nested
<clockConfig> envelope with timezoneInfo/timeFormat/brightnessLevel.
Removes cmd/example-init-speaker (superseded by setup pair).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
74 lines
2.2 KiB
Go
74 lines
2.2 KiB
Go
package setup
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// FactoryReset issues `sys factorydefault` over the device's port-17000
|
|
// diagnostic shell. The device wipes its persistent state (account
|
|
// pairing, Wi-Fi credentials, presets, source configuration) and reboots
|
|
// into setup mode — broadcasting its own `Bose SoundTouch XXXX` access
|
|
// point on 192.0.2.1.
|
|
//
|
|
// After this call the device is unreachable on the home network until
|
|
// the caller pushes new Wi-Fi credentials via PushWiFiCredentials (see
|
|
// wifi_provision.go).
|
|
func (m *Manager) FactoryReset(deviceIP string) (string, error) {
|
|
if m.NewTelnet == nil {
|
|
return "", errors.New("FactoryReset: Manager.NewTelnet is nil")
|
|
}
|
|
|
|
var logs strings.Builder
|
|
|
|
t := m.NewTelnet(deviceIP)
|
|
if err := t.Dial(); err != nil {
|
|
return logs.String(), fmt.Errorf("telnet dial %s:17000: %w", deviceIP, err)
|
|
}
|
|
|
|
defer func() { _ = t.Close() }()
|
|
|
|
banner, _ := t.Probe()
|
|
if banner != "" {
|
|
fmt.Fprintf(&logs, "Telnet banner: %q\n", strings.TrimSpace(banner))
|
|
}
|
|
|
|
resp, err := t.SendCommand("sys factorydefault")
|
|
if err != nil {
|
|
// A graceful close right after the command is normal — the device
|
|
// reboots immediately. We treat "connection closed" responses as
|
|
// success rather than failure.
|
|
if isExpectedDisconnect(err) {
|
|
fmt.Fprintf(&logs, "→ sys factorydefault\n(device disconnected — reset accepted)\n")
|
|
return logs.String(), nil
|
|
}
|
|
|
|
return logs.String(), fmt.Errorf("sys factorydefault: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(&logs, "→ sys factorydefault\n%s\n", strings.TrimRight(resp, "\r\n"))
|
|
|
|
if isCommandNotFound(resp) {
|
|
return logs.String(), fmt.Errorf("device rejected `sys factorydefault` (firmware does not expose this command)")
|
|
}
|
|
|
|
return logs.String(), nil
|
|
}
|
|
|
|
// isExpectedDisconnect reports whether an error from SendCommand is the
|
|
// normal "device closed the socket while rebooting" pattern, which we
|
|
// see during factory-reset.
|
|
func isExpectedDisconnect(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
msg := strings.ToLower(err.Error())
|
|
|
|
return strings.Contains(msg, "eof") ||
|
|
strings.Contains(msg, "connection reset") ||
|
|
strings.Contains(msg, "connection closed") ||
|
|
strings.Contains(msg, "broken pipe")
|
|
}
|