mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
224 lines
7.3 KiB
JavaScript
224 lines
7.3 KiB
JavaScript
const HEADER = 0x13;
|
|
const CHARGING_STATE = {
|
|
0: 'not charging',
|
|
1: 'reconditioning charging',
|
|
2: 'full charging',
|
|
3: 'trickle charging',
|
|
4: 'waiting',
|
|
5: 'charging fault',
|
|
};
|
|
|
|
const OI_MODES = {
|
|
1: 'off',
|
|
2: 'passive',
|
|
3: 'safe',
|
|
4: 'full',
|
|
};
|
|
|
|
const GROUP100_LAYOUT = [
|
|
{ id: 7, key: 'bumpsAndWheelDrops', bytes: 1, parser: parseBumps },
|
|
{ id: 8, key: 'wall', bytes: 1, parser: parseBool },
|
|
{ id: 9, key: 'cliffLeft', bytes: 1, parser: parseBool },
|
|
{ id: 10, key: 'cliffFrontLeft', bytes: 1, parser: parseBool },
|
|
{ id: 11, key: 'cliffFrontRight', bytes: 1, parser: parseBool },
|
|
{ id: 12, key: 'cliffRight', bytes: 1, parser: parseBool },
|
|
{ id: 13, key: 'virtualWall', bytes: 1, parser: parseBool },
|
|
{ id: 14, key: 'wheelOvercurrents', bytes: 1, parser: parseWheelCurrents },
|
|
{ id: 15, key: 'dirtDetect', bytes: 1, parser: parseUInt },
|
|
{ id: 16, key: 'dirtDetectLeft', bytes: 1, parser: parseUInt },
|
|
{ id: 17, key: 'infraredCharacterOmni', bytes: 1, parser: parseUInt },
|
|
{ id: 18, key: 'buttons', bytes: 1, parser: parseButtons },
|
|
{ id: 19, key: 'distanceMm', bytes: 2, parser: parseInt },
|
|
{ id: 20, key: 'angleDeg', bytes: 2, parser: parseInt },
|
|
{ id: 21, key: 'chargingState', bytes: 1, parser: parseChargingState },
|
|
{ id: 22, key: 'voltageMv', bytes: 2, parser: parseUInt },
|
|
{ id: 23, key: 'currentMa', bytes: 2, parser: parseInt },
|
|
{ id: 24, key: 'batteryTemperatureC', bytes: 1, parser: parseInt },
|
|
{ id: 25, key: 'batteryChargeMah', bytes: 2, parser: parseUInt },
|
|
{ id: 26, key: 'batteryCapacityMah', bytes: 2, parser: parseUInt },
|
|
{ id: 27, key: 'wallSignal', bytes: 2, parser: parseUInt },
|
|
{ id: 28, key: 'cliffLeftSignal', bytes: 2, parser: parseUInt },
|
|
{ id: 29, key: 'cliffFrontLeftSignal', bytes: 2, parser: parseUInt },
|
|
{ id: 30, key: 'cliffFrontRightSignal', bytes: 2, parser: parseUInt },
|
|
{ id: 31, key: 'cliffRightSignal', bytes: 2, parser: parseUInt },
|
|
{ id: 32, key: 'chargingSourcesAvailable', bytes: 1, parser: parseChargeSources },
|
|
{ id: 33, key: 'chargingSourcesReserved', bytes: 2, parser: parseUInt },
|
|
{ id: 34, key: 'chargingSources', bytes: 1, parser: parseChargeSources },
|
|
{ id: 35, key: 'oiMode', bytes: 1, parser: parseOiMode },
|
|
{ id: 36, key: 'songNumber', bytes: 1, parser: parseUInt },
|
|
{ id: 37, key: 'songPlaying', bytes: 1, parser: parseBool },
|
|
{ id: 38, key: 'streamPacketCount', bytes: 1, parser: parseUInt },
|
|
{ id: 39, key: 'requestedVelocity', bytes: 2, parser: parseInt },
|
|
{ id: 40, key: 'requestedRadius', bytes: 2, parser: parseInt },
|
|
{ id: 41, key: 'requestedRightVelocity', bytes: 2, parser: parseInt },
|
|
{ id: 42, key: 'requestedLeftVelocity', bytes: 2, parser: parseInt },
|
|
{ id: 43, key: 'encoderCountsLeft', bytes: 2, parser: parseUInt },
|
|
{ id: 44, key: 'encoderCountsRight', bytes: 2, parser: parseUInt },
|
|
{ id: 45, key: 'lightBumper', bytes: 1, parser: parseLightBumper },
|
|
{ id: 46, key: 'lightBumpLeftSignal', bytes: 2, parser: parseUInt },
|
|
{ id: 47, key: 'lightBumpFrontLeftSignal', bytes: 2, parser: parseUInt },
|
|
{ id: 48, key: 'lightBumpCenterLeftSignal', bytes: 2, parser: parseUInt },
|
|
{ id: 49, key: 'lightBumpCenterRightSignal', bytes: 2, parser: parseUInt },
|
|
{ id: 50, key: 'lightBumpFrontRightSignal', bytes: 2, parser: parseUInt },
|
|
{ id: 51, key: 'lightBumpRightSignal', bytes: 2, parser: parseUInt },
|
|
{ id: 52, key: 'infraredCharacterLeft', bytes: 1, parser: parseUInt },
|
|
{ id: 53, key: 'infraredCharacterRight', bytes: 1, parser: parseUInt },
|
|
{ id: 54, key: 'wheelLeftCurrentMa', bytes: 2, parser: parseInt },
|
|
{ id: 55, key: 'wheelRightCurrentMa', bytes: 2, parser: parseInt },
|
|
{ id: 56, key: 'mainBrushCurrentMa', bytes: 2, parser: parseInt },
|
|
{ id: 57, key: 'sideBrushCurrentMa', bytes: 2, parser: parseInt },
|
|
{ id: 58, key: 'stasis', bytes: 1, parser: parseBool },
|
|
];
|
|
|
|
const GROUP100_TOTAL = GROUP100_LAYOUT.reduce((sum, spec) => sum + spec.bytes, 0);
|
|
|
|
const TOP_LEVEL_PACKETS = {
|
|
100: GROUP100_TOTAL,
|
|
21: 1,
|
|
34: 1,
|
|
};
|
|
|
|
function parseSensorFrame(base64Data) {
|
|
if (!base64Data) return null;
|
|
const buf = Buffer.from(base64Data, 'base64');
|
|
if (buf.length < 4 || buf[0] !== HEADER) {
|
|
return null;
|
|
}
|
|
const nBytes = buf[1];
|
|
if (buf.length < nBytes + 3) {
|
|
return null;
|
|
}
|
|
const payload = buf.slice(2, 2 + nBytes);
|
|
const checksum = buf[2 + nBytes];
|
|
if (!validateChecksum(buf.slice(0, 2 + nBytes + 1), checksum)) {
|
|
return null;
|
|
}
|
|
const decoded = {};
|
|
let offset = 0;
|
|
while (offset < payload.length) {
|
|
const packetId = payload[offset++];
|
|
const size = TOP_LEVEL_PACKETS[packetId];
|
|
if (!size || offset + size > payload.length) {
|
|
return null;
|
|
}
|
|
const segment = payload.slice(offset, offset + size);
|
|
offset += size;
|
|
if (packetId === 100) {
|
|
Object.assign(decoded, decodeGroup100(segment));
|
|
} else if (packetId === 21 && decoded.chargingState == null) {
|
|
decoded.chargingState = parseChargingState(segment);
|
|
} else if (packetId === 34 && decoded.chargingSources == null) {
|
|
decoded.chargingSources = parseChargeSources(segment);
|
|
}
|
|
}
|
|
return decoded;
|
|
}
|
|
|
|
function decodeGroup100(buf) {
|
|
if (buf.length !== GROUP100_TOTAL) {
|
|
return {};
|
|
}
|
|
const values = {};
|
|
let offset = 0;
|
|
for (const spec of GROUP100_LAYOUT) {
|
|
const slice = buf.slice(offset, offset + spec.bytes);
|
|
offset += spec.bytes;
|
|
try {
|
|
values[spec.key] = spec.parser ? spec.parser(slice) : parseUInt(slice);
|
|
} catch (err) {
|
|
values[spec.key] = null;
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
|
|
function parseBool(buf) {
|
|
return Boolean(buf[0]);
|
|
}
|
|
|
|
function parseUInt(buf) {
|
|
return buf.readUIntBE(0, buf.length);
|
|
}
|
|
|
|
function parseInt(buf) {
|
|
return buf.readIntBE(0, buf.length);
|
|
}
|
|
|
|
function parseBumps(buf) {
|
|
const value = buf[0];
|
|
return {
|
|
bumpRight: Boolean(value & 0x01),
|
|
bumpLeft: Boolean(value & 0x02),
|
|
wheelDropRight: Boolean(value & 0x04),
|
|
wheelDropLeft: Boolean(value & 0x08),
|
|
};
|
|
}
|
|
|
|
function parseWheelCurrents(buf) {
|
|
const value = buf[0];
|
|
return {
|
|
sideBrush: Boolean(value & 0x01),
|
|
mainBrush: Boolean(value & 0x04),
|
|
rightWheel: Boolean(value & 0x08),
|
|
leftWheel: Boolean(value & 0x10),
|
|
};
|
|
}
|
|
|
|
const BUTTON_LABELS = ['clean', 'spot', 'dock', 'minute', 'hour', 'day', 'schedule', 'clock'];
|
|
function parseButtons(buf) {
|
|
const v = buf[0];
|
|
const result = {};
|
|
BUTTON_LABELS.forEach((label, idx) => {
|
|
result[label] = Boolean(v & (1 << idx));
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function parseChargingState(buf) {
|
|
const code = buf[0];
|
|
return {
|
|
code,
|
|
label: CHARGING_STATE[code] || 'unknown',
|
|
};
|
|
}
|
|
|
|
function parseChargeSources(buf) {
|
|
const value = buf[0];
|
|
return {
|
|
internalCharger: Boolean(value & 0x01),
|
|
homeBase: Boolean(value & 0x02),
|
|
raw: value,
|
|
};
|
|
}
|
|
|
|
function parseOiMode(buf) {
|
|
const code = buf[0];
|
|
return {
|
|
code,
|
|
label: OI_MODES[code] || 'unknown',
|
|
};
|
|
}
|
|
|
|
const LIGHT_BUMPER_LABELS = ['left', 'frontLeft', 'centerLeft', 'centerRight', 'frontRight', 'right'];
|
|
function parseLightBumper(buf) {
|
|
const value = buf[0];
|
|
const obj = {};
|
|
LIGHT_BUMPER_LABELS.forEach((label, idx) => {
|
|
obj[label] = Boolean(value & (1 << idx));
|
|
});
|
|
return obj;
|
|
}
|
|
|
|
function validateChecksum(frame, checksum) {
|
|
let sum = 0;
|
|
for (const byte of frame) {
|
|
sum = (sum + byte) & 0xff;
|
|
}
|
|
return (sum & 0xff) === 0;
|
|
}
|
|
|
|
module.exports = {
|
|
parseSensorFrame,
|
|
CHARGING_STATE,
|
|
};
|