Bose SoundTouch Toolkit

Documentation for controlling and preserving Bose SoundTouch devices

View the Project on GitHub gesellix/Bose-SoundTouch

IoT Implementation Guide

Overview

This guide provides technical implementation details for integrating with the Bose SoundTouch IoT configuration system. It covers the AWS IoT Core integration, certificate management, and device shadow operations.

Prerequisites

Architecture Components

Core System Design

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Mobile App    │    │   Alexa Voice    │    │  Web Interface  │
│                 │    │    Assistant     │    │                 │
└─────────┬───────┘    └─────────┬────────┘    └─────────┬───────┘
          │                      │                       │
          └──────────────────────┼───────────────────────┘
                                 │
                    ┌────────────▼──────────────┐
                    │      AWS IoT Core         │
                    │   (MQTT Broker +          │
                    │    Device Shadows)        │
                    └────────────┬──────────────┘
                                 │ MQTT/TLS
                    ┌────────────▼──────────────┐
                    │    SoundTouch Device      │
                    │                           │
                    │  ┌─────────────────────┐  │
                    │  │   IoT Service       │  │
                    │  │   (/opt/Bose/IoT)   │  │
                    │  └─────────────────────┘  │
                    │  ┌─────────────────────┐  │
                    │  │   BoseApp Service   │  │
                    │  │ (/opt/Bose/BoseApp) │  │
                    │  └─────────────────────┘  │
                    └───────────────────────────┘

Configuration Flow

1. Device Boot
     │
     ▼
2. Read IoT.xml (/mnt/nv/BoseApp-Persistence/1/IoT.xml)
     │
     ▼
3. Load Certificates (/mnt/nv/IoTCerts/)
     │
     ▼
4. Establish MQTT/TLS Connection
     │
     ▼
5. Subscribe to Device Shadow Topics
     │
     ▼
6. Publish Current Device State
     │
     ▼
7. Listen for Delta Messages

Implementation Details

1. Configuration File Management

IoT.xml Structure

<?xml version="1.0" encoding="UTF-8" ?>
<Configuration 
    clientID="{device-unique-uuid}" 
    iotEndpoint="{aws-iot-endpoint}" 
    deployment="{PROD|DEV|TEST}" />

Loading Configuration (C++ Implementation)

#include <rapidxml/rapidxml.hpp>
#include <fstream>

struct IoTConfig {
    std::string clientID;
    std::string iotEndpoint;
    std::string deployment;
};

IoTConfig loadIoTConfig(const std::string& configPath) {
    std::ifstream file(configPath);
    std::string content((std::istreambuf_iterator<char>(file)),
                        std::istreambuf_iterator<char>());
    
    rapidxml::xml_document<> doc;
    doc.parse<0>(&content[0]);
    
    auto configNode = doc.first_node("Configuration");
    
    IoTConfig config;
    config.clientID = configNode->first_attribute("clientID")->value();
    config.iotEndpoint = configNode->first_attribute("iotEndpoint")->value();
    config.deployment = configNode->first_attribute("deployment")->value();
    
    return config;
}

2. Certificate Management

Certificate Files Structure

/mnt/nv/IoTCerts/
├── iot-cert.pem.crt      # Device client certificate
├── iot-private.pem.key   # Device private key
└── default.pem           # Additional cert data

/var/lib/iot/
└── rootCA.crt           # AWS IoT Root CA

Certificate Registration Process

#include <openssl/x509.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>

class IoTCertificateManager {
private:
    static const std::string CERT_ENDPOINT;
    static const std::string CERT_PATH;
    static const std::string KEY_PATH;
    
public:
    bool generateCSR() {
        // Generate EC key pair
        EC_KEY* eckey = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
        EC_KEY_generate_key(eckey);
        
        // Create certificate request
        X509_REQ* req = X509_REQ_new();
        X509_REQ_set_version(req, 0);
        
        // Set subject name
        X509_NAME* name = X509_NAME_new();
        X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
                                  (unsigned char*)clientID.c_str(), -1, -1, 0);
        X509_REQ_set_subject_name(req, name);
        
        // Set public key
        EVP_PKEY* pkey = EVP_PKEY_new();
        EVP_PKEY_set1_EC_KEY(pkey, eckey);
        X509_REQ_set_pubkey(req, pkey);
        
