I am using the Adafruit nRF52 Feather development board and their implementation of the Nordic blob for Arduino. I am having the problem where the output data is as expected; however, strobing between the data as written and 0 for all byte values.
I was able to configure the Feather for 0x1818 Cycle Power Monitor output and was able to connect and read data using both the nRF connect Android app, as well as the Zwift cycle game using a generic USB BLE dongle connected to PC running Windows 10.
I was using a simple incrementing routine to test the instantaneous power output however as soon as the data is written, it is immediately reset to 0, the last written value does not persist. This continues for every update which I set at 1Hz. For example:
1-0, <1sec>, 2-0, <1sec>, 3-0, <1sec>, 4-0 ... and so on.
My code is heavily based on the Heart Rate Monitor example code Adafruit provides. The HRM does behave as one would expect, the last written value persists until rewritten. I suspect this strobing output is not the intended output of the BLE characteristic?
I understand that bugs could have been introduced with their Arduino library, I am just trying to determine if the problem is in my implementation or if I need to start digging though the library functions (which are still in development on Adafruit's part).
Has anybody seen anything like this before? Can anybody point me in a direction?
Thanks in advance, my code is below.
/********************************************************************* This is an example for our nRF52 based Bluefruit LE modules Pick one up today in the adafruit shop! Adafruit invests time and resources providing this open source code, please support Adafruit and open-source hardware by purchasing products from Adafruit! MIT license, check LICENSE for more information All text above, and the splash screen below must be included in any redistribution *********************************************************************/ #include <bluefruit.h> /* Cycling Power Service * CP Service: 0x1818 * CP Characteristic: 0x2A63 (Measurement) * CP Characteristic: 0x2A65 (Feature) * CP Characteristic: 0x2A5D (Location) */ BLEService cps = BLEService(UUID16_SVC_CYCLING_POWER); BLECharacteristic cpmc = BLECharacteristic(UUID16_CHR_CYCLING_POWER_MEASUREMENT); BLECharacteristic cpfc = BLECharacteristic(UUID16_CHR_CYCLING_POWER_FEATURE); BLECharacteristic cplc = BLECharacteristic(UUID16_CHR_SENSOR_LOCATION); BLEDis bledis; // DIS (Device Information Service) helper class instance BLEBas blebas; // BAS (Battery Service) helper class instance /* * Bunch of globals */ uint16_t powerOut = 0; // W, decimal //uint32_t cumulativeRevOut = 0; // Revolutions, binary //uint16_t lastRevTime = 0; // s, since last rev, binary //uint16_t totalEnergy = 0; // kJ, decimal uint16_t cpmcDef = 0x00; // cycle power config flags /* * Setup */ void setup() { Serial.begin(115200); while ( !Serial ) delay(10); // for nrf52840 with native usb, milliseconds Serial.println("Bluefruit52 Cycle Power Example"); Serial.println("-------------------------------"); // Initialise the Bluefruit module Serial.println("Initialise the Bluefruit nRF52 module"); Bluefruit.begin(); // Set the advertised device name (keep it short!) Serial.println("Setting Device Name to 'Feather52 CPS'"); Bluefruit.setName("Feather52 CPS"); // Set the connect/disconnect callback handlers Bluefruit.setConnectCallback(connect_callback); Bluefruit.setDisconnectCallback(disconnect_callback); // Configure and Start the Device Information Service Serial.println("Configuring the Device Information Service"); bledis.setManufacturer("Adafruit Industries"); bledis.setModel("Bluefruit Feather52"); bledis.begin(); // Start the BLE Battery Service and set it to 100% Serial.println("Configuring the Battery Service"); blebas.begin(); blebas.write(100); // Setup the Cycle Power Service using // BLEService and BLECharacteristic classes Serial.println("Configuring the Cycle Power Service"); setupCPS(); // Setup the advertising packet(s) Serial.println("Setting up the advertising payload(s)"); startAdv(); Serial.println("Ready Player One!!!"); Serial.println("\nAdvertising"); } /* * Functions */ void startAdv(void) { // Advertising packet Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); Bluefruit.Advertising.addTxPower(); // Include CPS Service UUID Bluefruit.Advertising.addService(cps); //defined above // Include Name Bluefruit.Advertising.addName(); /* Start Advertising * - Enable auto advertising if disconnected * - Interval: fast mode = 20 ms, slow mode = 152.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, 244); // 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 } void setupCPS(void) { // Configure the Cycling Power Monitor service // See: https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.cycling_power.xml // Supported Characteristics: // Name UUID Requirement Properties // ---------------------------- ------ ----------- ---------- // Cycle Power Measurement 0x2A63 Mandatory Notify // Cycle Power Feature 0x2A65 Mandatory Read // Sensor Location 0x2A38 Mandatory Read cps.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! // Configure the Cycle Power Measurement characteristic // See: https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.cycling_power_measurement.xml // Properties = Notify // Min Len = 2 // Max Len = 34 // B0:1 = UINT16 - Flag (MANDATORY) // b0 = Pedal power balance present; 0 = false, 1 = true // b1 = Pedal power balance reference; 0 = unknown, 1 = left // b2 = Accumulated torque present; 0 = false, 1 = true // b3 = Accumulated torque source; 0 = wheel, 1 = crank // b4 = Wheel revolution data present; 0 = false, 1 = true // b5 = Crank revolution data present; 0 = false, 1 = true // b6 = Extreme force magnatudes present; 0 = false, 1 = true // b7 = Extreme torque magnatues present; 0 = false, 1 = true // b8 = Extreme angles present; 0 = false, 1 = true // b9 = Top dead angle present; 0 = false, 1 = true // b10 = Bottom dead angle present; 0 = false, 1 = true // b11 = Accumulated energy present; 0 = false, 1 = true // b12 = Offset compensation indicator; 0 = false, 1 = true // b13 = Reseved // b14 = n/a // b15 = n/a // B2:3 = SINT16 - Instataineous power, Watts (decimal) // B4 = UINT8 - Pedal power balance, Percent (binary) 1/2 // B5:6 = UINT16 - Accumulated torque, Nm; res (binary) 1/32 // B7:10 = UINT32 - Cumulative wheel revolutions, (decimal) // B11:12 = UINT16 - Last wheel event time, second (binary) 1/2048 // B13:14 = UINT16 - Cumulative crank revolutions, (decimal) // B15:16 = UINT16 - Last crank event time, second (binary) 1/1024 // B17:18 = SINT16 - Max force magnitude, Newton (decimal) // B19:20 = SINT16 - Min force magnitude, Newton (decimal) // B21:22 = SINT16 - Max torque magnitude, Nm (binary) 1/1024 // B23:24 = SINT16 - Min torque magnitude, Nm (binary) 1/1024 // B25:26 = UINT12 - Max angle, degree (decimal) // B27:28 = UINT12 - Min angle, degree (decimal) // B29:30 = UINT16 - Top dead spot angle, degree (decimal) // B31:32 = UINT16 - Bottom dead spot angle, degree (decimal) // B33:34 = UINT16 - Accumulated energy, kJ (decimal) cpmc.setProperties(CHR_PROPS_NOTIFY); // because type "notify" cpmc.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); // readAccess, writeAccess cpmc.setFixedLen(35); // 35 bytes cpmc.setCccdWriteCallback(cccd_callback); // Optionally capture CCCD updates cpmc.begin(); /* * Because we have inconsistent field sizes, we have to cut up the message into bytes */ uint8_t cpmcData[35] = { (uint8_t)(cpmcDef & 0xff), (uint8_t)(cpmcDef >> 8), // flags (uint8_t)(powerOut & 0xff), (uint8_t)(powerOut >> 8), // inst. power 0, // bal 0, 0, // torque 0, 0, 0, 0, // cum. rev 0, 0, // wheel time 0, 0, // cum. crank 0, 0, // crank time 0, 0, // max force 0, 0, // min force 0, 0, // max tor 0, 0, // min tor 0, 0, // max ang 0, 0, // min ang 0, 0, // tdc 0, 0, // bdc 0, 0 }; // total energy cpmc.notify(cpmcData, 35); // Use .notify instead of .write! // Configure the Cycle Power Feature characteristic // See: https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.cycling_power_feature.xml // Properties = Read // Min Len = 1 // Max Len = 32 // B0:3 = UINT8 - Cycling Power Feature (MANDATORY) // b0 = Pedal power balance supported; 0 = false, 1 = true // b1 = Accumulated torque supported; 0 = false, 1 = true // b2 = Wheel revolution data supported; 0 = false, 1 = true // b3 = Crank revolution data supported; 0 = false, 1 = true // b4 = Extreme magnatudes supported; 0 = false, 1 = true // b5 = Extreme angles supported; 0 = false, 1 = true // b6 = Top/bottom dead angle supported; 0 = false, 1 = true // b7 = Accumulated energy supported; 0 = false, 1 = true // b8 = Offset compensation indicator supported; 0 = false, 1 = true // b9 = Offset compensation supported; 0 = false, 1 = true // b10 = Cycling power measurement characteristic content masking supported; 0 = false, 1 = true // b11 = Multiple sensor locations supported; 0 = false, 1 = true // b12 = Crank length adj. supported; 0 = false, 1 = true // b13 = Chain length adj. supported; 0 = false, 1 = true // b14 = Chain weight adj. supported; 0 = false, 1 = true // b15 = Span length adj. supported; 0 = false, 1 = true // b16 = Sensor measurement context; 0 = force, 1 = torque // b17 = Instantaineous measurement direction supported; 0 = false, 1 = true // b18 = Factory calibrated date supported; 0 = false, 1 = true // b19 = Enhanced offset compensation supported; 0 = false, 1 = true // b20:21 = Distribtue system support; 0 = legacy, 1 = not supported, 2 = supported, 3 = RFU // b22:32 = Reserved // 432107654321076543210 uint32_t cpfcDef = 0x0; // 000000000000000000000b; uint8_t cpfcData[4] = {(uint8_t)(cpfcDef & 0xff), (uint8_t)(cpfcDef >> 8), (uint8_t)(cpfcDef >> 16), (uint8_t)(cpfcDef >> 24)}; cpfc.setProperties(CHR_PROPS_READ); cpfc.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); cpfc.setFixedLen(4); cpfc.begin(); cpfc.write(cpfcData, 4); // Configure the Sensor Location characteristic // See: https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.sensor_location.xml // Properties = Read // Min Len = 1 // Max Len = 1 // B0:1 = UINT16 - Sensor Location // Dec. // 0 = Other // 1 = Top of shoe // 2 = In shoe // 3 = Hip // 4 = Front wheel // 5 = Left crank // 6 = Right crank // 7 = Left pedal // 8 = Right pedal // 9 = Front hub // 10 = Rear dropout // 11 = Chainstay // 12 = Rear wheel // 13 = Rear hub // 14 = Chest // 15 = Spider // 16 = Chain ring // 17:255 = Reserved cplc.setProperties(CHR_PROPS_READ); cplc.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); cplc.setFixedLen(1); cplc.begin(); cplc.write8(12); // Set the characteristic to 'Rear wheel' (12) } void connect_callback(uint16_t conn_handle) { char central_name[32] = { 0 }; Bluefruit.Gap.getPeerName(conn_handle, central_name, sizeof(central_name)); Serial.print("Connected to "); Serial.println(central_name); } /** * 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 * https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/master/cores/nRF5/nordic/softdevice/s140_nrf52_6.1.1_API/include/ble_hci.h */ void disconnect_callback(uint16_t conn_handle, uint8_t reason) { (void) conn_handle; (void) reason; Serial.println("Disconnected"); Serial.println("Advertising!"); } void cccd_callback(BLECharacteristic& chr, uint16_t cccd_value) { // Display the raw request packet Serial.print("CCCD Updated: "); //Serial.printBuffer(request->data, request->len); Serial.print(cccd_value); Serial.println(""); // Check the characteristic this CCCD update is associated with in case // this handler is used for multiple CCCD records. if (chr.uuid == cpmc.uuid) { if (chr.notifyEnabled()) { Serial.println("Cycle Power Measurement 'Notify' enabled"); } else { Serial.println("Cycle Power Measurement 'Notify' disabled"); } } } void loop() { digitalToggle(LED_RED); if ( Bluefruit.connected() ) { //test code powerOut ++; uint8_t cpmcData[35] = { (uint8_t)(cpmcDef & 0xff), (uint8_t)(cpmcDef >> 8), // flags (uint8_t)(powerOut & 0xff), (uint8_t)(powerOut >> 8), // inst. power 0, // bal 0, 0, // torque 0, 0, 0, 0, // cum. rev 0, 0, // wheel time 0, 0, // cum. crank 0, 0, // crank time 0, 0, // max force 0, 0, // min force 0, 0, // max tor 0, 0, // min tor 0, 0, // max ang 0, 0, // min ang 0, 0, // tdc 0, 0, // bdc 0, 0 }; // total energy // Note: We use .notify instead of .write! // If it is connected but CCCD is not enabled // The characteristic's value is still updated although notification is not sent if ( cpmc.notify(cpmcData, sizeof(cpmcData)) ){ Serial.print("Cycle Power Measurement updated to: "); Serial.println(powerOut); }else{ Serial.println("ERROR: Notify not set in the CCCD or not connected!"); } } // Only send update once per second delay(1000); }