├── LICENSE ├── README.md ├── examples ├── PrintRaceChronoRequestsToSerial │ └── PrintRaceChronoRequestsToSerial.ino ├── SendSomeData │ └── SendSomeData.ino └── TrackingAllowedPids │ └── TrackingAllowedPids.ino ├── library.properties └── src ├── RaceChrono.cpp ├── RaceChrono.h ├── RaceChronoESP32.cpp ├── RaceChronoNRF52.cpp └── RaceChronoPidMap.h /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Timur Iskhodzhanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arduino RaceChrono 2 | Library for Arduino for communicating with the RaceChrono app. 3 | 4 | ## Supported Hardware 5 | 6 | * Adafruit Feather nRF52832 7 | * Adafruit ItsyBitsy nRF52840 Express 8 | * ESP32 (work in progress!) 9 | 10 | ## Installation 11 | 12 | ### Using Git 13 | 14 | ```sh 15 | cd ~/Documents/Arduino/libraries/ # ~/Arduino/libraries on Mac OS 16 | git clone https://github.com/timurrrr/arduino-RaceChrono.git arduino-RaceChrono 17 | ``` 18 | 19 | ## API 20 | 21 | See the source code, specifically the [RaceChrono.h](src/RaceChrono.h) file. 22 | The code should have clarifying comments wherever there are any unclear bits. 23 | 24 | TODO: write actual documentation 25 | 26 | ## Examples 27 | 28 | See [examples](examples) folder. 29 | 30 | ## License 31 | 32 | This library is [licensed](LICENSE) under the [MIT Licence](http://en.wikipedia.org/wiki/MIT_License). 33 | -------------------------------------------------------------------------------- /examples/PrintRaceChronoRequestsToSerial/PrintRaceChronoRequestsToSerial.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | class PrintRaceChronoCommands : public RaceChronoBleCanHandler { 4 | public: 5 | void allowAllPids(uint16_t updateIntervalMs) { 6 | Serial.print("ALLOW ALL PIDS, update interval: "); 7 | Serial.print(updateIntervalMs); 8 | Serial.println(" ms."); 9 | } 10 | 11 | void denyAllPids() { 12 | Serial.println("DENY ALL PIDS."); 13 | } 14 | 15 | void allowPid(uint32_t pid, uint16_t updateIntervalMs) { 16 | Serial.print("ALLOW PID "); 17 | Serial.print(pid); 18 | Serial.print(" (0x"); 19 | Serial.print(pid, HEX); 20 | Serial.print("), update interval: "); 21 | Serial.print(updateIntervalMs); 22 | Serial.println(" ms."); 23 | } 24 | } raceChronoHandler; 25 | 26 | // Forward declaration to help put code in a natural reading order. 27 | void waitForConnection(); 28 | 29 | void setup() { 30 | uint32_t startTimeMs = millis(); 31 | Serial.begin(115200); 32 | while (!Serial && millis() - startTimeMs < 1000) { 33 | } 34 | 35 | Serial.println("Setting up BLE..."); 36 | RaceChronoBle.setUp("BLE CAN device demo", &raceChronoHandler); 37 | RaceChronoBle.startAdvertising(); 38 | 39 | Serial.println("BLE is set up, waiting for an incoming connection."); 40 | waitForConnection(); 41 | } 42 | 43 | void waitForConnection() { 44 | uint32_t iteration = 0; 45 | bool lastPrintHadNewline = false; 46 | while (!RaceChronoBle.waitForConnection(1000)) { 47 | Serial.print("."); 48 | if ((++iteration) % 10 == 0) { 49 | lastPrintHadNewline = true; 50 | Serial.println(); 51 | } else { 52 | lastPrintHadNewline = false; 53 | } 54 | } 55 | 56 | if (!lastPrintHadNewline) { 57 | Serial.println(); 58 | } 59 | 60 | Serial.println("Connected."); 61 | } 62 | 63 | void loop() { 64 | if (!RaceChronoBle.isConnected()) { 65 | Serial.println("RaceChrono disconnected! Waiting for a new connection."); 66 | waitForConnection(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/SendSomeData/SendSomeData.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // We're ignoring the requested PIDs and notify intervals here for simplicity. 4 | class IgnoreRaceChronoCommands : public RaceChronoBleCanHandler { 5 | void allowAllPids(uint16_t updateIntervalMs) {} 6 | void denyAllPids() {} 7 | void allowPid(uint32_t pid, uint16_t updateIntervalMs) {} 8 | } raceChronoHandler; 9 | 10 | // Forward declaration to help put code in a natural reading order. 11 | void waitForConnection(); 12 | 13 | void setup() { 14 | uint32_t startTimeMs = millis(); 15 | Serial.begin(115200); 16 | while (!Serial && millis() - startTimeMs < 1000) { 17 | } 18 | 19 | Serial.println("Setting up BLE..."); 20 | RaceChronoBle.setUp("BLE CAN device demo", &raceChronoHandler); 21 | RaceChronoBle.startAdvertising(); 22 | 23 | Serial.println("BLE is set up, waiting for an incoming connection."); 24 | waitForConnection(); 25 | } 26 | 27 | void waitForConnection() { 28 | uint32_t iteration = 0; 29 | bool lastLineHadNewline = false; 30 | while (!RaceChronoBle.waitForConnection(1000)) { 31 | Serial.print("."); 32 | if ((++iteration) % 10 == 0) { 33 | lastLineHadNewline = true; 34 | Serial.println(); 35 | } else { 36 | lastLineHadNewline = false; 37 | } 38 | } 39 | 40 | if (!lastLineHadNewline) { 41 | Serial.println(); 42 | } 43 | 44 | Serial.println("Connected."); 45 | } 46 | 47 | void loop() { 48 | if (!RaceChronoBle.isConnected()) { 49 | Serial.println("RaceChrono disconnected! Waiting for a new connection."); 50 | waitForConnection(); 51 | } 52 | 53 | for (uint16_t pid = 0; pid < 256; pid++) { 54 | // For this simple demo, just sent PID repeated 8 times as data. 55 | uint8_t data[8] = { pid, pid, pid, pid, pid, pid, pid, pid }; 56 | uint8_t len = 8; 57 | RaceChronoBle.sendCanData(pid, data, len); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/TrackingAllowedPids/TrackingAllowedPids.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // In this example, we use RaceChronoPidMap to keep track of the requested PIDs 4 | // and update intervals. You can use any POD type in as an "extra" in this map, 5 | // but in this example we don't actually need any extras. 6 | // If you do use an extra, keep it small for better performance. 7 | using NoExtra = struct {}; 8 | RaceChronoPidMap pidMap; 9 | 10 | void dumpMapToSerial() { 11 | Serial.println("Current state of the PID map:"); 12 | 13 | uint16_t updateIntervalForAllEntries; 14 | bool areAllPidsAllowed = 15 | pidMap.areAllPidsAllowed(&updateIntervalForAllEntries); 16 | if (areAllPidsAllowed) { 17 | Serial.print(" All PIDs are allowed, update interval: "); 18 | Serial.print(updateIntervalForAllEntries); 19 | Serial.println(" ms."); 20 | Serial.println(""); 21 | } 22 | 23 | if (pidMap.isEmpty()) { 24 | if (areAllPidsAllowed) { 25 | // This condition doesn't happen in this example, but will happen if you 26 | // query the map for incoming data in the "allow all" mode. 27 | Serial.println(" Map is empty."); 28 | Serial.println(""); 29 | } else { 30 | Serial.println(" No PIDs are allowed."); 31 | Serial.println(""); 32 | } 33 | return; 34 | } 35 | 36 | struct { 37 | void operator() (const void *entry) { 38 | uint32_t pid = pidMap.getPid(entry); 39 | uint16_t updateIntervalMs = pidMap.getUpdateIntervalMs(entry); 40 | 41 | Serial.print(" "); 42 | Serial.print(pid); 43 | Serial.print(" (0x"); 44 | Serial.print(pid, HEX); 45 | Serial.print("), update interval: "); 46 | Serial.print(updateIntervalMs); 47 | Serial.println(" ms."); 48 | } 49 | } dumpEntry; 50 | pidMap.forEach(dumpEntry); 51 | 52 | Serial.println(""); 53 | } 54 | 55 | class UpdateMapOnRaceChronoCommands : public RaceChronoBleCanHandler { 56 | public: 57 | void allowAllPids(uint16_t updateIntervalMs) { 58 | Serial.print("Command: ALLOW ALL PIDS, update interval: "); 59 | Serial.print(updateIntervalMs); 60 | Serial.println(" ms."); 61 | 62 | pidMap.allowAllPids(updateIntervalMs); 63 | 64 | dumpMapToSerial(); 65 | } 66 | 67 | void denyAllPids() { 68 | Serial.println("Command: DENY ALL PIDS"); 69 | 70 | pidMap.reset(); 71 | 72 | dumpMapToSerial(); 73 | } 74 | 75 | void allowPid(uint32_t pid, uint16_t updateIntervalMs) { 76 | Serial.print("Command: ALLOW PID "); 77 | Serial.print(pid); 78 | Serial.print(" (0x"); 79 | Serial.print(pid, HEX); 80 | Serial.print("), update interval: "); 81 | Serial.print(updateIntervalMs); 82 | Serial.println(" ms"); 83 | 84 | if (!pidMap.allowOnePid(pid, updateIntervalMs)) { 85 | Serial.println("WARNING: unable to handle this request!"); 86 | } 87 | 88 | dumpMapToSerial(); 89 | } 90 | 91 | void handleDisconnect() { 92 | Serial.println("Resetting the map."); 93 | 94 | pidMap.reset(); 95 | 96 | dumpMapToSerial(); 97 | } 98 | } raceChronoHandler; 99 | 100 | // Forward declaration to help put code in a natural reading order. 101 | void waitForConnection(); 102 | 103 | void setup() { 104 | uint32_t startTimeMs = millis(); 105 | Serial.begin(115200); 106 | while (!Serial && millis() - startTimeMs < 1000) { 107 | } 108 | 109 | Serial.println("Setting up BLE..."); 110 | RaceChronoBle.setUp("BLE CAN device demo", &raceChronoHandler); 111 | RaceChronoBle.startAdvertising(); 112 | 113 | Serial.println("BLE is set up, waiting for an incoming connection."); 114 | waitForConnection(); 115 | } 116 | 117 | void waitForConnection() { 118 | uint32_t iteration = 0; 119 | bool lastPrintHadNewline = false; 120 | while (!RaceChronoBle.waitForConnection(1000)) { 121 | Serial.print("."); 122 | if ((++iteration) % 10 == 0) { 123 | lastPrintHadNewline = true; 124 | Serial.println(); 125 | } else { 126 | lastPrintHadNewline = false; 127 | } 128 | } 129 | 130 | if (!lastPrintHadNewline) { 131 | Serial.println(); 132 | } 133 | 134 | Serial.println("Connected"); 135 | } 136 | 137 | void loop() { 138 | if (!RaceChronoBle.isConnected()) { 139 | Serial.println("RaceChrono disconnected!"); 140 | raceChronoHandler.handleDisconnect(); 141 | 142 | Serial.println("Waiting for a new connection."); 143 | waitForConnection(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=RaceChrono 2 | version=0.0.1 3 | author=Timur Iskhodzhanov 4 | maintainer=Timur Iskhodzhanov 5 | sentence=An Arduino library communicating with the RaceChrono app 6 | paragraph=Currently supports Adafruit nRF52 microcontrollers and the BLE CAN mode. 7 | category=Other 8 | url=https://github.com/timurrrr/arduino-RaceChrono 9 | architectures=* 10 | includes=RaceChrono.h 11 | -------------------------------------------------------------------------------- /src/RaceChrono.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "RaceChrono.h" 3 | 4 | void RaceChronoBleCanHandler::handlePidRequest( 5 | const uint8_t *data, uint16_t len) { 6 | // The protocol implemented in this file is based on 7 | // https://github.com/aollin/racechrono-ble-diy-device 8 | 9 | if (len < 1) { 10 | // TODO: figure out how to report errors. 11 | return; 12 | } 13 | 14 | switch (data[0]) { 15 | case 1: // Allow all CAN PIDs. 16 | if (len == 3) { 17 | uint16_t updateIntervalMs = data[1] << 8 | data[2]; 18 | allowAllPids(updateIntervalMs); 19 | return; 20 | } 21 | break; 22 | 23 | case 0: // Deny all CAN PIDs. 24 | if (len == 1) { 25 | denyAllPids(); 26 | return; 27 | } 28 | break; 29 | 30 | case 2: // Allow one more CAN PID. 31 | if (len == 7) { 32 | uint16_t updateIntervalMs = data[1] << 8 | data[2]; 33 | uint32_t pid = data[3] << 24 | data[4] << 16 | data[5] << 8 | data[6]; 34 | allowPid(pid, updateIntervalMs); 35 | return; 36 | } 37 | break; 38 | } 39 | 40 | // TODO: figure out how to report errors. 41 | } 42 | 43 | bool RaceChronoBleAgent::waitForConnection(uint32_t timeoutMs) { 44 | uint32_t startTimeMs = millis(); 45 | while (!isConnected()) { 46 | if (millis() - startTimeMs >= timeoutMs) { 47 | return false; 48 | } 49 | delay(100); 50 | } 51 | 52 | return true; 53 | } 54 | -------------------------------------------------------------------------------- /src/RaceChrono.h: -------------------------------------------------------------------------------- 1 | #ifndef __RACECHRONO_H 2 | #define __RACECHRONO_H 3 | 4 | #include "RaceChronoPidMap.h" 5 | 6 | class RaceChronoBleCanHandler { 7 | public: 8 | virtual ~RaceChronoBleCanHandler() {} 9 | 10 | virtual void allowAllPids(uint16_t updateIntervalMs); 11 | virtual void denyAllPids(); 12 | virtual void allowPid(uint32_t pid, uint16_t updateIntervalMs); 13 | 14 | void handlePidRequest(const uint8_t *data, uint16_t len); 15 | }; 16 | 17 | class RaceChronoBleAgent { 18 | public: 19 | // Seems like the length limit for 'bluetoothName' is 19 visible characters. 20 | virtual void setUp( 21 | const char *bluetoothName, RaceChronoBleCanHandler *handler) = 0; 22 | 23 | virtual void startAdvertising() = 0; 24 | 25 | // Returns true on success, false on failure. 26 | bool waitForConnection(uint32_t timeoutMs); 27 | 28 | virtual bool isConnected() const = 0; 29 | 30 | virtual void sendCanData(uint32_t pid, const uint8_t *data, uint8_t len) = 0; 31 | 32 | protected: 33 | static const uint16_t RACECHRONO_SERVICE_UUID = 0x1ff8; 34 | 35 | // RaceChrono uses two BLE characteristics: 36 | // 1) 0x02 to request which PIDs to send, and how frequently 37 | // 2) 0x01 to be notified of data received for those PIDs 38 | static const uint16_t PID_CHARACTERISTIC_UUID = 0x2; 39 | static const uint16_t CAN_BUS_CHARACTERISTIC_UUID = 0x1; 40 | }; 41 | 42 | #if defined(ARDUINO_ARCH_NRF52) 43 | #endif // ARDUINO_ARCH_NRF52 44 | 45 | extern RaceChronoBleAgent &RaceChronoBle; 46 | 47 | #endif // __RACECHRONO_H 48 | -------------------------------------------------------------------------------- /src/RaceChronoESP32.cpp: -------------------------------------------------------------------------------- 1 | #if defined(ARDUINO_ARCH_ESP32) 2 | 3 | #include 4 | #include "RaceChrono.h" 5 | 6 | namespace { 7 | 8 | class RaceChronoBleAgentESP32 9 | : public RaceChronoBleAgent, NimBLECharacteristicCallbacks { 10 | public: 11 | RaceChronoBleAgentESP32(); 12 | 13 | void setUp(const char *bluetoothName, RaceChronoBleCanHandler *handler); 14 | 15 | void startAdvertising(); 16 | 17 | // Returns true on success, false on failure. 18 | bool waitForConnection(uint32_t timeoutMs); 19 | 20 | bool isConnected() const; 21 | 22 | void sendCanData(uint32_t pid, const uint8_t *data, uint8_t len); 23 | 24 | private: 25 | void onWrite(BLECharacteristic *characteristic); 26 | 27 | RaceChronoBleCanHandler *_handler; 28 | 29 | BLEServer *_bleServer; 30 | BLEService *_bleService; 31 | 32 | NimBLECharacteristic *_pidRequestsCharacteristic; 33 | NimBLECharacteristic *_canBusDataCharacteristic; 34 | }; 35 | 36 | RaceChronoBleAgentESP32 RaceChronoESP32Instance; 37 | 38 | RaceChronoBleAgentESP32::RaceChronoBleAgentESP32() : 39 | _handler(nullptr), 40 | _bleServer(nullptr), 41 | _bleService(nullptr), 42 | _pidRequestsCharacteristic(nullptr), 43 | _canBusDataCharacteristic(nullptr) 44 | { 45 | } 46 | 47 | void RaceChronoBleAgentESP32::setUp( 48 | const char *bluetoothName, RaceChronoBleCanHandler *handler) { 49 | _handler = handler; 50 | 51 | BLEDevice::init(bluetoothName); 52 | NimBLEAdvertising *advertising = NimBLEDevice::getAdvertising(); 53 | advertising->setName(bluetoothName); 54 | 55 | BLEDevice::setSecurityIOCap(BLE_HS_IO_NO_INPUT_OUTPUT); 56 | _bleServer = BLEDevice::createServer(); 57 | _bleService = _bleServer->createService(RACECHRONO_SERVICE_UUID); 58 | 59 | _pidRequestsCharacteristic = 60 | _bleService->createCharacteristic( 61 | PID_CHARACTERISTIC_UUID, NIMBLE_PROPERTY::WRITE); 62 | _pidRequestsCharacteristic->setCallbacks(this); 63 | 64 | _canBusDataCharacteristic = 65 | _bleService->createCharacteristic( 66 | CAN_BUS_CHARACTERISTIC_UUID, 67 | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); 68 | 69 | _bleService->start(); 70 | } 71 | 72 | void RaceChronoBleAgentESP32::startAdvertising() { 73 | NimBLEAdvertising *advertising = NimBLEDevice::getAdvertising(); 74 | advertising->setMinInterval(32); 75 | // TODO: Why is this different from the value used for nRF52? 76 | advertising->setMaxInterval(160); 77 | advertising->addServiceUUID(_bleService->getUUID()); 78 | advertising->setScanResponse(false); 79 | 80 | advertising->start(); 81 | } 82 | 83 | bool RaceChronoBleAgentESP32::isConnected() const { 84 | return _bleServer->getConnectedCount() > 0; 85 | } 86 | 87 | void RaceChronoBleAgentESP32::sendCanData( 88 | uint32_t pid, const uint8_t *data, uint8_t len) { 89 | if (len > 8) { 90 | len = 8; 91 | } 92 | 93 | unsigned char buffer[20]; 94 | buffer[0] = pid & 0xFF; 95 | buffer[1] = (pid >> 8) & 0xFF; 96 | buffer[2] = (pid >> 16) & 0xFF; 97 | buffer[3] = (pid >> 24) & 0xFF; 98 | memcpy(buffer + 4, data, len); 99 | _canBusDataCharacteristic->setValue(buffer, 4 + len); 100 | _canBusDataCharacteristic->notify(); 101 | } 102 | 103 | void RaceChronoBleAgentESP32::onWrite(BLECharacteristic *characteristic) { 104 | NimBLEAttValue value = characteristic->getValue(); 105 | _handler->handlePidRequest(value.data(), value.length()); 106 | } 107 | 108 | } // namespace 109 | 110 | RaceChronoBleAgent &RaceChronoBle = RaceChronoESP32Instance; 111 | 112 | #endif // ARDUINO_ARCH_ESP32 113 | -------------------------------------------------------------------------------- /src/RaceChronoNRF52.cpp: -------------------------------------------------------------------------------- 1 | #if defined(ARDUINO_ARCH_NRF52) 2 | 3 | #include 4 | #include "RaceChrono.h" 5 | 6 | namespace { 7 | 8 | // TODO: Instead of using a global variable, figure out how to pass a callback 9 | // to setWriteCallback() in a way that we can reference a field of 10 | // RaceChronoBleAgentNRF52. 11 | RaceChronoBleCanHandler *handler = nullptr; 12 | 13 | void handle_racechrono_filter_request( 14 | uint16_t conn_hdl, BLECharacteristic *chr, uint8_t *data, uint16_t len) { 15 | handler->handlePidRequest(data, len); 16 | } 17 | 18 | 19 | class RaceChronoBleAgentNRF52 : public RaceChronoBleAgent { 20 | public: 21 | RaceChronoBleAgentNRF52(); 22 | 23 | void setUp(const char *bluetoothName, RaceChronoBleCanHandler *handler); 24 | 25 | void startAdvertising(); 26 | 27 | // Returns true on success, false on failure. 28 | bool waitForConnection(uint32_t timeoutMs); 29 | 30 | bool isConnected() const; 31 | 32 | void sendCanData(uint32_t pid, const uint8_t *data, uint8_t len); 33 | 34 | private: 35 | // BLEService docs: https://learn.adafruit.com/bluefruit-nrf52-feather-learning-guide/bleservice 36 | BLEService _service; 37 | 38 | // BLECharacteristic docs: https://learn.adafruit.com/bluefruit-nrf52-feather-learning-guide/blecharacteristic 39 | BLECharacteristic _pidRequestsCharacteristic; 40 | BLECharacteristic _canBusDataCharacteristic; 41 | }; 42 | 43 | RaceChronoBleAgentNRF52 RaceChronoNRF52Instance; 44 | 45 | RaceChronoBleAgentNRF52::RaceChronoBleAgentNRF52() : 46 | _service(RACECHRONO_SERVICE_UUID), 47 | _pidRequestsCharacteristic(PID_CHARACTERISTIC_UUID), 48 | _canBusDataCharacteristic(CAN_BUS_CHARACTERISTIC_UUID) 49 | { 50 | } 51 | 52 | void RaceChronoBleAgentNRF52::setUp( 53 | const char *bluetoothName, RaceChronoBleCanHandler *handler) { 54 | ::handler = handler; 55 | 56 | Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); 57 | Bluefruit.begin(); 58 | Bluefruit.setName(bluetoothName); 59 | 60 | _service.begin(); 61 | 62 | _pidRequestsCharacteristic.setProperties(CHR_PROPS_WRITE); 63 | _pidRequestsCharacteristic.setPermission(SECMODE_NO_ACCESS, SECMODE_OPEN); 64 | _pidRequestsCharacteristic.setWriteCallback(handle_racechrono_filter_request); 65 | _pidRequestsCharacteristic.begin(); 66 | 67 | _canBusDataCharacteristic.setProperties(CHR_PROPS_NOTIFY | CHR_PROPS_READ); 68 | _canBusDataCharacteristic.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); 69 | _canBusDataCharacteristic.begin(); 70 | 71 | Bluefruit.setTxPower(+4); 72 | } 73 | 74 | void RaceChronoBleAgentNRF52::startAdvertising() { 75 | Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); 76 | Bluefruit.Advertising.addTxPower(); 77 | Bluefruit.Advertising.addService(_service); 78 | Bluefruit.Advertising.addName(); 79 | Bluefruit.Advertising.restartOnDisconnect(true); 80 | 81 | // Fast mode interval: 20 ms, slow mode interval: 152.5 ms. 82 | // The numbers specified on the call are multiplied by (0.625 ms) units. 83 | Bluefruit.Advertising.setInterval(/* fast= */ 32, /* slow= */ 244); 84 | 85 | // Timeout for fast mode is 30 seconds. 86 | Bluefruit.Advertising.setFastTimeout(30); 87 | 88 | // Start advertising forever. 89 | Bluefruit.Advertising.start(/* timeout= */ 0); 90 | } 91 | 92 | bool RaceChronoBleAgentNRF52::isConnected() const { 93 | return Bluefruit.connected(); 94 | } 95 | 96 | void RaceChronoBleAgentNRF52::sendCanData( 97 | uint32_t pid, const uint8_t *data, uint8_t len) { 98 | if (len > 8) { 99 | len = 8; 100 | } 101 | 102 | unsigned char buffer[20]; 103 | buffer[0] = pid & 0xFF; 104 | buffer[1] = (pid >> 8) & 0xFF; 105 | buffer[2] = (pid >> 16) & 0xFF; 106 | buffer[3] = (pid >> 24) & 0xFF; 107 | memcpy(buffer + 4, data, len); 108 | _canBusDataCharacteristic.notify(buffer, 4 + len); 109 | } 110 | 111 | } // namespace 112 | 113 | RaceChronoBleAgent &RaceChronoBle = RaceChronoNRF52Instance; 114 | 115 | #endif // ARDUINO_ARCH_NRF52 116 | -------------------------------------------------------------------------------- /src/RaceChronoPidMap.h: -------------------------------------------------------------------------------- 1 | #ifndef __RACECHRONO_PID_MAP_H 2 | #define __RACECHRONO_PID_MAP_H 3 | 4 | #include 5 | 6 | // A map that allows keeping track of the requested update intervals for PIDs, 7 | // as well as any "extra" information useful for a given application. 8 | // 9 | // The implementation uses an array sorted by PID, which provides O(N) 10 | // runtime for insertions and O(log N) runtime for lookup. This could be 11 | // optimized by using a hash map for O(1) runtimes, but that would come at extra 12 | // code complexity, and extra memory usage. As long as the number of PIDs is 13 | // kept to a reasonable minimum, the runtime for lookup shouldn't be an issue, 14 | // and the runtime for insertions in negligible, given that it should only be 15 | // done once per session. 16 | template 17 | class RaceChronoPidMap { 18 | public: 19 | void reset() { 20 | numEntries = 0; 21 | updateIntervalForAllEntries = NOT_ALL_PIDS_ALLOWED; 22 | } 23 | 24 | // Returns true on success, false if the map is full. 25 | bool allowOnePid(uint32_t pid, uint16_t updateIntervalMs) { 26 | Entry *entry = 27 | findEntry(pid, /* defaultUpdateIntervalMs = */ updateIntervalMs); 28 | return entry != nullptr; 29 | } 30 | 31 | void allowAllPids(uint16_t updateIntervalMs) { 32 | updateIntervalForAllEntries = updateIntervalMs; 33 | } 34 | 35 | // Returns true if allowAllPids() was called since the last reset(). 36 | // You can optionally provide a *updateIntervalMsOut to get the last value 37 | // passed to allowAllPids(). 38 | bool areAllPidsAllowed(uint16_t *updateIntervalMsOut = nullptr) const { 39 | if (updateIntervalForAllEntries == NOT_ALL_PIDS_ALLOWED) { 40 | return false; 41 | } 42 | 43 | if (updateIntervalMsOut != nullptr) { 44 | *updateIntervalMsOut = updateIntervalForAllEntries; 45 | } 46 | return true; 47 | } 48 | 49 | bool isEmpty() const { 50 | return numEntries == 0; 51 | } 52 | 53 | // Iterate over the registered PIDs and call the functor on all the entries. 54 | // 55 | // A functor can be something like this: 56 | // struct { 57 | // void operator() (void *entry) { 58 | // uint32_t pid = pidMap.getPid(entry); 59 | // uint16_t updateIntervalMs = pidMap.getUpdateIntervalMs(entry); 60 | // MyExtra extra = pidMap.getExtra(entry); 61 | // ... 62 | // } 63 | // } myFunctor; 64 | // ... 65 | // pidMap.forEach(myFunctor); 66 | template 67 | void forEach(FuncType functor) { 68 | void* entry = getFirstEntryId(); 69 | 70 | while (entry != nullptr) { 71 | functor(entry); 72 | entry = getNextEntryId(entry); 73 | } 74 | } 75 | 76 | uint32_t getPid(const void *entry) const { 77 | Entry *realEntry = (Entry*)entry; 78 | return realEntry->pid; 79 | } 80 | 81 | uint16_t getUpdateIntervalMs(const void *entry) const { 82 | Entry *realEntry = (Entry*)entry; 83 | return realEntry->updateIntervalMs; 84 | } 85 | 86 | // Allows accessing the "extra" assosciated with the given entry. 87 | // This can be useful to track stuff such as when was the last message sent 88 | // for the PID assosciated with this entry. 89 | // TODO: add a const/const override. 90 | ExtraType* getExtra(void *entry) { 91 | Entry *realEntry = (Entry*)entry; 92 | return &realEntry->extra; 93 | } 94 | 95 | // Returns an opaque pointer for an entry for the given PID, or nullptr. 96 | // This is useful to avoid performing multiple lookups if you need to query 97 | // multiple things, such as the update interval, or the extra. 98 | // 99 | // A new entry is created in the map if allowAllPids() was called since the 100 | // last reset(). 101 | // Don't store the pointer anywhere, as it will be invalidated if reset() or 102 | // allowOnePid() is called. 103 | void* getEntryId(uint32_t pid) { 104 | uint16_t defaultUpdateIntervalMs = 105 | updateIntervalForAllEntries != NOT_ALL_PIDS_ALLOWED 106 | ? updateIntervalForAllEntries 107 | : DONT_CREATE_NEW_ENTRY; 108 | return findEntry(pid, defaultUpdateIntervalMs); 109 | } 110 | 111 | // Get the entry for the lowest PID registered in the map. 112 | // Can be useful e.g. for printing the state of the map. 113 | void* getFirstEntryId() { 114 | if (numEntries == 0) { 115 | return nullptr; 116 | } 117 | return &_map[0]; 118 | } 119 | 120 | // Returns the entry in the map after "entry", in the ascending PID order, 121 | // or nullptr if there are no more entries. 122 | // TODO: add a const/const override. 123 | void* getNextEntryId(void *entry) const { 124 | Entry *realEntry = (Entry*)entry; 125 | uint16_t index = realEntry - &_map[0]; 126 | if (index < 0 || index + 1 >= numEntries) { 127 | return nullptr; 128 | } 129 | return realEntry + 1; 130 | } 131 | 132 | private: 133 | struct Entry { 134 | uint32_t pid; 135 | uint16_t updateIntervalMs; 136 | ExtraType extra; 137 | 138 | // This is needed for std::lower_bound(). 139 | bool operator< (uint32_t other_pid) { return pid < other_pid; } 140 | }; 141 | 142 | static const uint16_t DONT_CREATE_NEW_ENTRY = 0xffff; 143 | static const uint16_t NOT_ALL_PIDS_ALLOWED = 0xffff; 144 | // Stores NOT_ALL_PIDS_ALLOWED, unless AllowAllPids() was called, in which 145 | // case it stores the default value to use for new entries. 146 | uint16_t updateIntervalForAllEntries = NOT_ALL_PIDS_ALLOWED; 147 | 148 | uint16_t numEntries = 0; 149 | Entry _map[MaxNumPids]; // Keep sorted by pid to allow O(log N) search. 150 | 151 | Entry* findEntry( 152 | uint32_t pid, 153 | uint16_t defaultUpdateIntervalMs) { 154 | Entry *insertionPosition = std::lower_bound(_map, _map + numEntries, pid); 155 | if (insertionPosition != _map + numEntries 156 | && insertionPosition->pid == pid) { 157 | // Found it. 158 | return insertionPosition; 159 | } 160 | 161 | if (defaultUpdateIntervalMs == DONT_CREATE_NEW_ENTRY) { 162 | return nullptr; 163 | } 164 | 165 | if (numEntries == MaxNumPids) { 166 | // Map is full. 167 | return nullptr; 168 | } 169 | 170 | std::copy_backward( 171 | insertionPosition, 172 | _map + numEntries, 173 | _map + numEntries + 1); 174 | numEntries++; 175 | insertionPosition->pid = pid; 176 | insertionPosition->updateIntervalMs = defaultUpdateIntervalMs; 177 | insertionPosition->extra = ExtraType(); 178 | return insertionPosition; 179 | } 180 | }; 181 | 182 | #endif // __RACECHRONO_PID_MAP_H 183 | --------------------------------------------------------------------------------