        // Sign request
        X509_REQ_sign(req, pkey, EVP_sha256());
        
        return sendCSRToEndpoint(req, pkey);
    }
    
    bool sendCSRToEndpoint(X509_REQ* req, EVP_PKEY* pkey) {
        // Send CSR to voice.api.bose.io/alexa/certificate
        // Receive certificate response
        // Store certificate and private key
        return true;
    }
};

const std::string IoTCertificateManager::CERT_ENDPOINT = 
    "https://voice.api.bose.io/alexa/certificate";
const std::string IoTCertificateManager::CERT_PATH = 
    "/mnt/nv/IoTCerts/iot-cert.pem.crt";
const std::string IoTCertificateManager::KEY_PATH = 
    "/mnt/nv/IoTCerts/iot-private.pem.key";

3. MQTT Connection Implementation

AWS IoT SDK Integration

#include <aws/iot/MqttClient.h>
#include <aws/iot/ShadowClient.h>

class IoTConnectionManager {
private:
    std::unique_ptr<awsiotsdk::MqttClient> mqttClient;
    std::unique_ptr<awsiotsdk::Shadow> shadowClient;
    IoTConfig config;
    
public:
    awsiotsdk::ResponseCode connect() {
        // Setup connection parameters
        std::string endpoint = config.iotEndpoint;
        uint16_t port = 8883; // MQTT over SSL
        
        // Load certificates
        std::string certPath = "/mnt/nv/IoTCerts/iot-cert.pem.crt";
        std::string keyPath = "/mnt/nv/IoTCerts/iot-private.pem.key";
        std::string rootCaPath = "/var/lib/iot/rootCA.crt";
        
        // Create network connection
        auto networkConnection = std::make_shared<awsiotsdk::network::MbedTLSConnection>(
            endpoint, port, rootCaPath, certPath, keyPath
        );
        
        // Create MQTT client
        mqttClient = awsiotsdk::MqttClient::Create(networkConnection);
        if (!mqttClient) {
            return awsiotsdk::ResponseCode::FAILURE;
        }
        
        // Connect with client ID
        auto connectPacket = awsiotsdk::mqtt::ConnectPacket::Create(
            config.clientID,
            true,  // cleanSession
            awsiotsdk::mqtt::QoS::QOS0,
            nullptr  // will options
        );
        
        return mqttClient->Connect(std::chrono::milliseconds(5000), connectPacket);
    }
    
    awsiotsdk::ResponseCode initializeShadow() {
        shadowClient = awsiotsdk::Shadow::Create(mqttClient);
        if (!shadowClient) {
            return awsiotsdk::ResponseCode::FAILURE;
        }
        
        // Subscribe to shadow delta
        auto deltaHandler = [this](const std::string& thingName,
                                  const std::string& payload) {
            handleShadowDelta(thingName, payload);
        };
        
        return shadowClient->PerformUpdateAsync(
            config.clientID,
            "",  // jsonString
            deltaHandler,
            std::chrono::seconds(10)
        );
    }
};

4. Device Shadow Operations

Shadow Message Structures

#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>

struct DeviceState {
    std::string deviceState;    // "CONNECTED" | "DISCONNECTED"
    std::string powerState;     // "ON" | "OFF"
    std::string zoneState;      // Zone configuration
    std::string groupState;     // Multi-room group info
};

class ShadowMessageBuilder {
public:
    static std::string createReportedState(const DeviceState& state) {
        rapidjson::Document doc;
        doc.SetObject();
        auto& allocator = doc.GetAllocator();
        
        // Create state object
        rapidjson::Value stateObj(rapidjson::kObjectType);
        rapidjson::Value reportedObj(rapidjson::kObjectType);
        
        // Add reported state fields
        reportedObj.AddMember("deviceState",
            rapidjson::Value(state.deviceState.c_str(), allocator),
            allocator);
        reportedObj.AddMember("powerState",
            rapidjson::Value(state.powerState.c_str(), allocator),
            allocator);
        reportedObj.AddMember("zoneState",
            rapidjson::Value(state.zoneState.c_str(), allocator),
            allocator);
        reportedObj.AddMember("groupState",
            rapidjson::Value(state.groupState.c_str(), allocator),
            allocator);
        
        stateObj.AddMember("reported", reportedObj, allocator);
        doc.AddMember("state", stateObj, allocator);
        
        // Serialize to string
        rapidjson::StringBuffer buffer;
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
        doc.Accept(writer);
        
        return buffer.GetString();
    }
    
