diff --git a/src/adapters/serialConnection.ts b/src/adapters/serialConnection.ts index 9e2b492..13a402b 100644 --- a/src/adapters/serialConnection.ts +++ b/src/adapters/serialConnection.ts @@ -25,7 +25,7 @@ export class SerialConnection extends MeshDevice { * through a transform stream (https://stackoverflow.com/questions/71262432) */ private pipePromise?: Promise; - /* Reference for the heartbeat ping interval so it can be canceled on disconnect. */ + /* Reference for the heartbeat ping interval so it can be canceled on disconnect. */ private heartbeatInterval?: ReturnType | undefined; /** @@ -161,9 +161,9 @@ export class SerialConnection extends MeshDevice { // The firmware requires at least one ping per 15 minutes, so this should be more than enough. this.heartbeatInterval = setInterval(() => { this.heartbeat().catch((err) => { - console.error('Heartbeat error', err); + console.error("Heartbeat error", err); }); - }, 60*1000); + }, 60 * 1000); } else { console.log("not readable or writable"); } @@ -193,11 +193,11 @@ export class SerialConnection extends MeshDevice { if (this.port?.readable) { await this.port?.close(); } - + // stop the interval when disconnecting. if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); - this.heartbeatInterval = undefined; + this.heartbeatInterval = undefined; } // ------- this.updateDeviceStatus(Types.DeviceStatusEnum.DeviceDisconnected); diff --git a/src/utils/transformHandler.ts b/src/utils/transformHandler.ts index a000bd6..c102cb3 100644 --- a/src/utils/transformHandler.ts +++ b/src/utils/transformHandler.ts @@ -3,51 +3,83 @@ import type { Logger } from "tslog"; import * as Protobuf from "@meshtastic/protobufs"; import * as Types from "../types.ts"; +// This function takes the raw binary stream from the radio +// and converts it into usable "packets" that are returned to the +// adapter for handling export const transformHandler = ( log: Logger, onReleaseEvent: SimpleEventDispatcher, onDeviceDebugLog: SimpleEventDispatcher, concurrentLogOutput: boolean, ) => { + // byteBuffer contains the data to be processed let byteBuffer = new Uint8Array([]); + + // return the actual transformer that will be called for each + // new chunk of data... return new TransformStream({ transform(chunk: Uint8Array, controller): void { log = log.getSubLogger({ name: "streamTransformer" }); onReleaseEvent.subscribe(() => { controller.terminate(); }); + + // add the latest chunk of data into the array byteBuffer = new Uint8Array([...byteBuffer, ...chunk]); + + // This loop looks for Meshtastic packets in the stream based on the + // protocol definition. byteBuffer may contain 0 or more packets at + // any time. let processingExhausted = false; while (byteBuffer.length !== 0 && !processingExhausted) { + // Look for the magic byte that indicates a packet is starting const framingIndex = byteBuffer.findIndex((byte) => byte === 0x94); + // Check the second confirmation byte const framingByte2 = byteBuffer[framingIndex + 1]; if (framingByte2 === 0xc3) { + // Check to see if there is content in the buffer before the packet starts + // Per the protocol spec, data that is outside of the packet + // is likely to be ascii debugging information from the radio + // This includes formatting escape codes. if (byteBuffer.subarray(0, framingIndex).length) { if (concurrentLogOutput) { + // dispatch the raw data as an event + // the consumer will have to translate the bytes into ascii onDeviceDebugLog.dispatch(byteBuffer.subarray(0, framingIndex)); } else { - log.warn( + // This takes the bytes, translates them into ascii, and logs them + const ascii_debug = Array.from( + byteBuffer.subarray(0, framingIndex), + ) + .map((code) => String.fromCharCode(code)) + .join(""); + log.trace( Types.EmitterScope.SerialConnection, Types.Emitter.Connect, - `⚠️ Found unneccesary message padding, removing: ${byteBuffer - .subarray(0, framingIndex) - .toString()}`, + `Debug from radio:\n ${ascii_debug}`, ); } + // Remove everything before the magic byte byteBuffer = byteBuffer.subarray(framingIndex); } + // the next two bytes define the length of the packet const msb = byteBuffer[2]; const lsb = byteBuffer[3]; + // If we have a valid length, and the byteBuffer is long enough, + // then we should have a full packet. Let's process it... if ( msb !== undefined && lsb !== undefined && byteBuffer.length >= 4 + (msb << 8) + lsb ) { + // extract just the right amount of bytes const packet = byteBuffer.subarray(4, 4 + (msb << 8) + lsb); + // check to make sure these bytes don't include a new packet start + // this would indicate a malformed packet... const malformedDetectorIndex = packet.findIndex( (byte) => byte === 0x94, ); @@ -64,9 +96,13 @@ export const transformHandler = ( Protobuf.Mesh.LogRecord_Level.WARNING, ); + // prune out the malformed packet byteBuffer = byteBuffer.subarray(malformedDetectorIndex); } else { + // since we have a valid packet, we can remove those bytes... byteBuffer = byteBuffer.subarray(3 + (msb << 8) + lsb + 1); + + // and return the packet to the pipe... controller.enqueue(packet); } } else {