Initial commit: NestJS meshtastic bridge bot
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user