492 lines
17 KiB
TypeScript
492 lines
17 KiB
TypeScript
|
|
import {
|
||
|
|
BLE_SERVICE_UUID,
|
||
|
|
BLE_CHAR_FLASHING_UUID,
|
||
|
|
BLE_CHAR_SERIAL_UUID,
|
||
|
|
CMD_INIT, CMD_DATA, CMD_END, CMD_ACK, CMD_ERR,
|
||
|
|
CHUNK_SIZE, BLE_TIMEOUT_MS, END_TIMEOUT_MS, END_ACK_INDEX, MAX_RETRIES,
|
||
|
|
type DeployProgress, type BLEACKResponse
|
||
|
|
} from '$types/deployer';
|
||
|
|
|
||
|
|
let _ackNotificationCount = 0;
|
||
|
|
function logAck(tag: string, msg: string) {
|
||
|
|
console.log(`[BLE-ACK#${++_ackNotificationCount}] ${tag}: ${msg}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
export class BLEHardwareDeployer {
|
||
|
|
private server: BluetoothRemoteGATTServer | null = null;
|
||
|
|
private service: BluetoothRemoteGATTService | null = null;
|
||
|
|
private flashingChar: BluetoothRemoteGATTCharacteristic | null = null;
|
||
|
|
private serialChar: BluetoothRemoteGATTCharacteristic | null = null;
|
||
|
|
private device: BluetoothDevice | null = null;
|
||
|
|
|
||
|
|
private onSerialData: ((text: string) => void) | null = null;
|
||
|
|
|
||
|
|
/** Map of expected ACK index → resolver pair. Supports concurrent waits. */
|
||
|
|
private ackResolvers = new Map<number, {
|
||
|
|
resolve: (value: BLEACKResponse) => void;
|
||
|
|
reject: (reason: Error) => void;
|
||
|
|
timer: ReturnType<typeof setTimeout>;
|
||
|
|
}>();
|
||
|
|
|
||
|
|
/** ACKs that arrived before waitForAck was called, keyed by index. */
|
||
|
|
private pendingAcks = new Map<number, BLEACKResponse>();
|
||
|
|
|
||
|
|
private _onDisconnected: (() => void) | null = null;
|
||
|
|
|
||
|
|
/** Bound handler refs so we can removeEventListener on cleanup/re-pair. */
|
||
|
|
private _onDeviceDisconnect: (() => void) | null = null;
|
||
|
|
private _onFlashingNotification: ((event: Event) => void) | null = null;
|
||
|
|
private _onSerialNotification: ((event: Event) => void) | null = null;
|
||
|
|
|
||
|
|
checkSupport(): boolean {
|
||
|
|
return typeof navigator !== 'undefined' && 'bluetooth' in navigator;
|
||
|
|
}
|
||
|
|
|
||
|
|
get isConnected(): boolean {
|
||
|
|
return this.server?.connected ?? false;
|
||
|
|
}
|
||
|
|
|
||
|
|
private withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
||
|
|
return Promise.race([
|
||
|
|
promise,
|
||
|
|
new Promise<T>((_, reject) => {
|
||
|
|
setTimeout(() => reject(new Error(`Timeout: ${label} (${ms}ms)`)), ms);
|
||
|
|
})
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
async pair(): Promise<void> {
|
||
|
|
if (!this.checkSupport()) {
|
||
|
|
throw new Error('Web Bluetooth tidak didukung di browser ini. Gunakan Chrome Android.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clean up any previous session's listeners before re-pairing
|
||
|
|
this.cleanup();
|
||
|
|
|
||
|
|
console.log('[BLE] step 1: requestDevice...');
|
||
|
|
this.device = await navigator.bluetooth.requestDevice({
|
||
|
|
filters: [{ namePrefix: 'Velxio' }],
|
||
|
|
optionalServices: [BLE_SERVICE_UUID]
|
||
|
|
});
|
||
|
|
console.log('[BLE] step 1: device selected:', this.device.name);
|
||
|
|
|
||
|
|
this._onDeviceDisconnect = () => {
|
||
|
|
console.log('[BLE] gattserverdisconnected event');
|
||
|
|
this._onDisconnected?.();
|
||
|
|
this.cleanup();
|
||
|
|
};
|
||
|
|
this.device.addEventListener('gattserverdisconnected', this._onDeviceDisconnect);
|
||
|
|
|
||
|
|
console.log('[BLE] step 2: gatt.connect...');
|
||
|
|
this.server = await this.withTimeout(
|
||
|
|
this.device.gatt!.connect(),
|
||
|
|
10000,
|
||
|
|
'gatt.connect'
|
||
|
|
);
|
||
|
|
console.log('[BLE] step 2: connected, mtu=' + this.server.mtu);
|
||
|
|
|
||
|
|
console.log('[BLE] step 3: getPrimaryService...');
|
||
|
|
this.service = await this.withTimeout(
|
||
|
|
this.server.getPrimaryService(BLE_SERVICE_UUID),
|
||
|
|
5000,
|
||
|
|
'getPrimaryService'
|
||
|
|
);
|
||
|
|
console.log('[BLE] step 3: service found');
|
||
|
|
|
||
|
|
console.log('[BLE] step 4: getCharacteristic flashing...');
|
||
|
|
this.flashingChar = await this.withTimeout(
|
||
|
|
this.service.getCharacteristic(BLE_CHAR_FLASHING_UUID),
|
||
|
|
5000,
|
||
|
|
'getCharacteristic(flashing)'
|
||
|
|
);
|
||
|
|
console.log('[BLE] step 4: flashing char found');
|
||
|
|
|
||
|
|
console.log('[BLE] step 5: getCharacteristic serial...');
|
||
|
|
this.serialChar = await this.withTimeout(
|
||
|
|
this.service.getCharacteristic(BLE_CHAR_SERIAL_UUID),
|
||
|
|
5000,
|
||
|
|
'getCharacteristic(serial)'
|
||
|
|
);
|
||
|
|
console.log('[BLE] step 5: serial char found');
|
||
|
|
|
||
|
|
console.log('[BLE] step 6: requestMTU(255)...');
|
||
|
|
try {
|
||
|
|
await this.withTimeout(
|
||
|
|
this.server.requestMTU(255),
|
||
|
|
3000,
|
||
|
|
'requestMTU'
|
||
|
|
);
|
||
|
|
console.log('[BLE] step 6: MTU negotiated, now ' + this.server.mtu);
|
||
|
|
} catch (e) {
|
||
|
|
console.log('[BLE] step 6: MTU fallback, mtu=' + this.server.mtu, e);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('[BLE] step 7: startNotifications...');
|
||
|
|
await this.withTimeout(
|
||
|
|
this.flashingChar.startNotifications(),
|
||
|
|
5000,
|
||
|
|
'startNotifications'
|
||
|
|
);
|
||
|
|
console.log('[BLE] step 7: notifications subscribed');
|
||
|
|
|
||
|
|
this._onFlashingNotification = (event: Event) => {
|
||
|
|
const target = event.target as BluetoothRemoteGATTCharacteristic;
|
||
|
|
const dv = target.value!;
|
||
|
|
const value = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
|
||
|
|
this.handleFlashingNotification(value);
|
||
|
|
};
|
||
|
|
this.flashingChar.addEventListener('characteristicvaluechanged', this._onFlashingNotification);
|
||
|
|
console.log('[BLE] pair() completed successfully');
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleFlashingNotification(value: Uint8Array) {
|
||
|
|
const cmd = value[0];
|
||
|
|
const index = value[1] | (value[2] << 8);
|
||
|
|
const cmdName = cmd === CMD_ACK ? 'ACK' : cmd === CMD_ERR ? 'ERR' : 'UNKNOWN';
|
||
|
|
logAck(cmdName, `index=${index} len=${value.length}`);
|
||
|
|
|
||
|
|
if (cmd === CMD_ACK) {
|
||
|
|
const ack: BLEACKResponse = { command: cmd, index, status: 'OK' };
|
||
|
|
|
||
|
|
// Check if someone is waiting for this exact index
|
||
|
|
const pending = this.ackResolvers.get(index);
|
||
|
|
if (pending) {
|
||
|
|
logAck(cmdName, `Found resolver for index=${index} — resolving`);
|
||
|
|
this.ackResolvers.delete(index);
|
||
|
|
clearTimeout(pending.timer);
|
||
|
|
pending.resolve(ack);
|
||
|
|
} else if (this.ackResolvers.has(0xFFFF) && index !== 0xFFFF) {
|
||
|
|
// No one waiting for this specific index right now,
|
||
|
|
// but someone IS waiting for END (0xFFFF) — don't
|
||
|
|
// let a stale chunk ACK resolve it.
|
||
|
|
logAck(cmdName, `index=${index} no resolver, END waitForAck(0xFFFF) active — dropping stale ACK`);
|
||
|
|
} else {
|
||
|
|
// Cache for a future waitForAck call
|
||
|
|
logAck(cmdName, `index=${index} no resolver — caching as pendingAck`);
|
||
|
|
this.pendingAcks.set(index, ack);
|
||
|
|
}
|
||
|
|
} else if (cmd === CMD_ERR) {
|
||
|
|
const msgLen = value[3];
|
||
|
|
const msg = msgLen > 0
|
||
|
|
? new TextDecoder().decode(value.slice(4, 4 + msgLen))
|
||
|
|
: 'Unknown error';
|
||
|
|
logAck(cmdName, `index=${index} ERR: ${msg}`);
|
||
|
|
|
||
|
|
const pending = this.ackResolvers.get(index);
|
||
|
|
if (pending) {
|
||
|
|
logAck(cmdName, `Found rejecter for index=${index} — rejecting`);
|
||
|
|
this.ackResolvers.delete(index);
|
||
|
|
clearTimeout(pending.timer);
|
||
|
|
pending.reject(new Error(`ERR: ${msg}`));
|
||
|
|
} else {
|
||
|
|
logAck(cmdName, `index=${index} no rejecter — ERR lost: ${msg}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async deployHex(hexContent: string, onProgress: (p: DeployProgress) => void): Promise<void> {
|
||
|
|
if (!this.flashingChar) throw new Error('Belum terhubung ke perangkat');
|
||
|
|
|
||
|
|
const binaryData = this.hexToBinary(hexContent);
|
||
|
|
const totalChunks = Math.ceil(binaryData.length / CHUNK_SIZE);
|
||
|
|
const totalCRC = this.crc32(binaryData);
|
||
|
|
|
||
|
|
console.log(`[BLE] deployHex: ${binaryData.length} bytes, ${totalChunks} chunks, CRC=0x${totalCRC.toString(16)}`);
|
||
|
|
|
||
|
|
onProgress({ state: 'transferring', message: 'Mengirim INIT...', totalChunks, completedChunks: 0 });
|
||
|
|
console.log('[BLE] D: sending INIT...');
|
||
|
|
await this.sendCommand(CMD_INIT, 0, this.uint32ToBytes(totalCRC));
|
||
|
|
console.log('[BLE] D: INIT done');
|
||
|
|
|
||
|
|
for (let i = 0; i < totalChunks; i++) {
|
||
|
|
const start = i * CHUNK_SIZE;
|
||
|
|
const end = Math.min(start + CHUNK_SIZE, binaryData.length);
|
||
|
|
const chunk = binaryData.slice(start, end);
|
||
|
|
const chunkCRC = this.crc32(chunk);
|
||
|
|
|
||
|
|
await this.sendCommandWithRetry(CMD_DATA, i, chunk, chunkCRC);
|
||
|
|
|
||
|
|
onProgress({
|
||
|
|
state: 'transferring',
|
||
|
|
message: `Mengirim chunk ${i + 1}/${totalChunks}`,
|
||
|
|
totalChunks,
|
||
|
|
completedChunks: i + 1
|
||
|
|
});
|
||
|
|
}
|
||
|
|
console.log('[BLE] D: all DATA chunks done');
|
||
|
|
|
||
|
|
onProgress({ state: 'flashing', message: 'Verifikasi & flashing...', totalChunks, completedChunks: totalChunks });
|
||
|
|
console.log('[BLE] D: END starting (30s timeout)...');
|
||
|
|
const startTime = Date.now();
|
||
|
|
try {
|
||
|
|
await this.sendCommand(CMD_END, END_ACK_INDEX, this.uint32ToBytes(totalCRC), END_TIMEOUT_MS);
|
||
|
|
const elapsed = Date.now() - startTime;
|
||
|
|
console.log(`[BLE] D: END ACK received after ${elapsed}ms, flash success!`);
|
||
|
|
} catch (err) {
|
||
|
|
const elapsed = Date.now() - startTime;
|
||
|
|
console.log(`[BLE] D: END FAILED after ${elapsed}ms:`, err);
|
||
|
|
throw err;
|
||
|
|
}
|
||
|
|
|
||
|
|
onProgress({ state: 'serial_bridge', message: 'Flash berhasil! Membuka Serial Monitor...', totalChunks, completedChunks: totalChunks });
|
||
|
|
}
|
||
|
|
|
||
|
|
private async sendCommand(cmd: number, index: number, data: Uint8Array, timeoutMs?: number): Promise<void> {
|
||
|
|
const cmdName = cmd === CMD_INIT ? 'INIT' : cmd === CMD_DATA ? 'DATA' : cmd === CMD_END ? 'END' : `0x${cmd.toString(16)}`;
|
||
|
|
const payload = new Uint8Array(4 + data.length + 4);
|
||
|
|
payload[0] = cmd;
|
||
|
|
payload[1] = index & 0xFF;
|
||
|
|
payload[2] = (index >> 8) & 0xFF;
|
||
|
|
payload[3] = data.length;
|
||
|
|
payload.set(data, 4);
|
||
|
|
|
||
|
|
const crc = this.crc32(data);
|
||
|
|
payload.set([
|
||
|
|
crc & 0xFF, (crc >> 8) & 0xFF, (crc >> 16) & 0xFF, (crc >> 24) & 0xFF
|
||
|
|
], 4 + data.length);
|
||
|
|
|
||
|
|
const ackTimeout = timeoutMs ?? BLE_TIMEOUT_MS;
|
||
|
|
console.log(`[BLE-CMD] ${cmdName} idx=${index}: waitForAck timeout=${ackTimeout}ms start`);
|
||
|
|
const ackPromise = this.waitForAck(index, ackTimeout);
|
||
|
|
try {
|
||
|
|
console.log(`[BLE-CMD] ${cmdName} idx=${index}: writing payload (${payload.length} bytes)...`);
|
||
|
|
await this.withTimeout(
|
||
|
|
this.flashingChar!.writeValueWithResponse(payload),
|
||
|
|
3000,
|
||
|
|
'writeValueWithResponse'
|
||
|
|
);
|
||
|
|
console.log(`[BLE-CMD] ${cmdName} idx=${index}: write done, now waiting for ACK...`);
|
||
|
|
} catch (e) {
|
||
|
|
console.log(`[BLE-CMD] ${cmdName} idx=${index}: write failed, still waiting for ACK`, e);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* END (flash ~8s): Android BLE stack sometimes fails to deliver the
|
||
|
|
* notification through the normal characteristicvaluechanged handler
|
||
|
|
* while a writeValueWithResponse await is still in progress on some
|
||
|
|
* Qualcomm chipsets. Race the ackPromise against a poll loop that
|
||
|
|
* yields to the event loop every 200ms and checks pendingAcks (which
|
||
|
|
* the notification handler fills if the resolver path was missed). */
|
||
|
|
if (cmd === CMD_END) {
|
||
|
|
const deadline = Date.now() + ackTimeout;
|
||
|
|
const pollPromise: Promise<void> = new Promise(resolve => {
|
||
|
|
(async () => {
|
||
|
|
while (Date.now() < deadline) {
|
||
|
|
if (this.pendingAcks.has(index)) {
|
||
|
|
this.pendingAcks.delete(index);
|
||
|
|
const entry = this.ackResolvers.get(index);
|
||
|
|
if (entry) {
|
||
|
|
clearTimeout(entry.timer);
|
||
|
|
this.ackResolvers.delete(index);
|
||
|
|
}
|
||
|
|
console.log(`[BLE-CMD] END idx=${index}: ACK received via poll (pendingAcks)`);
|
||
|
|
resolve();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
await new Promise<void>(r => setTimeout(r, 200));
|
||
|
|
}
|
||
|
|
console.log(`[BLE-CMD] END idx=${index}: poll deadline passed — ackPromise must win`);
|
||
|
|
resolve(); // resolve poll so the race falls through to ackPromise
|
||
|
|
})();
|
||
|
|
});
|
||
|
|
await Promise.race([ackPromise, pollPromise]);
|
||
|
|
console.log(`[BLE-CMD] END idx=${index}: ackPromise or poll resolved`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await ackPromise;
|
||
|
|
console.log(`[BLE-CMD] ${cmdName} idx=${index}: ackPromise resolved OK`);
|
||
|
|
}
|
||
|
|
|
||
|
|
private async sendCommandWithRetry(cmd: number, index: number, data: Uint8Array, chunkCRC: number): Promise<void> {
|
||
|
|
const payload = new Uint8Array(4 + data.length + 4);
|
||
|
|
payload[0] = cmd;
|
||
|
|
payload[1] = index & 0xFF;
|
||
|
|
payload[2] = (index >> 8) & 0xFF;
|
||
|
|
payload[3] = data.length;
|
||
|
|
payload.set(data, 4);
|
||
|
|
payload.set([
|
||
|
|
chunkCRC & 0xFF, (chunkCRC >> 8) & 0xFF, (chunkCRC >> 16) & 0xFF, (chunkCRC >> 24) & 0xFF
|
||
|
|
], 4 + data.length);
|
||
|
|
|
||
|
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||
|
|
try {
|
||
|
|
const ackPromise = this.waitForAck(index);
|
||
|
|
await this.flashingChar!.writeValueWithResponse(payload);
|
||
|
|
await ackPromise;
|
||
|
|
return;
|
||
|
|
} catch (err) {
|
||
|
|
if (attempt === MAX_RETRIES - 1) throw err;
|
||
|
|
await new Promise(r => setTimeout(r, 500));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private waitForAck(expectedIndex: number, timeoutMs: number = BLE_TIMEOUT_MS): Promise<BLEACKResponse> {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
// Check for a cached ACK that arrived before we started waiting
|
||
|
|
const cached = this.pendingAcks.get(expectedIndex);
|
||
|
|
if (cached) {
|
||
|
|
this.pendingAcks.delete(expectedIndex);
|
||
|
|
logAck('WAIT', `pendingAcks hit for index=${expectedIndex} actual=${cached.index} — resolving immediately`);
|
||
|
|
resolve(cached);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
logAck('WAIT', `Registering resolver for index=${expectedIndex} timeout=${timeoutMs}ms`);
|
||
|
|
|
||
|
|
const timer = setTimeout(() => {
|
||
|
|
const entry = this.ackResolvers.get(expectedIndex);
|
||
|
|
if (entry) {
|
||
|
|
this.ackResolvers.delete(expectedIndex);
|
||
|
|
logAck('WAIT', `TIMEOUT after ${timeoutMs}ms for index=${expectedIndex} — rejecting`);
|
||
|
|
reject(new Error(`Timeout menunggu ACK untuk chunk ${expectedIndex}`));
|
||
|
|
} else {
|
||
|
|
logAck('WAIT', `Timeout fired but resolver already consumed for index=${expectedIndex} — ignoring`);
|
||
|
|
}
|
||
|
|
}, timeoutMs);
|
||
|
|
|
||
|
|
this.ackResolvers.set(expectedIndex, { resolve, reject, timer });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async startSerialMonitor(onData: (text: string) => void): Promise<void> {
|
||
|
|
if (!this.serialChar) throw new Error('Belum terhubung ke perangkat');
|
||
|
|
|
||
|
|
this.onSerialData = onData;
|
||
|
|
|
||
|
|
await this.serialChar.startNotifications();
|
||
|
|
this._onSerialNotification = (event: Event) => {
|
||
|
|
const target = event.target as BluetoothRemoteGATTCharacteristic;
|
||
|
|
const dv = target.value!;
|
||
|
|
const value = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
|
||
|
|
const text = new TextDecoder().decode(value);
|
||
|
|
this.onSerialData?.(text);
|
||
|
|
};
|
||
|
|
this.serialChar.addEventListener('characteristicvaluechanged', this._onSerialNotification);
|
||
|
|
}
|
||
|
|
|
||
|
|
async sendSerialInput(text: string): Promise<void> {
|
||
|
|
if (!this.serialChar) throw new Error('Belum terhubung ke perangkat');
|
||
|
|
const encoder = new TextEncoder();
|
||
|
|
await this.serialChar.writeValueWithoutResponse(encoder.encode(text));
|
||
|
|
}
|
||
|
|
|
||
|
|
stopSerialMonitor(): void {
|
||
|
|
if (this.serialChar && this._onSerialNotification) {
|
||
|
|
this.serialChar.removeEventListener('characteristicvaluechanged', this._onSerialNotification);
|
||
|
|
this.serialChar.stopNotifications();
|
||
|
|
}
|
||
|
|
this._onSerialNotification = null;
|
||
|
|
this.onSerialData = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
disconnect(): void {
|
||
|
|
this.stopSerialMonitor();
|
||
|
|
if (this.flashingChar) {
|
||
|
|
this.flashingChar.stopNotifications();
|
||
|
|
}
|
||
|
|
this.server?.disconnect();
|
||
|
|
this.cleanup();
|
||
|
|
}
|
||
|
|
|
||
|
|
onDisconnected(callback: () => void): void {
|
||
|
|
this._onDisconnected = callback;
|
||
|
|
}
|
||
|
|
|
||
|
|
private cleanup(): void {
|
||
|
|
// Remove device-level event listeners
|
||
|
|
if (this.device && this._onDeviceDisconnect) {
|
||
|
|
this.device.removeEventListener('gattserverdisconnected', this._onDeviceDisconnect);
|
||
|
|
}
|
||
|
|
this._onDeviceDisconnect = null;
|
||
|
|
|
||
|
|
// Remove flashing notification listener
|
||
|
|
if (this.flashingChar && this._onFlashingNotification) {
|
||
|
|
this.flashingChar.removeEventListener('characteristicvaluechanged', this._onFlashingNotification);
|
||
|
|
}
|
||
|
|
this._onFlashingNotification = null;
|
||
|
|
|
||
|
|
// Remove serial notification listener
|
||
|
|
if (this.serialChar && this._onSerialNotification) {
|
||
|
|
this.serialChar.removeEventListener('characteristicvaluechanged', this._onSerialNotification);
|
||
|
|
}
|
||
|
|
this._onSerialNotification = null;
|
||
|
|
|
||
|
|
this.server = null;
|
||
|
|
this.service = null;
|
||
|
|
this.flashingChar = null;
|
||
|
|
this.serialChar = null;
|
||
|
|
this.device = null;
|
||
|
|
// Clear all pending ACK resolvers
|
||
|
|
for (const [idx, entry] of this.ackResolvers) {
|
||
|
|
clearTimeout(entry.timer);
|
||
|
|
}
|
||
|
|
this.ackResolvers.clear();
|
||
|
|
this.pendingAcks.clear();
|
||
|
|
this.onSerialData = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private hexToBinary(hexContent: string): Uint8Array {
|
||
|
|
const bytes: number[] = [];
|
||
|
|
let extendedAddress = 0;
|
||
|
|
|
||
|
|
for (const line of hexContent.split('\n')) {
|
||
|
|
const trimmed = line.trim();
|
||
|
|
if (!trimmed || trimmed[0] !== ':') continue;
|
||
|
|
|
||
|
|
const byteCount = parseInt(trimmed.substring(1, 3), 16);
|
||
|
|
const address = parseInt(trimmed.substring(3, 7), 16) + extendedAddress;
|
||
|
|
const recordType = parseInt(trimmed.substring(7, 9), 16);
|
||
|
|
|
||
|
|
if (recordType === 0x00) {
|
||
|
|
for (let i = 0; i < byteCount; i++) {
|
||
|
|
const byteOffset = 9 + i * 2;
|
||
|
|
if (byteOffset + 2 <= trimmed.length) {
|
||
|
|
const byteVal = parseInt(trimmed.substring(byteOffset, byteOffset + 2), 16);
|
||
|
|
bytes[address + i] = byteVal;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (recordType === 0x04) {
|
||
|
|
extendedAddress = parseInt(trimmed.substring(9, 13), 16) << 16;
|
||
|
|
} else if (recordType === 0x01) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = new Uint8Array(bytes.length);
|
||
|
|
for (let i = 0; i < bytes.length; i++) {
|
||
|
|
result[i] = bytes[i] ?? 0;
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
private crc32Table: Uint32Array | null = null;
|
||
|
|
|
||
|
|
private crc32(data: Uint8Array): number {
|
||
|
|
if (!this.crc32Table) {
|
||
|
|
this.crc32Table = new Uint32Array(256);
|
||
|
|
for (let i = 0; i < 256; i++) {
|
||
|
|
let c = i;
|
||
|
|
for (let j = 0; j < 8; j++) {
|
||
|
|
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
||
|
|
}
|
||
|
|
this.crc32Table[i] = c;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let crc = 0xFFFFFFFF;
|
||
|
|
for (let i = 0; i < data.length; i++) {
|
||
|
|
crc = this.crc32Table[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
|
||
|
|
}
|
||
|
|
return (crc ^ 0xFFFFFFFF) >>> 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
private uint32ToBytes(value: number): Uint8Array {
|
||
|
|
return new Uint8Array([
|
||
|
|
value & 0xFF,
|
||
|
|
(value >> 8) & 0xFF,
|
||
|
|
(value >> 16) & 0xFF,
|
||
|
|
(value >> 24) & 0xFF
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|