    static DeviceState parseDesiredState(const std::string& json) {
        rapidjson::Document doc;
        doc.Parse(json.c_str());
        
        DeviceState state;
        if (doc.HasMember("state") && doc["state"].HasMember("desired")) {
            auto& desired = doc["state"]["desired"];
            
            if (desired.HasMember("powerState")) {
                state.powerState = desired["powerState"].GetString();
            }
            if (desired.HasMember("zoneState")) {
                state.zoneState = desired["zoneState"].GetString();
            }
            if (desired.HasMember("groupState")) {
                state.groupState = desired["groupState"].GetString();
            }
        }
        
        return state;
    }
};

Shadow Update Implementation

class IoTShadowManager {
private:
    std::shared_ptr<awsiotsdk::Shadow> shadowClient;
    std::string thingName;
    DeviceState currentState;
    
public:
    awsiotsdk::ResponseCode updateDeviceState(const DeviceState& newState) {
        currentState = newState;
        
        std::string payload = ShadowMessageBuilder::createReportedState(newState);
        
        auto responseHandler = [](const std::string& thingName,
                                awsiotsdk::ShadowRequestType requestType,
                                awsiotsdk::ShadowResponseType responseType,
                                rapidjson::Document& payload) {
            if (responseType == awsiotsdk::ShadowResponseType::Accepted) {
                // Shadow update successful
                std::cout << "Shadow updated successfully" << std::endl;
            } else {
                // Handle rejection
                std::cout << "Shadow update rejected" << std::endl;
            }
        };
        
        return shadowClient->PerformUpdateAsync(
            thingName,
            payload,
            responseHandler,
            std::chrono::seconds(10)
        );
    }
    
    void handleShadowDelta(const std::string& thingName, 
                          const std::string& payload) {
        DeviceState desiredState = ShadowMessageBuilder::parseDesiredState(payload);
        
        // Apply desired state changes to device
        if (!desiredState.powerState.empty()) {
            applyPowerStateChange(desiredState.powerState);
        }
        
        if (!desiredState.zoneState.empty()) {
            applyZoneStateChange(desiredState.zoneState);
        }
        
        if (!desiredState.groupState.empty()) {
            applyGroupStateChange(desiredState.groupState);
        }
        
        // Report updated state back to shadow
        updateDeviceState(currentState);
    }
};

5. Service Integration

Shepherd Service Configuration

<!-- /opt/Bose/etc/Shepherd-noncore.xml -->
<ShepherdConfig>
  <daemon name="STSCertified"/>
  <daemon name="IoT">
    <env name="IOT_CONFIG_PATH">/mnt/nv/BoseApp-Persistence/1/IoT.xml</env>
    <env name="IOT_CERT_PATH">/mnt/nv/IoTCerts</env>
  </daemon>
  <daemon name="TPDA">
    <arg>-c</arg>
    <arg>/opt/Bose/etc/Voice.xml</arg>
  </daemon>
</ShepherdConfig>

System Startup Integration

#!/bin/bash
# /etc/init.d/SoundTouch fragment

# Create IoT directories
mkdir -p /mnt/nv/BoseLog /mnt/nv/IoTCerts /mnt/nv/BoseApp-Persistence/1
mkdir -m 700 -p /mnt/nv/BoseApp-Persistence/1/Keys

# Set proper permissions for certificate storage
chmod 700 /mnt/nv/IoTCerts
chown iot:iot /mnt/nv/IoTCerts

# Start shepherd daemon manager
shepherdd --config-dir /opt/Bose/etc --run-dir /var/run/shepherd

Error Handling and Debugging

Connection Retry Logic

