Add channel-2 #command auto-responder (v0.18.0)
CI / build + unit (pull_request) Successful in 41s

Event-driven CommandService dispatches exact case-sensitive #command
matches on COMMAND_CHANNEL (default 2) through a single-registration-point
registry; first command #ping -> #pong!. Replies ride COMMAND_CHANNEL:
directed DM back to the sender when present, else broadcast. Failing
handler/send only logs a warn.

DI: CommandModule's factory injects MeshService and attaches itself via
MeshService.attachCommands() — a constructor-level cycle leaves one side
null in NestJS 12's injector (verified), so no forwardRef is used.

Tests 106 -> 121; docs synced (AGENTS.md, MESHTASTIC.md, README).
This commit is contained in:
2026-09-25 17:11:52 +00:00
parent 2b2e494023
commit 931b42bb1e
11 changed files with 322 additions and 3 deletions
+17 -1
View File
@@ -11,6 +11,8 @@ import * as net from 'node:net';
// type-only import: node:sqlite is only required at runtime by whoever opens a database (mesh.module.ts / tests)
import type { DatabaseSync } from 'node:sqlite';
import type * as Proto from '@meshtastic/protobufs';
// type-only: the instance arrives via attachCommands() in production (command.module.ts) or the constructor in tests — no runtime import cycle with src/commands
import type { CommandService } from '../commands/command.service.js';
import { OpencodeService } from '../opencode/opencode.service.js';
import {
BROADCAST_ADDR,
@@ -211,11 +213,23 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
private db: DatabaseSync | null = null;
/** In-process opencode notifier for rx texts on the notify channel; null in tests that don't wire one. */
private readonly opencode: OpencodeService | null;
/** Event-driven `#command` auto-responder (COMMAND_CHANNEL); null until wired — via the constructor (tests) or attachCommands() (DI). */
private commands: CommandService | null = null;
private pollTimer: NodeJS.Timeout | null = null;
constructor(db?: DatabaseSync, opencode?: OpencodeService) {
constructor(
db?: DatabaseSync,
opencode?: OpencodeService,
commands?: CommandService,
) {
this.db = db ?? null;
this.opencode = opencode ?? null;
this.commands = commands ?? null;
}
/** Wire the `#command` auto-responder after construction — used by command.module.ts to break the DI cycle (a constructor-level MeshService <-> CommandService cycle leaves one side null in NestJS's injector). */
attachCommands(commands: CommandService): void {
this.commands = commands;
}
private socket: net.Socket | null = null;
@@ -736,6 +750,8 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
at: Date.now(),
});
this.notifyRx(pkt.channel ?? 0, text, pkt.from, pkt.id, pkt.to);
// event-driven #command auto-responder (COMMAND_CHANNEL) — fire-and-forget like the notifier
void this.commands?.onRx(pkt.channel ?? 0, text, pkt.from, pkt.id);
} else if (decoded && decoded.portnum === PORT_POSITION_APP) {
// live position update: the Position is serialized in Data.payload
let pos: Proto.Mesh.Position | undefined;