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

336 lines
11 KiB
TypeScript

import { JBDProtocol, READ_CMD, WRITE_CMD, CMD_ENTER_FACTORY, CMD_EXIT_FACTORY } from './jbdProtocol';
type LogType = 'tx' | 'rx' | 'info' | 'error';
type LogCallback = (type: LogType, message: string) => void;
export class SerialService {
private port: any | null = null;
private reader: ReadableStreamDefaultReader | null = null;
private transportLock: Promise<void> = Promise.resolve();
private logCallback: LogCallback | null = null;
public setLogCallback(callback: LogCallback | null) {
this.logCallback = callback;
}
public log(type: LogType, message: string) {
if (this.logCallback) {
this.logCallback(type, message);
}
}
private toHex(data: Uint8Array): string {
return Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
}
public async connect(): Promise<void> {
if (!('serial' in navigator)) {
throw new Error('Web Serial API not supported');
}
if (this.port && this.port.writable) {
return;
}
// @ts-ignore
const port = await navigator.serial.requestPort();
try {
await port.open({ baudRate: 9600 });
this.port = port;
this.log('info', 'Serial port connected');
} catch (e: any) {
console.error("Serial port open failed:", e);
this.log('error', 'Connection failed: ' + e.message);
throw e;
}
}
public async disconnect(): Promise<void> {
try {
await this.transportLock;
} catch (e) {
console.warn("Transport lock error during disconnect:", e);
}
if (this.reader) {
try {
await this.reader.cancel();
} catch (e) {
console.warn("Reader cancel failed:", e);
}
try {
this.reader.releaseLock();
} catch (e) {
console.warn("Reader releaseLock failed:", e);
}
this.reader = null;
}
if (this.port) {
try {
await this.port.close();
this.log('info', 'Serial port disconnected');
} catch (e) {
console.warn("Port close failed:", e);
}
}
this.port = null;
}
public async enterFactoryModeRead(): Promise<void> {
await this.writeRegisterRaw(0x00, new Uint8Array([0x56, 0x78]));
}
public async enterFactoryModeWrite(): Promise<void> {
await this.writeRegisterRaw(0x01, new Uint8Array([0x28, 0x28]));
}
public async exitFactoryMode(): Promise<void> {
await this.writeRegisterRaw(0x01, new Uint8Array([0x00, 0x00]));
}
public async sendCommand(command: number, data: Uint8Array = new Uint8Array(0), mode: number = READ_CMD): Promise<Uint8Array> {
const result = this.transportLock.then(() => this.executeCommandUnsafe(command, data, mode));
this.transportLock = result.then(() => {}).catch(() => {});
return result;
}
public async sendRaw(data: Uint8Array): Promise<void> {
const result = this.transportLock.then(async () => {
if (!this.port || !this.port.writable) throw new Error('Port not open');
this.log('tx', this.toHex(data));
const writer = this.port.writable.getWriter();
try {
await writer.write(data);
} finally {
writer.releaseLock();
}
});
this.transportLock = result.then(() => {}).catch(() => {});
return result;
}
public async readRaw(): Promise<Uint8Array> {
return this.transportLock.then(async () => {
if (!this.port || !this.port.readable) throw new Error('Port not readable');
const reader = this.port.readable.getReader();
this.reader = reader;
try {
const timeoutMs = 2000;
const timerId = setTimeout(() => reader.cancel(), timeoutMs);
let result;
try {
result = await reader.read();
} catch (e) {
if (e instanceof Error && e.message.includes('abort')) {
} else {
throw e;
}
return new Uint8Array(0);
} finally {
clearTimeout(timerId);
}
const { value, done } = result;
if (done && !value) {
return new Uint8Array(0);
}
if (value) {
this.log('rx', this.toHex(value));
return value;
}
return new Uint8Array(0);
} finally {
this.reader = null;
reader.releaseLock();
}
});
}
public async readRegister(reg: number): Promise<number> {
const data = await this.sendCommand(reg, new Uint8Array(0), READ_CMD);
if (data.length < 2) return 0;
return (data[0] << 8) | data[1];
}
public async readRegisterBytes(reg: number): Promise<Uint8Array> {
return await this.sendCommand(reg, new Uint8Array(0), READ_CMD);
}
public async writeRegister(reg: number, value: number): Promise<void> {
const data = new Uint8Array([(value >> 8) & 0xFF, value & 0xFF]);
await this.sendCommand(reg, data, WRITE_CMD);
}
private async writeRegisterRaw(reg: number, data: Uint8Array): Promise<void> {
await this.sendCommand(reg, data, WRITE_CMD);
}
public async writeRegisterBytes(reg: number, data: Uint8Array): Promise<void> {
await this.sendCommand(reg, data, WRITE_CMD);
}
private async executeCommandUnsafe(command: number, data: Uint8Array, mode: number): Promise<Uint8Array> {
if (!this.port || !this.port.writable) throw new Error('Port not open');
const packet = JBDProtocol.createPacket(command, data, mode);
// LOG TX
this.log('tx', this.toHex(packet));
const writer = this.port.writable.getWriter();
try {
await writer.write(packet);
} finally {
writer.releaseLock();
}
return await this.readResponse();
}
private async readResponse(): Promise<Uint8Array> {
if (!this.port || !this.port.readable) throw new Error('Port not readable');
const reader = this.port.readable.getReader();
this.reader = reader;
try {
let buffer: number[] = [];
const timeoutMs = 1500;
const startTime = Date.now();
while (true) {
const elapsedTime = Date.now() - startTime;
const remainingTime = timeoutMs - elapsedTime;
if (remainingTime <= 0) throw new Error('Read timeout');
const timerId = setTimeout(() => reader.cancel(), remainingTime);
let result;
try {
result = await reader.read();
} catch (e) {
if (Date.now() - startTime >= timeoutMs) {
throw new Error('Read timeout');
}
throw e;
} finally {
clearTimeout(timerId);
}
const { value, done } = result;
if (done) {
if (Date.now() - startTime >= timeoutMs) {
throw new Error('Read timeout');
}
throw new Error('Stream closed');
}
if (value) {
this.log('rx', this.toHex(value));
for(let byte of value) buffer.push(byte);
}
// Process Buffer
while (true) {
const startIndex = buffer.indexOf(0xDD);
if (startIndex === -1) {
// No start byte, clear useless buffer but keep potential partials if needed?
// Actually safer to just clear if no DD found at all and buffer is huge.
if (buffer.length > 200) buffer.length = 0;
break;
}
// Clean up bytes before start index
if (startIndex > 0) {
buffer.splice(0, startIndex);
continue; // Re-evaluate
}
// We have DD at 0.
if (buffer.length < 4) break; // Need more data for basic header
// Standard logic: length is at index 3
const declaredLen = buffer[3];
const standardPacketLen = declaredLen + 7;
let packetFound = false;
let packetLen = 0;
// Strategy 1: Trust declared length if it makes sense and ends with 0x77
if (declaredLen > 0 && buffer.length >= standardPacketLen) {
if (buffer[standardPacketLen - 1] === 0x77) {
packetLen = standardPacketLen;
packetFound = true;
}
}
// Strategy 2: If declared length is 0 (anomaly) or Strategy 1 failed (bad length byte),
// scan for the next 0x77 to find the packet boundary.
if (!packetFound) {
// Start searching for 0x77 after the header (index 3)
// Minimum packet size is 7 bytes (DD Cmd Status Len ChkH ChkL 77)
for (let i = 6; i < buffer.length; i++) {
if (buffer[i] === 0x77) {
packetLen = i + 1;
// Optimization: Don't just take the first 77 if it's too short to be valid?
// But we just want to try parsing it.
packetFound = true;
break;
}
}
}
if (packetFound) {
const packet = new Uint8Array(buffer.slice(0, packetLen));
const { payload, error } = JBDProtocol.parseResponse(packet);
if (payload) {
// Success! Remove this packet from buffer and return payload
// (NOTE: This returns the FIRST valid packet found.
// If multiple are queued, subsequent calls will handle them?
// No, executeCommandUnsafe expects ONE return.
// But we might have read garbage before.)
return payload;
} else {
// Checksum failed or structure invalid
this.log('error', `Packet Err: ${error}`);
// If scanning found a 77 but checksum failed, it might be a coincidence data byte 0x77.
// We should probably consume the buffer up to that point?
// Or just consume the start byte (0xDD) and retry?
// Safer to consume just the start byte to try finding another sync.
buffer.shift();
continue;
}
} else {
// Packet incomplete, wait for more data
break;
}
}
}
} finally {
this.reader = null;
reader.releaseLock();
}
}
public async toggleMosfet(charge: boolean, discharge: boolean): Promise<void> {
const cBit = charge ? 0 : 1;
const dBit = discharge ? 0 : 1;
const controlByte = cBit | (dBit << 1);
await this.sendCommand(0xE1, new Uint8Array([0x00, controlByte]), WRITE_CMD);
}
}
export const serialService = new SerialService();