Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
108aa400d3 |
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user