class ConnectionRetryManager {
private:
    int maxRetries = 10;
    int retryDelaySeconds = 5;
    
public:
    awsiotsdk::ResponseCode connectWithRetry(IoTConnectionManager& manager) {
        for (int attempt = 1; attempt <= maxRetries; ++attempt) {
            std::cout << "Connection attempt " << attempt 
                     << " to MQTT port at host " << config.iotEndpoint << std::endl;
            
            auto result = manager.connect();
            if (result == awsiotsdk::ResponseCode::SUCCESS) {
                std::cout << "Successfully connected to MQTT server" << std::endl;
                return result;
            }
            
            std::cout << "MQTT port not available. Retrying in " 
                     << retryDelaySeconds << " seconds" << std::endl;
            
            std::this_thread::sleep_for(std::chrono::seconds(retryDelaySeconds));
            retryDelaySeconds *= 2; // Exponential backoff
        }
        
        std::cerr << "Failed to connect after " << maxRetries << " attempts" << std::endl;
        return awsiotsdk::ResponseCode::FAILURE;
    }
};

Logging and Monitoring

class IoTLogger {
public:
    static void logConnectionStatus(const std::string& status) {
        std::cout << "[IoT] Connection status: " << status << std::endl;
    }
    
    static void logShadowResponse(awsiotsdk::ShadowResponseType response, 
                                 const std::string& payload) {
        if (response == awsiotsdk::ShadowResponseType::Accepted) {
            std::cout << "[IoT] Shadow response: accepted. Payload: " << payload << std::endl;
        } else {
            std::cout << "[IoT] Shadow response: rejected" << std::endl;
        }
    }
    
    static void logCertificateStatus(bool success) {
        if (success) {
            std::cout << "[IoT] Certificate generated successfully" << std::endl;
        } else {
            std::cerr << "[IoT] Failed to generate iot certificate" << std::endl;
        }
    }
};

Testing and Validation

Unit Test Example

#include <gtest/gtest.h>

class IoTConfigTest : public ::testing::Test {
protected:
    void SetUp() override {
        // Create test configuration file
        std::ofstream file("/tmp/test_iot.xml");
        file << R"(<?xml version="1.0" encoding="UTF-8" ?>
<Configuration clientID="test-client-id" 
               iotEndpoint="test.iot.amazonaws.com" 
               deployment="TEST" />)";
        file.close();
    }
};

TEST_F(IoTConfigTest, LoadConfiguration) {
    auto config = loadIoTConfig("/tmp/test_iot.xml");
    
    EXPECT_EQ(config.clientID, "test-client-id");
    EXPECT_EQ(config.iotEndpoint, "test.iot.amazonaws.com");
    EXPECT_EQ(config.deployment, "TEST");
}

TEST_F(IoTConfigTest, ShadowMessageBuilder) {
    DeviceState state;
    state.deviceState = "CONNECTED";
    state.powerState = "ON";
    
    std::string json = ShadowMessageBuilder::createReportedState(state);
    
    // Verify JSON contains expected fields
    EXPECT_TRUE(json.find("\"deviceState\":\"CONNECTED\"") != std::string::npos);
    EXPECT_TRUE(json.find("\"powerState\":\"ON\"") != std::string::npos);
}

Security Best Practices

  1. Certificate Management
    • Store private keys with 600 permissions
    • Rotate certificates regularly
    • Use hardware security modules when available
  2. Network Security
    • Always use TLS 1.2 or higher
    • Validate certificate chains
    • Implement certificate pinning
  3. Configuration Security
    • Encrypt sensitive configuration data
    • Use secure storage for credentials
    • Implement configuration validation

Troubleshooting Common Issues

Certificate Problems

# Check certificate validity
openssl x509 -in /mnt/nv/IoTCerts/iot-cert.pem.crt -text -noout

# Verify private key matches certificate
openssl x509 -noout -modulus -in /mnt/nv/IoTCerts/iot-cert.pem.crt | openssl md5
openssl rsa -noout -modulus -in /mnt/nv/IoTCerts/iot-private.pem.key | openssl md5

Connection Issues

# Test MQTT connectivity
mosquitto_pub -h a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com \
              -p 8883 --cafile /var/lib/iot/rootCA.crt \
              --cert /mnt/nv/IoTCerts/iot-cert.pem.crt \
              --key /mnt/nv/IoTCerts/iot-private.pem.key \
              -t '$aws/things/test/shadow/update' \
              -m '{"state":{"reported":{"test":"value"}}}'

Service Debugging

# Check service status
ps aux | grep IoT

# Monitor system logs
tail -f /mnt/nv/BoseLog/IoT.log

# Check Shepherd status
shepherdd --status

MQTT Monitoring and Research

Direct Device Credential Access

