Add MCP server for interacting with meshtastic devices and testing framework / TUI (#10194)
* Start of MCP server and test suite * Add MCP server for interacting with meshtastic devices and testing framework / TUI * Update mcp-server/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix mcp-server review feedback from thread Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/91dc128a-ed50-4d07-8bb2-3dc6623a05f7 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Enhance StreamAPI and PhoneAPI for improved log record handling and concurrency control * Semgrep fixes * Trunk and semgrep fixes * optimize pio streaming tee file writes Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/04e26c6b-6a2b-45be-bbeb-79ae4d0be633 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * chore: remove redundant log handle assignment Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/04e26c6b-6a2b-45be-bbeb-79ae4d0be633 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Consolidate type imports and remove placeholder test files * Add tests for config persistence and more exchange messages * Refactor position test to validate on-demand request/reply behavior * Remove position request/reply test and update README for telemetry behavior * Fix transmit history file to get removed on factory reset --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
parent
8fd0a7f283
commit
6b15571e14
@@ -17,6 +17,7 @@
|
||||
#include "Router.h"
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "error.h"
|
||||
#include "main.h"
|
||||
@@ -509,6 +510,12 @@ bool NodeDB::factoryReset(bool eraseBleBonds)
|
||||
}
|
||||
#endif
|
||||
spiLock->unlock();
|
||||
|
||||
// rmDir above nuked the .dat file, but TransmitHistory's in-memory
|
||||
// cache auto-flushes every 5 min and would resurrect it.
|
||||
if (transmitHistory) {
|
||||
transmitHistory->clear();
|
||||
}
|
||||
// second, install default state (this will deal with the duplicate mac address issue)
|
||||
installDefaultNodeDatabase();
|
||||
installDefaultDeviceState();
|
||||
|
||||
+18
-2
@@ -17,6 +17,7 @@
|
||||
#include "TypeConversions.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "main.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include "xmodem.h"
|
||||
|
||||
#if FromRadio_size > MAX_TO_FROM_RADIO_SIZE
|
||||
@@ -190,8 +191,23 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
break;
|
||||
#endif
|
||||
case meshtastic_ToRadio_heartbeat_tag:
|
||||
LOG_DEBUG("Got client heartbeat");
|
||||
heartbeatReceived = true;
|
||||
// nonce==1 is a special "nodeinfo ping" trigger: force a fresh
|
||||
// NodeInfo broadcast on the 60-second shorterTimeout path so
|
||||
// peers can re-learn our public key after a reboot or
|
||||
// factory_reset without waiting out the normal 10-minute
|
||||
// NodeInfo send cooldown. Mirrors the TCP/UDP path in
|
||||
// `src/mesh/api/PacketAPI.cpp:74-79` for serial clients.
|
||||
// Default nonce (0) remains a plain keepalive that triggers
|
||||
// a queue-status reply.
|
||||
if (toRadioScratch.heartbeat.nonce == 1) {
|
||||
if (nodeInfoModule) {
|
||||
LOG_INFO("Broadcasting nodeinfo ping (serial)");
|
||||
nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true);
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("Got client heartbeat");
|
||||
heartbeatReceived = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Ignore nop messages
|
||||
|
||||
+35
-10
@@ -2,6 +2,7 @@
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Throttle.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#define START1 0x94
|
||||
@@ -177,6 +178,9 @@ void StreamAPI::emitTxBuffer(size_t len)
|
||||
txBuf[3] = len & 0xff;
|
||||
|
||||
auto totalLen = len + HEADER_LEN;
|
||||
// Serialize stream writes against `emitLogRecord` so a LOG_ firing
|
||||
// mid-packet-emission can't interleave bytes on the wire.
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
stream->write(txBuf, totalLen);
|
||||
stream->flush();
|
||||
}
|
||||
@@ -195,21 +199,42 @@ void StreamAPI::emitRebooted()
|
||||
|
||||
void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg)
|
||||
{
|
||||
// In case we send a FromRadio packet
|
||||
memset(&fromRadioScratch, 0, sizeof(fromRadioScratch));
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_log_record_tag;
|
||||
fromRadioScratch.log_record.level = level;
|
||||
// IMPORTANT: do NOT touch `fromRadioScratch` or `txBuf` here — those
|
||||
// belong to the main packet-emission path and a LOG_ firing during
|
||||
// `writeStream()` would corrupt an in-flight encode. We keep a
|
||||
// dedicated `fromRadioScratchLog` + `txBufLog` for log records and
|
||||
// only serialize the actual `stream->write` call via `streamLock` so
|
||||
// a concurrent packet emission doesn't interleave bytes on the wire.
|
||||
memset(&fromRadioScratchLog, 0, sizeof(fromRadioScratchLog));
|
||||
fromRadioScratchLog.which_payload_variant = meshtastic_FromRadio_log_record_tag;
|
||||
fromRadioScratchLog.log_record.level = level;
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
fromRadioScratch.log_record.time = rtc_sec;
|
||||
strncpy(fromRadioScratch.log_record.source, src, sizeof(fromRadioScratch.log_record.source) - 1);
|
||||
fromRadioScratchLog.log_record.time = rtc_sec;
|
||||
strncpy(fromRadioScratchLog.log_record.source, src, sizeof(fromRadioScratchLog.log_record.source) - 1);
|
||||
|
||||
auto num_printed =
|
||||
vsnprintf(fromRadioScratch.log_record.message, sizeof(fromRadioScratch.log_record.message) - 1, format, arg);
|
||||
if (num_printed > 0 && fromRadioScratch.log_record.message[num_printed - 1] ==
|
||||
vsnprintf(fromRadioScratchLog.log_record.message, sizeof(fromRadioScratchLog.log_record.message) - 1, format, arg);
|
||||
if (num_printed > 0 && fromRadioScratchLog.log_record.message[num_printed - 1] ==
|
||||
'\n') // Strip any ending newline, because we have records for framing instead.
|
||||
fromRadioScratch.log_record.message[num_printed - 1] = '\0';
|
||||
emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch));
|
||||
fromRadioScratchLog.log_record.message[num_printed - 1] = '\0';
|
||||
|
||||
size_t len =
|
||||
pb_encode_to_bytes(txBufLog + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratchLog);
|
||||
if (len != 0) {
|
||||
txBufLog[0] = START1;
|
||||
txBufLog[1] = START2;
|
||||
txBufLog[2] = (len >> 8) & 0xff;
|
||||
txBufLog[3] = len & 0xff;
|
||||
|
||||
auto totalLen = len + HEADER_LEN;
|
||||
// Serialize stream writes against `emitTxBuffer` so a packet
|
||||
// emission in flight on another task doesn't interleave bytes
|
||||
// with this log record.
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
stream->write(txBufLog, totalLen);
|
||||
stream->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// Hookable to find out when connection changes
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "PhoneAPI.h"
|
||||
#include "Stream.h"
|
||||
#include "concurrency/Lock.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <cstdarg>
|
||||
|
||||
@@ -89,4 +90,27 @@ class StreamAPI : public PhoneAPI
|
||||
|
||||
/// Low level function to emit a protobuf encapsulated log record
|
||||
void emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg);
|
||||
|
||||
private:
|
||||
/// Dedicated scratch + tx buffer for LogRecord emission.
|
||||
///
|
||||
/// The main packet emission path (`writeStream` -> `getFromRadio` ->
|
||||
/// `emitTxBuffer`) holds `fromRadioScratch` (from PhoneAPI) and `txBuf`
|
||||
/// from the moment `getFromRadio` starts encoding until `emitTxBuffer`
|
||||
/// finishes pushing bytes to the stream. If a `LOG_` macro fires during
|
||||
/// that window and we emit through the API, the old implementation
|
||||
/// re-used `fromRadioScratch` / `txBuf` and corrupted whatever the main
|
||||
/// path had already encoded. Symptoms on the host were
|
||||
/// `google.protobuf.message.DecodeError: Error parsing message with type
|
||||
/// 'meshtastic.protobuf.FromRadio'` — any tool with
|
||||
/// `config.security.debug_log_api_enabled=true` under traffic would see
|
||||
/// torn frames every few messages.
|
||||
///
|
||||
/// Giving the log path its own scratch + txBuf means the main path is
|
||||
/// never clobbered. We still need `streamLock` to serialize the actual
|
||||
/// `stream->write` call so a log emission and a packet emission don't
|
||||
/// interleave on the wire.
|
||||
meshtastic_FromRadio fromRadioScratchLog = {};
|
||||
uint8_t txBufLog[MAX_STREAM_BUF_SIZE] = {0};
|
||||
concurrency::Lock streamLock;
|
||||
};
|
||||
@@ -255,6 +255,21 @@ bool TransmitHistory::saveToDisk()
|
||||
return false;
|
||||
}
|
||||
|
||||
void TransmitHistory::clear()
|
||||
{
|
||||
history.clear();
|
||||
lastMillis.clear();
|
||||
dirty = false;
|
||||
lastDiskSave = 0; // so the next legit broadcast persists immediately
|
||||
|
||||
spiLock->lock();
|
||||
if (FSCom.exists(FILENAME)) {
|
||||
FSCom.remove(FILENAME);
|
||||
}
|
||||
spiLock->unlock();
|
||||
LOG_INFO("TransmitHistory: cleared in-memory state + on-disk file");
|
||||
}
|
||||
|
||||
#else
|
||||
// No filesystem available — provide stub with in-memory tracking
|
||||
TransmitHistory *transmitHistory = nullptr;
|
||||
@@ -290,4 +305,10 @@ bool TransmitHistory::saveToDisk()
|
||||
return true;
|
||||
}
|
||||
|
||||
void TransmitHistory::clear()
|
||||
{
|
||||
history.clear();
|
||||
lastMillis.clear();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -76,6 +76,13 @@ class TransmitHistory
|
||||
*/
|
||||
bool saveToDisk();
|
||||
|
||||
/**
|
||||
* Wipe in-memory throttle state + remove the on-disk file. Required
|
||||
* alongside rmDir("/prefs") in factoryReset — otherwise the 5-min
|
||||
* auto-flush resurrects the file from the still-populated maps.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
private:
|
||||
TransmitHistory() = default;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user