"Initial_commit"
This commit is contained in:
335
services/serialService.ts
Normal file
335
services/serialService.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
|
||||
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();
|
||||
Reference in New Issue
Block a user