├── known_devices.txt ├── README.md └── src └── beacon_detect.cpp /known_devices.txt: -------------------------------------------------------------------------------- 1 | MyBeacon 01234567-ABCD-0123-4567-ABCD-01234567:FFE0-FFE1 2 | SomeOtherBeacon 01234567-ABCD-0123-4567-ABCD-01234567:FFE0-FFE1 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BLE-detect 2 | 3 | This is a very low latency BLE based vehicle arrival and departure detection system reporting over MQTT. Originally designed to work with Home Assistant, it can also be used with any other automation system that supports MQTT. 4 | 5 | ## Principle 6 | 7 | BLE-detect uses an inexpensive Raspberry Pi Zero W board to detect nearby BLE beacons using passive scanning. The scanning and detection is performed in hardware by the Bluetooth chip and offers very reliable and extremely low latency detection in the milliseconds range. A BLE beacon is placed in a car and the Pi Zero W is placed close to the driveway the car passes through. When the car drives by, its BLE beacon signal is received by the detector and MQTT messages are sent for the car entering and exiting the scanner range. Multiple cars can be differentiated by their beacon GUIDs. 8 | 9 | ![image](https://user-images.githubusercontent.com/60828821/159815790-ddfde66c-9e0d-44e3-a955-cbe94c7a1f95.png) 10 | 11 | Home assistant processes the MQTT messages and exposes a sensor that can be used in further automations. 12 | 13 | ## Hardware and software setup 14 | 15 | * A Raspberry Pi Zero W. Doing the scanning, it connects to the MQTT broker over either wifi or over wired Ethernet (requires an additional Ethernet module connected to the GPIO or USB). Wired is recommended for best long term stability. 16 | * A BLE beacon in the car. iBeacon or compatible clones are supported. It is recommended to use a beacon with permanent USB connection to the car, which will be turned on with the cars ignition. The DSD Tech SH-A10 beacon was tested, but other clones should work too. Their GUIDs, tx power and signal repeat delays can be freely configured. 17 | * An MQTT broker. We recommend Mosquitto. 18 | * An automation system like Home Assistant. 19 | 20 | ## Prerequisites 21 | 22 | Install Bluetooth dev libraries 23 | 24 | `sudo apt install libbluetooth-dev` 25 | 26 | Install paho MQTT client libraries 27 | 28 | ``` 29 | git clone https://github.com/eclipse/paho.mqtt.c 30 | cd paho.mqtt.c 31 | make 32 | sudo make install 33 | ``` 34 | 35 | ## Configuring 36 | 37 | Clone this repo, go to the src folder and edit `beacon-detect.cpp`. Under configuration, customize the appropriate defines for your system: 38 | 39 | `DEVICEFILE` the absolute path pointing to the beacon identity file, which contains the authorized GUIDs of your beacons in your car(s). There is an example `known_devices.txt` supplied. Edit it for your beacons. You can add new devices here or revoke existing ones. 40 | 41 | `LOGFILE` the absolute path where BLE-detect will put its log file. 42 | 43 | `ADDRESS` the URL to your MQTT broker. 44 | 45 | `MIN_DETECTION_RSSI` the minimum signal strength in dBm the BLE signal need for a successful detection. You can adjust the detection range with this. 46 | 47 | Note: if the paths above are wrong, you will get a SEGFAULT. There is no error checking at this time. 48 | 49 | ## Building 50 | 51 | When you have the configuration customized to your needs, compile the tool. Make sure you're in the `src` folder: 52 | 53 | `g++ -o bdetect ./beacon_detect.cpp -lbluetooth -lpaho-mqtt3c -Wno-psabi` 54 | 55 | Launch the binary to start the detector: 56 | 57 | `sudo ./bdetect` 58 | 59 | The user running the binary needs access permissions to the Bluetooth device. 60 | 61 | ## MQTT messages 62 | 63 | When a beacon enters or exits the detection range, and the beacon was recognized as known / authorized, the tool will publish an MQTT message that looks like this: 64 | 65 | `blescanner/MyBeacon { "state" : "on" }` 66 | 67 | The MyBeacon name is replaced by the name of the recognized beacon, as it appears in the know_devices.txt file (see section above). The state will be `on` when the beacon is entering the range or `off` when it leaves the range. 68 | 69 | For Home Assistant, an MQTT sensor can be configured like this: 70 | 71 | ```yaml 72 | sensor: 73 | - platform: mqtt 74 | name: car_beacon 75 | state_topic: "blescanner/MyBeacon" 76 | value_template: "{{ value_json.state }}" 77 | ``` 78 | 79 | ## A note on general BLE security 80 | 81 | Do not use a BLE based arrival detection to perform safety critical operations like unlocking doors or opening garages. A BLE beacon signal can be trivial to clone by using readily available smartphone apps and it can be done without you noticing from a distance. You can however use such detection for non critical arrival automations, like turning on lights or announcing an arrival. You can also use it for departure automations, like locking doors or arming an alarm system when your vehicle leaves the driveway. Changing your GUIDs from time to time is probably a good idea too. 82 | -------------------------------------------------------------------------------- /src/beacon_detect.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Low latency BLE arrival detection and reporting 3 | // 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | #include 21 | 22 | typedef long long int64; 23 | 24 | extern "C" { 25 | #include 26 | #include 27 | } 28 | 29 | 30 | // Configuration 31 | // ================================================================================================================ 32 | 33 | #define DEVICEFILE "/home/pi/bdetect/known_devices.txt" 34 | #define LOGFILE "/home/pi/bdetect/log.txt" 35 | 36 | #define ADDRESS "tcp://192.168.5.100:1883" 37 | #define CLIENTID "BLE Beacon scanner" 38 | 39 | #define MIN_DETECTION_RSSI -101 // in dBm 40 | 41 | 42 | // Logging 43 | // ================================================================================================================ 44 | 45 | #define KNRM "\x1B[0m" 46 | #define KRED "\x1B[31;1m" 47 | #define KGRN "\x1B[32;1m" 48 | #define KYEL "\x1B[33;1m" 49 | #define KBLU "\x1B[34;1m" 50 | #define KMAG "\x1B[35;1m" 51 | #define KCYA "\x1B[36;1m" 52 | #define KWHT "\x1B[37;1m" 53 | 54 | void log(const std::string &module, const std::string &color, const char *format, ...) 55 | { 56 | static char Buffer[500]; 57 | 58 | va_list args; 59 | va_start(args, format); 60 | vsprintf(Buffer, format, args); 61 | va_end(args); 62 | 63 | auto now = std::chrono::system_clock::now(); 64 | auto now_c = std::chrono::system_clock::to_time_t(now); 65 | std::tm *t = std::localtime(&now_c); 66 | 67 | static char Buffer2[500]; 68 | sprintf(Buffer2, "%4d%02d%02d %02d:%02d:%02d", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); 69 | 70 | printf("%s [%s%s" KNRM "] %s\n", Buffer2, color.c_str(), module.c_str(), Buffer); 71 | 72 | FILE *fp = fopen(LOGFILE, "a"); 73 | fprintf(fp, "%s [%s] %s\n", Buffer2, module.c_str(), Buffer); 74 | fclose(fp); 75 | } 76 | 77 | 78 | // Extract the beacon GUID from manufacturer payload 79 | // ================================================================================================================ 80 | 81 | static std::string ParseGUID(uint8_t *Data, int len) 82 | { 83 | char Buffer[40]; 84 | 85 | // Apple iBeacon 86 | if( Data[0] == 0x4c && Data[1] == 0x00 && Data[2] == 0x02 ) { 87 | sprintf(Buffer, "%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X:%02X%02X-%02X%02X", Data[4], Data[5], Data[6], Data[7], Data[8], Data[9], Data[10], Data[11], Data[12], Data[13], Data[14], Data[15], Data[16], Data[17], Data[18], Data[19], Data[20], Data[21], Data[22], Data[23]); 88 | return( Buffer ); 89 | } 90 | 91 | // Microsoft BLE beacon 92 | if( Data[0] == 0x06 && Data[1] == 0x00 && Data[2] == 0x01 ) { 93 | sprintf(Buffer, "%02X/%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", Data[3], Data[10], Data[11], Data[12], Data[13], Data[14], Data[15], Data[16], Data[17], Data[18], Data[19], Data[20], Data[21], Data[22], Data[23], Data[24], Data[25]); 94 | return( Buffer ); 95 | } 96 | 97 | return( "" ); 98 | } 99 | 100 | static int ParseRSSI(uint8_t *Data, int len) 101 | { 102 | // Apple iBeacon 103 | if( Data[0] == 0x4c && Data[1] == 0x00 && Data[2] == 0x02 ) { 104 | return( Data[24] ); 105 | } 106 | 107 | return( 0 ); 108 | } 109 | 110 | 111 | // Timer 112 | // ================================================================================================================ 113 | 114 | static int64 GetTickTimer(void) 115 | { 116 | static int64 clocksPerSec = 0; 117 | static struct tms tmdummy; 118 | 119 | // Get number of clock ticks per second 120 | if( !clocksPerSec ) clocksPerSec = (int64)sysconf(_SC_CLK_TCK); 121 | 122 | // Return system time in msec 123 | return( (int64)(times(&tmdummy) * 1000LL / clocksPerSec) ); 124 | } 125 | 126 | 127 | // Bluetooth HCI low level 128 | // ================================================================================================================ 129 | 130 | class CHCIDevice { 131 | 132 | private: 133 | int p_device; 134 | 135 | void p_ParseEIR(uint8_t *eir, int eir_len, std::string &guid); 136 | 137 | public: 138 | CHCIDevice(); 139 | ~CHCIDevice(); 140 | 141 | bool IsDeviceValid() const { return( p_device >= 0 ); } 142 | 143 | void SetScanParameters(bool active, bool whitelist); 144 | 145 | void ScanEnabled(bool state); 146 | 147 | int GetNextScannedDevice(bdaddr_t &bdaddr, std::string &guid, int &rssi); 148 | 149 | std::string IdentifyDevice(bdaddr_t &bdaddr); 150 | 151 | }; 152 | 153 | CHCIDevice::CHCIDevice() 154 | { 155 | int devid = hci_get_route(NULL); 156 | 157 | p_device = hci_open_dev(devid); 158 | 159 | fcntl(p_device, F_SETFL, fcntl(p_device, F_GETFL, 0) | O_NONBLOCK); 160 | 161 | hci_filter nf; 162 | hci_filter_clear(&nf); 163 | hci_filter_set_ptype(HCI_EVENT_PKT, &nf); 164 | hci_filter_set_event(EVT_LE_META_EVENT, &nf); 165 | 166 | if( setsockopt(p_device, SOL_HCI, HCI_FILTER, &nf, sizeof(nf)) < 0 ) { 167 | log("BTLE", KRED, "Could not set socket options"); 168 | p_device = -1; 169 | } 170 | } 171 | 172 | CHCIDevice::~CHCIDevice() 173 | { 174 | if( p_device >= 0 ) { 175 | hci_close_dev(p_device); 176 | } 177 | } 178 | 179 | void CHCIDevice::SetScanParameters(bool active, bool whitelist) 180 | { 181 | int error = hci_le_set_scan_parameters(p_device, active ? 1 : 0, htobs(0x0010), htobs(0x0010), 0, whitelist ? 1 : 0, 1000); 182 | 183 | if( error < 0 ) { 184 | log("BTLE", KRED, "hci_le_set_scan_parameters failed %d", error); 185 | } 186 | } 187 | 188 | void CHCIDevice::ScanEnabled(bool state) 189 | { 190 | int error = hci_le_set_scan_enable(p_device, state ? 0x01 : 0, 0, 1000); 191 | 192 | if( error < 0 ) { 193 | log("BTLE", KRED, "hci_le_set_scan_enable failed %d", error); 194 | } 195 | } 196 | 197 | void CHCIDevice::p_ParseEIR(uint8_t *eir, int eir_len, std::string &guid) 198 | { 199 | int offset = 0; 200 | 201 | while( offset < eir_len ) { 202 | 203 | int field_len = eir[0]; 204 | 205 | // Check for the end of EIR 206 | if( !field_len ) break; 207 | 208 | if( offset + field_len > eir_len ) break; 209 | 210 | switch( eir[1] ) { 211 | 212 | case 8: 213 | case 9: 214 | /*int name_len = field_len - 1; 215 | if( name_len <= 50 ) { 216 | char buf[50] = { 0 }; 217 | memcpy(buf, &eir[2], name_len); 218 | name = buf; 219 | }*/ 220 | break; 221 | 222 | case 0xff: 223 | { 224 | guid = ParseGUID(&eir[2], field_len); 225 | break; 226 | } 227 | 228 | } 229 | 230 | offset += field_len + 1; 231 | eir += field_len + 1; 232 | } 233 | } 234 | 235 | int CHCIDevice::GetNextScannedDevice(bdaddr_t &bdaddr, std::string &guid, int &rssi) 236 | { 237 | // Return value: 0 = ok, data available 238 | // 1 = no data available yet 239 | // 2 = error 240 | 241 | fd_set set; 242 | FD_ZERO(&set); 243 | FD_SET(p_device, &set); 244 | 245 | timeval timeout; 246 | timeout.tv_sec = 2; 247 | timeout.tv_usec = 0; 248 | 249 | select(p_device + 1, &set, NULL, NULL, &timeout); 250 | 251 | unsigned char buf[HCI_MAX_EVENT_SIZE]; 252 | 253 | int len = read(p_device, buf, sizeof(buf)); 254 | 255 | if( len < 0 ) 256 | return( ( errno == EAGAIN || errno == EINTR ) ? 1 : 2 ); 257 | 258 | unsigned char *ptr = buf + (1 + HCI_EVENT_HDR_SIZE); 259 | len -= (1 + HCI_EVENT_HDR_SIZE); 260 | 261 | evt_le_meta_event *meta = (evt_le_meta_event *)ptr; 262 | 263 | if( meta->subevent != 0x02 ) return( 2 ); 264 | 265 | le_advertising_info *info = (le_advertising_info *)(meta->data + 1); 266 | 267 | p_ParseEIR(info->data, info->length, guid); 268 | 269 | bdaddr = info->bdaddr; 270 | 271 | rssi = (int)((char)info->data[info->length]) - 256; 272 | 273 | //log("BTLE", KGRN, "rssi = -%ddBm", rssi); 274 | 275 | return( 0 ); 276 | } 277 | 278 | 279 | // MQTT client 280 | // ================================================================================================================ 281 | 282 | class CMQTT { 283 | 284 | private: 285 | MQTTClient p_client; 286 | 287 | const int p_timeoutConnect = 2; 288 | const int p_timeoutDelivery = 5; 289 | 290 | public: 291 | CMQTT(); 292 | ~CMQTT(); 293 | 294 | bool Connect(); 295 | void Disconnect(); 296 | 297 | void Yield(); 298 | 299 | void Publish(const std::string &name, const std::string &state); 300 | 301 | }; 302 | 303 | CMQTT::CMQTT() 304 | { 305 | MQTTClient_create(&p_client, ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL); 306 | } 307 | 308 | CMQTT::~CMQTT() 309 | { 310 | MQTTClient_destroy(&p_client); 311 | } 312 | 313 | bool CMQTT::Connect() 314 | { 315 | log("MQTT", KCYA, "Connecting with broker at %s...", ADDRESS); 316 | 317 | MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer; 318 | conn_opts.keepAliveInterval = 60; 319 | conn_opts.cleansession = 0; 320 | conn_opts.connectTimeout = p_timeoutConnect; 321 | 322 | int rc = MQTTClient_connect(p_client, &conn_opts); 323 | 324 | if( rc != MQTTCLIENT_SUCCESS ) { 325 | log("MQTT", KRED, "Failed to connect, return code %d", rc); 326 | return( false ); 327 | } 328 | 329 | log("MQTT", KCYA, "OK Connected !"); 330 | 331 | return( true ); 332 | } 333 | 334 | void CMQTT::Disconnect() 335 | { 336 | MQTTClient_disconnect(p_client, p_timeoutDelivery * 1000); 337 | } 338 | 339 | void CMQTT::Yield() 340 | { 341 | MQTTClient_yield(); 342 | 343 | if( !MQTTClient_isConnected(p_client) ) { 344 | log("MQTT", KRED, "MQTT broker disconnected after yield, attempting to reestablish connection"); 345 | Connect(); 346 | } 347 | } 348 | 349 | void CMQTT::Publish(const std::string &name, const std::string &state) 350 | { 351 | if( !MQTTClient_isConnected(p_client) ) { 352 | log("MQTT", KRED, "MQTT broker disconnected, attempting to reestablish connection"); 353 | Connect(); 354 | } 355 | 356 | if( MQTTClient_isConnected(p_client) ) { 357 | 358 | std::string topic = "blescanner/" + name; 359 | std::string payload = "{ \"state\" : \"" + state + "\" }"; 360 | 361 | MQTTClient_message pubmsg = MQTTClient_message_initializer; 362 | pubmsg.payload = (void *)payload.c_str(); 363 | pubmsg.payloadlen = payload.length(); 364 | pubmsg.qos = 1; 365 | pubmsg.retained = 0; 366 | 367 | MQTTClient_deliveryToken token; 368 | 369 | int rc = MQTTClient_publishMessage(p_client, topic.c_str(), &pubmsg, &token); 370 | 371 | if( rc == MQTTCLIENT_SUCCESS ) { 372 | 373 | rc = MQTTClient_waitForCompletion(p_client, token, p_timeoutDelivery * 1000); 374 | 375 | if( rc == MQTTCLIENT_SUCCESS ) { 376 | log("MQTT", KCYA, "Message with delivery token %d delivered", token); 377 | } else 378 | log("MQTT", KRED, "Message could not be delivered (%d)", rc); 379 | 380 | } else 381 | 382 | log("MQTT", KRED, "Message publish failed with code %d", rc); 383 | 384 | } 385 | } 386 | 387 | 388 | // Active BLE device 389 | // ================================================================================================================ 390 | 391 | struct SActiveDevice { 392 | 393 | std::string guid; 394 | bdaddr_t bdaddr; 395 | std::string knownName; 396 | int type; // 0 = empty, 1 = unknown, 2 = known 397 | int64 lastseen; 398 | int rssiMax; 399 | 400 | SActiveDevice() : type(0), lastseen(0) { } 401 | 402 | }; 403 | 404 | 405 | // MQTT actions 406 | // ================================================================================================================ 407 | 408 | void MessageNewDevice(CMQTT &MQTT, SActiveDevice &Device, int rssi) 409 | { 410 | char addr[18]; 411 | ba2str(&Device.bdaddr, addr); 412 | 413 | if( Device.type == 2 ) { 414 | log("BTLE", KBLU, "New known device %s (%ddBm) - %s", addr, rssi, Device.knownName.c_str()); 415 | MQTT.Publish(Device.knownName, "on"); 416 | } else 417 | log("BTLE", KBLU, "New device %s (%ddBm) - [%s]", addr, rssi, Device.guid.c_str()); 418 | } 419 | 420 | void MessageLostDevice(CMQTT &MQTT, SActiveDevice &Device) 421 | { 422 | char addr[18]; 423 | ba2str(&Device.bdaddr, addr); 424 | 425 | if( Device.type == 2 ) { 426 | log("BTLE", KBLU, "Lost known device %s (%ddBm max) - %s", addr, Device.rssiMax, Device.knownName.c_str()); 427 | MQTT.Publish(Device.knownName, "off"); 428 | } else 429 | log("BTLE", KBLU, "Lost device %s (%ddBm max) - %s", addr, Device.rssiMax, Device.guid.c_str()); 430 | } 431 | 432 | 433 | // Device list management 434 | // ================================================================================================================ 435 | 436 | static std::list ActiveDevices; 437 | 438 | const int lostTimeoutKnown = 4 * 1000; // in msec 439 | const int lostTimeoutUnknown = 30 * 1000; // in msec 440 | 441 | SActiveDevice *FindActiveDevice(const bdaddr_t &bdaddr) 442 | { 443 | for( auto &i : ActiveDevices ) { 444 | if( i.type > 0 ) { 445 | if( !bacmp(&bdaddr, &i.bdaddr) ) return( &i ); 446 | } 447 | } 448 | 449 | return( nullptr ); 450 | } 451 | 452 | void CollectLostDevices(CMQTT &MQTT, int64 timeNow) 453 | { 454 | for( auto i = ActiveDevices.begin(); i != ActiveDevices.end(); ++i ) { 455 | if( i->type > 0 ) { 456 | int dt = (int)(timeNow - i->lastseen); 457 | if( ( i->type == 2 && dt > lostTimeoutKnown ) || ( i->type == 1 && dt > lostTimeoutUnknown ) ) { 458 | MessageLostDevice(MQTT, *i); 459 | ActiveDevices.erase(i); 460 | break; 461 | } 462 | } 463 | } 464 | } 465 | 466 | 467 | // Known devices configuration file 468 | // ================================================================================================================ 469 | 470 | std::string SearchKnownDevices(const std::string &guid) 471 | { 472 | std::string result; 473 | 474 | FILE *fp = fopen(DEVICEFILE, "r"); 475 | 476 | if( fp ) { 477 | 478 | char Buffer[200]; 479 | 480 | while( !feof(fp) && result.empty() ) { 481 | fgets(Buffer, 200, fp); 482 | char *token = strtok(Buffer, " \t\n\r"); 483 | std::string name = token ? token : ""; 484 | if( !name.empty() ) { 485 | token = strtok(nullptr, " \t\n\r"); 486 | if( token && std::string(token) == guid ) result = name; 487 | } 488 | } 489 | 490 | fclose(fp); 491 | 492 | } 493 | 494 | return( result ); 495 | } 496 | 497 | 498 | // Main 499 | // ================================================================================================================ 500 | 501 | const int64 yieldTime = 10 * 1000; 502 | 503 | static bool running = true; 504 | 505 | void SignalCallback(int) 506 | { 507 | running = false; 508 | } 509 | 510 | int main() 511 | { 512 | signal(SIGINT, SignalCallback); 513 | 514 | log("STRT", KYEL, "------ BLE Detect started ! ------"); 515 | 516 | CMQTT MQTT; 517 | MQTT.Connect(); 518 | 519 | static int64 lastYieldTime = GetTickTimer(); 520 | 521 | CHCIDevice HCIDevice; 522 | HCIDevice.SetScanParameters(false, false); 523 | HCIDevice.ScanEnabled(true); 524 | 525 | while( running ) { 526 | 527 | bdaddr_t bdaddr; 528 | std::string guid; 529 | int rssi; 530 | 531 | int result = HCIDevice.GetNextScannedDevice(bdaddr, guid, rssi); 532 | 533 | int64 timeNow = GetTickTimer(); 534 | 535 | if( result == 2 ) break; 536 | 537 | if( result == 0 && rssi >= MIN_DETECTION_RSSI ) { 538 | 539 | SActiveDevice *ActiveDevice = FindActiveDevice(bdaddr); 540 | 541 | if( !ActiveDevice ) { 542 | 543 | SActiveDevice newDevice; 544 | newDevice.bdaddr = bdaddr; 545 | newDevice.guid = guid; 546 | newDevice.knownName = SearchKnownDevices(guid); 547 | newDevice.type = newDevice.knownName.empty() ? 1 : 2; 548 | newDevice.lastseen = timeNow; 549 | newDevice.rssiMax = rssi; 550 | ActiveDevices.push_back(newDevice); 551 | 552 | MessageNewDevice(MQTT, newDevice, rssi); 553 | 554 | } else { 555 | 556 | ActiveDevice->lastseen = timeNow; 557 | if( rssi > ActiveDevice->rssiMax ) ActiveDevice->rssiMax = rssi; 558 | 559 | } 560 | 561 | } 562 | 563 | CollectLostDevices(MQTT, timeNow); 564 | 565 | if( timeNow - lastYieldTime >= yieldTime || timeNow < lastYieldTime ) { 566 | lastYieldTime = timeNow; 567 | MQTT.Yield(); 568 | } 569 | 570 | } 571 | 572 | log("EXIT", KYEL, "SIGINT received, shutting down"); 573 | 574 | HCIDevice.ScanEnabled(false); 575 | 576 | MQTT.Disconnect(); 577 | 578 | return 0; 579 | } 580 | 581 | 582 | --------------------------------------------------------------------------------