With device certificates and private keys available from firmware backups, it’s technically possible to monitor MQTT traffic:

# Subscribe to your device's shadow events only
CLIENT_ID="577ecfcc-2db3-4989-92c9-76d7704f9fb3"  # Your device's UUID
mosquitto_sub -h a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com \
              -p 8883 --cafile /var/lib/iot/rootCA.crt \
              --cert /mnt/nv/IoTCerts/iot-cert.pem.crt \
              --key /mnt/nv/IoTCerts/iot-private.pem.key \
              -t "\$aws/things/$CLIENT_ID/shadow/update/accepted"

Security Constraints and Limitations

AWS IoT Policy Restrictions

Device certificates are bound to restrictive policies:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:Connect",
      "Resource": "arn:aws:iot:us-east-1:*:client/${iot:ClientId}"
    },
    {
      "Effect": "Allow",
      "Action": ["iot:Publish", "iot:Subscribe", "iot:Receive"],
      "Resource": [
        "arn:aws:iot:us-east-1:*:topic/$aws/things/${iot:ClientId}/shadow/*"
      ]
    }
  ]
}

Limitations:

Alternative Monitoring Approaches

# Capture MQTT traffic patterns without authentication
tcpdump -i eth0 -s0 -w soundtouch_iot.pcap host a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com

# Monitor connection patterns in real-time
tcpdump -i eth0 -n -A "host a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com and port 8883"

# Extract timing and packet size information
tcpdump -i eth0 -ttt -s0 "host a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com"

Local MQTT Broker for Testing

# Set up local Mosquitto broker
sudo apt-get install mosquitto mosquitto-clients

# Configure TLS (optional)
cat > /etc/mosquitto/conf.d/tls.conf << EOF
port 8883
cafile /path/to/ca.crt
certfile /path/to/server.crt
keyfile /path/to/server.key
require_certificate true
use_identity_as_username true
EOF

# Test local shadow operations
mosquitto_pub -h localhost -p 8883 \
              -t '$aws/things/test-device/shadow/update' \
              -m '{"state":{"reported":{"deviceState":"CONNECTED"}}}'

Message Analysis and Documentation

Expected shadow message patterns:

// Power state transitions
{
  "state": {
    "reported": {
      "deviceState": "CONNECTED",
      "powerState": "ON|OFF"
    }
  },
  "timestamp": 1703875200
}

// Audio control updates  
{
  "state": {
    "reported": {
      "volume": 25,
      "muted": false,
      "source": "SPOTIFY"
    }
  }
}

// Multi-room coordination
{
  "state": {
    "reported": {
      "zoneState": "master|slave",
      "groupMembers": ["device1", "device2"],
      "groupName": "Living Room"
    }
  }
}

Important Warnings:

Recommended Usage:

Research Implementation Example

class IoTResearchMonitor {
private:
    std::string deviceClientId;
    std::ofstream messageLog;
    
public:
    void captureMessagePatterns() {
        // Subscribe only to owned device topics
        std::string topic = "$aws/things/" + deviceClientId + "/shadow/update/accepted";
        
        auto messageHandler = [this](const std::string& topic, const std::string& payload) {
            // Log message structure for analysis
            messageLog << "Topic: " << topic << std::endl;
            messageLog << "Payload: " << payload << std::endl;
            messageLog << "Timestamp: " << getCurrentTimestamp() << std::endl;
            messageLog << "---" << std::endl;
            
            // Parse and document state transitions
            documentStateTransition(payload);
        };
        
        // WARNING: Only use with your own device certificates
        connectToAWSIoT(messageHandler);
    }
    
    void documentStateTransition(const std::string& json) {
        // Analyze JSON structure for local implementation
        rapidjson::Document doc;
        doc.Parse(json.c_str());
        
        if (doc.HasMember("state") && doc["state"].HasMember("reported")) {
            // Document field types and value ranges
            auto& reported = doc["state"]["reported"];
            
            for (auto& field : reported.GetObject()) {
                std::cout << "Field: " << field.name.GetString() 
                         << ", Type: " << getJSONType(field.value) << std::endl;
            }
        }
    }
};

This implementation guide provides the foundation for integrating with the Bose SoundTouch IoT system using AWS IoT Core, certificate-based authentication, and device shadow operations. The monitoring capabilities should be used responsibly and only for research purposes to develop local alternatives.