Fix Bosch BLE protocol: correct UUIDs, 2-byte data IDs, new dashboard fields
This commit is contained in:
parent
a41459f10b
commit
906fde8ad1
@ -1,21 +1,30 @@
|
||||
/**
|
||||
* Bosch Smart System BLE Protocol Decoder
|
||||
* Decodes varint-encoded data from the Bosch BLE characteristic.
|
||||
*
|
||||
* Confirmed from Phase 2 testing (nRF Connect):
|
||||
* - Characteristic: 00000011-EAA2-11E9-81B4-2A2AE2DBCCE4
|
||||
* - Data rate: ~2 messages/second
|
||||
* - Message format: 0x30 header, varint-encoded TLV pairs
|
||||
* - Confirmed fields: assist mode, battery %, speed, cadence
|
||||
* - Unknown data IDs found: 0x9865, 0x981A, 0x9874, 0x80C4, 0x808A, 0x8091
|
||||
* Real protocol confirmed from:
|
||||
* - Endless Sphere reverse engineering
|
||||
* - Soarcer/bosch-garmin-bridge
|
||||
* - Phase 2 nRF Connect captures
|
||||
*
|
||||
* Message format: [0x30] [length] [id_hi] [id_lo] [0x08] [varint value...]
|
||||
* Multiple messages can be concatenated in a single notification.
|
||||
*
|
||||
* Known data IDs:
|
||||
* 0x982D = Speed (raw / 10 = km/h)
|
||||
* 0x985A = Cadence (raw / 2 = RPM)
|
||||
* 0x985B = Human Power (W)
|
||||
* 0x985D = Motor Power (W)
|
||||
* 0x8088 = Battery (%)
|
||||
* 0x9809 = Assist Mode (0=OFF 1=ECO 2=TOUR 3=eMTB 4=TURBO)
|
||||
*/
|
||||
|
||||
const BoschProtocol = (() => {
|
||||
// Bosch BLE UUIDs
|
||||
const SERVICE_UUID = '0000fee7-0000-1000-8000-00805f9b34fb';
|
||||
// Correct Bosch Smart System BLE UUIDs (eaa2 base)
|
||||
const SERVICE_UUID = '00000010-eaa2-11e9-81b4-2a2ae2dbcce4';
|
||||
const CHAR_UUID = '00000011-eaa2-11e9-81b4-2a2ae2dbcce4';
|
||||
const WRITE_UUID = '00000012-eaa2-11e9-81b4-2a2ae2dbcce4';
|
||||
|
||||
// Assist modes mapping
|
||||
// Assist modes
|
||||
const ASSIST_MODES = {
|
||||
0: 'OFF',
|
||||
1: 'ECO',
|
||||
@ -24,21 +33,18 @@ const BoschProtocol = (() => {
|
||||
4: 'TURBO'
|
||||
};
|
||||
|
||||
// Known data IDs from Phase 2 testing
|
||||
const KNOWN_IDS = {
|
||||
0x01: 'speed', // in 0.1 km/h
|
||||
0x02: 'torque', // in Nm
|
||||
0x03: 'totalKm', // in meters (divide by 1000)
|
||||
0x04: 'mode', // assist mode index
|
||||
0x05: 'battery', // percentage (0-100)
|
||||
0x06: 'cadence' // RPM
|
||||
// Confirmed data IDs from Endless Sphere + Soarcer research
|
||||
const DATA_IDS = {
|
||||
0x982D: { name: 'speed', divisor: 10 }, // km/h
|
||||
0x985A: { name: 'cadence', divisor: 2 }, // RPM
|
||||
0x985B: { name: 'humanPower', divisor: 1 }, // W
|
||||
0x985D: { name: 'motorPower', divisor: 1 }, // W
|
||||
0x8088: { name: 'battery', divisor: 1 }, // %
|
||||
0x9809: { name: 'mode', divisor: 1 }, // assist mode index
|
||||
};
|
||||
|
||||
// Unknown data IDs discovered during testing — need real-ride validation
|
||||
const UNKNOWN_IDS = [0x9865, 0x981A, 0x9874, 0x80C4, 0x808A, 0x8091];
|
||||
|
||||
/**
|
||||
* Decode a varint from a DataView at a given offset.
|
||||
* Decode a protobuf-style varint from a DataView at offset.
|
||||
* Returns { value, bytesRead }
|
||||
*/
|
||||
function decodeVarint(dataView, offset) {
|
||||
@ -58,28 +64,21 @@ const BoschProtocol = (() => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a signed varint (zigzag encoding).
|
||||
*/
|
||||
function decodeSignedVarint(dataView, offset) {
|
||||
const { value, bytesRead } = decodeVarint(dataView, offset);
|
||||
const decoded = (value >>> 1) ^ -(value & 1);
|
||||
return { value: decoded, bytesRead };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a Bosch BLE notification payload.
|
||||
* Handles the 0x30 header byte confirmed from Phase 2 testing.
|
||||
* Parse a Bosch BLE notification.
|
||||
* Handles both 0x30 (telemetry) and 0x10 (config) headers.
|
||||
* Multiple messages can be concatenated.
|
||||
*/
|
||||
function parseNotification(dataView) {
|
||||
const data = {
|
||||
speed: 0,
|
||||
torque: 0,
|
||||
totalKm: 0,
|
||||
mode: 'OFF',
|
||||
battery: null,
|
||||
speed: null,
|
||||
cadence: null,
|
||||
humanPower: null,
|
||||
motorPower: null,
|
||||
battery: null,
|
||||
mode: null,
|
||||
raw: [],
|
||||
unknownFields: {}
|
||||
unknownFields: {},
|
||||
messageCount: 0
|
||||
};
|
||||
|
||||
// Store raw bytes for debugging
|
||||
@ -92,53 +91,57 @@ const BoschProtocol = (() => {
|
||||
try {
|
||||
let offset = 0;
|
||||
|
||||
// Skip header byte(s) — 0x30 confirmed from Phase 2, also handle 0x01/0x02
|
||||
const header = dataView.getUint8(0);
|
||||
if (header === 0x30 || header === 0x01 || header === 0x02) {
|
||||
offset = 1;
|
||||
while (offset < dataView.byteLength) {
|
||||
// Read header — expect 0x30 (telemetry) or 0x10 (config)
|
||||
const header = dataView.getUint8(offset);
|
||||
if (header !== 0x30 && header !== 0x10) {
|
||||
offset++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse TLV pairs
|
||||
while (offset < dataView.byteLength - 1) {
|
||||
// Read tag — may be 1 or 2 bytes
|
||||
let tag = dataView.getUint8(offset);
|
||||
offset++;
|
||||
|
||||
// Check for 2-byte tags (high bit set patterns from unknown IDs)
|
||||
if ((tag & 0x80) !== 0 && offset < dataView.byteLength) {
|
||||
tag = (tag << 8) | dataView.getUint8(offset);
|
||||
if (offset >= dataView.byteLength) break;
|
||||
|
||||
// Read length
|
||||
const length = dataView.getUint8(offset);
|
||||
offset++;
|
||||
|
||||
if (length === 0) continue;
|
||||
|
||||
const msgEnd = offset + length;
|
||||
if (msgEnd > dataView.byteLength) break;
|
||||
|
||||
// Read 2-byte data ID
|
||||
if (offset + 1 >= dataView.byteLength) break;
|
||||
const idHi = dataView.getUint8(offset);
|
||||
const idLo = dataView.getUint8(offset + 1);
|
||||
const dataId = (idHi << 8) | idLo;
|
||||
offset += 2;
|
||||
|
||||
// Length 0x02 = just the ID, value is 0
|
||||
if (length === 2) {
|
||||
applyField(data, dataId, 0);
|
||||
data.messageCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip 0x08 marker if present
|
||||
if (offset < msgEnd && dataView.getUint8(offset) === 0x08) {
|
||||
offset++;
|
||||
}
|
||||
|
||||
// Decode varint value
|
||||
if (offset < msgEnd) {
|
||||
const { value, bytesRead } = decodeVarint(dataView, offset);
|
||||
offset += bytesRead;
|
||||
|
||||
switch (tag) {
|
||||
case 0x01: // Speed in 0.1 km/h
|
||||
data.speed = value / 10;
|
||||
break;
|
||||
case 0x02: // Torque in Nm
|
||||
data.torque = value;
|
||||
break;
|
||||
case 0x03: // Total distance in meters
|
||||
data.totalKm = value / 1000;
|
||||
break;
|
||||
case 0x04: // Assist mode
|
||||
data.mode = ASSIST_MODES[value] || 'OFF';
|
||||
break;
|
||||
case 0x05: // Battery percentage
|
||||
data.battery = value;
|
||||
break;
|
||||
case 0x06: // Cadence RPM
|
||||
data.cadence = value;
|
||||
break;
|
||||
default:
|
||||
// Log unknown fields for future identification
|
||||
data.unknownFields[tag.toString(16)] = value;
|
||||
break;
|
||||
applyField(data, dataId, value);
|
||||
data.messageCount++;
|
||||
}
|
||||
|
||||
if (bytesRead === 0) break; // Safety
|
||||
// Skip to end of this message
|
||||
if (offset < msgEnd) {
|
||||
offset = msgEnd;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Bosch parse error:', e);
|
||||
@ -147,14 +150,33 @@ const BoschProtocol = (() => {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a decoded data field to the result object.
|
||||
*/
|
||||
function applyField(data, dataId, rawValue) {
|
||||
const field = DATA_IDS[dataId];
|
||||
if (field) {
|
||||
const value = field.divisor > 1 ? rawValue / field.divisor : rawValue;
|
||||
switch (field.name) {
|
||||
case 'speed': data.speed = Math.round(value * 10) / 10; break;
|
||||
case 'cadence': data.cadence = Math.round(value); break;
|
||||
case 'humanPower': data.humanPower = rawValue; break;
|
||||
case 'motorPower': data.motorPower = rawValue; break;
|
||||
case 'battery': data.battery = rawValue; break;
|
||||
case 'mode': data.mode = ASSIST_MODES[rawValue] || 'OFF'; break;
|
||||
}
|
||||
} else {
|
||||
data.unknownFields['0x' + dataId.toString(16).toUpperCase()] = rawValue;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
SERVICE_UUID,
|
||||
CHAR_UUID,
|
||||
WRITE_UUID,
|
||||
ASSIST_MODES,
|
||||
KNOWN_IDS,
|
||||
UNKNOWN_IDS,
|
||||
DATA_IDS,
|
||||
decodeVarint,
|
||||
decodeSignedVarint,
|
||||
parseNotification
|
||||
};
|
||||
})();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user