Publish on 'test' channel (index 3) by default
CI / build + unit (pull_request) Successful in 1m0s

This commit is contained in:
2026-09-24 17:38:18 +00:00
parent f640f6658c
commit 5c9aa13212
7 changed files with 23 additions and 19 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
## API ## API
1. **Live base URL** — `http://bot.dev.kkhost.pl` (Traefik → docker-compose stack on this host; container port 3000 via external network dev_net, no published ports). 1. **Live base URL** — `http://bot.dev.kkhost.pl` (Traefik → docker-compose stack on this host; container port 3000 via external network dev_net, no published ports).
2. **Endpoints** — all under the `/api` global prefix: `GET /api/status` (`{ connected, myNodeNum, host, port }`), `POST /api/send` (body `{ "text": "...", "to"?: <node number | node name | "!b" | "broadcast"> }` — numbers pass through as-is (integer 1..0xffffffff), names resolve like `GET /api/nodes/:id`, omitted/""/"!b"/"broadcast" → broadcast; unknown or ambiguous targets are a 400. Returns `{ id }`), `GET /api/channels` (the device's Meshtastic channels as `{ index, name, uplinkEnabled, downlinkEnabled }[]`; psk/keys redacted), `GET /api/messages` (message history newest first: rx text with `from`, tx sends with `to` + `status` pending→delivered/failed via routing ACKs — broadcasts stay pending because firmware strips want_ack; last 100, in-memory, reset on reconnect), `GET /api/nodes` (known nodes sorted by num as `{ num, longName?, shortName?, hwModel?, batteryLevel?, lastHeard: number|null, position?: { lat, lon, alt? } }[]`, from the initial sync plus live POSITION_APP updates; reset on reconnect) and `GET /api/nodes/:id` (lookup by exact node number/numeric string, then case-insensitive exact long/short name, else unique substring; 404 when ambiguous or missing). Scalar API docs are served at `/` (OpenAPI JSON at `/docs-json`). Keep this list in sync with the controller. All `POST /api/send` traffic targets channel index 2 (`testowy`) on purpose — never the primary channel; override via `MESH_SEND_CHANNEL` (default `2`). 2. **Endpoints** — all under the `/api` global prefix: `GET /api/status` (`{ connected, myNodeNum, host, port }`), `POST /api/send` (body `{ "text": "...", "to"?: <node number | node name | "!b" | "broadcast"> }` — numbers pass through as-is (integer 1..0xffffffff), names resolve like `GET /api/nodes/:id`, omitted/""/"!b"/"broadcast" → broadcast; unknown or ambiguous targets are a 400. Returns `{ id }`), `GET /api/channels` (the device's Meshtastic channels as `{ index, name, uplinkEnabled, downlinkEnabled }[]`; psk/keys redacted), `GET /api/messages` (message history newest first: rx text with `from`, tx sends with `to` + `status` pending→delivered/failed via routing ACKs — broadcasts stay pending because firmware strips want_ack; last 100, in-memory, reset on reconnect), `GET /api/nodes` (known nodes sorted by num as `{ num, longName?, shortName?, hwModel?, batteryLevel?, lastHeard: number|null, position?: { lat, lon, alt? } }[]`, from the initial sync plus live POSITION_APP updates; reset on reconnect) and `GET /api/nodes/:id` (lookup by exact node number/numeric string, then case-insensitive exact long/short name, else unique substring; 404 when ambiguous or missing). Scalar API docs are served at `/` (OpenAPI JSON at `/docs-json`). Keep this list in sync with the controller. All `POST /api/send` traffic targets channel index 3 (`test`) on purpose — never the primary channel; override via `MESH_SEND_CHANNEL` (default `3`).
## Other ## Other
+4
View File
@@ -1,5 +1,9 @@
# Changelog # Changelog
## v0.5.0 — 2026-09-24 17:37
- Publishing channel moved from `testowy` (index 2) to the new `test` channel (index 3); `MESH_SEND_CHANNEL` default is now `3`. All docs/comments referencing the old pinning updated accordingly.
## v0.4.0 — 2026-09-24 14:39 ## v0.4.0 — 2026-09-24 14:39
- `POST /api/send` now accepts node names and explicit broadcast sentinels in `to`: omitted/"" or `"!b"`/`"broadcast"` (case-insensitive, trimmed) → broadcast address `0xffffffff`; numbers pass through as-is (must be an integer in 1..0xffffffff); numeric strings are parsed the same way; anything else is looked up by name via the existing node DB lookup (exact long/short name, then unique substring). Unknown or ambiguous targets are a 400 with `no node matching '<to>'`. New service method `MeshService.resolveDest(to?)` does the resolution — `sendText()` keeps its resolved-number signature. - `POST /api/send` now accepts node names and explicit broadcast sentinels in `to`: omitted/"" or `"!b"`/`"broadcast"` (case-insensitive, trimmed) → broadcast address `0xffffffff`; numbers pass through as-is (must be an integer in 1..0xffffffff); numeric strings are parsed the same way; anything else is looked up by name via the existing node DB lookup (exact long/short name, then unique substring). Unknown or ambiguous targets are a 400 with `no node matching '<to>'`. New service method `MeshService.resolveDest(to?)` does the resolution — `sendText()` keeps its resolved-number signature.
+2 -2
View File
@@ -50,7 +50,7 @@ broadcast address `0xffffffff`; anything else is a node number or name.
DMs are unicast and wantAck-tracked via `GET /api/messages`; broadcasts stay DMs are unicast and wantAck-tracked via `GET /api/messages`; broadcasts stay
pending because firmware strips want_ack before TX. pending because firmware strips want_ack before TX.
All sends always target channel index 2 (`testowy`) — never the primary All sends always target channel index 3 (`test`) — never the primary
channel. Override with `MESH_SEND_CHANNEL`. channel. Override with `MESH_SEND_CHANNEL`.
## Env vars ## Env vars
@@ -58,7 +58,7 @@ channel. Override with `MESH_SEND_CHANNEL`.
- `MESH_HOST` (default `192.168.1.4`) — bridge host - `MESH_HOST` (default `192.168.1.4`) — bridge host
- `MESH_PORT` (default `4403`) — bridge port - `MESH_PORT` (default `4403`) — bridge port
- `MESH_RECONNECT_MS` (default `5000`) — reconnect interval in ms - `MESH_RECONNECT_MS` (default `5000`) — reconnect interval in ms
- `MESH_SEND_CHANNEL` (default `2`) — channel index all sends target (`testowy`; never the primary channel) - `MESH_SEND_CHANNEL` (default `3`) — channel index all sends target (`test`; never the primary channel)
- `PORT` (default `3000`) — HTTP listen port - `PORT` (default `3000`) — HTTP listen port
Note: the serial bridge accepts only ONE client at a time — if another app is Note: the serial bridge accepts only ONE client at a time — if another app is
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mesh-bot", "name": "mesh-bot",
"version": "0.4.0", "version": "0.5.0",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
+5 -5
View File
@@ -57,7 +57,7 @@ export class MeshController {
@ApiOperation({ @ApiOperation({
summary: 'Send a text message over the mesh', summary: 'Send a text message over the mesh',
description: description:
'Queues a TEXT_MESSAGE_APP packet to the resolved target (node number, node name, or broadcast). DMs are unicast and wantAck-tracked via GET /api/messages; broadcasts stay pending because firmware strips want_ack. All sends ride the pinned test channel (index 2, "testowy" — never the primary channel) regardless of DM or broadcast, since modern firmware PKI handles unicast encryption/channel anyway. Returns the Meshtastic packet id.', 'Queues a TEXT_MESSAGE_APP packet to the resolved target (node number, node name, or broadcast). DMs are unicast and wantAck-tracked via GET /api/messages; broadcasts stay pending because firmware strips want_ack. All sends ride the pinned test channel (index 3, "test" — never the primary channel) regardless of DM or broadcast, since modern firmware PKI handles unicast encryption/channel anyway. Returns the Meshtastic packet id.',
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -93,8 +93,8 @@ export class MeshController {
example: [ example: [
{ index: 0, name: 'Default', uplinkEnabled: true, downlinkEnabled: true }, { index: 0, name: 'Default', uplinkEnabled: true, downlinkEnabled: true },
{ {
index: 2, index: 3,
name: 'testowy', name: 'test',
uplinkEnabled: false, uplinkEnabled: false,
downlinkEnabled: false, downlinkEnabled: false,
}, },
@@ -118,8 +118,8 @@ export class MeshController {
id: 456789, id: 456789,
dir: 'tx', dir: 'tx',
to: 42, to: 42,
text: 'hello testowy', text: 'hello test',
channel: 2, channel: 3,
at: 1758000123456, at: 1758000123456,
status: 'delivered', status: 'delivered',
}, },
+8 -8
View File
@@ -295,11 +295,11 @@ describe('MeshService connect', () => {
}); });
describe('MeshService send', () => { describe('MeshService send', () => {
it('sends on the test channel (index 2) by default, never primary', async () => { it('sends on the test channel (index 3) by default, never primary', async () => {
const bridge = await startBridge(); const bridge = await startBridge();
process.env.MESH_HOST = '127.0.0.1'; process.env.MESH_HOST = '127.0.0.1';
process.env.MESH_PORT = String(bridge.port); process.env.MESH_PORT = String(bridge.port);
delete process.env.MESH_SEND_CHANNEL; // default path: "testowy" (index 2) delete process.env.MESH_SEND_CHANNEL; // default path: "test" (index 3)
const svc = new MeshService(); const svc = new MeshService();
try { try {
svc.onModuleInit(); svc.onModuleInit();
@@ -309,7 +309,7 @@ describe('MeshService send', () => {
const out: Uint8Array[] = []; const out: Uint8Array[] = [];
bridge.sockets[0].on('data', (c) => out.push(c)); bridge.sockets[0].on('data', (c) => out.push(c));
const id = await svc.sendText(42, 'hello testowy'); const id = await svc.sendText(42, 'hello test');
expect(id).toBeGreaterThan(0); expect(id).toBeGreaterThan(0);
const readOutbound = (): P.Mesh.MeshPacket | undefined => { const readOutbound = (): P.Mesh.MeshPacket | undefined => {
@@ -331,7 +331,7 @@ describe('MeshService send', () => {
await waitFor(() => readOutbound() !== undefined); await waitFor(() => readOutbound() !== undefined);
const pkt = readOutbound()!; const pkt = readOutbound()!;
expect(pkt.channel).toBe(2); // "testowy" — never the primary channel (0) expect(pkt.channel).toBe(3); // "test" — never the primary channel (0)
expect(pkt.to).toBe(42); expect(pkt.to).toBe(42);
const decoded = const decoded =
pkt.payloadVariant?.case === 'decoded' pkt.payloadVariant?.case === 'decoded'
@@ -339,7 +339,7 @@ describe('MeshService send', () => {
: undefined; : undefined;
expect( expect(
new TextDecoder().decode(decoded?.payload ?? new Uint8Array()), new TextDecoder().decode(decoded?.payload ?? new Uint8Array()),
).toBe('hello testowy'); ).toBe('hello test');
} finally { } finally {
delete process.env.MESH_SEND_CHANNEL; delete process.env.MESH_SEND_CHANNEL;
svc.onModuleDestroy(); svc.onModuleDestroy();
@@ -537,13 +537,13 @@ describe('MeshService message history', () => {
() => bridge.sockets.length === 1 && svc.status().connected, () => bridge.sockets.length === 1 && svc.status().connected,
); );
const id = await svc.sendText(42, 'hello testowy'); const id = await svc.sendText(42, 'hello test');
expect(svc.messages()[0]).toMatchObject({ expect(svc.messages()[0]).toMatchObject({
id, id,
dir: 'tx', dir: 'tx',
to: 42, to: 42,
text: 'hello testowy', text: 'hello test',
channel: 2, channel: 3,
status: 'pending', status: 'pending',
}); });
+2 -2
View File
@@ -71,9 +71,9 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
process.env.MESH_RECONNECT_MS ?? '5000', process.env.MESH_RECONNECT_MS ?? '5000',
10, 10,
); );
// sends are pinned to the test channel ("testowy", index 2) on purpose — never the primary channel // sends are pinned to the test channel ("test", index 3) on purpose — never the primary channel
private readonly sendChannel = parseInt( private readonly sendChannel = parseInt(
process.env.MESH_SEND_CHANNEL ?? '2', process.env.MESH_SEND_CHANNEL ?? '3',
10, 10,
); );