Files
MeshBot/src/mesh/codec.ts
T
2026-09-24 16:19:32 +02:00

145 lines
4.6 KiB
TypeScript

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;
// verified against installed @meshtastic/protobufs Portnums.PortNum.POSITION_APP
export const PORT_POSITION_APP = 3;
// verified against installed @meshtastic/protobufs Portnums.PortNum.ROUTING_APP
export const PORT_ROUTING_APP = 5;
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);
}
/** POSITION_APP packets carry a serialized meshtastic.Position in Data.payload. */
export async function decodePosition(
bytes: Uint8Array,
): Promise<import('@meshtastic/protobufs').Mesh.Position> {
const P = await loadProto();
return fromBinary(P.Mesh.PositionSchema, bytes);
}
/** ROUTING_APP packets carry a serialized meshtastic.Routing in Data.payload (ACK/NACK for want_ack sends). */
export async function decodeRouting(
bytes: Uint8Array,
): Promise<import('@meshtastic/protobufs').Mesh.Routing> {
const P = await loadProto();
return fromBinary(P.Mesh.RoutingSchema, bytes);
}
/** Human-readable name for a Routing.Error code (from the installed @meshtastic/protobufs enum), `reason <n>` fallback. */
export async function describeRoutingError(code: number): Promise<string> {
const P = await loadProto();
const hit = Object.entries(P.Mesh.Routing_Error).find(
([, v]) => typeof v === 'number' && v === code,
);
return hit ? String(hit[0]) : `reason ${code}`;
}
let hwModelNames: Map<number, string> | null = null;
/** Reverse lookup of meshtastic.HardwareModel (enum value -> name), loaded once. */
export async function loadHwModelNames(): Promise<Map<number, string>> {
if (!hwModelNames) {
const P = await loadProto();
hwModelNames = new Map(
Object.entries(P.Mesh.HardwareModel).map(([k, v]) => [
Number(k),
String(v),
]),
);
}
return hwModelNames;
}
export async function buildTextToRadio(
to: number,
text: string,
channelIndex: number,
): 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: channelIndex,
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 };
}
export async function buildWantConfigToRadio(
wantConfigId: number,
): Promise<Uint8Array> {
const P = await loadProto();
const toRadio = create(P.Mesh.ToRadioSchema, {
payloadVariant: { case: 'wantConfigId', value: wantConfigId },
});
return toBinary(P.Mesh.ToRadioSchema, toRadio);
}