166 lines
4.6 KiB
TypeScript
166 lines
4.6 KiB
TypeScript
import { create, toBinary } from '@bufbuild/protobuf';
|
|
import * as P from '@meshtastic/protobufs';
|
|
import * as net from 'node:net';
|
|
import type { AddressInfo } from 'node:net';
|
|
import { frame } from './codec.js';
|
|
|
|
/** In-process TCP stand-in for the meshtastic-serial-bridge; hands out accepted sockets. */
|
|
export function startBridge(
|
|
onSocket?: (s: net.Socket) => void,
|
|
): Promise<{ port: number; sockets: net.Socket[]; close: () => void }> {
|
|
const sockets: net.Socket[] = [];
|
|
const server = net.createServer((socket) => {
|
|
onSocket?.(socket); // attached at accept time, before any data can arrive
|
|
sockets.push(socket);
|
|
});
|
|
return new Promise((resolve) => {
|
|
server.listen(0, '127.0.0.1', () => {
|
|
resolve({
|
|
port: (server.address() as AddressInfo).port,
|
|
sockets,
|
|
close: () => server.close(),
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function waitFor(
|
|
cond: () => boolean | Promise<boolean>,
|
|
timeoutMs = 5000,
|
|
): Promise<void> {
|
|
const t0 = Date.now();
|
|
while (!(await cond())) {
|
|
if (Date.now() - t0 > timeoutMs) throw new Error('timed out waiting');
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
}
|
|
}
|
|
|
|
/** Plain-object NodeInfo fields (init shape, not the runtime message type) accepted by nodeInfoFrame. */
|
|
export interface NodeInfoFields {
|
|
user?: {
|
|
id?: string;
|
|
longName?: string;
|
|
shortName?: string;
|
|
hwModel?: number;
|
|
};
|
|
position?: { latitudeI?: number; longitudeI?: number; altitude?: number };
|
|
lastHeard?: number;
|
|
deviceMetrics?: { batteryLevel?: number };
|
|
snr?: number;
|
|
viaMqtt?: boolean;
|
|
hopsAway?: number;
|
|
}
|
|
|
|
// init shape for create(): all fields optional (protobuf-es' MessageInit is not exported)
|
|
type Init<T> = {
|
|
[P in keyof T as P extends '$unknown' | '$typeName' ? never : P]?: T[P];
|
|
};
|
|
|
|
/** DeviceMetadata frame as streamed once during the initial sync. */
|
|
export function metadataFrame(md: Init<P.Mesh.DeviceMetadata>): Uint8Array {
|
|
const fr = create(P.Mesh.FromRadioSchema, {
|
|
payloadVariant: { case: 'metadata', value: md },
|
|
});
|
|
return frame(toBinary(P.Mesh.FromRadioSchema, fr));
|
|
}
|
|
|
|
// Config/ModuleConfig section frames as streamed one-per-section during the initial sync.
|
|
// (meshtastic.Config / meshtastic.ModuleConfig live in their own root namespaces, not under Mesh)
|
|
export function configSectionFrame(
|
|
sectionCase: P.Config.Config['payloadVariant']['case'],
|
|
value: unknown,
|
|
): Uint8Array {
|
|
const cfg = create(P.Config.ConfigSchema, {
|
|
payloadVariant: {
|
|
case: sectionCase,
|
|
value,
|
|
} as P.Config.Config['payloadVariant'],
|
|
});
|
|
return frame(
|
|
toBinary(
|
|
P.Mesh.FromRadioSchema,
|
|
create(P.Mesh.FromRadioSchema, {
|
|
payloadVariant: { case: 'config', value: cfg },
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
export function moduleConfigSectionFrame(
|
|
sectionCase: P.ModuleConfig.ModuleConfig['payloadVariant']['case'],
|
|
value: unknown,
|
|
): Uint8Array {
|
|
const mc = create(P.ModuleConfig.ModuleConfigSchema, {
|
|
payloadVariant: {
|
|
case: sectionCase,
|
|
value,
|
|
} as P.ModuleConfig.ModuleConfig['payloadVariant'],
|
|
});
|
|
return frame(
|
|
toBinary(
|
|
P.Mesh.FromRadioSchema,
|
|
create(P.Mesh.FromRadioSchema, {
|
|
payloadVariant: { case: 'moduleConfig', value: mc },
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
export function nodeInfoFrame(
|
|
num: number,
|
|
fields?: NodeInfoFields,
|
|
): Uint8Array {
|
|
const ni = create(P.Mesh.NodeInfoSchema, { num, ...fields });
|
|
return frame(
|
|
toBinary(
|
|
P.Mesh.FromRadioSchema,
|
|
create(P.Mesh.FromRadioSchema, {
|
|
payloadVariant: { case: 'nodeInfo', value: ni },
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
/** TELEMETRY_APP packet frame with a serialized Telemetry in Data.payload (live telemetry from any node). */
|
|
export function telemetryPacketFrame(
|
|
from: number,
|
|
variant: P.Telemetry.Telemetry['variant'],
|
|
): Uint8Array {
|
|
const data = create(P.Mesh.DataSchema, {
|
|
portnum: P.Portnums.PortNum.TELEMETRY_APP,
|
|
payload: toBinary(
|
|
P.Telemetry.TelemetrySchema,
|
|
create(P.Telemetry.TelemetrySchema, { variant }),
|
|
),
|
|
});
|
|
const pkt = create(P.Mesh.MeshPacketSchema, {
|
|
from,
|
|
id: 1,
|
|
payloadVariant: { case: 'decoded', value: data },
|
|
});
|
|
return frame(
|
|
toBinary(
|
|
P.Mesh.FromRadioSchema,
|
|
create(P.Mesh.FromRadioSchema, {
|
|
payloadVariant: { case: 'packet', value: pkt },
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
export const KAMIL_NUM = 123456789;
|
|
|
|
export function kamilNodeInfo(): NodeInfoFields {
|
|
return {
|
|
user: {
|
|
id: '!kamil',
|
|
longName: 'Kamil SP8MK/P',
|
|
shortName: 'KK',
|
|
hwModel: P.Mesh.HardwareModel.TBEAM,
|
|
},
|
|
position: { latitudeI: 521049637, longitudeI: 209833152, altitude: 123 },
|
|
lastHeard: 1758000000,
|
|
deviceMetrics: { batteryLevel: 87 },
|
|
};
|
|
}
|