mirror of
https://github.com/colonelpanichacks/flock-you.git
synced 2026-06-11 14:43:30 -07:00
Add BLE GATT server, serial host detection, and companion mode
Enable DeFlock mobile app connectivity via BLE GATT notifications, and desktop host detection via USB serial heartbeat. When a companion is connected, WiFi AP is disabled to free radio bandwidth and BLE scan duty cycle is increased for better detection performance. - BLE GATT server advertising service UUID a1b2c3d4-e5f6-7890-abcd-ef0123456789 with TX characteristic (NOTIFY) for streaming detection JSON - Chunked BLE notification sender respecting negotiated MTU - "event":"detection" field added to JSON output for DeFlock parser - Serial host detection via heartbeat timeout (5s) - Companion mode: WiFi AP off + scan duration 2s→3s when connected - Scan interval/duration converted from #define to mutable variables
This commit is contained in:
+147
-20
@@ -43,9 +43,9 @@
|
||||
#define DETECT_BEEP_DURATION 150
|
||||
#define HEARTBEAT_DURATION 100
|
||||
|
||||
// BLE scanning
|
||||
#define BLE_SCAN_DURATION 2 // seconds per scan
|
||||
#define BLE_SCAN_INTERVAL 3000 // ms between scans
|
||||
// BLE scanning (mutable — companion mode increases scan duty cycle)
|
||||
static int fyBleScanDuration = 2; // seconds per scan
|
||||
static unsigned long fyBleScanInterval = 3000; // ms between scans
|
||||
|
||||
// Detection storage
|
||||
#define MAX_DETECTIONS 200
|
||||
@@ -144,6 +144,19 @@ static unsigned long fyLastHB = 0;
|
||||
static NimBLEScan* fyBLEScan = NULL;
|
||||
static AsyncWebServer fyServer(80);
|
||||
|
||||
// BLE GATT server (DeFlock app connectivity)
|
||||
#define FY_SERVICE_UUID "a1b2c3d4-e5f6-7890-abcd-ef0123456789"
|
||||
#define FY_TX_CHAR_UUID "a1b2c3d4-e5f6-7890-abcd-ef01234567aa"
|
||||
static NimBLEServer* fyBLEServer = NULL;
|
||||
static NimBLECharacteristic* fyTxChar = NULL;
|
||||
static volatile bool fyBLEClientConnected = false;
|
||||
static volatile uint16_t fyNegotiatedMTU = 23;
|
||||
|
||||
// Serial host detection (USB heartbeat from DeFlock desktop)
|
||||
static volatile bool fySerialHostConnected = false;
|
||||
static unsigned long fyLastSerialHeartbeat = 0;
|
||||
#define FY_SERIAL_TIMEOUT_MS 5000
|
||||
|
||||
// Phone GPS state (updated via browser Geolocation API -> /api/gps)
|
||||
static double fyGPSLat = 0;
|
||||
static double fyGPSLon = 0;
|
||||
@@ -368,6 +381,74 @@ static int fyAddDetection(const char* mac, const char* name, int rssi,
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BLE GATT SERVER (DeFlock companion connectivity)
|
||||
// ============================================================================
|
||||
|
||||
// Forward declaration
|
||||
static void fyOnCompanionChange();
|
||||
|
||||
class FYServerCallbacks : public NimBLEServerCallbacks {
|
||||
void onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) override {
|
||||
fyBLEClientConnected = true;
|
||||
printf("[FLOCK-YOU] BLE client connected\n");
|
||||
fyOnCompanionChange();
|
||||
}
|
||||
void onDisconnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) override {
|
||||
fyBLEClientConnected = false;
|
||||
fyNegotiatedMTU = 23;
|
||||
printf("[FLOCK-YOU] BLE client disconnected\n");
|
||||
NimBLEDevice::startAdvertising();
|
||||
fyOnCompanionChange();
|
||||
}
|
||||
void onMTUChange(uint16_t mtu, ble_gap_conn_desc* desc) override {
|
||||
fyNegotiatedMTU = mtu;
|
||||
printf("[FLOCK-YOU] MTU negotiated: %u\n", mtu);
|
||||
}
|
||||
};
|
||||
|
||||
static void fySendBLE(const char* data, size_t len) {
|
||||
if (!fyBLEClientConnected || !fyTxChar) return;
|
||||
uint16_t chunkSize = fyNegotiatedMTU - 3;
|
||||
if (chunkSize < 1) chunkSize = 1;
|
||||
if (len <= chunkSize) {
|
||||
fyTxChar->setValue((const uint8_t*)data, len);
|
||||
fyTxChar->notify();
|
||||
} else {
|
||||
size_t offset = 0;
|
||||
while (offset < len) {
|
||||
size_t remaining = len - offset;
|
||||
size_t send = remaining < chunkSize ? remaining : chunkSize;
|
||||
fyTxChar->setValue((const uint8_t*)(data + offset), send);
|
||||
fyTxChar->notify();
|
||||
offset += send;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COMPANION MODE (WiFi AP vs BLE/serial)
|
||||
// ============================================================================
|
||||
|
||||
static void fyOnCompanionChange() {
|
||||
if (fyBLEClientConnected || fySerialHostConnected) {
|
||||
// Companion mode — disable WiFi AP, boost BLE scanning
|
||||
WiFi.softAPdisconnect(true);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
fyBleScanDuration = 3;
|
||||
printf("[FLOCK-YOU] Companion mode: WiFi AP OFF, scan duration %ds\n",
|
||||
fyBleScanDuration);
|
||||
} else {
|
||||
// Standalone mode — re-enable WiFi AP and web dashboard
|
||||
WiFi.mode(WIFI_AP);
|
||||
delay(100);
|
||||
WiFi.softAP(FY_AP_SSID, FY_AP_PASS);
|
||||
fyBleScanDuration = 2;
|
||||
printf("[FLOCK-YOU] Standalone mode: WiFi AP ON (%s), scan duration %ds\n",
|
||||
FY_AP_SSID, fyBleScanDuration);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BLE SCANNING
|
||||
// ============================================================================
|
||||
@@ -440,24 +521,34 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks {
|
||||
addrStr.c_str(), name.c_str(), rssi, method,
|
||||
idx >= 0 ? fyDet[idx].count : 0);
|
||||
|
||||
// JSON serial output (Flask-compatible format for live ingestion)
|
||||
// Build GPS fragment if available
|
||||
// JSON output — build into buffer for serial + BLE
|
||||
char gpsBuf[80] = "";
|
||||
if (fyGPSIsFresh()) {
|
||||
snprintf(gpsBuf, sizeof(gpsBuf),
|
||||
",\"gps\":{\"latitude\":%.8f,\"longitude\":%.8f,\"accuracy\":%.1f}",
|
||||
fyGPSLat, fyGPSLon, fyGPSAcc);
|
||||
}
|
||||
char jsonBuf[512];
|
||||
int jsonLen;
|
||||
if (isRaven) {
|
||||
printf("{\"detection_method\":\"%s\",\"protocol\":\"bluetooth_le\","
|
||||
"\"mac_address\":\"%s\",\"device_name\":\"%s\","
|
||||
"\"rssi\":%d,\"is_raven\":true,\"raven_fw\":\"%s\"%s}\n",
|
||||
method, addrStr.c_str(), name.c_str(), rssi, ravenFW, gpsBuf);
|
||||
jsonLen = snprintf(jsonBuf, sizeof(jsonBuf),
|
||||
"{\"event\":\"detection\",\"detection_method\":\"%s\","
|
||||
"\"protocol\":\"bluetooth_le\",\"mac_address\":\"%s\","
|
||||
"\"device_name\":\"%s\",\"rssi\":%d,"
|
||||
"\"is_raven\":true,\"raven_fw\":\"%s\"%s}",
|
||||
method, addrStr.c_str(), name.c_str(), rssi, ravenFW, gpsBuf);
|
||||
} else {
|
||||
printf("{\"detection_method\":\"%s\",\"protocol\":\"bluetooth_le\","
|
||||
"\"mac_address\":\"%s\",\"device_name\":\"%s\","
|
||||
"\"rssi\":%d%s}\n",
|
||||
method, addrStr.c_str(), name.c_str(), rssi, gpsBuf);
|
||||
jsonLen = snprintf(jsonBuf, sizeof(jsonBuf),
|
||||
"{\"event\":\"detection\",\"detection_method\":\"%s\","
|
||||
"\"protocol\":\"bluetooth_le\",\"mac_address\":\"%s\","
|
||||
"\"device_name\":\"%s\",\"rssi\":%d%s}",
|
||||
method, addrStr.c_str(), name.c_str(), rssi, gpsBuf);
|
||||
}
|
||||
printf("%s\n", jsonBuf);
|
||||
// Append newline for BLE framing and send
|
||||
if (jsonLen > 0 && jsonLen < (int)sizeof(jsonBuf) - 1) {
|
||||
jsonBuf[jsonLen] = '\n';
|
||||
fySendBLE(jsonBuf, jsonLen + 1);
|
||||
}
|
||||
|
||||
if (!fyTriggered) {
|
||||
@@ -929,8 +1020,11 @@ void setup() {
|
||||
printf(" Buzzer: %s\n", fyBuzzerOn ? "ON" : "OFF");
|
||||
printf("========================================\n");
|
||||
|
||||
// Init BLE scanner FIRST -- start scanning immediately
|
||||
NimBLEDevice::init("");
|
||||
// Init BLE with device name and large MTU for GATT notifications
|
||||
NimBLEDevice::init("flockyou");
|
||||
NimBLEDevice::setMTU(512);
|
||||
|
||||
// BLE scanner setup
|
||||
fyBLEScan = NimBLEDevice::getScan();
|
||||
fyBLEScan->setAdvertisedDeviceCallbacks(new FYBLECallbacks());
|
||||
fyBLEScan->setActiveScan(true);
|
||||
@@ -938,10 +1032,27 @@ void setup() {
|
||||
fyBLEScan->setWindow(99);
|
||||
|
||||
// Kick off the first scan right away
|
||||
fyBLEScan->start(BLE_SCAN_DURATION, false);
|
||||
fyBLEScan->start(fyBleScanDuration, false);
|
||||
fyLastBleScan = millis();
|
||||
printf("[FLOCK-YOU] BLE scanning ACTIVE\n");
|
||||
|
||||
// BLE GATT server — DeFlock app connectivity
|
||||
fyBLEServer = NimBLEDevice::createServer();
|
||||
fyBLEServer->setCallbacks(new FYServerCallbacks());
|
||||
NimBLEService* pService = fyBLEServer->createService(FY_SERVICE_UUID);
|
||||
fyTxChar = pService->createCharacteristic(
|
||||
FY_TX_CHAR_UUID,
|
||||
NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
pService->start();
|
||||
|
||||
NimBLEAdvertising* pAdv = NimBLEDevice::getAdvertising();
|
||||
pAdv->addServiceUUID(FY_SERVICE_UUID);
|
||||
pAdv->setName("flockyou");
|
||||
pAdv->setScanResponse(true);
|
||||
pAdv->start();
|
||||
printf("[FLOCK-YOU] BLE GATT server advertising (service %s)\n", FY_SERVICE_UUID);
|
||||
|
||||
// Crow calls play WHILE BLE is already scanning
|
||||
fyBootBeep();
|
||||
|
||||
@@ -957,17 +1068,33 @@ void setup() {
|
||||
|
||||
printf("[FLOCK-YOU] Detection methods: MAC prefix, device name, manufacturer ID, Raven UUID\n");
|
||||
printf("[FLOCK-YOU] Dashboard: http://192.168.4.1\n");
|
||||
printf("[FLOCK-YOU] Ready - no WiFi connection needed, BLE + AP only\n\n");
|
||||
printf("[FLOCK-YOU] Ready - BLE GATT + AP mode\n\n");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Serial host detection (heartbeat from DeFlock desktop app)
|
||||
if (Serial.available()) {
|
||||
while (Serial.available()) Serial.read(); // drain buffer
|
||||
fyLastSerialHeartbeat = millis();
|
||||
if (!fySerialHostConnected) {
|
||||
fySerialHostConnected = true;
|
||||
printf("[FLOCK-YOU] Serial host connected\n");
|
||||
fyOnCompanionChange();
|
||||
}
|
||||
} else if (fySerialHostConnected &&
|
||||
millis() - fyLastSerialHeartbeat >= FY_SERIAL_TIMEOUT_MS) {
|
||||
fySerialHostConnected = false;
|
||||
printf("[FLOCK-YOU] Serial host disconnected (timeout)\n");
|
||||
fyOnCompanionChange();
|
||||
}
|
||||
|
||||
// BLE scanning cycle
|
||||
if (millis() - fyLastBleScan >= BLE_SCAN_INTERVAL && !fyBLEScan->isScanning()) {
|
||||
fyBLEScan->start(BLE_SCAN_DURATION, false);
|
||||
if (millis() - fyLastBleScan >= fyBleScanInterval && !fyBLEScan->isScanning()) {
|
||||
fyBLEScan->start(fyBleScanDuration, false);
|
||||
fyLastBleScan = millis();
|
||||
}
|
||||
|
||||
if (!fyBLEScan->isScanning() && millis() - fyLastBleScan > BLE_SCAN_DURATION * 1000) {
|
||||
if (!fyBLEScan->isScanning() && millis() - fyLastBleScan > (unsigned long)fyBleScanDuration * 1000) {
|
||||
fyBLEScan->clearResults();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user