Send want_config_id on connect to trigger initial sync (#6)
CI / build + unit (push) Successful in 39s

Send want_config_id on connect to trigger initial sync (#6)

Firmware only streams myInfo/channels/configComplete after receiving a
ToRadio with want_config_id — the service previously wrote nothing on
connect, so /api/channels stayed [] and myNodeNum null. v0.1.4.
This commit was merged in pull request #6.
This commit is contained in:
2026-09-23 22:42:03 +02:00
parent 3062f40899
commit adfbc9800b
6 changed files with 97 additions and 10 deletions
+10
View File
@@ -88,3 +88,13 @@ export async function buildTextToRadio(
});
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);
}
+69 -7
View File
@@ -6,13 +6,16 @@ import { describe, expect, it } from 'vitest';
import { extractFrames, frame } from './codec.js';
import { MeshService } from './mesh.service.js';
function startBridge(): Promise<{
function startBridge(onSocket?: (s: net.Socket) => void): Promise<{
port: number;
sockets: net.Socket[];
close: () => void;
}> {
const sockets: net.Socket[] = [];
const server = net.createServer((socket) => sockets.push(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({
@@ -117,6 +120,63 @@ describe('MeshService channel pipeline', () => {
});
});
describe('MeshService connect', () => {
it('sends want_config_id on connect and applies the initial sync', async () => {
const out: Uint8Array[] = [];
const bridge = await startBridge((s) => s.on('data', (c) => out.push(c)));
process.env.MESH_HOST = '127.0.0.1';
process.env.MESH_PORT = String(bridge.port);
const svc = new MeshService();
try {
svc.onModuleInit();
await waitFor(() => svc.status().connected && out.length > 0);
const merged = new Uint8Array(out.reduce((n, c) => n + c.length, 0));
let off = 0;
for (const c of out) {
merged.set(c, off);
off += c.length;
}
const { packets } = extractFrames(merged);
expect(packets.length).toBeGreaterThan(0);
const tr = fromBinary(P.Mesh.ToRadioSchema, packets[0]);
expect(tr.payloadVariant?.case).toBe('wantConfigId');
const nonce =
tr.payloadVariant && tr.payloadVariant.case === 'wantConfigId'
? tr.payloadVariant.value
: -1;
expect(nonce).toBeGreaterThan(0);
// feed back the initial sync: myInfo, then configCompleteId echoing the nonce
bridge.sockets[0].write(
frame(
toBinary(
P.Mesh.FromRadioSchema,
create(P.Mesh.FromRadioSchema, {
payloadVariant: { case: 'myInfo', value: { myNodeNum: 1234 } },
}),
),
),
);
bridge.sockets[0].write(
frame(
toBinary(
P.Mesh.FromRadioSchema,
create(P.Mesh.FromRadioSchema, {
payloadVariant: { case: 'configCompleteId', value: nonce },
}),
),
),
);
await waitFor(() => svc.status().myNodeNum === 1234);
} finally {
svc.onModuleDestroy();
bridge.close();
}
});
});
describe('MeshService send', () => {
it('sends on the test channel (index 2) by default, never primary', async () => {
const bridge = await startBridge();
@@ -143,11 +203,13 @@ describe('MeshService send', () => {
off += c.length;
}
const { packets } = extractFrames(merged);
if (!packets.length) return undefined;
const tr = fromBinary(P.Mesh.ToRadioSchema, packets[0]);
return tr.payloadVariant?.case === 'packet'
? tr.payloadVariant.value
: undefined;
// outbound now starts with the want_config_id frame — find the text packet
for (const p of packets) {
const tr = fromBinary(P.Mesh.ToRadioSchema, p);
if (tr.payloadVariant?.case === 'packet')
return tr.payloadVariant.value;
}
return undefined;
};
await waitFor(() => readOutbound() !== undefined);
+9
View File
@@ -9,8 +9,10 @@ import * as net from 'node:net';
import {
PORT_TEXT_MESSAGE_APP,
buildTextToRadio,
buildWantConfigToRadio,
decodeFromRadio,
extractFrames,
frame,
} from './codec.js';
@Injectable()
@@ -94,6 +96,13 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
// fresh sync on every (re)connect — drop stale channels from the old session
this.channelMap = new Map();
this.logger.log(`bridge connected ${this.host}:${this.port}`);
const nonce = Math.floor(Math.random() * 0xffffffff) || 1;
buildWantConfigToRadio(nonce)
.then((bytes) => socket.write(frame(bytes)))
.catch((err: unknown) =>
this.logger.error(`want_config failed: ${String(err)}`),
);
this.logger.log(`requested initial sync (want_config_id=${nonce})`);
});
socket.on('data', (chunk) => {
const merged = new Uint8Array(this.buffer.length + chunk.length);