#include "NRF52Bluetooth.h" #include "BLEDfuSecure.h" #include "BluetoothCommon.h" #include "PowerFSM.h" #include "configuration.h" #include "main.h" #include "mesh/PhoneAPI.h" #include "mesh/mesh-pb-constants.h" #include #include static BLEService meshBleService = BLEService(BLEUuid(MESH_SERVICE_UUID_16)); static BLECharacteristic fromNum = BLECharacteristic(BLEUuid(FROMNUM_UUID_16)); static BLECharacteristic fromRadio = BLECharacteristic(BLEUuid(FROMRADIO_UUID_16)); static BLECharacteristic toRadio = BLECharacteristic(BLEUuid(TORADIO_UUID_16)); static BLECharacteristic logRadio = BLECharacteristic(BLEUuid(LOGRADIO_UUID_16)); static BLEDis bledis; // DIS (Device Information Service) helper class instance static BLEBas blebas; // BAS (Battery Service) helper class instance #ifndef BLE_DFU_SECURE static BLEDfu bledfu; // DFU software update helper service #else static BLEDfuSecure bledfusecure; // DFU software update helper service #endif // This scratch buffer is used for various bluetooth reads/writes - but it is safe because only one bt operation can be in // process at once // static uint8_t trBytes[_max(_max(_max(_max(ToRadio_size, RadioConfig_size), User_size), MyNodeInfo_size), FromRadio_size)]; static uint8_t fromRadioBytes[meshtastic_FromRadio_size]; static uint8_t toRadioBytes[meshtastic_ToRadio_size]; // Last ToRadio value received from the phone static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; static uint16_t connectionHandle; class BluetoothPhoneAPI : public PhoneAPI { /** * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) */ virtual void onNowHasData(uint32_t fromRadioNum) override { PhoneAPI::onNowHasData(fromRadioNum); LOG_INFO("BLE notify fromNum"); fromNum.notify32(fromRadioNum); } /// Check the current underlying physical link to see if the client is currently connected virtual bool checkIsConnected() override { return Bluefruit.connected(connectionHandle); } public: BluetoothPhoneAPI() { api_type = TYPE_BLE; } }; static BluetoothPhoneAPI *bluetoothPhoneAPI; void onConnect(uint16_t conn_handle) { // Get the reference to current connection BLEConnection *connection = Bluefruit.Connection(conn_handle); connectionHandle = conn_handle; char central_name[32] = {0}; connection->getPeerName(central_name, sizeof(central_name)); LOG_INFO("BLE Connected to %s", central_name); // negotiate connections params as soon as possible ble_gap_conn_params_t newParams; newParams.min_conn_interval = 24; newParams.max_conn_interval = 40; newParams.slave_latency = 5; newParams.conn_sup_timeout = 400; sd_ble_gap_conn_param_update(conn_handle, &newParams); // Notify UI (or any other interested firmware components) meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); bluetoothStatus->updateStatus(&newStatus); } /** * Callback invoked when a connection is dropped * @param conn_handle connection where this event happens * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h */ void onDisconnect(uint16_t conn_handle, uint8_t reason) { LOG_INFO("BLE Disconnected, reason = 0x%x", reason); if (bluetoothPhoneAPI) { bluetoothPhoneAPI->close(); } // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection memset(lastToRadio, 0, sizeof(lastToRadio)); // Notify UI (or any other interested firmware components) meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newStatus); } void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) { // Display the raw request packet LOG_INFO("CCCD Updated: %u", cccd_value); // Check the characteristic this CCCD update is associated with in case // this handler is used for multiple CCCD records. // According to the GATT spec: cccd value = 0x0001 means notifications are enabled // and cccd value = 0x0002 means indications are enabled if (chr->uuid == fromNum.uuid || chr->uuid == logRadio.uuid) { auto result = cccd_value == 2 ? chr->indicateEnabled(conn_hdl) : chr->notifyEnabled(conn_hdl); if (result) { LOG_INFO("Notify/Indicate enabled"); } else { LOG_INFO("Notify/Indicate disabled"); } } } void startAdv(void) { // Advertising packet Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); // IncludeService UUID // Bluefruit.ScanResponse.addService(meshBleService); Bluefruit.ScanResponse.addTxPower(); Bluefruit.ScanResponse.addName(); // Include Name // Bluefruit.Advertising.addName(); Bluefruit.Advertising.addService(meshBleService); /* Start Advertising * - Enable auto advertising if disconnected * - Interval: fast mode = 20 ms, slow mode = 417,5 ms * - Timeout for fast mode is 30 seconds * - Start(timeout) with timeout = 0 will advertise forever (until connected) * * For recommended advertising interval * https://developer.apple.com/library/content/qa/qa1931/_index.html */ Bluefruit.Advertising.restartOnDisconnect(true); Bluefruit.Advertising.setInterval(32, 668); // in unit of 0.625 ms Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds. FIXME, we should stop advertising after X } // Just ack that the caller is allowed to read static void authorizeRead(uint16_t conn_hdl) { ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_READ}; reply.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS; sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); } /** * client is starting read, pull the bytes from our API class */ void onFromRadioAuthorize(uint16_t conn_hdl, BLECharacteristic *chr, ble_gatts_evt_read_t *request) { if (request->offset == 0) { // If the read is long, we will get multiple authorize invocations - we only populate data on the first size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes); // Someone is going to read our value as soon as this callback returns. So fill it with the next message in the queue // or make empty if the queue is empty fromRadio.write(fromRadioBytes, numBytes); } else { // LOG_INFO("Ignore successor read"); } authorizeRead(conn_hdl); } void onToRadioWrite(uint16_t conn_hdl, BLECharacteristic *chr, uint8_t *data, uint16_t len) { LOG_INFO("toRadioWriteCb data %p, len %u", data, len); if (memcmp(lastToRadio, data, len) != 0) { LOG_DEBUG("New ToRadio packet"); memcpy(lastToRadio, data, len); bluetoothPhoneAPI->handleToRadio(data, len); } else { LOG_DEBUG("Drop dup ToRadio packet we just saw"); } } void setupMeshService(void) { bluetoothPhoneAPI = new BluetoothPhoneAPI(); meshBleService.begin(); // Note: You must call .begin() on the BLEService before calling .begin() on // any characteristic(s) within that service definition.. Calling .begin() on // a BLECharacteristic will cause it to be added to the last BLEService that // was 'begin()'ed! auto secMode = config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN ? SECMODE_OPEN : SECMODE_ENC_NO_MITM; fromNum.setProperties(CHR_PROPS_NOTIFY | CHR_PROPS_READ); fromNum.setPermission(secMode, SECMODE_NO_ACCESS); // FIXME, secure this!!! fromNum.setFixedLen( 0); // Variable len (either 0 or 4) FIXME consider changing protocol so it is fixed 4 byte len, where 0 means empty fromNum.setMaxLen(4); fromNum.setCccdWriteCallback(onCccd); // Optionally capture CCCD updates // We don't yet need to hook the fromNum auth callback // fromNum.setReadAuthorizeCallback(fromNumAuthorizeCb); fromNum.write32(0); // Provide default fromNum of 0 fromNum.begin(); fromRadio.setProperties(CHR_PROPS_READ); fromRadio.setPermission(secMode, SECMODE_NO_ACCESS); fromRadio.setMaxLen(sizeof(fromRadioBytes)); fromRadio.setReadAuthorizeCallback( onFromRadioAuthorize, false); // We don't call this callback via the adafruit queue, because we can safely run in the BLE context fromRadio.setBuffer(fromRadioBytes, sizeof(fromRadioBytes)); // we preallocate our fromradio buffer so we won't waste space // for two copies fromRadio.begin(); toRadio.setProperties(CHR_PROPS_WRITE); toRadio.setPermission(secMode, secMode); // FIXME secure this! toRadio.setFixedLen(0); toRadio.setMaxLen(512); toRadio.setBuffer(toRadioBytes, sizeof(toRadioBytes)); // We don't call this callback via the adafruit queue, because we can safely run in the BLE context toRadio.setWriteCallback(onToRadioWrite, false); toRadio.begin(); logRadio.setProperties(CHR_PROPS_INDICATE | CHR_PROPS_NOTIFY | CHR_PROPS_READ); logRadio.setPermission(secMode, SECMODE_NO_ACCESS); logRadio.setMaxLen(512); logRadio.setCccdWriteCallback(onCccd); logRadio.write32(0); logRadio.begin(); } static uint32_t configuredPasskey; void NRF52Bluetooth::shutdown() { // Shutdown bluetooth for minimum power draw LOG_INFO("Disable NRF52 bluetooth"); Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onUnwantedPairing); // Actively refuse (during factory reset) disconnect(); Bluefruit.Advertising.stop(); } void NRF52Bluetooth::startDisabled() { // Setup Bluetooth nrf52Bluetooth->setup(); // Shutdown bluetooth for minimum power draw Bluefruit.Advertising.stop(); Bluefruit.setTxPower(-40); // Minimum power LOG_INFO("Disable NRF52 Bluetooth. (Workaround: tx power min, advertise stopped)"); } bool NRF52Bluetooth::isConnected() { return Bluefruit.connected(connectionHandle); } int NRF52Bluetooth::getRssi() { return 0; // FIXME figure out where to source this } void NRF52Bluetooth::setup() { // Initialise the Bluefruit module LOG_INFO("Init the Bluefruit nRF52 module"); Bluefruit.autoConnLed(false); Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); Bluefruit.begin(); // Clear existing data. Bluefruit.Advertising.stop(); Bluefruit.Advertising.clearData(); Bluefruit.ScanResponse.clearData(); if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { configuredPasskey = config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN ? config.bluetooth.fixed_pin : random(100000, 999999); auto pinString = std::to_string(configuredPasskey); LOG_INFO("Bluetooth pin set to '%i'", configuredPasskey); Bluefruit.Security.setPIN(pinString.c_str()); Bluefruit.Security.setIOCaps(true, false, false); Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onPairingPasskey); Bluefruit.Security.setPairCompleteCallback(NRF52Bluetooth::onPairingCompleted); Bluefruit.Security.setSecuredCallback(NRF52Bluetooth::onConnectionSecured); meshBleService.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); } else { Bluefruit.Security.setIOCaps(false, false, false); meshBleService.setPermission(SECMODE_OPEN, SECMODE_OPEN); } // Set the advertised device name (keep it short!) Bluefruit.setName(getDeviceName()); // Set the connect/disconnect callback handlers Bluefruit.Periph.setConnectCallback(onConnect); Bluefruit.Periph.setDisconnectCallback(onDisconnect); // Set slave latency to 5 to conserve power // Despite name this does not impact data transfer // https://docs.silabs.com/bluetooth/2.13/bluetooth-general-system-and-performance/optimizing-current-consumption-in-bluetooth-low-energy-devices Bluefruit.Periph.setConnSlaveLatency(5); // TODO: Adafruit defaul min, max interval seems to be (20,30) [in 1.25 ms units] -> (25.00, 31.25) milliseconds // so using formula Interval Max * (Slave Latency + 1) ≤ 2 seconds // max slave latency we can use is 30 (max available in BLE) // and even double max inteval (see apple doc linked above for formulas) // See Periph.SetConnInterval method // Tweak this later for even more power savings once those changes are confirmed to work well. // Changing min, max interval may slow BLE transfer a bit - bumping slave latency will most likely not. #ifndef BLE_DFU_SECURE bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); bledfu.begin(); // Install the DFU helper #else bledfusecure.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); // add by WayenWeng bledfusecure.begin(); // Install the DFU helper #endif // Configure and Start the Device Information Service LOG_INFO("Init the Device Information Service"); bledis.setModel(optstr(HW_VERSION)); bledis.setFirmwareRev(optstr(APP_VERSION)); bledis.begin(); // Start the BLE Battery Service and set it to 100% LOG_INFO("Init the Battery Service"); blebas.begin(); blebas.write(0); // Unknown battery level for now // Setup the Heart Rate Monitor service using // BLEService and BLECharacteristic classes LOG_INFO("Init the Mesh bluetooth service"); setupMeshService(); // Setup the advertising packet(s) LOG_INFO("Set up the advertising payload(s)"); startAdv(); LOG_INFO("Advertise"); } void NRF52Bluetooth::resumeAdvertising() { Bluefruit.Advertising.restartOnDisconnect(true); Bluefruit.Advertising.setInterval(32, 668); // in unit of 0.625 ms Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode Bluefruit.Advertising.start(0); } /// Given a level between 0-100, update the BLE attribute void updateBatteryLevel(uint8_t level) { blebas.write(level); } void NRF52Bluetooth::clearBonds() { LOG_INFO("Clear bluetooth bonds!"); bond_print_list(BLE_GAP_ROLE_PERIPH); bond_print_list(BLE_GAP_ROLE_CENTRAL); Bluefruit.Periph.clearBonds(); Bluefruit.Central.clearBonds(); } void NRF52Bluetooth::onConnectionSecured(uint16_t conn_handle) { LOG_INFO("BLE connection secured"); } bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { char passkey1[4] = {passkey[0], passkey[1], passkey[2], '\0'}; char passkey2[4] = {passkey[3], passkey[4], passkey[5], '\0'}; LOG_INFO("BLE pair process started with passkey %s %s", passkey1, passkey2); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); // Get passkey as string // Note: possible leading zeros std::string textkey; for (uint8_t i = 0; i < 6; i++) textkey += (char)passkey[i]; // Notify UI (or other components) of pairing event and passkey meshtastic::BluetoothStatus newStatus(textkey); bluetoothStatus->updateStatus(&newStatus); #if HAS_SCREEN && !defined(MESHTASTIC_EXCLUDE_SCREEN) if (screen) { std::string passkey = std::to_string(configuredPasskey); std::string ble_message = "Bluetooth\nPIN\n" + passkey.substr(0, 3) + " " + passkey.substr(3, 6); screen->showSimpleBanner(ble_message.c_str(), 30000); } #endif if (match_request) { uint32_t start_time = millis(); while (millis() < start_time + 30000) { if (!Bluefruit.connected(conn_handle)) break; } } LOG_INFO("BLE passkey pair: match_request=%i", match_request); return true; } // Actively refuse new BLE pairings // After clearing bonds (at factory reset), clients seem initially able to attempt to re-pair, even with advertising disabled. // On NRF52Bluetooth::shutdown, we change the pairing callback to this method, to aggressively refuse any connection attempts. bool NRF52Bluetooth::onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { NRF52Bluetooth::disconnect(); return false; } // Disconnect any BLE connections void NRF52Bluetooth::disconnect() { uint8_t connection_num = Bluefruit.connected(); if (connection_num) { // Close all connections. We're only expecting one. for (uint8_t i = 0; i < connection_num; i++) Bluefruit.disconnect(i); // Wait for disconnection while (Bluefruit.connected()) yield(); LOG_INFO("Ended BLE connection"); } } void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_status) { if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { LOG_INFO("BLE pair success"); meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); bluetoothStatus->updateStatus(&newConnectedStatus); } else { LOG_INFO("BLE pair failed"); // Notify UI (or any other interested firmware components) meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newDisconnectedStatus); } // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { screen->endAlert(); } } void NRF52Bluetooth::sendLog(const uint8_t *logMessage, size_t length) { if (!isConnected() || length > 512) return; if (logRadio.indicateEnabled()) logRadio.indicate(logMessage, (uint16_t)length); else logRadio.notify(logMessage, (uint16_t)length); }