161 lines
5.2 KiB
TypeScript
161 lines
5.2 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;
|
|
// verified against installed @meshtastic/protobufs Portnums.PortNum.TELEMETRY_APP
|
|
export const PORT_TELEMETRY_APP = 67;
|
|
|
|
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);
|
|
}
|
|
|
|
/** TELEMETRY_APP packets carry a serialized meshtastic.Telemetry in Data.payload. */
|
|
export async function decodeTelemetry(
|
|
bytes: Uint8Array,
|
|
): Promise<import('@meshtastic/protobufs').Telemetry.Telemetry> {
|
|
const P = await loadProto();
|
|
return fromBinary(P.Telemetry.TelemetrySchema, 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;
|
|
}
|
|
|
|
/** Random non-zero uint32 packet id. */
|
|
export function randomPacketId(): number {
|
|
let id = Math.floor(Math.random() * 0x100000000) >>> 0;
|
|
if (id === 0) id = 1; // non-zero uint32
|
|
return id;
|
|
}
|
|
|
|
export async function buildTextToRadio(
|
|
to: number,
|
|
text: string,
|
|
channelIndex: number,
|
|
): Promise<{ bytes: Uint8Array; id: number }> {
|
|
const P = await loadProto();
|
|
const id = randomPacketId();
|
|
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);
|
|
}
|