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 = 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 { 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 { 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 { await this.writeRegisterRaw(0x00, new Uint8Array([0x56, 0x78])); } public async enterFactoryModeWrite(): Promise { await this.writeRegisterRaw(0x01, new Uint8Array([0x28, 0x28])); } public async exitFactoryMode(): Promise { await this.writeRegisterRaw(0x01, new Uint8Array([0x00, 0x00])); } public async sendCommand(command: number, data: Uint8Array = new Uint8Array(0), mode: number = READ_CMD): Promise { const result = this.transportLock.then(() => this.executeCommandUnsafe(command, data, mode)); this.transportLock = result.then(() => {}).catch(() => {}); return result; } public async sendRaw(data: Uint8Array): Promise { 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 { 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 { 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 { return await this.sendCommand(reg, new Uint8Array(0), READ_CMD); } public async writeRegister(reg: number, value: number): Promise { const data = new Uint8Array([(value >> 8) & 0xFF, value & 0xFF]); await this.sendCommand(reg, data, WRITE_CMD); } private async writeRegisterRaw(reg: number, data: Uint8Array): Promise { await this.sendCommand(reg, data, WRITE_CMD); } public async writeRegisterBytes(reg: number, data: Uint8Array): Promise { await this.sendCommand(reg, data, WRITE_CMD); } private async executeCommandUnsafe(command: number, data: Uint8Array, mode: number): Promise { 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 { 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 { 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();