Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Commit 2c9b833

Browse files
staticoclaude
andcommitted
Add --bot flag for auto-reply to ping/test messages
Responds with pong/ack including SNR, RSSI, and hop count info. Supports both broadcast and DM contexts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8844954 commit 2c9b833

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ let httpPort: number | undefined;
289289
let useTls = false;
290290
let insecure = false;
291291
let pcapFile: string | undefined;
292+
let bot = false;
292293

293294
for (let i = 0; i < args.length; i++) {
294295
const arg = args[i];
@@ -370,6 +371,8 @@ for (let i = 0; i < args.length; i++) {
370371
useTls = true;
371372
} else if (arg === "--insecure" || arg === "-k") {
372373
insecure = true;
374+
} else if (arg === "--bot" || arg === "-b") {
375+
bot = true;
373376
} else if (arg === "--pcap") {
374377
if (i + 1 >= args.length) {
375378
console.error("--pcap requires a file path");
@@ -398,6 +401,7 @@ Options:
398401
--port, -P HTTP port number (default: 4403 if no port in address)
399402
--tls, -T Use HTTPS instead of HTTP
400403
--insecure, -k Accept self-signed SSL certificates
404+
--bot, -b Auto-reply to "ping" and "test" messages
401405
--pcap <file> Write packets to pcap file for analysis
402406
--enable-logging, -L Enable verbose logging to ~/.config/meshtastic-cli/log
403407
--help, -h Show this help message
@@ -491,6 +495,7 @@ const { waitUntilExit } = render(
491495
useTls,
492496
insecure,
493497
pcapFile,
498+
bot,
494499
}),
495500
{
496501
incrementalRendering: true, // Only update changed lines (Ink 6.5.0+)

src/ui/App.tsx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,10 @@ interface AppProps {
108108
useTls?: boolean;
109109
insecure?: boolean;
110110
pcapFile?: string;
111+
bot?: boolean;
111112
}
112113

113-
export function App({ address, packetStore, nodeStore, skipConfig = false, skipNodes = false, meshViewUrl, useFahrenheit = false, httpPort, useTls = false, insecure = false, pcapFile }: AppProps) {
114+
export function App({ address, packetStore, nodeStore, skipConfig = false, skipNodes = false, meshViewUrl, useFahrenheit = false, httpPort, useTls = false, insecure = false, pcapFile, bot = false }: AppProps) {
114115
const { exit } = useApp();
115116
const { stdout } = useStdout();
116117
const [transport, setTransport] = useState<Transport | null>(null);
@@ -145,6 +146,8 @@ export function App({ address, packetStore, nodeStore, skipConfig = false, skipN
145146
const nodeUpdateQueueRef = useRef<NodeData[]>([]);
146147
const nodeUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
147148
const pcapWriterRef = useRef<PcapWriter | null>(null);
149+
const transportRef = useRef<Transport | null>(null);
150+
transportRef.current = transport;
148151

149152
// Reboot modal state
150153
const [showRebootModal, setShowRebootModal] = useState(false);
@@ -583,6 +586,57 @@ export function App({ address, packetStore, nodeStore, skipConfig = false, skipN
583586
setDmMessages(db.getDMMessages(myNodeNum, selectedConvo.nodeNum));
584587
}
585588
}
589+
590+
// Bot auto-reply to "ping" and "test"
591+
if (bot && mp.from !== myNodeNum && transportRef.current) {
592+
const trimmed = (packet.payload as string).trim().toLowerCase();
593+
if (trimmed === "ping" || trimmed === "test") {
594+
const parts = [trimmed === "ping" ? "pong" : "ack"];
595+
if (mp.rxSnr != null) parts.push(`SNR:${mp.rxSnr}dB`);
596+
if (mp.rxRssi != null && mp.rxRssi !== 0) parts.push(`RSSI:${mp.rxRssi}dBm`);
597+
if (mp.hopStart) parts.push(`hops:${mp.hopStart - (mp.hopLimit ?? 0)}/${mp.hopStart}`);
598+
const reply = parts.join(" | ");
599+
const isDM = mp.to !== BROADCAST_ADDR;
600+
601+
const packetId = Math.floor(Math.random() * 0xffffffff);
602+
const data = create(Mesh.DataSchema, {
603+
portnum: Portnums.PortNum.TEXT_MESSAGE_APP,
604+
payload: new TextEncoder().encode(reply),
605+
});
606+
const meshPacket = create(Mesh.MeshPacketSchema, {
607+
id: packetId,
608+
from: myNodeNum,
609+
to: isDM ? mp.from : BROADCAST_ADDR,
610+
channel: isDM ? 0 : mp.channel,
611+
wantAck: true,
612+
payloadVariant: { case: "decoded", value: data },
613+
});
614+
const toRadio = create(Mesh.ToRadioSchema, {
615+
payloadVariant: { case: "packet", value: meshPacket },
616+
});
617+
618+
const t = transportRef.current;
619+
const binary = toBinary(Mesh.ToRadioSchema, toRadio);
620+
t.send(binary).then(() => {
621+
const botMsg: db.DbMessage = {
622+
packetId,
623+
fromNode: myNodeNum,
624+
toNode: isDM ? mp.from : BROADCAST_ADDR,
625+
channel: isDM ? 0 : mp.channel,
626+
text: reply,
627+
timestamp: Math.floor(Date.now() / 1000),
628+
status: "pending",
629+
};
630+
db.insertMessage(botMsg);
631+
if (!isDM) {
632+
setMessages(prev => [...prev, botMsg].slice(-100));
633+
}
634+
Logger.info("Bot", `Auto-replied "${reply}" to ${formatNodeId(mp.from)}`, { isDM });
635+
}).catch(() => {
636+
Logger.warn("Bot", `Failed to auto-reply to ${formatNodeId(mp.from)}`);
637+
});
638+
}
639+
}
586640
}
587641

588642
// Handle routing ACK/NAK for our sent messages

0 commit comments

Comments
 (0)