OpusBike/webapp/js/boschProtocol.js

161 lines
5.0 KiB
JavaScript
Raw Normal View History

/**
* 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
};
})();