251 lines
8.6 KiB
TypeScript
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),
|
|
};
|
|
}
|
|
}
|