Skip to content

Fix issues in nodeSerialAdapter #118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: serial
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/adapters/nodeSerialConnection.ts
Original file line number Diff line number Diff line change
@@ -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<typeof setInterval> | 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();
17 changes: 17 additions & 0 deletions src/meshDevice.ts
Original file line number Diff line number Diff line change
@@ -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<number> {
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<number> {
const routeDiscovery = new Protobuf.Mesh.RouteDiscovery({
46 changes: 41 additions & 5 deletions src/utils/nodeTransformHandler.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>,
onReleaseEvent: SimpleEventDispatcher<boolean>,
onDeviceDebugLog: SimpleEventDispatcher<Uint8Array>,
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 {