Initial commit: NestJS meshtastic bridge bot

This commit is contained in:
2026-09-23 17:36:11 +00:00
commit 83e46b8f07
21 changed files with 6248 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
import { create, fromBinary, toBinary } from '@bufbuild/protobuf';
let proto: typeof import('@meshtastic/protobufs') | null = null;
async function loadProto(): Promise<typeof import('@meshtastic/protobufs')> {
if (!proto) proto = await import('@meshtastic/protobufs');
return proto!;
}
export const BROADCAST_ADDR = 0xffffffff;
// official portnums.proto: TEXT_MESSAGE_APP=1 (task spec draft said 3 — the enum value wins)
export const PORT_TEXT_MESSAGE_APP = 1;
const START1 = 0x94;
const START2 = 0xc3;
const MAX_LEN = 1024;
export function extractFrames(buf: Uint8Array): {
packets: Uint8Array[];
rest: Uint8Array;
} {
const packets: Uint8Array[] = [];
let i = 0;
for (;;) {
if (i >= buf.length) return { packets, rest: new Uint8Array(0) };
const isHeaderStart =
buf[i] === START1 && i + 1 < buf.length && buf[i + 1] === START2;
if (!isHeaderStart) {
// keep a trailing lone 0x94 — it may be the first byte of a split header
if (buf[i] === START1 && i + 1 >= buf.length)
return { packets, rest: buf.subarray(i) };
i++;
continue;
}
if (i + 4 > buf.length) return { packets, rest: buf.subarray(i) }; // partial header
const len = (buf[i + 2] << 8) | buf[i + 3];
if (len > MAX_LEN) {
i++; // bogus header — rescan from next byte
continue;
}
if (i + 4 + len > buf.length) return { packets, rest: buf.subarray(i) }; // partial payload
packets.push(buf.subarray(i + 4, i + 4 + len));
i += 4 + len;
}
}
export function frame(payload: Uint8Array): Uint8Array {
const out = new Uint8Array(4 + payload.length);
out[0] = START1;
out[1] = START2;
out[2] = (payload.length >> 8) & 0xff;
out[3] = payload.length & 0xff;
out.set(payload, 4);
return out;
}
export async function decodeFromRadio(
bytes: Uint8Array,
): Promise<import('@meshtastic/protobufs').Mesh.FromRadio> {
const P = await loadProto();
return fromBinary(P.Mesh.FromRadioSchema, bytes);
}
export async function buildTextToRadio(
to: number,
text: string,
): Promise<{ bytes: Uint8Array; id: number }> {
const P = await loadProto();
let id = Math.floor(Math.random() * 0x100000000) >>> 0;
if (id === 0) id = 1; // non-zero uint32
const meshPacket = create(P.Mesh.MeshPacketSchema, {
to,
id,
wantAck: true,
channel: 0,
payloadVariant: {
case: 'decoded',
value: {
portnum: P.Portnums.PortNum.TEXT_MESSAGE_APP,
payload: new TextEncoder().encode(text),
wantResponse: true,
},
},
});
const toRadio = create(P.Mesh.ToRadioSchema, {
payloadVariant: { case: 'packet', value: meshPacket },
});
return { bytes: frame(toBinary(P.Mesh.ToRadioSchema, toRadio)), id };
}