diff --git a/src/adapters/nodeSerialConnection.ts b/src/adapters/nodeSerialConnection.ts index ca2f8db..57f283f 100644 --- a/src/adapters/nodeSerialConnection.ts +++ b/src/adapters/nodeSerialConnection.ts @@ -16,6 +16,9 @@ export class NodeSerialConnection extends MeshDevice { /**Path to the serial port being opened. */ private portPath: string | undefined; + /* Reference for the heartbeat ping interval so it can be canceled on disconnect. */ + private heartbeatInterval?: ReturnType | undefined; + /** * Fires when `disconnect()` is called, used to instruct serial port and * readers to release their locks @@ -91,6 +94,18 @@ export class NodeSerialConnection extends MeshDevice { this.readFromRadio(concurrentLogOutput); this.updateDeviceStatus(Types.DeviceStatusEnum.DeviceConnected); + + this.configure().catch(() => { + // TODO: FIX, workaround for `wantConfigId` not getting acks. + }); + + // Set up an interval to send a heartbeat ping once every minute. + // 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); + }); + }, 60 * 1000); } else { console.log("not readable or writable"); } @@ -123,6 +138,13 @@ export class NodeSerialConnection 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.updateDeviceStatus(Types.DeviceStatusEnum.DeviceDisconnected); this.complete(); diff --git a/src/meshDevice.ts b/src/meshDevice.ts index 4977831..1f734f2 100755 --- a/src/meshDevice.ts +++ b/src/meshDevice.ts @@ -705,6 +705,23 @@ export abstract class MeshDevice { return this.sendRaw(toRadio.toBinary()); } + /** Serial connection requires a heartbeat ping to stay connected, otherwise times out after 15 minutes */ + public heartbeat(): Promise { + this.log.debug( + Types.Emitter[Types.Emitter.Ping], + "❤️ Send heartbeat ping to radio", + ); + + const toRadio = new Protobuf.Mesh.ToRadio({ + payloadVariant: { + case: "heartbeat", + value: {}, + }, + }); + + return this.sendRaw(toRadio.toBinary()); + } + /** Sends a trace route packet to the designated node */ public async traceRoute(destination: number): Promise { const routeDiscovery = new Protobuf.Mesh.RouteDiscovery({ diff --git a/src/utils/nodeTransformHandler.ts b/src/utils/nodeTransformHandler.ts index 9faf0e2..5d9fa34 100644 --- a/src/utils/nodeTransformHandler.ts +++ b/src/utils/nodeTransformHandler.ts @@ -4,49 +4,83 @@ import type { Logger } from "tslog"; import * as Protobuf from "../protobufs.js"; import * as Types from "../types.js"; +// 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 nodeTransformHandler = ( logger: Logger, onReleaseEvent: SimpleEventDispatcher, onDeviceDebugLog: SimpleEventDispatcher, concurrentLogOutput: boolean, ) => { + // byteBuffer contains the data to be processed let byteBuffer = new Uint8Array([]); const log = logger.getSubLogger({ name: "streamTransfer" }); + + // return the actual transformer that will be called for each + // new chunk of data... return new Transform({ transform(chunk: Buffer | Uint8Array, _encoding, controller) { onReleaseEvent.subscribe(() => { controller(); }); + + // 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, ); @@ -62,10 +96,12 @@ export const nodeTransformHandler = ( .toString()}`, 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... this.push(packet); } } else {