Hi everyone,
I'm trying to build a custom BLE monitor display for RaceChrono using an ESP32 (M5Stack Atom). I am following the official racechrono-ble-diy-device documentation and examples.
https://github.com/aollin/racechrono-ble-diy-device
The Problem:
The device is discovered by the app and the connection is stable. However, the app never starts sending telemetry data, and I don't see any read/write activity on the characteristics.
Technical Details:
Hardware: ESP32 (M5Stack Atom)
Library: NimBLE-Arduino (to ensure better Android compatibility)
Service UUID: 00001ff8-0000-1000-8000-00805f9b34fb
Config Characteristic (0x05): Properties: READ | WRITE | INDICATE.
Data Characteristic (0x06): Properties: WRITE | WRITE_NR.
MTU: Set to 517.
Device Name: RC_Matrix_V1
What I've tried so far:
Configuration Push: I've implemented an automatic push of the monitor configuration (Message Type 0x01) via indicate() and notify() every 10 seconds after connection.
Minimal Config: I reduced the request to just one channel (Channel 1: Elapsed Time) to rule out payload issues.
Cache Clearing: I've tried unpairing the device, clearing the Bluetooth cache on Android, and restarting the app/phone.
Advertising: Using "General Discoverable" flags and adding the Service UUID to the advertising packet.
Observation:
In the Serial Monitor, I see "Device Connected", and my code successfully calls indicate() / notify() for the configuration. However, the app never writes any telemetry data to the Data Characteristic (0x06), and after some time, the connection is simply closed.
My Setup in App:
Settings → Add other device → RaceChrono DIY → Bluetooth LE → [My Device] → Monitor → OK.
Has anyone encountered this behavior? Is there a specific requirement for the handshake or a specific Android version/setting that might be blocking the telemetry stream?
Any help would be greatly appreciated!
Best regards,
Marc
`#include <M5Unified.h>
#include <NimBLEDevice.h>
#include <Adafruit_NeoPixel.h>
#include <string> // Standard C++ String Library
// --- Hardware Konfiguration ---
#define LED_PIN 27
#define NUM_LEDS 25
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
// --- Global Variables ---
NimBLEServer* pServer = NULL;
NimBLECharacteristic* pConfigCharacteristic = NULL;
NimBLECharacteristic* pNotifyCharacteristic = NULL;
volatile bool isDeviceConnected = false;
volatile bool monitorsConfigured = false;
// --- Hardware Abstraction ---
void setMatrixColor(uint8_t r, uint8_t g, uint8_t b) {
Serial.printf("Matrix Color Set to: R:%d G:%d B:%d\n", r, g, b);
for (int i = 0; i < NUM_LEDS; i++) {
strip.setPixelColor(i, strip.Color(r, g, b));
}
strip.show();
}
// --- Forward Declarations ---
void configureMonitors();
void monitorConfigWriteCallback(uint8_t* data, uint16_t len, BLECharacteristic* pChar);
void monitorNotificationWriteCallback(uint8_t* data, uint16_t len, BLECharacteristic* pChar);
// --- Helper Functions ---
String formatElapsedTime(uint32_t ms) {
uint32_t hours = ms / 3600000;
uint32_t minutes = (ms % 3600000) / 60000;
uint32_t seconds = (ms % 60000) / 1000;
uint32_t millis = ms % 1000;
char buffer[20];
snprintf(buffer, sizeof(buffer), "%02lu:%02lu:%02lu.%03lu",
hours, minutes, seconds, millis);
return String(buffer);
}
void processTelemetryData(uint8_t* data, uint16_t len) {
if (data == NULL || len < 4) return;
// Extract elapsed time (first 4 bytes, Little-Endian)
uint32_t elapsedTime = (uint32_t)data[0] |
((uint32_t)data[1] << 8) |
((uint32_t)data[2] << 16) |
((uint32_t)data[3] << 24);
Serial.printf("[%lu] Elapsed Time: %s\n", millis() / 1000, formatElapsedTime(elapsedTime).c_str());
// Optional: Process further channels
if (len >= 8) {
uint32_t currentLapTime = (uint32_t)data[4] |
((uint32_t)data[5] << 8) |
((uint32_t)data[6] << 16) |
((uint32_t)data[7] << 24);
Serial.printf("[%lu] Current Lap Time: %s\n", millis() / 1000, formatElapsedTime(currentLapTime).c_str());
}
}
// --- BLE Callbacks ---
class MyServerCallbacks : public NimBLEServerCallbacks {
void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override {
isDeviceConnected = true;
setMatrixColor(0, 255, 0);
Serial.println(String(millis() / 1000) + " Device Connected");
}
void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override {
isDeviceConnected = false;
monitorsConfigured = false;
setMatrixColor(255, 0, 0);
Serial.println(String(millis() / 1000) + " Device Disconnected");
NimBLEDevice::startAdvertising();
}
};
class ConfigCallback : public NimBLECharacteristicCallbacks {
void onWrite(NimBLECharacteristic* pChar, NimBLEConnInfo& connInfo) override {
std::string value = pChar->getValue();
uint16_t len = value.length();
uint8_t firstByte = (len > 0) ? (uint8_t)value[0] : 0;
Serial.printf("Config Write received: %d bytes. First byte: 0x%02X\n", len, firstByte);
}
void onRead(NimBLECharacteristic* pChar, NimBLEConnInfo& connInfo) override {
Serial.println("Config read by App - triggering configuration...");
configureMonitors();
}
};
class NotifyCallback : public NimBLECharacteristicCallbacks {
void onWrite(NimBLECharacteristic* pChar, NimBLEConnInfo& connInfo) override {
std::string value = pChar->getValue();
if (!value.empty()) {
uint8_t* data = (uint8_t*)value.data();
uint16_t len = value.length();
processTelemetryData(data, len);
setMatrixColor(0, 255, 255); // Cyan bei Datenempfang
Serial.printf("Notification Received: %d bytes\n", len);
}
}
void onRead(NimBLECharacteristic* pChar, NimBLEConnInfo& connInfo) override {
Serial.println(String(millis() / 1000) + " App prüft Data-Charakteristik!");
uint8_t ready[] = { 0x01 };
pChar->setValue(ready, 1);
}
};
ConfigCallback configCB;
NotifyCallback notifyCB;
// --- configureMonitors Integration according to aollin specification ---
void configureMonitors() {
if (!isDeviceConnected) return;
Serial.println(String(millis() / 1000) + " Sending monitor configuration to RaceChrono...");
// Data structure according to RaceChrono BLE DIY protocol for remote displays:
// Byte 0: 0x01 (Message Type: Monitor Configuration)
// Byte 1: Update interval (e.g. 2 = 5 Hz / every 200ms, 5 = 2 Hz, 10 = 1 Hz)
// Byte 2: Number of requested data channels
// Byte 3+: The desired channel IDs that the display wants to receive
uint8_t configData[] = {
0x01, // Type: Monitor Configuration
2, // Update rate: 5 Hz
1, // Number of channels (reduced to minimum)
0x01 // Channel 1: Elapsed time
};
pConfigCharacteristic->setValue(configData, sizeof(configData));
// Use notify() instead of indicate() to avoid issues with missing ACKs
pConfigCharacteristic->notify();
monitorsConfigured = true;
Serial.println(String(millis() / 1000) + " Monitor configuration successfully transmitted!");
}
// --- Setup Functions ---
void bluetoothStart() {
Serial.println(String(millis() / 1000) + " Initializing NimBLE...");
NimBLEDevice::init("RC_Matrix_V1");
NimBLEDevice::setMTU(517);
pServer = NimBLEDevice::createServer();
if (pServer == NULL) return;
pServer->setCallbacks(new MyServerCallbacks());
#define serviceUUID "00001ff8-0000-1000-8000-00805f9b34fb"
#define configUUID "00000005-0000-1000-8000-00805f9b34fb"
#define notifyUUID "00000006-0000-1000-8000-00805f9b34fb"
NimBLEService* pService = pServer->createService(serviceUUID);
if (pService == NULL) return;
// 0x05: Config - INDICATE und WRITE
pConfigCharacteristic = pService->createCharacteristic(
configUUID,
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::INDICATE
);
pConfigCharacteristic->setCallbacks(&configCB);
// 0x06: Data - WRITE und WRITE_NR
pNotifyCharacteristic = pService->createCharacteristic(
notifyUUID,
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR
);
pNotifyCharacteristic->setCallbacks(¬ifyCB);
// NimBLE: Services werden automatisch beim Server-Start gestartet
NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->addServiceUUID(serviceUUID);
NimBLEDevice::startAdvertising();
Serial.println(String(millis() / 1000) + " Bluetooth Advertising Started");
}
void monitorConfigWriteCallback(uint8_t* data, uint16_t len, BLECharacteristic* pChar) {
if (data == NULL) return;
Serial.printf("Config Received: %d bytes\n", len);
}
void monitorNotificationWriteCallback(uint8_t* data, uint16_t len, BLECharacteristic* pChar) {
if (data == NULL) return;
processTelemetryData(data, len);
setMatrixColor(0, 255, 255); // Cyan bei Datenempfang
Serial.printf("Notification Received: %d bytes\n", len);
}
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
Serial.begin(115200);
strip.begin();
setMatrixColor(128, 128, 128);
delay(2000);
Serial.println(String(millis() / 1000) + " System Ready.");
bluetoothStart();
}
void loop() {
M5.update();
if (isDeviceConnected) {
// If a device is connected, try to send the configuration every 10 seconds,
// as long as we haven't received telemetry data yet (monitorsConfigured = true).
static unsigned long lastConfigAttempt = 0;
if (!monitorsConfigured) {
unsigned long now = millis();
if (now - lastConfigAttempt > 10000) { // Increased to 10 seconds
lastConfigAttempt = now;
Serial.println(String(now / 1000) + " Automatic configuration transmission...");
configureMonitors();
}
}
}
delay(10);
}`