161 lines
5.0 KiB
JavaScript
161 lines
5.0 KiB
JavaScript
/**
|
|
* 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
|
|
*/
|
|
|
|
const BoschProtocol = (() => {
|
|
// Bosch BLE UUIDs
|
|
const SERVICE_UUID = '0000fee7-0000-1000-8000-00805f9b34fb';
|
|
const CHAR_UUID = '00000011-eaa2-11e9-81b4-2a2ae2dbcce4';
|
|
|
|
// Assist modes mapping
|
|
const ASSIST_MODES = {
|
|
0: 'OFF',
|
|
1: 'ECO',
|
|
2: 'TOUR',
|
|
3: 'eMTB',
|
|
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
|
|
};
|
|
|
|
// 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.
|
|
* Returns { value, bytesRead }
|
|
*/
|
|
function decodeVarint(dataView, offset) {
|
|
let result = 0;
|
|
let shift = 0;
|
|
let bytesRead = 0;
|
|
|
|
while (offset + bytesRead < dataView.byteLength) {
|
|
const byte = dataView.getUint8(offset + bytesRead);
|
|
result |= (byte & 0x7F) << shift;
|
|
bytesRead++;
|
|
if ((byte & 0x80) === 0) break;
|
|
shift += 7;
|
|
}
|
|
|
|
return { value: result, bytesRead };
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
function parseNotification(dataView) {
|
|
const data = {
|
|
speed: 0,
|
|
torque: 0,
|
|
totalKm: 0,
|
|
mode: 'OFF',
|
|
battery: null,
|
|
cadence: null,
|
|
raw: [],
|
|
unknownFields: {}
|
|
};
|
|
|
|
// Store raw bytes for debugging
|
|
for (let i = 0; i < dataView.byteLength; i++) {
|
|
data.raw.push(dataView.getUint8(i));
|
|
}
|
|
|
|
if (dataView.byteLength < 4) return data;
|
|
|
|
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;
|
|
}
|
|
|
|
// 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);
|
|
offset++;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (bytesRead === 0) break; // Safety
|
|
}
|
|
} catch (e) {
|
|
console.warn('Bosch parse error:', e);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
return {
|
|
SERVICE_UUID,
|
|
CHAR_UUID,
|
|
ASSIST_MODES,
|
|
KNOWN_IDS,
|
|
UNKNOWN_IDS,
|
|
decodeVarint,
|
|
decodeSignedVarint,
|
|
parseNotification
|
|
};
|
|
})();
|