104 lines
20 KiB
Markdown
104 lines
20 KiB
Markdown
# Changelog
|
||
|
||
## v0.11.0 — 2026-09-25 01:30
|
||
|
||
- Outbound notification webhook (#17): when an inbound RX `TEXT_MESSAGE_APP` packet arrives on channel `MESH_NOTIFY_CHANNEL` (default `3`, read at construction exactly like `MESH_SEND_CHANNEL`), the service fires a fire-and-forget HTTP POST to `MESH_NOTIFY_URL` with JSON body `{text, from}` (`from` omitted when absent) — no retries, no queue, no response handling; failures only log a warn. Unset or empty `MESH_NOTIFY_URL` disables the feature entirely (no fetch at all). Uses Node 22's global `fetch` — zero new dependencies.
|
||
- Tests: three new specs in a `MeshService notify webhook` block (global fetch stubbed via `vi.stubGlobal`): rx text on channel 3 → exactly one POST with the URL, `content-type: application/json`, and body `{text, from}`; rx text on another channel → captured in history but no call; `MESH_NOTIFY_URL` unset or empty → no call. All 47 pre-existing specs stay green (50 total).
|
||
- Docs: README env-var list (+2 vars), AGENTS.md API/env note.
|
||
|
||
## v0.10.0 — 2026-09-25 00:30
|
||
|
||
- Message history is now persisted to SQLite via the stdlib `node:sqlite` `DatabaseSync` (zero new dependencies). DB file path from env `MESH_DB_PATH` (default `./data/mesh-bot.db`, parent dir auto-created); poller interval from env `MESH_POLL_MS` (default `1000` ms — "frequent pooling, configurable"). Schema: one table `messages(id INTEGER PRIMARY KEY, dir TEXT NOT NULL, to_node INTEGER, from_node INTEGER, text TEXT NOT NULL, channel INTEGER NOT NULL, at INTEGER NOT NULL, status TEXT, ack_error TEXT)`, keyed by packet id.
|
||
- Write path (the "pooling"): a `setInterval` poller started in `onModuleInit` re-upserts the ENTIRE current in-memory history buffer (≤100 rows) every tick with `INSERT OR REPLACE INTO messages ...`, then enforces retention with one statement keeping only the newest 1000 rows. Deliberately zero bookkeeping — trivial load at this scale.
|
||
- Read path: on startup (before the bridge connect) the service seeds the history buffer from the database (`SELECT ... ORDER BY at DESC LIMIT 100`), so message history SURVIVES PROCESS RESTARTS. On bridge reconnect the node/channel/device/telemetry capture state resets exactly as before, but the message history is no longer cleared — it's now an append-only log backed by SQLite (tx records that were still `pending` at disconnect stay pending).
|
||
- Testability: `MeshService` takes an optional `DatabaseSync` constructor argument (`null` = persistence disabled) so all 42 pre-existing specs construct without a database and touch no disk; new specs use `new DatabaseSync(':memory:')`. `flushHistory()` is public so tests can drive a poller tick without real timers.
|
||
- Production wiring: `MeshModule` now provides `MeshService` through a factory that opens the file DB from env (mkdir -p on the parent dir + idempotent table init). `docker-compose.yml` adds a named volume (`mesh_data:/app/data`, matching the Dockerfile WORKDIR `/app`) so the DB survives container recreation, and passes through `MESH_DB_PATH`/`MESH_POLL_MS`.
|
||
- Docs: README (new env vars + persistence note), AGENTS.md endpoint list, Swagger description of `GET /api/messages`, and the MCP `get_messages` tool description all updated from "in-memory only, reset on reconnect" to the SQLite-backed wording.
|
||
- Tests: five new specs in a `MeshService SQLite persistence` block — poller tick persists an injected row without any manual flush; an ACK status mutation reaches the DB after the next tick; startup seeding loads the newest 100 of 150 pre-populated rows into `messages()`; retention prunes a 1200-row table to the newest 1000; and history survives a bridge reconnect (unlike channels/nodes/telemetry).
|
||
|
||
## v0.9.0 — 2026-09-24 23:45
|
||
|
||
- New `POST /api/messages` endpoint (and matching MCP tool `inject_message`, now nine tools): synthetic inbox injection for external triggers. Body `{ "text": "...", "from"?: <node number> }` records an rx entry in the message history as if received on the mesh (channel 0), so external systems can push messages into the bot's inbox to trigger communication later. NOT transmitted over RF — nothing is written to the bridge or air; the record appears in `GET /api/messages` like any other rx message and is subject to the same last-100 cap. Returns `{ id }` with a random uint32 packet id (same generation as tx sends).
|
||
- New service method `MeshService.injectMessage(text, from?)`: pushes an rx record through the existing `pushHistory` cap pattern; `from` is optional and omitted from the record when absent (integer in 1..0xffffffff when provided — invalid values throw like `resolveDest`). Packet-id generation extracted from `buildTextToRadio` into a shared `randomPacketId()` helper so tx sends and injected records use one source of truth.
|
||
- Validation: empty/missing text → 400 `text must be a non-empty string`; invalid `from` (non-integer, <1 or >0xffffffff) → 400 `invalid node number '<n>'`. Same messages surface as MCP tool errors for `inject_message`.
|
||
- Swagger: `POST /api/messages` documented with the synthetic-injection caveat and an example. README + AGENTS.md endpoint/tool lists updated.
|
||
- Tests: new service specs (injected rx records newest-first via `messages()`, `from` omitted when absent, empty-text/invalid-from rejection, cap at 100 dropping the oldest, shared buffer with captured packets); MCP spec updated to nine tools incl. an `inject_message` call check asserting nothing reaches the bridge socket (no ToRadio 'packet' variant on the wire).
|
||
|
||
## v0.8.0 — 2026-09-24 21:40
|
||
|
||
- New `GET /api/telemetry` endpoint (and matching MCP tool `get_telemetry`, now eight tools): the telemetry stream captured from live `TELEMETRY_APP` packets (portnum 67) — device metrics (uptime/battery/voltage/channel+air utilization) and environment sensor readings where nodes have them. Records are `{ from, at /* epoch ms */, kind, data }[]` newest first: `kind` is the `meshtastic.Telemetry` oneof case name (`deviceMetrics`, `environmentMetrics`, `airQualityMetrics`, `powerMetrics`, `localStats`, `healthMetrics`, `hostMetrics` or `trafficManagementStats`) and `data` is that variant's payload. In-memory only, last 100 (same bounded pattern as message history), reset on every bridge reconnect.
|
||
- `MeshService.telemetry()` captures the new portnum in the existing packet handler: the `Data.payload` is decoded as a `meshtastic.Telemetry`, and packets without a variant payload are ignored. The variant payload goes through the same module-scope `jsonSafe` converter added in v0.7.1 — telemetry variants carry proto `uint64` fields (e.g. host-metrics byte counters) that decode to native bigint and would 500 Express' `res.json` otherwise, so they come back as decimal strings; unset optional fields are omitted from `data`.
|
||
- Verified against the installed `@meshtastic/protobufs` 2.8.0: `PortNum.TELEMETRY_APP = 67`; `Telemetry` is a `fixed32 time` plus oneof variant with exactly those eight cases; `DeviceMetrics` fields are batteryLevel/voltage/channelUtilization/airUtilTx/uptimeSeconds and `EnvironmentMetrics` the full sensor set (temperature, relativeHumidity, barometricPressure, gasResistance, voltage, current, iaq, distance, lux variants, wind/rainfall/soil/lightning, adc channels).
|
||
- Swagger: `GET /api/telemetry` documented with the kind values and an example carrying one record per kind. README + AGENTS.md endpoint/tool lists updated.
|
||
- Tests: fake-bridge gains a shared `telemetryPacketFrame` builder; new telemetry specs (device-metrics + environment variants → correct from/kind/data, cap at 100 dropping the oldest, reset on reconnect); MCP spec updated to eight tools incl. a `get_telemetry` call check.
|
||
|
||
## v0.7.1 — 2026-09-24 21:10
|
||
|
||
- Fix `GET /api/device` and MCP `device_info` crashing with `TypeError: Do not know how to serialize a BigInt` on live hardware (fw 2.7.15): protobuf-es decodes proto `uint64` fields as native JS `bigint`, which Express' `res.json`/`JSON.stringify` reject, and `MeshService.deviceInfo()` passed raw decoded proto objects through unconverted. Known affected fields: `Config.PowerConfig.powermonEnables` (in `localConfig.power`) and host-metrics byte counters (`freememBytes`/`diskfree*`).
|
||
- Fix at the single choke point: one small recursive converter (`jsonSafe`, bigint → decimal string, plain objects/arrays/primitives pass through) applied in `deviceInfo()` to everything that comes from raw decoded proto messages — DeviceMetadata plus every localConfig and moduleConfig section. No per-field patching; any uint64 field in those messages is covered generically. Values that were bigint now serialize as strings (e.g. `"powermonEnables": "123"`); no other endpoint/shape changes, redaction logic untouched.
|
||
- Tests: new device-info spec feeds a power config section frame with `powermonEnables` set to a real bigint through the fake bridge and asserts the value comes back as its string form (`"123"`) and that `JSON.stringify(deviceInfo())` succeeds — the old fixtures never set a uint64 field, which is why this slipped past the suite.
|
||
|
||
## v0.7.0 — 2026-09-24 20:21
|
||
|
||
- New `GET /api/device` endpoint (and matching MCP tool `device_info`, now seven tools): device info captured during the initial sync — this device's node number, firmware version and metadata (`meshtastic.DeviceMetadata` as-is; it carries no key material), plus per-section config and module-config messages keyed by section name. In-memory only, reset on every bridge reconnect like channels/nodes.
|
||
- `MeshService.deviceInfo()` captures the new FromRadio cases during sync: `metadata` (DeviceMetadata), `config` (one `meshtastic.Config` per section — oneof `payloadVariant` cases device/position/power/network/display/lora/bluetooth/security/sessionkey/deviceUi) and `moduleConfig` (one ModuleConfig per module). Verified against the installed `@meshtastic/protobufs` 2.8.0: FromRadio carries `meshtastic.Config`, not `LocalConfig` (that type lives in a separate localonly namespace and is not part of FromRadio).
|
||
- **Mandatory redaction (trust boundary):** key material never leaves the process — security section `publicKey`/`privateKey`/`adminKey` and network section `wifiPsk` are replaced with `"REDACTED"` in all output.
|
||
- Node summaries (`GET /api/nodes`, `GET /api/nodes/:id`, MCP `list_nodes`/`get_node`) gain optional fields from the NodeInfo: `snr` (dB), `hopsAway`, `viaMqtt` — present only when the device reported them. Note: `meshtastic.NodeInfo` in 2.8.0 has no firmware version field, so per-node firmwareVersion is not captured (device-level firmware version comes from DeviceMetadata via `/api/device`).
|
||
- Swagger: `GET /api/device` documented with the redaction note and an example; nodes endpoint example/description extended for the new optional fields. README + AGENTS.md endpoint lists updated.
|
||
- Tests: fake-bridge gains `metadataFrame`, `configSectionFrame`, `moduleConfigSectionFrame`; new device-info specs (metadata → firmwareVersion, config/module-config sections captured with security key material absent from the JSON output), NodeInfo snr/hopsAway/viaMqtt in summaries; MCP spec updated to seven tools incl. a `device_info` call check.
|
||
|
||
## v0.6.0 — 2026-09-24 18:28
|
||
|
||
- New MCP (Model Context Protocol) server endpoint at `POST /mcp` (outside the `/api` prefix, like Scalar at `/`), served in-process via the official `@modelcontextprotocol/sdk` Streamable HTTP transport in stateless mode (`sessionIdGenerator: undefined`; a fresh `McpServer` + transport per request). Six tools map 1:1 to existing `MeshService` methods with no new logic: `mesh_status`, `send_message(text, to?)` (same target resolution as `POST /api/send` — omitted/""/"!b"/"broadcast" → broadcast, numbers pass through, names via the node DB; resolution errors surface as tool errors with the same messages), `list_channels`, `list_nodes`, `get_node(id)` (tool error when missing/ambiguous), and `get_messages`. Tool results are JSON in a text content block.
|
||
- New dependency: `@modelcontextprotocol/sdk` (^1.30.1; zod comes with it). Note: the SDK's root entry is broken in all published 1.x tarballs (upstream issue #2273 — the exports map points to a dist index that was never shipped), so imports use the documented subpaths (`@modelcontextprotocol/sdk/server/mcp.js`, `.../server/streamableHttp.js`, `.../inMemory.js`, `.../client/index.js`).
|
||
- Test helpers shared by both specs extracted from `mesh.service.spec.ts` into `src/mesh/fake-bridge.ts` (fake bridge, waitFor, nodeInfoFrame, KAMIL fixtures); new `mcp.controller.spec.ts` drives the MCP server over an in-memory transport pair — all six tools listed with descriptions/schemas, and mesh_status / send_message (broadcast + name resolution + error messages) / get_node / list_channels / list_nodes / get_messages return correct shapes.
|
||
- README gains an MCP section; AGENTS.md endpoint list updated for `/mcp`.
|
||
|
||
## 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
|
||
|
||
- `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.
|
||
- Swagger: `SendTextDto.to` documented as `number | string` with the full resolution order and the DM-vs-broadcast caveat (DMs are unicast wantAck-tracked via `GET /api/messages`; broadcasts stay pending because firmware strips want_ack); `@ApiOperation` notes all sends still ride channel index 2 (`testowy`) regardless of DM or broadcast.
|
||
- README + AGENTS.md endpoint lists updated for the new `to` semantics.
|
||
- Unit tests: `resolveDest` coverage on the fake bridge — omitted/""/"!b"/"BROADCAST" → broadcast, number passthrough, numeric string, exact longName match → num (node DB seeded via framed FromRadio nodeInfo), unknown name → `no node matching`, and invalid numbers (0, negative, non-integer, > 0xffffffff) rejected.
|
||
|
||
## v0.3.0 — 2026-09-24 14:08
|
||
|
||
- Message history with delivery tracking, exposed as `GET /api/messages` (newest first): inbound TEXT_MESSAGE_APP packets (`dir:'rx'`, with `from`) and our sends (`dir:'tx'`, with `to` + `status`). tx records start `pending` and move to `delivered`/`failed` when the routing ACK/NACK arrives — ROUTING_APP packets carry a serialized `meshtastic.Routing` in `Data.payload` whose oneof variant is `errorReason` (`Routing_Error`: 0 NONE = delivered, anything else failed with a human-readable `ackError`, e.g. NO_ROUTE/GOT_NAK/TIMEOUT, fallback `reason <n>`), matched to our sends via `Data.requestId`. Broadcasts stay `pending` because firmware strips want_ack pre-TX (documented behavior, no timer). History is a bounded in-memory buffer of the last 100 records (newest kept) and resets on every bridge reconnect alongside channels/nodes.
|
||
- Codec: new `PORT_ROUTING_APP = 5` constant (verified against the installed `@meshtastic/protobufs` PortNum enum), `decodeRouting()` for ROUTING_APP payloads, and `describeRoutingError()` which maps a Routing.Error code to its name from the installed package's `Mesh.Routing_Error` enum.
|
||
- Swagger: `GET /api/messages` documented with the pending→delivered/failed lifecycle, the broadcast caveat, and an example matching the record shape; README + AGENTS.md endpoint lists updated.
|
||
|
||
## v0.2.0 — 2026-09-24 13:08
|
||
|
||
- Node database collected during the initial sync (`FromRadio.nodeInfo` frames, upserted by node number) and kept fresh by live `POSITION_APP` packets (the Position is serialized in `Data.payload`; unknown senders get a stub entry). The map resets on every bridge reconnect alongside channels.
|
||
- New endpoints: `GET /api/nodes` — lean summaries sorted by node number as `{ num, longName?, shortName?, hwModel?, batteryLevel?, lastHeard (epoch secs | null), position? { lat, lon, alt? } }[]`; names/hwModel/battery appear only when the device reported them, `hwModel` is the enum's string name (UNSET omitted), and `position` only when both coordinates are known. `GET /api/nodes/:id` — lookup by exact node number (numbers/numeric strings), then case-insensitive exact long/short name, else unique case-insensitive substring; 404 with a clear message when nothing or more than one node matches.
|
||
- Codec: new `PORT_POSITION_APP = 3` constant (verified against the installed `@meshtastic/protobufs` PortNum enum), `decodePosition()` for POSITION_APP payloads, and `loadHwModelNames()` reverse lookup of the HardwareModel enum.
|
||
- Swagger docs made genuinely useful: `Mesh` tag on the controller (description via `DocumentBuilder.addTag` — v12's `ApiTagOptions` has no description field) plus `@ApiOperation` summary/description and `@ApiResponse` examples on all endpoints, existing and new.
|
||
- Unit tests build real framed FromRadio bytes for nodeInfo/POSITION_APP: sync collection + summary shape (lat/lon = value × 1e-7, battery), stub entry from an unknown POSITION_APP sender, lookup ladder (exact num / numeric string / exact name / unique substring) with ambiguous and missing → null, and reconnect reset.
|
||
|
||
## v0.1.4 — 2026-09-23 20:33
|
||
|
||
- Send a `ToRadio` with `want_config_id` (random non-zero uint32 nonce) on every bridge connect, so the firmware streams its initial sync data (`myInfo`, channels, `config_complete_id`) — previously nothing was written on connect, which is why `/api/channels` stayed empty and `myNodeNum` stayed null in production. New codec helper `buildWantConfigToRadio`; unit test captures the outbound frame on the fake bridge, asserts the `wantConfigId` oneof case with a non-zero value, then feeds back `myInfo` + `configCompleteId` frames and checks service state.
|
||
|
||
## v0.1.3 — 2026-09-23 19:58
|
||
|
||
- Sends are now pinned to the test channel `testowy` (channel index 2) instead of the hardcoded primary channel (`channel: 0`). The target channel is configurable via `MESH_SEND_CHANNEL` (default `2`) and parsed once at service construction; `buildTextToRadio` takes the channel index as a parameter. Unit tests now decode the outbound ToRadio frame on the fake bridge and assert `packet.channel === 2` on the default send path.
|
||
|
||
## v0.1.2 — 2026-09-23 19:51
|
||
|
||
- Added a Gitea Actions CI pipeline (`.gitea/workflows/ci.yml`), mirroring flight's runner conventions: on push/PR to `main` (plus manual dispatch) one job runs `npm ci`, `nest build`, the vitest unit suite, and a production Docker image build — with retry loops for the runner's flaky uplink. No app code changed.
|
||
|
||
## v0.1.1 — 2026-09-23 19:38
|
||
|
||
- Fixed the default bridge port to `4403`, the meshtastic-serial-bridge's actual TCP port (the old value pointed at a closed port). Updated the `MESH_PORT` default in code, the docker-compose environment, and docs.
|
||
|
||
## v0.1.0 — 2026-09-23 18:54
|
||
|
||
- New `GET /api/channels` endpoint listing the device's Meshtastic channels as `{ index, name, uplinkEnabled, downlinkEnabled }`; psk/keys are redacted and never exposed over HTTP. Channels are collected from `FromRadio.channel` frames streamed during initial sync (before `config_complete_id`) and reset on every bridge reconnect so stale data is dropped after a disconnect.
|
||
- All endpoints moved under the `/api` namespace via `app.setGlobalPrefix("api")`: `GET /status` → `GET /api/status`, `POST /send` → `POST /api/send`.
|
||
- Scalar API docs served at `/` (OpenAPI JSON at `/docs-json`) using `@nestjs/swagger` + `@scalar/nestjs-api-reference`; added light swagger decorators (`ApiProperty` on the send body, `ApiOperation` summaries) so the docs are useful.
|
||
- Reconnect interval is now configurable via `MESH_RECONNECT_MS` (default 5000 ms).
|