diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 7df6720a2..714e61108 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -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 diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 2d9230a21..2c3362877 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -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 diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index c724871cb..d3ad9ba79 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -2,6 +2,7 @@ #include "PhoneAPI.h" #include "Stream.h" +#include "concurrency/Lock.h" #include "concurrency/OSThread.h" #include @@ -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; }; \ No newline at end of file