mirror of
https://github.com/mattintech/pyBTMCP.git
synced 2026-07-11 12:01:53 +00:00
v1.4.0: BLE disconnect simulation with teardown support
Features:
- Add BLE disconnect simulation to test client reconnection behavior
- Support immediate disconnect, timed advertising pause, and full BLE teardown
- Add preset test buttons in admin UI (Quick Drop, 5s Pause, BLE Restart, 10s Restart)
- Add simulate_ble_disconnect MCP tool with duration_ms and teardown params
- Add POST /devices/{id}/disconnect API endpoint
Fixes:
- Fix treadmill distance not updating in admin UI (publish values periodically)
- Fix HR variation sending to non-heart_rate devices
- Fix BLE connection ID 0 being treated as "not connected"
- Clean up misleading startup log messages
Firmware: 1.1.0, API: 1.4.0, UI: 1.5.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
// ============================================
|
||||
// Firmware Version
|
||||
// ============================================
|
||||
#define FIRMWARE_VERSION "1.0.7"
|
||||
#define FIRMWARE_VERSION "1.1.0"
|
||||
|
||||
// ============================================
|
||||
// AP Mode Configuration
|
||||
|
||||
@@ -44,13 +44,23 @@ public:
|
||||
// State
|
||||
bool isClientConnected() const { return deviceConnected; }
|
||||
|
||||
// Disconnect simulation
|
||||
void disconnectClient(); // Force disconnect + immediate re-advertise
|
||||
void disconnectClientForDuration(int ms); // Disconnect + pause advertising for duration
|
||||
void teardownForDuration(int ms); // Full BLE deinit + reinit after duration
|
||||
|
||||
private:
|
||||
BleService() = default;
|
||||
|
||||
bool deviceConnected = false;
|
||||
unsigned long lastNotify = 0;
|
||||
unsigned long advertisingResumeTime = 0; // When to resume advertising (0 = not paused)
|
||||
bool advertisingPaused = false;
|
||||
unsigned long teardownResumeTime = 0; // When to reinit BLE after teardown
|
||||
bool teardownPending = false;
|
||||
|
||||
void initBLE();
|
||||
void reinitBLE(); // Reinit BLE and restore services
|
||||
};
|
||||
|
||||
#define bleService BleService::getInstance()
|
||||
|
||||
@@ -47,11 +47,8 @@ void setup() {
|
||||
|
||||
Serial.println("\nReady!");
|
||||
if (!configService.isConfigured()) {
|
||||
Serial.println("Configure at: http://192.168.4.1");
|
||||
} else {
|
||||
Serial.println("Connecting to WiFi...");
|
||||
Serial.println("Configure at: http://192.168.4.1\n");
|
||||
}
|
||||
Serial.println("Waiting for MQTT commands...\n");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
|
||||
@@ -18,9 +18,11 @@ static NimBLEService* pBatteryService = nullptr;
|
||||
static NimBLEService* pFitnessMachineService = nullptr;
|
||||
|
||||
static bool bleInitialized = false;
|
||||
static uint16_t currentConnId = 0; // Track connection ID for disconnect
|
||||
static bool advertisingPausedFlag = false; // Shared with callbacks
|
||||
|
||||
// Forward declare for callback
|
||||
static void onBleConnect();
|
||||
static void onBleConnect(NimBLEServer* server);
|
||||
static void onBleDisconnect();
|
||||
|
||||
// ============================================
|
||||
@@ -28,7 +30,7 @@ static void onBleDisconnect();
|
||||
// ============================================
|
||||
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||
void onConnect(NimBLEServer* pServer) override {
|
||||
onBleConnect();
|
||||
onBleConnect(pServer);
|
||||
}
|
||||
|
||||
void onDisconnect(NimBLEServer* pServer) override {
|
||||
@@ -36,15 +38,26 @@ class ServerCallbacks : public NimBLEServerCallbacks {
|
||||
}
|
||||
};
|
||||
|
||||
static void onBleConnect() {
|
||||
static void onBleConnect(NimBLEServer* server) {
|
||||
// Track connection ID for forced disconnect
|
||||
// getPeerInfo(0) gets the first (most recent) connected peer
|
||||
NimBLEConnInfo peerInfo = server->getPeerInfo(0);
|
||||
currentConnId = peerInfo.getConnHandle();
|
||||
deviceState.setBleClientConnected(true);
|
||||
Serial.println("BLE client connected");
|
||||
Serial.print("BLE client connected (connId: ");
|
||||
Serial.print(currentConnId);
|
||||
Serial.println(")");
|
||||
}
|
||||
|
||||
static void onBleDisconnect() {
|
||||
currentConnId = 0;
|
||||
deviceState.setBleClientConnected(false);
|
||||
Serial.println("BLE client disconnected");
|
||||
NimBLEDevice::startAdvertising();
|
||||
|
||||
// Only auto-resume advertising if not paused
|
||||
if (!advertisingPausedFlag) {
|
||||
NimBLEDevice::startAdvertising();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -63,8 +76,24 @@ void BleService::setup() {
|
||||
}
|
||||
|
||||
void BleService::loop() {
|
||||
// Check if BLE should be reinitialized after teardown
|
||||
if (teardownPending && teardownResumeTime > 0 && millis() >= teardownResumeTime) {
|
||||
teardownResumeTime = 0;
|
||||
teardownPending = false;
|
||||
reinitBLE();
|
||||
}
|
||||
|
||||
if (!deviceState.isBleStarted()) return;
|
||||
|
||||
// Check if advertising should be resumed after timed pause
|
||||
if (advertisingPaused && advertisingResumeTime > 0 && millis() >= advertisingResumeTime) {
|
||||
advertisingResumeTime = 0;
|
||||
advertisingPaused = false;
|
||||
advertisingPausedFlag = false;
|
||||
NimBLEDevice::startAdvertising();
|
||||
Serial.println("Advertising resumed after timed pause");
|
||||
}
|
||||
|
||||
// Send notifications at regular intervals
|
||||
if (millis() - lastNotify >= BLE_NOTIFY_INTERVAL) {
|
||||
lastNotify = millis();
|
||||
@@ -269,3 +298,84 @@ void BleService::notifyTreadmill(uint16_t speed, int16_t incline, uint32_t dista
|
||||
pTreadmillData->setValue(data, 11);
|
||||
pTreadmillData->notify();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Disconnect Simulation
|
||||
// ============================================
|
||||
void BleService::disconnectClient() {
|
||||
if (!pServer || !deviceState.getConnectionState().bleClientConnected) {
|
||||
Serial.println("No BLE client connected to disconnect");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("Forcing BLE client disconnect (immediate re-advertise)");
|
||||
pServer->disconnect(currentConnId);
|
||||
// onBleDisconnect callback will handle re-advertising
|
||||
}
|
||||
|
||||
void BleService::disconnectClientForDuration(int ms) {
|
||||
if (!pServer || !deviceState.getConnectionState().bleClientConnected) {
|
||||
Serial.println("No BLE client connected to disconnect");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.print("Forcing BLE client disconnect, pausing advertising for ");
|
||||
Serial.print(ms);
|
||||
Serial.println("ms");
|
||||
|
||||
// Set flags before disconnect so callback knows not to auto-resume
|
||||
advertisingPaused = true;
|
||||
advertisingPausedFlag = true;
|
||||
advertisingResumeTime = millis() + ms;
|
||||
|
||||
pServer->disconnect(currentConnId);
|
||||
// onBleDisconnect callback will NOT resume advertising due to flag
|
||||
}
|
||||
|
||||
void BleService::teardownForDuration(int ms) {
|
||||
Serial.print("Tearing down BLE stack, will reinit in ");
|
||||
Serial.print(ms);
|
||||
Serial.println("ms");
|
||||
|
||||
// Clear all service/characteristic pointers
|
||||
pHeartRateMeasurement = nullptr;
|
||||
pBatteryLevel = nullptr;
|
||||
pTreadmillData = nullptr;
|
||||
pHeartRateService = nullptr;
|
||||
pBatteryService = nullptr;
|
||||
pFitnessMachineService = nullptr;
|
||||
pServer = nullptr;
|
||||
pAdvertising = nullptr;
|
||||
|
||||
// Full BLE deinit
|
||||
NimBLEDevice::deinit(true);
|
||||
bleInitialized = false;
|
||||
currentConnId = 0;
|
||||
deviceConnected = false;
|
||||
deviceState.setBleClientConnected(false);
|
||||
|
||||
// Schedule reinit
|
||||
teardownPending = true;
|
||||
teardownResumeTime = millis() + ms;
|
||||
|
||||
Serial.println("BLE stack torn down - device will disappear from scans");
|
||||
}
|
||||
|
||||
void BleService::reinitBLE() {
|
||||
Serial.println("Reinitializing BLE stack after teardown...");
|
||||
|
||||
// Reinit the BLE stack
|
||||
initBLE();
|
||||
|
||||
// Restore the previous device type configuration
|
||||
DeviceType currentType = deviceState.getDeviceType();
|
||||
if (currentType == DeviceType::HEART_RATE) {
|
||||
setupHeartRate();
|
||||
Serial.println("Restored Heart Rate service");
|
||||
} else if (currentType == DeviceType::TREADMILL) {
|
||||
setupTreadmill();
|
||||
Serial.println("Restored Treadmill service");
|
||||
}
|
||||
|
||||
Serial.println("BLE stack reinitialized - device visible again");
|
||||
}
|
||||
|
||||
@@ -30,10 +30,11 @@ void MqttService::loop() {
|
||||
connectToMQTT();
|
||||
mqtt.loop();
|
||||
|
||||
// Periodic status report
|
||||
// Periodic status and values report
|
||||
if (mqttConnected && millis() - lastStatusReport >= STATUS_REPORT_INTERVAL) {
|
||||
lastStatusReport = millis();
|
||||
publishStatus();
|
||||
publishValues(); // Include current values (e.g., accumulated distance)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +84,11 @@ void MqttService::connectToMQTT() {
|
||||
// Subscribe to control topics
|
||||
String configTopic = String("ble-sim/") + configService.getDeviceId() + "/config";
|
||||
String setTopic = String("ble-sim/") + configService.getDeviceId() + "/set";
|
||||
String disconnectTopic = String("ble-sim/") + configService.getDeviceId() + "/disconnect";
|
||||
|
||||
mqtt.subscribe(configTopic.c_str());
|
||||
mqtt.subscribe(setTopic.c_str());
|
||||
mqtt.subscribe(disconnectTopic.c_str());
|
||||
|
||||
Serial.print("Subscribed to: ");
|
||||
Serial.println(configTopic);
|
||||
@@ -161,6 +164,23 @@ void MqttService::handleMessage(char* topic, byte* payload, unsigned int length)
|
||||
|
||||
publishValues();
|
||||
}
|
||||
// Handle BLE disconnect command
|
||||
else if (topicStr == baseTopic + "/disconnect") {
|
||||
int duration = doc["duration_ms"] | 0; // 0 = immediate resume
|
||||
bool teardown = doc["teardown"] | false; // Full BLE stack teardown
|
||||
|
||||
if (teardown) {
|
||||
// Full teardown - device disappears from scans
|
||||
int teardownDuration = duration > 0 ? duration : 3000; // Default 3s for teardown
|
||||
bleService.teardownForDuration(teardownDuration);
|
||||
} else if (duration > 0) {
|
||||
// Just disconnect and pause advertising
|
||||
bleService.disconnectClientForDuration(duration);
|
||||
} else {
|
||||
// Just disconnect, immediate re-advertise
|
||||
bleService.disconnectClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MqttService::publishStatus() {
|
||||
|
||||
Reference in New Issue
Block a user