Files
MeshBot/CHANGELOG.md
T
2026-09-25 09:37:30 +02:00

21 KiB
Raw Blame History

Changelog

v0.11.3 — 2026-09-25 07:28

  • Added MESHTASTIC.md communication-rules file; referenced from AGENTS.md.

v0.11.2 — 2026-09-25 06:41

  • Self-echo filter in the notify webhook: notifyRx now skips the POST (with a single debug log line) when an explicit from equals our own node number (myNodeNum, set from FromRadio myInfo during initial sync) — our own sends must never trigger notifications of ourselves. Injections without from still notify; only an explicit from matching our node is filtered.
  • Tests: two new specs in the existing MeshService notify webhook block — rx text on the notify channel from our own node → no fetch; injected message with from = our own node → no fetch (own node number set via a myInfo frame, like the real initial sync). All 52 pre-existing specs stay green (54 total).
  • Docs: README env-var note + AGENTS.md webhook line ("messages from our own node are never notified").

v0.11.1 — 2026-09-25 01:49

  • Injected messages now fire the outbound notify webhook too: injectMessage calls the same extracted private helper (notifyRx(channel, text, from?)) as the bridge RX path (which lost its inline block), with channel 0 — so a synthetic rx record triggers the webhook only when MESH_NOTIFY_CHANNEL is set to 0; no forced semantics change, default stays 3.
  • docker-compose.yml: passthrough for MESH_NOTIFY_URL / MESH_NOTIFY_CHANNEL via ${VAR:-default} interpolation from .env (URL empty = disabled, channel default 3).
  • Docs: README env-var note + AGENTS.md webhook line.
  • Tests: two new specs in the existing MeshService notify webhook block — inject with from → exactly one POST {text, from}; without → body omits from. All 50 pre-existing specs stay green (52 total).

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).