Files
JBD_BMS_Tools/services/jbdProtocol.ts
2025-12-19 00:55:55 +09:00

251 lines
8.6 KiB
TypeScript

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<ntcCount; i++) {
// Safety check for buffer overflow
if (offset + 1 >= 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),
};
}
}