import { BMSBasicInfo, BMSCellInfo, ProtectionStatus } from '../types'; export const START_BYTE = 0xDD; export const READ_CMD = 0xA5; export const WRITE_CMD = 0x5A; export const END_BYTE = 0x77; export const CMD_BASIC_INFO = 0x03; export const CMD_CELL_INFO = 0x04; export const CMD_HW_VERSION = 0x05; export const CMD_MOSFET_CTRL = 0xE1; export const CMD_ENTER_FACTORY = 0x00; export const CMD_EXIT_FACTORY = 0x01; // EEPROM Registers export const REG_COVP = 0x24; export const REG_COVP_REL = 0x25; export const REG_CUVP = 0x26; export const REG_CUVP_REL = 0x27; export const REG_POVP = 0x20; export const REG_POVP_REL = 0x21; export const REG_PUVP = 0x22; export const REG_PUVP_REL = 0x23; export const REG_CHG_OT = 0x18; export const REG_CHG_OT_REL = 0x19; export const REG_CHG_UT = 0x1A; export const REG_CHG_UT_REL = 0x1B; export const REG_DSG_OT = 0x1C; export const REG_DSG_OT_REL = 0x1D; export const REG_DSG_UT = 0x1E; export const REG_DSG_UT_REL = 0x1F; export const REG_CELL_V_DELAYS = 0x3D; export const REG_PACK_V_DELAYS = 0x3C; export const REG_CHG_T_DELAYS = 0x3A; export const REG_DSG_T_DELAYS = 0x3B; export const REG_CHG_OC_DELAYS = 0x3E; export const REG_DSG_OC_DELAYS = 0x3F; export const REG_COVP_HIGH = 0x36; export const REG_CUVP_HIGH = 0x37; export const REG_FUNC_CONFIG = 0x2D; export const REG_NTC_CONFIG = 0x2E; export const REG_BAL_START = 0x2A; export const REG_BAL_WINDOW = 0x2B; export const REG_DESIGN_CAP = 0x10; export const REG_CYCLE_CAP = 0x11; export const REG_DSG_RATE = 0x14; export const REG_CAP_100 = 0x12; export const REG_CAP_80 = 0x32; export const REG_CAP_60 = 0x33; export const REG_CAP_40 = 0x34; export const REG_CAP_20 = 0x35; export const REG_CAP_0 = 0x13; export const REG_FET_CTRL = 0x30; export const REG_LED_TIMER = 0x31; export const REG_SHUNT_RES = 0x2C; export const REG_CELL_CNT = 0x2F; export const REG_CYCLE_CNT = 0x17; export const REG_SERIAL_NUM = 0x16; export const REG_MFG_DATE = 0x15; export const REG_MFG_NAME = 0xA0; export const REG_DEVICE_NAME = 0xA1; export const REG_BARCODE = 0xA2; export class JBDProtocol { public static calculateChecksum(payload: Uint8Array): number { let sum = 0; for (let i = 0; i < payload.length; i++) { sum += payload[i]; } return (0x10000 - sum) & 0xFFFF; } public static createPacket(command: number, data: Uint8Array = new Uint8Array(0), mode: number = READ_CMD): Uint8Array { const payloadLength = data.length; const packet = new Uint8Array(7 + payloadLength); packet[0] = START_BYTE; packet[1] = mode; packet[2] = command; packet[3] = payloadLength; packet.set(data, 4); const checksumPayload = packet.slice(2, 4 + payloadLength); const checksum = this.calculateChecksum(checksumPayload); packet[4 + payloadLength] = (checksum >> 8) & 0xFF; packet[4 + payloadLength + 1] = checksum & 0xFF; packet[4 + payloadLength + 2] = END_BYTE; return packet; } public static parseResponse(data: Uint8Array): { payload: Uint8Array | null, error?: string } { if (data.length < 7) return { payload: null, error: "Too short" }; if (data[0] !== START_BYTE) return { payload: null, error: "Invalid Start Byte" }; if (data[data.length - 1] !== END_BYTE) return { payload: null, error: "Invalid End Byte" }; // Standard length check: data[3] should match internal payload length // But some BMS versions send 0x00 at data[3] even with data present. // So we rely on the ACTUAL packet size passed to us to determine payload. // Structure: [DD] [Mode] [Cmd] [Len] [DATA...] [ChkH] [ChkL] [77] // Indices: 0 1 2 3 4... N-3 N-2 N-1 // Checksum covers from Cmd (index 2) to end of Data (index N-3 inclusive) // Checksum values are at N-3 (High) and N-2 (Low) const endOfDataIndex = data.length - 3; const checksumPayload = data.slice(2, endOfDataIndex); const calculatedChecksum = this.calculateChecksum(checksumPayload); const receivedChecksum = (data[data.length - 3] << 8) | data[data.length - 2]; if (calculatedChecksum !== receivedChecksum && receivedChecksum != 0) { // NOTE: We return the payload even on checksum error for debugging purposes if needed, // or strictly enforce it. // For now, strict enforcement but detailed error. // However, seeing the user log, if checksum logic on hardware is weird, we might need lax mode. // Based on the log provided: // Data: 03 00 00 0A ... A5 // User Checksum: 00 00 // If the hardware puts 00 00 checksum, it might be ignoring checksum. if (receivedChecksum === 0 && calculatedChecksum !== 0) { // Some clones send 0 checksum? Let's treat it as a warning but allow it? // No, let's report error. } return { payload: null, error: `CS Mismatch (Calc:${calculatedChecksum.toString(16).toUpperCase()} Recv:${receivedChecksum.toString(16).toUpperCase()})` }; } // Payload is from index 4 to endOfDataIndex return { payload: data.slice(4, endOfDataIndex) }; } public static parseString(payload: Uint8Array): string { if (payload.length === 0) return ""; const len = payload[0]; if (payload.length < 1 + len) return ""; const strBytes = payload.slice(1, 1 + len); return new TextDecoder().decode(strBytes); } public static encodeString(str: string, maxLen: number = 31): Uint8Array { const encoder = new TextEncoder(); const bytes = encoder.encode(str); const len = Math.min(bytes.length, maxLen); const result = new Uint8Array(len + 1); result[0] = len; result.set(bytes.slice(0, len), 1); return result; } public static parseDate(val: number): string { const year = (val >> 9) + 2000; const month = (val >> 5) & 0x0F; const day = val & 0x1F; return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; } public static encodeDate(dateStr: string): number { const d = new Date(dateStr); const year = d.getFullYear(); const month = d.getMonth() + 1; const day = d.getDate(); return ((year - 2000) << 9) | ((month & 0x0F) << 5) | (day & 0x1F); } public static parseBasicInfo(payload: Uint8Array): BMSBasicInfo { const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength); const packVoltage = view.getUint16(0, false) / 100; const current = view.getInt16(2, false) / 100; const remainingCapacity = view.getUint16(4, false) / 100; const fullCapacity = view.getUint16(6, false) / 100; const cycleCount = view.getUint16(8, false); const productionDateInt = view.getUint16(10, false); const productionDate = this.parseDate(productionDateInt); const balanceStatus = view.getUint16(12, false); const balanceStatusHigh = view.getUint16(14, false); const fullBalance = balanceStatus | (balanceStatusHigh << 16); const protectionStatusRaw = view.getUint16(16, false); const protectionStatus = this.parseProtectionStatus(protectionStatusRaw); const version = view.getUint8(18); const rsoc = view.getUint8(19); const mosfetRaw = view.getUint8(20); const mosfetStatus = { charge: (mosfetRaw & 1) === 1, discharge: ((mosfetRaw >> 1) & 1) === 1 }; const ntcCount = view.getUint8(22); const ntcTemps: number[] = []; let offset = 23; for(let i=0; i= payload.byteLength) break; const rawTemp = view.getUint16(offset, false); ntcTemps.push((rawTemp - 2731) / 10); offset += 2; } return { packVoltage, current, remainingCapacity, fullCapacity, cycleCount, productionDate, balanceStatus: fullBalance, protectionStatus, version, rsoc, mosfetStatus, ntcCount, ntcTemps }; } public static parseCellInfo(payload: Uint8Array): BMSCellInfo { const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength); const voltages: number[] = []; for (let i = 0; i < payload.length; i += 2) { if (i + 1 < payload.length) { voltages.push(view.getUint16(i, false) / 1000); } } return { voltages }; } private static parseProtectionStatus(raw: number): ProtectionStatus { return { covp: !!(raw & 1), cuvp: !!((raw >> 1) & 1), povp: !!((raw >> 2) & 1), puvp: !!((raw >> 3) & 1), chgot: !!((raw >> 4) & 1), chgut: !!((raw >> 5) & 1), dsgot: !!((raw >> 6) & 1), dsgut: !!((raw >> 7) & 1), chgoc: !!((raw >> 8) & 1), dsgoc: !!((raw >> 9) & 1), sc: !!((raw >> 10) & 1), afe: !!((raw >> 11) & 1), }; } }