From f3b0709872783189b908674b234651bdf2a600e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sun, 12 Jul 2026 17:42:04 +0200 Subject: [PATCH 01/28] indicator: RP2040 peripherals for the main firmware The SenseCAP Indicator RP2040 co-processor serves as a generic peripheral bridge over a serial protobuf link (interdevice.proto): - FakeI2C implements TwoWire and tunnels write and read transactions, so the standard sensor drivers and the I2C scan work unmodified on the bridged second bus (WIRE1) - FakeUART forwards GPS NMEA to the regular GPS driver - SD card access with chunked file transfers, paged directory listings and card statistics; device-ui loads map tiles and map styles from the card behind the RP2040 - link at 2M baud with 4KB chunks, message structs kept off task stacks Log messages carrying their own bracket tag render it like a thread name. Replaces the earlier IndicatorSensor/COBS approach. --- platformio.ini | 2 +- src/RedirectablePrint.cpp | 17 +- src/configuration.h | 3 +- src/detect/ScanI2CTwoWire.cpp | 11 +- src/gps/GPS.cpp | 8 +- src/gps/GPS.h | 8 +- src/graphics/tftSetup.cpp | 85 +++++ src/main.cpp | 24 +- src/mesh/IndicatorSerial.cpp | 344 ++++++++++++++++++ src/mesh/IndicatorSerial.h | 104 ++++++ src/mesh/comms/FakeI2C.cpp | 118 ++++++ src/mesh/comms/FakeI2C.h | 67 ++++ src/mesh/comms/FakeUART.cpp | 100 +++++ src/mesh/comms/FakeUART.h | 62 ++++ .../generated/meshtastic/interdevice.pb.cpp | 24 +- .../generated/meshtastic/interdevice.pb.h | 298 ++++++++++++--- .../Telemetry/EnvironmentTelemetry.cpp | 8 - .../Telemetry/Sensor/IndicatorSensor.cpp | 166 --------- .../Telemetry/Sensor/IndicatorSensor.h | 19 - src/serialization/cobs.cpp | 129 ------- src/serialization/cobs.h | 75 ---- .../seeed-sensecap-indicator/variant.h | 12 +- 22 files changed, 1219 insertions(+), 465 deletions(-) create mode 100644 src/mesh/IndicatorSerial.cpp create mode 100644 src/mesh/IndicatorSerial.h create mode 100644 src/mesh/comms/FakeI2C.cpp create mode 100644 src/mesh/comms/FakeI2C.h create mode 100644 src/mesh/comms/FakeUART.cpp create mode 100644 src/mesh/comms/FakeUART.h delete mode 100644 src/modules/Telemetry/Sensor/IndicatorSensor.cpp delete mode 100644 src/modules/Telemetry/Sensor/IndicatorSensor.h delete mode 100644 src/serialization/cobs.cpp delete mode 100644 src/serialization/cobs.h diff --git a/platformio.ini b/platformio.ini index 250de5004e1..9dddcfde660 100644 --- a/platformio.ini +++ b/platformio.ini @@ -127,7 +127,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/effbb925a7cbd3ab794dfcc5fa16228217d18408.zip + https://github.com/meshtastic/device-ui/archive/dcb45b7d5ab6df2fcc2d66e10df7c2c0fcf3ff51.zip ; Common libs for environmental measurements in telemetry module [environmental_base] diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 3ca197ca470..cb078a71a76 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -78,6 +78,17 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l if (!std::isprint(static_cast(printBuf[f])) && printBuf[f] != '\n') printBuf[f] = '#'; } + // A leading "[Tag] " in the message simulates the thread name tag (used + // by contexts without an OSThread, e.g. the device-ui task). Print it + // before applying the level color so it renders like a real thread name. + size_t tagLen = 0; + if (printBuf[0] == '[') { + const char *end = (const char *)memchr(printBuf, ']', len < 24 ? len : 24); + if (end && (size_t)(end - printBuf) + 1 < len && end[1] == ' ') + tagLen = end - printBuf + 2; + } + if (tagLen) + Print::write(printBuf, tagLen); if (color && logLevel != nullptr) { if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) Print::write("\u001b[34m", 5); @@ -88,7 +99,7 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) Print::write("\u001b[31m", 5); } - len = Print::write(printBuf, len); + len = tagLen + Print::write(printBuf + tagLen, len - tagLen); if (color && logLevel != nullptr) { Print::write("\u001b[0m", 4); } @@ -161,7 +172,9 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, #endif } auto thread = concurrency::OSThread::currentThread; - if (thread) { + // A message starting with "[Tag] " brings its own tag (e.g. device-ui + // logging from the UI task, where currentThread is unrelated) + if (thread && format[0] != '[') { print("["); // printf("%p ", thread); // assert(thread->ThreadName.length()); diff --git a/src/configuration.h b/src/configuration.h index aaba2bbfd98..fd4f148455e 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -395,7 +395,8 @@ along with this program. If not, see . #ifndef WIRE_INTERFACES_COUNT // Officially an NRF52 macro // Repurposed cross-platform to identify devices using Wire1 -#if defined(I2C_SDA1) || defined(PIN_WIRE1_SDA) +// The SenseCAP Indicator has a second bus bridged to the RP2040 (FakeI2C) +#if defined(I2C_SDA1) || defined(PIN_WIRE1_SDA) || defined(SENSECAP_INDICATOR) #define WIRE_INTERFACES_COUNT 2 #elif HAS_WIRE #define WIRE_INTERFACES_COUNT 1 diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 151e559f15b..de9850a873f 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -8,6 +8,9 @@ #if defined(ARCH_PORTDUINO) #include "linux/LinuxHardwareI2C.h" #endif +#if defined(SENSECAP_INDICATOR) +#include "mesh/comms/FakeI2C.h" +#endif #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32) #include "meshUtils.h" // vformat @@ -245,7 +248,11 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) #if WIRE_INTERFACES_COUNT == 2 if (port == I2CPort::WIRE1) { +#if defined(SENSECAP_INDICATOR) + i2cBus = FakeWire; // WIRE1 is bridged to the RP2040 +#else i2cBus = &Wire1; +#endif } else { #endif i2cBus = &Wire; @@ -908,7 +915,9 @@ TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) if (address.port == ScanI2C::I2CPort::WIRE) { return &Wire; } else { -#if WIRE_INTERFACES_COUNT == 2 +#if defined(SENSECAP_INDICATOR) + return FakeWire; // WIRE1 is bridged to the RP2040 +#elif WIRE_INTERFACES_COUNT == 2 return &Wire1; #else return &Wire; diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 430030422b2..d5e3d751c42 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -46,7 +46,9 @@ template std::size_t array_count(const T (&)[N]) #define GPS_SERIAL_PORT Serial1 #endif -#if defined(ARCH_NRF52) +#if defined(SENSECAP_INDICATOR) +FakeUART *GPS::_serial_gps = FakeSerial; +#elif defined(ARCH_NRF52) Uart *GPS::_serial_gps = &GPS_SERIAL_PORT; #elif defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32) HardwareSerial *GPS::_serial_gps = &GPS_SERIAL_PORT; @@ -1428,6 +1430,7 @@ void GPS::publishUpdate() int32_t GPS::runOnce() { +#if !defined(SENSECAP_INDICATOR) if (!GPSInitFinished) { if (!_serial_gps || config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { LOG_INFO("GPS set to not-present. Skip probe"); @@ -1448,6 +1451,7 @@ int32_t GPS::runOnce() GPSInitFinished = true; publishUpdate(); } +#endif // ======================== GPS_ACTIVE state ======================== // In GPS_ACTIVE state, GPS is powered on and we're receiving NMEA messages. @@ -1910,8 +1914,10 @@ std::unique_ptr GPS::createGps() } else return nullptr; #endif +#if !defined(SENSECAP_INDICATOR) if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all return nullptr; +#endif auto new_gps = std::unique_ptr(new GPS()); new_gps->rx_gpio = _rx_gpio; diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 678cc2845b3..fd628bbb1cf 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -13,6 +13,10 @@ #include "input/UpDownInterruptImpl1.h" #include "modules/PositionModule.h" +#ifdef SENSECAP_INDICATOR +#include "mesh/comms/FakeUART.h" +#endif + // Allow defining the polarity of the ENABLE output. default is active high #ifndef GPS_EN_ACTIVE #define GPS_EN_ACTIVE 1 @@ -219,7 +223,9 @@ class GPS : private concurrency::OSThread CallbackObserver notifyDeepSleepObserver = CallbackObserver(this, &GPS::prepareDeepSleep); /** If !NULL we will use this serial port to construct our GPS */ -#if defined(ARCH_RP2040) +#if defined(SENSECAP_INDICATOR) + static FakeUART *_serial_gps; +#elif defined(ARCH_RP2040) static SerialUART *_serial_gps; #elif defined(ARCH_NRF52) static Uart *_serial_gps; diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 708cd896747..1422246f0ee 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -14,6 +14,85 @@ #include #endif +#ifdef SENSECAP_INDICATOR +#include "graphics/map/RemoteSDService.h" +#include "input/I2CKeyboardScanner.h" +#include "mesh/IndicatorSerial.h" +#include "mesh/comms/FakeI2C.h" + +// Serves the UI map tiles from the SD card behind the RP2040, chunk-wise +// over the interdevice link +class IndicatorRemoteFS : public IRemoteFS +{ + public: + bool readChunk(const char *path, uint32_t offset, uint8_t *buf, uint32_t len, uint32_t *bytesRead, + uint32_t *fileSize) override + { + if (!sensecapIndicator) + return false; + memset(&result, 0, sizeof(result)); + if (!sensecapIndicator->file_read(path, offset, len, &result) || !result.success) + return false; + uint32_t n = result.filedata.size; + if (n > len) + n = len; + memcpy(buf, result.filedata.bytes, n); + *bytesRead = n; + // lv_fs positions are 32 bit, map tiles never come close + *fileSize = (uint32_t)result.file_size; + return true; + } + + bool writeChunk(const char *path, uint32_t offset, const uint8_t *buf, uint32_t len, bool create) override + { + if (!sensecapIndicator) + return false; + memset(&result, 0, sizeof(result)); + return sensecapIndicator->file_write(path, offset, buf, len, create, &result) && result.success; + } + + bool sdInfo(RemoteSdInfo &info) override + { + if (!sensecapIndicator) + return false; + meshtastic_SdCardInfo result = meshtastic_SdCardInfo_init_zero; + if (!sensecapIndicator->sd_info(&result)) + return false; + info.present = result.present; + info.cardType = (uint8_t)result.card_type; + info.fatType = (uint8_t)result.fat_type; + info.cardSize = result.card_size; + info.usedBytes = result.used_bytes; + info.freeBytes = result.free_bytes; + return true; + } + + bool listDir(const char *path, std::set &entries) override + { + if (!sensecapIndicator) + return false; + uint32_t offset = 0; + while (true) { + memset(&listing, 0, sizeof(listing)); + if (!sensecapIndicator->list_directory(path, offset, &listing) || !listing.success) + return false; + for (pb_size_t i = 0; i < listing.filenames_count; i++) + entries.insert(listing.filenames[i]); + offset += listing.filenames_count; + if (listing.filenames_count == 0 || offset >= listing.total_count) + break; + } + return true; + } + + private: + // Several KB each, kept off the UI task stack. All file operations + // originate from the single UI task. + meshtastic_FileTransfer result; + meshtastic_DirectoryListing listing; +}; +#endif + DeviceScreen *deviceScreen = nullptr; #ifndef TFT_TASK_STACK_SIZE @@ -40,6 +119,12 @@ void tft_task_handler(void *param = nullptr) void tftSetup(void) { +#ifdef SENSECAP_INDICATOR + RemoteSDService::setBackend(new IndicatorRemoteFS()); + // the second bus is bridged to the RP2040, keep the keyboard scan off the + // uninitialized local Wire1 + I2CKeyboardScanner::setSecondaryBus(FakeWire); +#endif #ifndef ARCH_PORTDUINO deviceScreen = &DeviceScreen::create(); PacketAPI::create(PacketServer::init()); diff --git a/src/main.cpp b/src/main.cpp index cc7c239a4e0..0a777062d32 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,6 +30,11 @@ #include "detect/ScanI2C.h" #include "error.h" +#ifdef SENSECAP_INDICATOR // on the indicator run the additional serial port for the RP2040 +#include "IndicatorSerial.h" +#include "mesh/comms/FakeI2C.h" +#endif + #if !MESHTASTIC_EXCLUDE_I2C #include "detect/ScanI2CConsumer.h" #include "detect/ScanI2CTwoWire.h" @@ -561,7 +566,10 @@ void setup() #endif #if !MESHTASTIC_EXCLUDE_I2C -#if defined(I2C_SDA1) && defined(ARCH_RP2040) +#if defined(SENSECAP_INDICATOR) + // The Sensecap Indicator has its second I2C bus on the RP2040, bridged + // over serial as FakeWire. No local interface to initialize. +#elif defined(I2C_SDA1) && defined(ARCH_RP2040) Wire1.setSDA(I2C_SDA1); Wire1.setSCL(I2C_SCL1); Wire1.begin(); @@ -624,6 +632,18 @@ void setup() mcp23017EarlyInit(); #endif +#ifdef SENSECAP_INDICATOR + // Power the RP2040 co-processor and start the interdevice link before the + // I2C scan, so that its bus can be probed through the bridge +#ifdef SENSOR_POWER_CTRL_EXPANDER + pinMode(SENSOR_POWER_CTRL_EXPANDER, OUTPUT); + digitalWrite(SENSOR_POWER_CTRL_EXPANDER, SENSOR_POWER_ON_EXPANDER); +#endif + sensecapIndicator = new SensecapIndicator(Serial2); + if (!sensecapIndicator->wait_ready(3000)) + LOG_WARN("RP2040 co-processor not answering, bridged I2C bus unavailable"); +#endif + #if !MESHTASTIC_EXCLUDE_I2C // We need to scan here to decide if we have a screen for nodeDB.init() and because power has been applied to // accessories @@ -632,7 +652,7 @@ void setup() LOG_INFO("Scan for i2c devices"); #endif -#if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)) +#if defined(SENSECAP_INDICATOR) || defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)) i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); #endif diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp new file mode 100644 index 00000000000..d3ad797cc22 --- /dev/null +++ b/src/mesh/IndicatorSerial.cpp @@ -0,0 +1,344 @@ +#ifdef SENSECAP_INDICATOR + +#include "IndicatorSerial.h" +#include "concurrency/LockGuard.h" +#include "mesh/comms/FakeUART.h" +#include +#include +#include + +SensecapIndicator *sensecapIndicator; + +SensecapIndicator::SensecapIndicator(HardwareSerial &serial) : OSThread("SensecapIndicator") +{ + if (!running) { + _serial = &serial; + // Twice the largest frame: the pump runs from the cooperative main + // loop, which can stall for tens of ms while data keeps arriving + _serial->setRxBufferSize(2 * PB_BUFSIZE); + _serial->setPins(SENSOR_RP2040_RXD, SENSOR_RP2040_TXD); + _serial->begin(SENSOR_BAUD_RATE); + running = true; + LOG_DEBUG("Start indicator communication thread"); + } +} + +int32_t SensecapIndicator::runOnce() +{ + if (running) { + concurrency::LockGuard guard(&link_lock); + pump(); + return (10); + } else { + LOG_DEBUG("Not running"); + return (1000); + } +} + +// Read whatever is available on the link and process complete packets +void SensecapIndicator::pump() +{ + size_t space_left = PB_BUFSIZE - pb_rx_size; + pb_rx_size += serial_check((char *)pb_rx_buf + pb_rx_size, space_left); + check_packet(); +} + +// Pump the link until `flag` goes true or the timeout expires +bool SensecapIndicator::wait_response(bool &flag, uint32_t timeout_ms) +{ + uint32_t start = millis(); + while (!flag) { + pump(); + if (flag) + break; + if (millis() - start >= timeout_ms) + return false; + delay(1); + } + return true; +} + +// assign the next correlation id to a request, skipping the unsolicited 0 +uint32_t SensecapIndicator::stamp_request(meshtastic_InterdeviceMessage &request) +{ + if (++next_request_id == 0) + next_request_id = 1; + request.id = next_request_id; + expected_id = next_request_id; + return next_request_id; +} + +bool SensecapIndicator::i2c_transact(meshtastic_InterdeviceMessage &request, meshtastic_I2CResult *result, uint32_t timeout_ms) +{ + concurrency::LockGuard guard(&link_lock); + if (packets_received == 0) + return false; // co-processor has never talked to us, fail fast instead of timing out + + stamp_request(request); + i2c_result_ready = false; + if (!send_uplink_unlocked(request)) + return false; + + if (!wait_response(i2c_result_ready, timeout_ms)) + return false; + *result = i2c_result; + i2c_result_ready = false; + return true; +} + +bool SensecapIndicator::file_request(meshtastic_InterdeviceMessage &request, meshtastic_FileTransfer *out, uint32_t timeout_ms) +{ + concurrency::LockGuard guard(&link_lock); + if (packets_received == 0) + return false; + + stamp_request(request); + file_response_ready = false; + pending_file = out; + if (!send_uplink_unlocked(request) || !wait_response(file_response_ready, timeout_ms)) { + pending_file = NULL; + return false; + } + return true; +} + +bool SensecapIndicator::file_read(const char *path, uint32_t offset, uint32_t length, meshtastic_FileTransfer *out, + uint32_t timeout_ms) +{ + meshtastic_InterdeviceMessage &msg = tx_message; + memset(&msg, 0, sizeof(msg)); + msg.which_data = meshtastic_InterdeviceMessage_file_transfer_tag; + msg.data.file_transfer.operation = meshtastic_FileOperation_GET; + strncpy(msg.data.file_transfer.filepath, path, sizeof(msg.data.file_transfer.filepath) - 1); + msg.data.file_transfer.offset = offset; + msg.data.file_transfer.length = length; + return file_request(msg, out, timeout_ms); +} + +bool SensecapIndicator::file_write(const char *path, uint32_t offset, const uint8_t *data, size_t len, bool create, + meshtastic_FileTransfer *out, uint32_t timeout_ms) +{ + meshtastic_InterdeviceMessage &msg = tx_message; + memset(&msg, 0, sizeof(msg)); + msg.which_data = meshtastic_InterdeviceMessage_file_transfer_tag; + msg.data.file_transfer.operation = create ? meshtastic_FileOperation_POST : meshtastic_FileOperation_PUT; + strncpy(msg.data.file_transfer.filepath, path, sizeof(msg.data.file_transfer.filepath) - 1); + msg.data.file_transfer.offset = offset; + if (len > sizeof(msg.data.file_transfer.filedata.bytes)) + return false; + msg.data.file_transfer.filedata.size = len; + memcpy(msg.data.file_transfer.filedata.bytes, data, len); + return file_request(msg, out, timeout_ms); +} + +bool SensecapIndicator::file_remove(const char *path, meshtastic_FileTransfer *out, uint32_t timeout_ms) +{ + meshtastic_InterdeviceMessage &msg = tx_message; + memset(&msg, 0, sizeof(msg)); + msg.which_data = meshtastic_InterdeviceMessage_file_transfer_tag; + msg.data.file_transfer.operation = meshtastic_FileOperation_DELETE; + strncpy(msg.data.file_transfer.filepath, path, sizeof(msg.data.file_transfer.filepath) - 1); + return file_request(msg, out, timeout_ms); +} + +bool SensecapIndicator::list_directory(const char *path, uint32_t offset, meshtastic_DirectoryListing *out, uint32_t timeout_ms) +{ + concurrency::LockGuard guard(&link_lock); + if (packets_received == 0) + return false; + + meshtastic_InterdeviceMessage &msg = tx_message; + memset(&msg, 0, sizeof(msg)); + msg.which_data = meshtastic_InterdeviceMessage_directory_listing_tag; + strncpy(msg.data.directory_listing.directory, path, sizeof(msg.data.directory_listing.directory) - 1); + msg.data.directory_listing.offset = offset; + + stamp_request(msg); + dir_response_ready = false; + pending_dir = out; + if (!send_uplink_unlocked(msg) || !wait_response(dir_response_ready, timeout_ms)) { + pending_dir = NULL; + return false; + } + return true; +} + +bool SensecapIndicator::sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms) +{ + concurrency::LockGuard guard(&link_lock); + if (packets_received == 0) + return false; + + meshtastic_InterdeviceMessage &msg = tx_message; + memset(&msg, 0, sizeof(msg)); + msg.which_data = meshtastic_InterdeviceMessage_get_sd_info_tag; + msg.data.get_sd_info = true; + + stamp_request(msg); + sd_info_ready = false; + pending_sd_info = out; + if (!send_uplink_unlocked(msg) || !wait_response(sd_info_ready, timeout_ms)) { + pending_sd_info = NULL; + return false; + } + return true; +} + +bool SensecapIndicator::wait_ready(uint32_t timeout_ms) +{ + concurrency::LockGuard guard(&link_lock); + uint32_t start = millis(); + while (packets_received == 0) { + pump(); + if (packets_received > 0) + break; + if (millis() - start >= timeout_ms) + return false; + delay(1); + } + return true; +} + +bool SensecapIndicator::send_uplink(const meshtastic_InterdeviceMessage &message) +{ + concurrency::LockGuard guard(&link_lock); + return send_uplink_unlocked(message); +} + +// callers must hold link_lock: pb_tx_buf is shared +bool SensecapIndicator::send_uplink_unlocked(const meshtastic_InterdeviceMessage &message) +{ + pb_tx_buf[0] = MT_MAGIC_0; + pb_tx_buf[1] = MT_MAGIC_1; + + pb_ostream_t stream = pb_ostream_from_buffer(pb_tx_buf + MT_HEADER_SIZE, PB_BUFSIZE); + if (!pb_encode(&stream, meshtastic_InterdeviceMessage_fields, &message)) { + LOG_DEBUG("pb_encode failed"); + return false; + } + + // Store the payload length in the header + pb_tx_buf[2] = stream.bytes_written / 256; + pb_tx_buf[3] = stream.bytes_written % 256; + + bool rv = send((const char *)pb_tx_buf, MT_HEADER_SIZE + stream.bytes_written); + + return rv; +} + +size_t SensecapIndicator::serial_check(char *buf, size_t space_left) +{ + size_t bytes_read = 0; + while (bytes_read < space_left && _serial->available()) { + buf[bytes_read++] = _serial->read(); + } + return bytes_read; +} + +void SensecapIndicator::check_packet() +{ + if (pb_rx_size < MT_HEADER_SIZE) { + // We don't even have a header yet + return; + } + + if (pb_rx_buf[0] != MT_MAGIC_0 || pb_rx_buf[1] != MT_MAGIC_1) { + LOG_DEBUG("Got bad magic"); + memset(pb_rx_buf, 0, PB_BUFSIZE); + pb_rx_size = 0; + return; + } + + uint16_t payload_len = pb_rx_buf[2] << 8 | pb_rx_buf[3]; + if ((size_t)payload_len + MT_HEADER_SIZE > PB_BUFSIZE) { + // oversized frame can never complete, resync on the next magic + LOG_DEBUG("Got packet claiming to be ridiculous length"); + memset(pb_rx_buf, 0, PB_BUFSIZE); + pb_rx_size = 0; + return; + } + + if ((size_t)(payload_len + 4) > pb_rx_size) { + // Packet not complete yet + return; + } + + // We have a complete packet, handle it + handle_packet(payload_len); +} + +bool SensecapIndicator::handle_packet(size_t payload_len) +{ + meshtastic_InterdeviceMessage &message = rx_message; + memset(&message, 0, sizeof(message)); + + // Decode the protobuf and shift forward any remaining bytes in the buffer + // (which, if present, belong to the packet that we're going to process on the + // next loop) + pb_istream_t stream = pb_istream_from_buffer(pb_rx_buf + MT_HEADER_SIZE, payload_len); + bool status = pb_decode(&stream, meshtastic_InterdeviceMessage_fields, &message); + memmove(pb_rx_buf, pb_rx_buf + MT_HEADER_SIZE + payload_len, PB_BUFSIZE - MT_HEADER_SIZE - payload_len); + pb_rx_size -= MT_HEADER_SIZE + payload_len; + + if (!status) { + LOG_DEBUG("Decoding failed"); + return false; + } + packets_received++; + switch (message.which_data) { + case meshtastic_InterdeviceMessage_nmea_tag: + // send String to NMEA processing + FakeSerial->stuff_buffer(message.data.nmea, strlen(message.data.nmea)); + return true; + case meshtastic_InterdeviceMessage_i2c_result_tag: + // response for the transaction i2c_transact() is waiting on + if (message.id == expected_id) { + i2c_result = message.data.i2c_result; + i2c_result_ready = true; + } else { + LOG_DEBUG("Drop stale i2c response id=%u", message.id); + } + return true; + case meshtastic_InterdeviceMessage_file_transfer_tag: + if (pending_file && message.id == expected_id) { + *pending_file = message.data.file_transfer; + pending_file = NULL; + file_response_ready = true; + } else { + LOG_DEBUG("Drop stale file response id=%u", message.id); + } + return true; + case meshtastic_InterdeviceMessage_directory_listing_tag: + if (pending_dir && message.id == expected_id) { + *pending_dir = message.data.directory_listing; + pending_dir = NULL; + dir_response_ready = true; + } else { + LOG_DEBUG("Drop stale listing response id=%u", message.id); + } + return true; + case meshtastic_InterdeviceMessage_sd_info_tag: + if (pending_sd_info && message.id == expected_id) { + *pending_sd_info = message.data.sd_info; + pending_sd_info = NULL; + sd_info_ready = true; + } else { + LOG_DEBUG("Drop stale sd info response id=%u", message.id); + } + return true; + default: + // the other messages really only flow downstream + LOG_DEBUG("Got a message of unexpected type"); + return false; + } +} + +bool SensecapIndicator::send(const char *buf, size_t len) +{ + size_t wrote = _serial->write(buf, len); + if (wrote == len) + return true; + return false; +} + +#endif // SENSECAP_INDICATOR \ No newline at end of file diff --git a/src/mesh/IndicatorSerial.h b/src/mesh/IndicatorSerial.h new file mode 100644 index 00000000000..f84b2cc3d47 --- /dev/null +++ b/src/mesh/IndicatorSerial.h @@ -0,0 +1,104 @@ +#pragma once + +#ifndef INDICATORSERIAL_H +#define INDICATORSERIAL_H + +#ifdef SENSECAP_INDICATOR + +#include "concurrency/Lock.h" +#include "concurrency/OSThread.h" +#include "configuration.h" + +#include "mesh/generated/meshtastic/interdevice.pb.h" + +// Magic number at the start of all MT packets +#define MT_MAGIC_0 0x94 +#define MT_MAGIC_1 0xc3 + +// The header is the magic number plus a 16-bit payload-length field +#define MT_HEADER_SIZE 4 + +// Wait this many msec if there's nothing new on the channel +#define NO_NEWS_PAUSE 25 + +#define PB_BUFSIZE meshtastic_InterdeviceMessage_size + MT_HEADER_SIZE + +class SensecapIndicator : public concurrency::OSThread +{ + public: + SensecapIndicator(HardwareSerial &serial); + int32_t runOnce() override; + // Standalone send (e.g. NMEA from FakeUART), takes link_lock to + // serialize the shared TX buffer against the request methods + bool send_uplink(const meshtastic_InterdeviceMessage &message); + + // Send a request and pump the link until the matching response arrives. + // Cooperative threading means nothing else runs while we wait, so the + // response cannot be consumed behind our back. + bool i2c_transact(meshtastic_InterdeviceMessage &request, meshtastic_I2CResult *result, uint32_t timeout_ms = 100); + + // Synchronous file operations against the SD card attached to the RP2040. + // The response (chunk data, file size, status) is written to *out. + // Returns false when the link is down or the request timed out. + bool file_read(const char *path, uint32_t offset, uint32_t length, meshtastic_FileTransfer *out, uint32_t timeout_ms = 1000); + // Sequential chunked write: create=true starts a new file (offset must be 0), + // create=false appends (offset must equal the current file size) + bool file_write(const char *path, uint32_t offset, const uint8_t *data, size_t len, bool create, meshtastic_FileTransfer *out, + uint32_t timeout_ms = 1000); + bool file_remove(const char *path, meshtastic_FileTransfer *out, uint32_t timeout_ms = 1000); + // List directory entries starting at entry number `offset` (paged, + // subdirectories get a trailing slash) + bool list_directory(const char *path, uint32_t offset, meshtastic_DirectoryListing *out, uint32_t timeout_ms = 1000); + // SD card statistics, answered from a cache on the co-processor. + // used/free read zero while its background FAT scan is still running + bool sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms = 2000); + + // True once at least one valid packet was received from the RP2040. + // Pumps the link until then or until the timeout expires. Used to defer + // bridge traffic until the co-processor has booted. + bool wait_ready(uint32_t timeout_ms); + + private: + // The UI task requests map tiles while the main loop pumps the link; + // every send/pump sequence must hold this lock + concurrency::Lock link_lock; + pb_byte_t pb_tx_buf[PB_BUFSIZE]; + pb_byte_t pb_rx_buf[PB_BUFSIZE]; + size_t pb_rx_size = 0; // Number of bytes currently in the buffer + HardwareSerial *_serial = &Serial2; + bool running = false; + uint32_t packets_received = 0; + meshtastic_I2CResult i2c_result = meshtastic_I2CResult_init_zero; + bool i2c_result_ready = false; + // Statically allocated message structs: with 4KB file chunks an + // InterdeviceMessage is ~4.6KB, too large for task stacks. rx is used + // by handle_packet (under link_lock); tx only by the file/dir/sd + // requests, which all originate from the single UI task. + meshtastic_InterdeviceMessage rx_message; + meshtastic_InterdeviceMessage tx_message; + // Response destinations for the file operation in flight + meshtastic_FileTransfer *pending_file = NULL; + meshtastic_DirectoryListing *pending_dir = NULL; + meshtastic_SdCardInfo *pending_sd_info = NULL; + bool file_response_ready = false; + bool dir_response_ready = false; + bool sd_info_ready = false; + // responses echo the id of the request they answer, so a reply that + // arrives after its request timed out cannot satisfy a later request + uint32_t next_request_id = 0; + uint32_t expected_id = 0; + uint32_t stamp_request(meshtastic_InterdeviceMessage &request); + bool send_uplink_unlocked(const meshtastic_InterdeviceMessage &message); + bool file_request(meshtastic_InterdeviceMessage &request, meshtastic_FileTransfer *out, uint32_t timeout_ms); + bool wait_response(bool &flag, uint32_t timeout_ms); + void pump(); + size_t serial_check(char *buf, size_t space_left); + void check_packet(); + bool handle_packet(size_t payload_len); + bool send(const char *buf, size_t len); +}; + +extern SensecapIndicator *sensecapIndicator; + +#endif // SENSECAP_INDICATOR +#endif // INDICATORSERIAL_H \ No newline at end of file diff --git a/src/mesh/comms/FakeI2C.cpp b/src/mesh/comms/FakeI2C.cpp new file mode 100644 index 00000000000..956881acb84 --- /dev/null +++ b/src/mesh/comms/FakeI2C.cpp @@ -0,0 +1,118 @@ +#ifdef SENSECAP_INDICATOR + +#include "FakeI2C.h" +#include "../IndicatorSerial.h" + +FakeI2C *FakeWire = new FakeI2C(); + +void FakeI2C::beginTransmission(uint8_t address) +{ + _txAddress = address; + _txLen = 0; + _txPending = true; +} + +size_t FakeI2C::write(uint8_t data) +{ + if (!_txPending || _txLen >= MAX_WRITE) + return 0; + _txBuf[_txLen++] = data; + return 1; +} + +size_t FakeI2C::write(const uint8_t *data, size_t len) +{ + size_t n = 0; + while (n < len && write(data[n])) + n++; + return n; +} + +uint8_t FakeI2C::endTransmission(bool stopBit) +{ + if (!stopBit) { + // Keep the buffered write pending, it is combined with the following + // requestFrom() into a single write+read transaction + return 0; + } + uint8_t rv = transact(_txAddress, _txBuf, _txLen, 0); + _txLen = 0; + _txPending = false; + return rv; +} + +size_t FakeI2C::requestFrom(uint8_t address, size_t len, bool stopBit) +{ + (void)stopBit; + if (len > MAX_READ) + len = MAX_READ; + + const uint8_t *wbuf = NULL; + size_t wlen = 0; + if (_txPending && _txAddress == address) { + wbuf = _txBuf; + wlen = _txLen; + } + uint8_t rv = transact(address, wbuf, wlen, len); + _txLen = 0; + _txPending = false; + return rv == 0 ? _rxLen : 0; +} + +int FakeI2C::available() +{ + return (int)(_rxLen - _rxPos); +} + +int FakeI2C::read() +{ + return _rxPos < _rxLen ? _rxBuf[_rxPos++] : -1; +} + +int FakeI2C::peek() +{ + return _rxPos < _rxLen ? _rxBuf[_rxPos] : -1; +} + +// Run one tunneled transaction, returns TwoWire endTransmission error codes: +// 0 success, 1 data too long, 2 NACK on address, 3 NACK on data, 4 other, 5 timeout +uint8_t FakeI2C::transact(uint8_t address, const uint8_t *wbuf, size_t wlen, size_t rlen) +{ + _rxLen = 0; + _rxPos = 0; + + if (!sensecapIndicator) + return 4; + + // static: ~4.6KB struct, too large for task stacks; I2C transactions + // only originate from the cooperative main loop + static meshtastic_InterdeviceMessage msg; + memset(&msg, 0, sizeof(msg)); + msg.which_data = meshtastic_InterdeviceMessage_i2c_transaction_tag; + msg.data.i2c_transaction.address = address; + msg.data.i2c_transaction.read_len = rlen; + if (wlen > sizeof(msg.data.i2c_transaction.write_data.bytes)) + return 1; + msg.data.i2c_transaction.write_data.size = wlen; + if (wlen) + memcpy(msg.data.i2c_transaction.write_data.bytes, wbuf, wlen); + + meshtastic_I2CResult result = meshtastic_I2CResult_init_zero; + if (!sensecapIndicator->i2c_transact(msg, &result)) + return 5; + + switch (result.status) { + case meshtastic_I2CResult_Status_OK: + _rxLen = result.read_data.size > MAX_READ ? MAX_READ : result.read_data.size; + memcpy(_rxBuf, result.read_data.bytes, _rxLen); + return 0; + case meshtastic_I2CResult_Status_NACK_ADDRESS: + return 2; + case meshtastic_I2CResult_Status_NACK_DATA: + return 3; + default: + return 4; + } +} + +#endif // SENSECAP_INDICATOR diff --git a/src/mesh/comms/FakeI2C.h b/src/mesh/comms/FakeI2C.h new file mode 100644 index 00000000000..1ba7a27d0b4 --- /dev/null +++ b/src/mesh/comms/FakeI2C.h @@ -0,0 +1,67 @@ +#pragma once + +#ifdef SENSECAP_INDICATOR + +#include "../generated/meshtastic/interdevice.pb.h" +#include + +/** + * TwoWire implementation that tunnels bus traffic to the RP2040 over the + * interdevice serial link, making the sensors attached to the secondary MCU + * usable by the regular sensor drivers of the main firmware. + * + * beginTransmission()/write() buffer an outgoing write. The buffered write is + * executed as a single serial round trip on endTransmission(true). A write + * followed by endTransmission(false) and requestFrom() is combined into one + * write+read transaction with repeated start, matching driver access + * patterns. + * + * Constructed on bus number 0: TwoWire::begin() is final and cannot be + * intercepted, but it returns without touching hardware when the bus is + * already initialized, which is always true for bus 0 (local touch panel). + */ +class FakeI2C : public TwoWire +{ + public: + FakeI2C() : TwoWire(0) {} + + bool end() override { return true; } + bool setClock(uint32_t) override { return true; } + + void beginTransmission(uint8_t address) override; + uint8_t endTransmission(bool stopBit) override; + uint8_t endTransmission() override { return endTransmission(true); } + + size_t requestFrom(uint8_t address, size_t len, bool stopBit) override; + size_t requestFrom(uint8_t address, size_t len) override { return requestFrom(address, len, true); } + + void onReceive(const std::function &) override {} + void onRequest(const std::function &) override {} + + size_t write(uint8_t data) override; + size_t write(const uint8_t *data, size_t len) override; + int available() override; + int read() override; + int peek() override; + void flush() override {} + + private: + // Derived from the generated protobuf limits (interdevice.options) + static const size_t MAX_WRITE = sizeof(meshtastic_I2CTransaction{}.write_data.bytes); + static const size_t MAX_READ = sizeof(meshtastic_I2CResult{}.read_data.bytes); + + uint8_t _txAddress = 0; + uint8_t _txBuf[MAX_WRITE]; + size_t _txLen = 0; + bool _txPending = false; + + uint8_t _rxBuf[MAX_READ]; + size_t _rxLen = 0; + size_t _rxPos = 0; + + uint8_t transact(uint8_t address, const uint8_t *wbuf, size_t wlen, size_t rlen); +}; + +extern FakeI2C *FakeWire; + +#endif // SENSECAP_INDICATOR diff --git a/src/mesh/comms/FakeUART.cpp b/src/mesh/comms/FakeUART.cpp new file mode 100644 index 00000000000..94f9ee16839 --- /dev/null +++ b/src/mesh/comms/FakeUART.cpp @@ -0,0 +1,100 @@ +#include "FakeUART.h" + +#ifdef SENSECAP_INDICATOR + +FakeUART *FakeSerial = new FakeUART(); + +FakeUART::FakeUART() {} + +void FakeUART::begin(unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPin, bool invert, unsigned long timeout_ms, + uint8_t rxfifo_full_thrhd) +{ + baudrate = baud; + buf_clear(); + LOG_DEBUG("FakeUART::begin(%lu)", baud); +} + +void FakeUART::end() +{ + buf_clear(); +} + +int FakeUART::available() +{ + return buf_avail(); +} + +int FakeUART::peek() +{ + if (buf_tail == buf_head) + return -1; + return buf[buf_tail]; +} + +int FakeUART::read() +{ + if (buf_tail == buf_head) + return -1; + uint8_t ret = buf[buf_tail]; + buf_tail = (buf_tail + 1) % BUF_SIZE; + return ret; +} + +void FakeUART::flush(bool wait) +{ + buf_clear(); +} + +uint32_t FakeUART::baudRate() +{ + return baudrate; +} + +void FakeUART::updateBaudRate(unsigned long speed) +{ + baudrate = speed; +} + +size_t FakeUART::setRxBufferSize(size_t size) +{ + return size; +} + +size_t FakeUART::write(const char *buffer) +{ + return write((char *)buffer, strlen(buffer)); +} + +size_t FakeUART::write(uint8_t *buffer, size_t size) +{ + return write((char *)buffer, size); +} + +size_t FakeUART::write(char *buffer, size_t size) +{ + // static: ~4.6KB struct, too large for task stacks; only written from + // the cooperative main loop (GPS thread) + static meshtastic_InterdeviceMessage message; + memset(&message, 0, sizeof(message)); + if (size >= sizeof(message.data.nmea)) { + size = sizeof(message.data.nmea) - 1; // truncate, keep room for the terminator + } + memcpy(message.data.nmea, buffer, size); + message.which_data = meshtastic_InterdeviceMessage_nmea_tag; + LOG_DEBUG("FakeUART::write(%s)", message.data.nmea); + sensecapIndicator->send_uplink(message); + return size; +} + +size_t FakeUART::stuff_buffer(const char *buffer, size_t size) +{ + // push buffer byte-wise, stop when full + for (size_t i = 0; i < size; i++) { + if (!buf_push(buffer[i])) { + return i; + } + } + return size; +} + +#endif // SENSECAP_INDICATOR \ No newline at end of file diff --git a/src/mesh/comms/FakeUART.h b/src/mesh/comms/FakeUART.h new file mode 100644 index 00000000000..4fad9ebb531 --- /dev/null +++ b/src/mesh/comms/FakeUART.h @@ -0,0 +1,62 @@ +#pragma once + +#ifndef FAKEUART_H +#define FAKEUART_H + +#ifdef SENSECAP_INDICATOR + +#include "../IndicatorSerial.h" +#include +#include + +class FakeUART : public Stream +{ + public: + FakeUART(); + + void begin(unsigned long baud, uint32_t config = 0x800001c, int8_t rxPin = -1, int8_t txPin = -1, bool invert = false, + unsigned long timeout_ms = 20000UL, uint8_t rxfifo_full_thrhd = 112); + void end(); + int available(); + int peek(); + int read(); + void flush(bool wait = true); + uint32_t baudRate(); + void updateBaudRate(unsigned long speed); + size_t setRxBufferSize(size_t size); + size_t write(const char *buffer); + size_t write(char *buffer, size_t size); + size_t write(uint8_t *buffer, size_t size); + + size_t stuff_buffer(const char *buffer, size_t size); + virtual size_t write(uint8_t c) { return write(&c, 1); } + + private: + unsigned long baudrate = 115200; + + // Self-contained single-producer/single-consumer byte ring buffer. + // (An external RingBuf.h is ambiguous in this build: SdFat ships a + // conflicting header via device-ui.) + static const size_t BUF_SIZE = 2048; + uint8_t buf[BUF_SIZE]; + volatile size_t buf_head = 0; // write index (producer) + volatile size_t buf_tail = 0; // read index (consumer) + + bool buf_push(uint8_t c) + { + size_t next = (buf_head + 1) % BUF_SIZE; + if (next == buf_tail) + return false; // full + buf[buf_head] = c; + buf_head = next; + return true; + } + void buf_clear() { buf_tail = buf_head; } + size_t buf_avail() { return (buf_head + BUF_SIZE - buf_tail) % BUF_SIZE; } +}; + +extern FakeUART *FakeSerial; + +#endif // SENSECAP_INDICATOR + +#endif // FAKEUART_H \ No newline at end of file diff --git a/src/mesh/generated/meshtastic/interdevice.pb.cpp b/src/mesh/generated/meshtastic/interdevice.pb.cpp index e3913f78c9d..eb8f528d39f 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.cpp +++ b/src/mesh/generated/meshtastic/interdevice.pb.cpp @@ -1,15 +1,33 @@ /* Automatically generated nanopb constant definitions */ -/* Generated by nanopb-0.4.9.1 */ +/* Generated by nanopb-0.4.9 */ #include "meshtastic/interdevice.pb.h" #if PB_PROTO_HEADER_VERSION != 40 #error Regenerate this file with the current version of nanopb generator. #endif -PB_BIND(meshtastic_SensorData, meshtastic_SensorData, AUTO) +PB_BIND(meshtastic_FileTransfer, meshtastic_FileTransfer, 4) + + +PB_BIND(meshtastic_DirectoryListing, meshtastic_DirectoryListing, 4) + + +PB_BIND(meshtastic_I2CTransaction, meshtastic_I2CTransaction, 2) + + +PB_BIND(meshtastic_SdCardInfo, meshtastic_SdCardInfo, AUTO) + + +PB_BIND(meshtastic_I2CResult, meshtastic_I2CResult, 2) + + +PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 4) + + + + -PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 2) diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index c381438ebe6..c316c926663 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -1,5 +1,5 @@ /* Automatically generated nanopb header */ -/* Generated by nanopb-0.4.9.1 */ +/* Generated by nanopb-0.4.9 */ #ifndef PB_MESHTASTIC_MESHTASTIC_INTERDEVICE_PB_H_INCLUDED #define PB_MESHTASTIC_MESHTASTIC_INTERDEVICE_PB_H_INCLUDED @@ -10,38 +10,117 @@ #endif /* Enum definitions */ -typedef enum _meshtastic_MessageType { - meshtastic_MessageType_ACK = 0, - meshtastic_MessageType_COLLECT_INTERVAL = 160, /* in ms */ - meshtastic_MessageType_BEEP_ON = 161, /* duration ms */ - meshtastic_MessageType_BEEP_OFF = 162, /* cancel prematurely */ - meshtastic_MessageType_SHUTDOWN = 163, - meshtastic_MessageType_POWER_ON = 164, - meshtastic_MessageType_SCD41_TEMP = 176, - meshtastic_MessageType_SCD41_HUMIDITY = 177, - meshtastic_MessageType_SCD41_CO2 = 178, - meshtastic_MessageType_AHT20_TEMP = 179, - meshtastic_MessageType_AHT20_HUMIDITY = 180, - meshtastic_MessageType_TVOC_INDEX = 181 -} meshtastic_MessageType; +/* Defines the supported file operations */ +typedef enum _meshtastic_FileOperation { + meshtastic_FileOperation_GET = 0, + meshtastic_FileOperation_POST = 1, + meshtastic_FileOperation_PUT = 2, + meshtastic_FileOperation_DELETE = 3 +} meshtastic_FileOperation; + +typedef enum _meshtastic_SdCardInfo_CardType { + meshtastic_SdCardInfo_CardType_NONE = 0, + meshtastic_SdCardInfo_CardType_MMC = 1, + meshtastic_SdCardInfo_CardType_SD = 2, + meshtastic_SdCardInfo_CardType_SDHC = 3, + meshtastic_SdCardInfo_CardType_SDXC = 4, + meshtastic_SdCardInfo_CardType_UNKNOWN_CARD = 5 +} meshtastic_SdCardInfo_CardType; + +typedef enum _meshtastic_SdCardInfo_FatType { + meshtastic_SdCardInfo_FatType_UNKNOWN_FAT = 0, + meshtastic_SdCardInfo_FatType_FAT16 = 1, + meshtastic_SdCardInfo_FatType_FAT32 = 2, + meshtastic_SdCardInfo_FatType_EXFAT = 3 +} meshtastic_SdCardInfo_FatType; + +typedef enum _meshtastic_I2CResult_Status { + meshtastic_I2CResult_Status_OK = 0, + meshtastic_I2CResult_Status_NACK_ADDRESS = 1, + meshtastic_I2CResult_Status_NACK_DATA = 2, + meshtastic_I2CResult_Status_ERROR = 3 +} meshtastic_I2CResult_Status; /* Struct definitions */ -typedef struct _meshtastic_SensorData { - /* The message type */ - meshtastic_MessageType type; - pb_size_t which_data; - union { - float float_value; - uint32_t uint32_value; - } data; -} meshtastic_SensorData; +typedef PB_BYTES_ARRAY_T(4096) meshtastic_FileTransfer_filedata_t; +/* Message for file operations */ +typedef struct _meshtastic_FileTransfer { + meshtastic_FileOperation operation; /* File operation (GET, POST, PUT, DELETE) */ + char filepath[255]; /* Path of the file on the SD card */ + meshtastic_FileTransfer_filedata_t filedata; /* Chunk content (POST/PUT request, GET response) */ + bool success; /* Response: Was the operation successful? */ + char message[255]; /* Response: Status message */ + uint64_t offset; /* Byte offset of this chunk within the file (ranged GET/PUT) */ + /* GET request: number of bytes to read, 0 = max chunk size. A response + carries at most the filedata max_size (see interdevice.options) per + chunk; larger requests are truncated, visible in the filedata length. */ + uint32_t length; + uint64_t file_size; /* GET response: total size of the file */ +} meshtastic_FileTransfer; +/* Message for structured directory listing */ +typedef struct _meshtastic_DirectoryListing { + char directory[255]; /* Path of the directory */ + /* One page of entry names, full FAT LFN length so they round-trip into + FileTransfer.filepath. Page size is the max_count in + interdevice.options; page through with offset and total_count. */ + pb_size_t filenames_count; + char filenames[16][256]; + bool success; /* Response: Was the operation successful? */ + char message[255]; /* Response: Status message */ + uint32_t offset; /* Request: skip this many entries (paging) */ + uint32_t total_count; /* Response: total number of entries in the directory */ +} meshtastic_DirectoryListing; + +typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CTransaction_write_data_t; +/* A single I2C transaction: an optional write followed by an optional + read with repeated start, matching the TwoWire usage of sensor drivers + (beginTransmission/write.../endTransmission(false)/requestFrom) */ +typedef struct _meshtastic_I2CTransaction { + uint32_t address; /* 7-bit device address */ + meshtastic_I2CTransaction_write_data_t write_data; /* Bytes to write, may be empty */ + /* Number of bytes to read after the write, 0 = write-only. Bounded by + the read_data max_size of I2CResult (see interdevice.options); larger + requests are truncated, visible in the returned byte count. */ + uint32_t read_len; +} meshtastic_I2CTransaction; + +/* SD card statistics */ +typedef struct _meshtastic_SdCardInfo { + bool present; /* Card initialized and usable */ + meshtastic_SdCardInfo_CardType card_type; + meshtastic_SdCardInfo_FatType fat_type; + uint64_t card_size; /* Filesystem size in bytes */ + uint64_t used_bytes; /* Used bytes (may be expensive to compute on FAT32) */ + uint64_t free_bytes; /* Free bytes */ +} meshtastic_SdCardInfo; + +typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CResult_read_data_t; +/* Result of an I2CTransaction */ +typedef struct _meshtastic_I2CResult { + meshtastic_I2CResult_Status status; + meshtastic_I2CResult_read_data_t read_data; /* Data read from the device, empty for write-only transactions */ +} meshtastic_I2CResult; + +typedef PB_BYTES_ARRAY_T(128) meshtastic_InterdeviceMessage_i2c_scan_result_t; +/* Main message for interdevice communication */ typedef struct _meshtastic_InterdeviceMessage { pb_size_t which_data; union { char nmea[1024]; - meshtastic_SensorData sensor; + uint16_t beep; + meshtastic_I2CTransaction i2c_transaction; + meshtastic_I2CResult i2c_result; + bool i2c_scan; /* Request: scan the secondary I2C bus */ + meshtastic_InterdeviceMessage_i2c_scan_result_t i2c_scan_result; /* Response: 7-bit addresses of discovered devices */ + meshtastic_FileTransfer file_transfer; + meshtastic_DirectoryListing directory_listing; + bool get_sd_info; /* Request: SD card statistics */ + meshtastic_SdCardInfo sd_info; /* Response */ } data; + /* Correlates a response with its request: responses echo the id of the + request they answer. 0 for unsolicited messages (e.g. the nmea stream). */ + uint32_t id; } meshtastic_InterdeviceMessage; @@ -50,53 +129,174 @@ extern "C" { #endif /* Helper constants for enums */ -#define _meshtastic_MessageType_MIN meshtastic_MessageType_ACK -#define _meshtastic_MessageType_MAX meshtastic_MessageType_TVOC_INDEX -#define _meshtastic_MessageType_ARRAYSIZE ((meshtastic_MessageType)(meshtastic_MessageType_TVOC_INDEX+1)) +#define _meshtastic_FileOperation_MIN meshtastic_FileOperation_GET +#define _meshtastic_FileOperation_MAX meshtastic_FileOperation_DELETE +#define _meshtastic_FileOperation_ARRAYSIZE ((meshtastic_FileOperation)(meshtastic_FileOperation_DELETE+1)) + +#define _meshtastic_SdCardInfo_CardType_MIN meshtastic_SdCardInfo_CardType_NONE +#define _meshtastic_SdCardInfo_CardType_MAX meshtastic_SdCardInfo_CardType_UNKNOWN_CARD +#define _meshtastic_SdCardInfo_CardType_ARRAYSIZE ((meshtastic_SdCardInfo_CardType)(meshtastic_SdCardInfo_CardType_UNKNOWN_CARD+1)) -#define meshtastic_SensorData_type_ENUMTYPE meshtastic_MessageType +#define _meshtastic_SdCardInfo_FatType_MIN meshtastic_SdCardInfo_FatType_UNKNOWN_FAT +#define _meshtastic_SdCardInfo_FatType_MAX meshtastic_SdCardInfo_FatType_EXFAT +#define _meshtastic_SdCardInfo_FatType_ARRAYSIZE ((meshtastic_SdCardInfo_FatType)(meshtastic_SdCardInfo_FatType_EXFAT+1)) + +#define _meshtastic_I2CResult_Status_MIN meshtastic_I2CResult_Status_OK +#define _meshtastic_I2CResult_Status_MAX meshtastic_I2CResult_Status_ERROR +#define _meshtastic_I2CResult_Status_ARRAYSIZE ((meshtastic_I2CResult_Status)(meshtastic_I2CResult_Status_ERROR+1)) + +#define meshtastic_FileTransfer_operation_ENUMTYPE meshtastic_FileOperation + + + +#define meshtastic_SdCardInfo_card_type_ENUMTYPE meshtastic_SdCardInfo_CardType +#define meshtastic_SdCardInfo_fat_type_ENUMTYPE meshtastic_SdCardInfo_FatType + +#define meshtastic_I2CResult_status_ENUMTYPE meshtastic_I2CResult_Status /* Initializer values for message structs */ -#define meshtastic_SensorData_init_default {_meshtastic_MessageType_MIN, 0, {0}} -#define meshtastic_InterdeviceMessage_init_default {0, {""}} -#define meshtastic_SensorData_init_zero {_meshtastic_MessageType_MIN, 0, {0}} -#define meshtastic_InterdeviceMessage_init_zero {0, {""}} +#define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, 0, "", 0, 0, 0} +#define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, 0, "", 0, 0} +#define meshtastic_I2CTransaction_init_default {0, {0, {0}}, 0} +#define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0} +#define meshtastic_I2CResult_init_default {_meshtastic_I2CResult_Status_MIN, {0, {0}}} +#define meshtastic_InterdeviceMessage_init_default {0, {""}, 0} +#define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, 0, "", 0, 0, 0} +#define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, 0, "", 0, 0} +#define meshtastic_I2CTransaction_init_zero {0, {0, {0}}, 0} +#define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0} +#define meshtastic_I2CResult_init_zero {_meshtastic_I2CResult_Status_MIN, {0, {0}}} +#define meshtastic_InterdeviceMessage_init_zero {0, {""}, 0} /* Field tags (for use in manual encoding/decoding) */ -#define meshtastic_SensorData_type_tag 1 -#define meshtastic_SensorData_float_value_tag 2 -#define meshtastic_SensorData_uint32_value_tag 3 +#define meshtastic_FileTransfer_operation_tag 1 +#define meshtastic_FileTransfer_filepath_tag 2 +#define meshtastic_FileTransfer_filedata_tag 3 +#define meshtastic_FileTransfer_success_tag 4 +#define meshtastic_FileTransfer_message_tag 5 +#define meshtastic_FileTransfer_offset_tag 6 +#define meshtastic_FileTransfer_length_tag 7 +#define meshtastic_FileTransfer_file_size_tag 8 +#define meshtastic_DirectoryListing_directory_tag 1 +#define meshtastic_DirectoryListing_filenames_tag 2 +#define meshtastic_DirectoryListing_success_tag 3 +#define meshtastic_DirectoryListing_message_tag 4 +#define meshtastic_DirectoryListing_offset_tag 5 +#define meshtastic_DirectoryListing_total_count_tag 6 +#define meshtastic_I2CTransaction_address_tag 1 +#define meshtastic_I2CTransaction_write_data_tag 2 +#define meshtastic_I2CTransaction_read_len_tag 3 +#define meshtastic_SdCardInfo_present_tag 1 +#define meshtastic_SdCardInfo_card_type_tag 2 +#define meshtastic_SdCardInfo_fat_type_tag 3 +#define meshtastic_SdCardInfo_card_size_tag 4 +#define meshtastic_SdCardInfo_used_bytes_tag 5 +#define meshtastic_SdCardInfo_free_bytes_tag 6 +#define meshtastic_I2CResult_status_tag 1 +#define meshtastic_I2CResult_read_data_tag 2 #define meshtastic_InterdeviceMessage_nmea_tag 1 -#define meshtastic_InterdeviceMessage_sensor_tag 2 +#define meshtastic_InterdeviceMessage_beep_tag 2 +#define meshtastic_InterdeviceMessage_i2c_transaction_tag 3 +#define meshtastic_InterdeviceMessage_i2c_result_tag 4 +#define meshtastic_InterdeviceMessage_i2c_scan_tag 5 +#define meshtastic_InterdeviceMessage_i2c_scan_result_tag 6 +#define meshtastic_InterdeviceMessage_file_transfer_tag 7 +#define meshtastic_InterdeviceMessage_directory_listing_tag 8 +#define meshtastic_InterdeviceMessage_get_sd_info_tag 9 +#define meshtastic_InterdeviceMessage_sd_info_tag 10 +#define meshtastic_InterdeviceMessage_id_tag 15 /* Struct field encoding specification for nanopb */ -#define meshtastic_SensorData_FIELDLIST(X, a) \ -X(a, STATIC, SINGULAR, UENUM, type, 1) \ -X(a, STATIC, ONEOF, FLOAT, (data,float_value,data.float_value), 2) \ -X(a, STATIC, ONEOF, UINT32, (data,uint32_value,data.uint32_value), 3) -#define meshtastic_SensorData_CALLBACK NULL -#define meshtastic_SensorData_DEFAULT NULL +#define meshtastic_FileTransfer_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, operation, 1) \ +X(a, STATIC, SINGULAR, STRING, filepath, 2) \ +X(a, STATIC, SINGULAR, BYTES, filedata, 3) \ +X(a, STATIC, SINGULAR, BOOL, success, 4) \ +X(a, STATIC, SINGULAR, STRING, message, 5) \ +X(a, STATIC, SINGULAR, UINT64, offset, 6) \ +X(a, STATIC, SINGULAR, UINT32, length, 7) \ +X(a, STATIC, SINGULAR, UINT64, file_size, 8) +#define meshtastic_FileTransfer_CALLBACK NULL +#define meshtastic_FileTransfer_DEFAULT NULL + +#define meshtastic_DirectoryListing_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, directory, 1) \ +X(a, STATIC, REPEATED, STRING, filenames, 2) \ +X(a, STATIC, SINGULAR, BOOL, success, 3) \ +X(a, STATIC, SINGULAR, STRING, message, 4) \ +X(a, STATIC, SINGULAR, UINT32, offset, 5) \ +X(a, STATIC, SINGULAR, UINT32, total_count, 6) +#define meshtastic_DirectoryListing_CALLBACK NULL +#define meshtastic_DirectoryListing_DEFAULT NULL + +#define meshtastic_I2CTransaction_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, address, 1) \ +X(a, STATIC, SINGULAR, BYTES, write_data, 2) \ +X(a, STATIC, SINGULAR, UINT32, read_len, 3) +#define meshtastic_I2CTransaction_CALLBACK NULL +#define meshtastic_I2CTransaction_DEFAULT NULL + +#define meshtastic_SdCardInfo_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, present, 1) \ +X(a, STATIC, SINGULAR, UENUM, card_type, 2) \ +X(a, STATIC, SINGULAR, UENUM, fat_type, 3) \ +X(a, STATIC, SINGULAR, UINT64, card_size, 4) \ +X(a, STATIC, SINGULAR, UINT64, used_bytes, 5) \ +X(a, STATIC, SINGULAR, UINT64, free_bytes, 6) +#define meshtastic_SdCardInfo_CALLBACK NULL +#define meshtastic_SdCardInfo_DEFAULT NULL + +#define meshtastic_I2CResult_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, status, 1) \ +X(a, STATIC, SINGULAR, BYTES, read_data, 2) +#define meshtastic_I2CResult_CALLBACK NULL +#define meshtastic_I2CResult_DEFAULT NULL #define meshtastic_InterdeviceMessage_FIELDLIST(X, a) \ X(a, STATIC, ONEOF, STRING, (data,nmea,data.nmea), 1) \ -X(a, STATIC, ONEOF, MESSAGE, (data,sensor,data.sensor), 2) +X(a, STATIC, ONEOF, UINT32, (data,beep,data.beep), 2) \ +X(a, STATIC, ONEOF, MESSAGE, (data,i2c_transaction,data.i2c_transaction), 3) \ +X(a, STATIC, ONEOF, MESSAGE, (data,i2c_result,data.i2c_result), 4) \ +X(a, STATIC, ONEOF, BOOL, (data,i2c_scan,data.i2c_scan), 5) \ +X(a, STATIC, ONEOF, BYTES, (data,i2c_scan_result,data.i2c_scan_result), 6) \ +X(a, STATIC, ONEOF, MESSAGE, (data,file_transfer,data.file_transfer), 7) \ +X(a, STATIC, ONEOF, MESSAGE, (data,directory_listing,data.directory_listing), 8) \ +X(a, STATIC, ONEOF, BOOL, (data,get_sd_info,data.get_sd_info), 9) \ +X(a, STATIC, ONEOF, MESSAGE, (data,sd_info,data.sd_info), 10) \ +X(a, STATIC, SINGULAR, UINT32, id, 15) #define meshtastic_InterdeviceMessage_CALLBACK NULL #define meshtastic_InterdeviceMessage_DEFAULT NULL -#define meshtastic_InterdeviceMessage_data_sensor_MSGTYPE meshtastic_SensorData +#define meshtastic_InterdeviceMessage_data_i2c_transaction_MSGTYPE meshtastic_I2CTransaction +#define meshtastic_InterdeviceMessage_data_i2c_result_MSGTYPE meshtastic_I2CResult +#define meshtastic_InterdeviceMessage_data_file_transfer_MSGTYPE meshtastic_FileTransfer +#define meshtastic_InterdeviceMessage_data_directory_listing_MSGTYPE meshtastic_DirectoryListing +#define meshtastic_InterdeviceMessage_data_sd_info_MSGTYPE meshtastic_SdCardInfo -extern const pb_msgdesc_t meshtastic_SensorData_msg; +extern const pb_msgdesc_t meshtastic_FileTransfer_msg; +extern const pb_msgdesc_t meshtastic_DirectoryListing_msg; +extern const pb_msgdesc_t meshtastic_I2CTransaction_msg; +extern const pb_msgdesc_t meshtastic_SdCardInfo_msg; +extern const pb_msgdesc_t meshtastic_I2CResult_msg; extern const pb_msgdesc_t meshtastic_InterdeviceMessage_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ -#define meshtastic_SensorData_fields &meshtastic_SensorData_msg +#define meshtastic_FileTransfer_fields &meshtastic_FileTransfer_msg +#define meshtastic_DirectoryListing_fields &meshtastic_DirectoryListing_msg +#define meshtastic_I2CTransaction_fields &meshtastic_I2CTransaction_msg +#define meshtastic_SdCardInfo_fields &meshtastic_SdCardInfo_msg +#define meshtastic_I2CResult_fields &meshtastic_I2CResult_msg #define meshtastic_InterdeviceMessage_fields &meshtastic_InterdeviceMessage_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_INTERDEVICE_PB_H_MAX_SIZE meshtastic_InterdeviceMessage_size -#define meshtastic_InterdeviceMessage_size 1026 -#define meshtastic_SensorData_size 9 +#define meshtastic_DirectoryListing_size 4656 +#define meshtastic_FileTransfer_size 4645 +#define meshtastic_I2CResult_size 261 +#define meshtastic_I2CTransaction_size 271 +#define meshtastic_InterdeviceMessage_size 4665 +#define meshtastic_SdCardInfo_size 39 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 876d9ca0759..98003ecec42 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -123,10 +123,6 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "Sensor/SPA06Sensor.h" #endif -#ifdef SENSECAP_INDICATOR -#include "Sensor/IndicatorSensor.h" -#endif - #if __has_include() #include "Sensor/TSL2561Sensor.h" #endif @@ -167,10 +163,6 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) // Not a real I2C device addSensor(i2cScanner, ScanI2C::DeviceType::NONE); #else -#ifdef SENSECAP_INDICATOR - // Not a real I2C device, uses UART - addSensor(i2cScanner, ScanI2C::DeviceType::NONE); -#endif #if HAS_SPA06 && __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::SPA06); #endif diff --git a/src/modules/Telemetry/Sensor/IndicatorSensor.cpp b/src/modules/Telemetry/Sensor/IndicatorSensor.cpp deleted file mode 100644 index 26a4bc5fc52..00000000000 --- a/src/modules/Telemetry/Sensor/IndicatorSensor.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(SENSECAP_INDICATOR) - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "IndicatorSensor.h" -#include "TelemetrySensor.h" -#include "serialization/cobs.h" -#include -#include - -IndicatorSensor::IndicatorSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SENSOR_UNSET, "Indicator") {} - -#define SENSOR_BUF_SIZE (512) - -uint8_t buf[SENSOR_BUF_SIZE]; // recv -uint8_t data[SENSOR_BUF_SIZE]; // decode - -#define ACK_PKT_PARA "ACK" - -enum sensor_pkt_type { - PKT_TYPE_ACK = 0x00, // uin32_t - PKT_TYPE_CMD_COLLECT_INTERVAL = 0xA0, // uin32_t - PKT_TYPE_CMD_BEEP_ON = 0xA1, // uin32_t ms: on time - PKT_TYPE_CMD_BEEP_OFF = 0xA2, - PKT_TYPE_CMD_SHUTDOWN = 0xA3, // uin32_t - PKT_TYPE_CMD_POWER_ON = 0xA4, - PKT_TYPE_SENSOR_SCD41_TEMP = 0xB0, // float - PKT_TYPE_SENSOR_SCD41_HUMIDITY = 0xB1, // float - PKT_TYPE_SENSOR_SCD41_CO2 = 0xB2, // float - PKT_TYPE_SENSOR_AHT20_TEMP = 0xB3, // float - PKT_TYPE_SENSOR_AHT20_HUMIDITY = 0xB4, // float - PKT_TYPE_SENSOR_TVOC_INDEX = 0xB5, // float -}; - -static int cmd_send(uint8_t cmd, const char *p_data, uint8_t len) -{ - uint8_t send_buf[32] = {0}; - uint8_t send_data[32] = {0}; - - if (len > 31) { - return -1; - } - - uint8_t index = 1; - - send_data[0] = cmd; - - if (len > 0 && p_data != NULL) { - memcpy(&send_data[1], p_data, len); - index += len; - } - cobs_encode_result ret = cobs_encode(send_buf, sizeof(send_buf), send_data, index); - - // LOG_DEBUG("cobs TX status:%d, len:%d, type 0x%x", ret.status, ret.out_len, cmd); - - if (ret.status == COBS_ENCODE_OK) { - return uart_write_bytes(SENSOR_PORT_NUM, send_buf, ret.out_len + 1); - } - - return -1; -} - -bool IndicatorSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("%s: init", sensorName); - setup(); - return true; -} - -void IndicatorSensor::setup() -{ - uart_config_t uart_config = { - .baud_rate = SENSOR_BAUD_RATE, - .data_bits = UART_DATA_8_BITS, - .parity = UART_PARITY_DISABLE, - .stop_bits = UART_STOP_BITS_1, - .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, - .source_clk = UART_SCLK_APB, - }; - int intr_alloc_flags = 0; - char buffer[11]; - - uart_driver_install(SENSOR_PORT_NUM, SENSOR_BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags); - uart_param_config(SENSOR_PORT_NUM, &uart_config); - uart_set_pin(SENSOR_PORT_NUM, SENSOR_RP2040_TXD, SENSOR_RP2040_RXD, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); - cmd_send(PKT_TYPE_CMD_POWER_ON, NULL, 0); - // measure and send only once every minute, for the phone API - const char *interval = ultoa(60000, buffer, 10); - cmd_send(PKT_TYPE_CMD_COLLECT_INTERVAL, interval, strlen(interval) + 1); -} - -bool IndicatorSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - cobs_decode_result ret; - int len = uart_read_bytes(SENSOR_PORT_NUM, buf, (SENSOR_BUF_SIZE - 1), 100 / portTICK_PERIOD_MS); - - float value = 0.0; - uint8_t *p_buf_start = buf; - uint8_t *p_buf_end = buf; - if (len > 0) { - while (p_buf_start < (buf + len)) { - p_buf_end = p_buf_start; - while (p_buf_end < (buf + len)) { - if (*p_buf_end == 0x00) { - break; - } - p_buf_end++; - } - // decode buf - memset(data, 0, sizeof(data)); - ret = cobs_decode(data, sizeof(data), p_buf_start, p_buf_end - p_buf_start); - - // LOG_DEBUG("cobs RX status:%d, len:%d, type:0x%x ", ret.status, ret.out_len, data[0]); - - if (ret.out_len > 1 && ret.status == COBS_DECODE_OK) { - - value = 0.0; - uint8_t pkt_type = data[0]; - switch (pkt_type) { - case PKT_TYPE_SENSOR_SCD41_CO2: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("CO2: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - break; - } - - case PKT_TYPE_SENSOR_AHT20_TEMP: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("Temp: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.temperature = value; - break; - } - - case PKT_TYPE_SENSOR_AHT20_HUMIDITY: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("Humidity: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.relative_humidity = value; - break; - } - - case PKT_TYPE_SENSOR_TVOC_INDEX: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("Tvoc: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - measurement->variant.environment_metrics.has_iaq = true; - measurement->variant.environment_metrics.iaq = value; - break; - } - default: - break; - } - } - - p_buf_start = p_buf_end + 1; // next message - } - return true; - } - return false; -} - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/IndicatorSensor.h b/src/modules/Telemetry/Sensor/IndicatorSensor.h deleted file mode 100644 index 22a0d9c833c..00000000000 --- a/src/modules/Telemetry/Sensor/IndicatorSensor.h +++ /dev/null @@ -1,19 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "TelemetrySensor.h" - -class IndicatorSensor : public TelemetrySensor -{ - public: - IndicatorSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; - - private: - void setup(); -}; - -#endif \ No newline at end of file diff --git a/src/serialization/cobs.cpp b/src/serialization/cobs.cpp deleted file mode 100644 index afb868f50fa..00000000000 --- a/src/serialization/cobs.cpp +++ /dev/null @@ -1,129 +0,0 @@ -#include "cobs.h" -#include - -#ifdef SENSECAP_INDICATOR - -cobs_encode_result cobs_encode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) -{ - - cobs_encode_result result = {0, COBS_ENCODE_OK}; - - if (!dst_buf_ptr || !src_ptr) { - result.status = COBS_ENCODE_NULL_POINTER; - return result; - } - - const uint8_t *src_read_ptr = src_ptr; - const uint8_t *src_end_ptr = src_read_ptr + src_len; - uint8_t *dst_buf_start_ptr = dst_buf_ptr; - uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; - uint8_t *dst_code_write_ptr = dst_buf_ptr; - uint8_t *dst_write_ptr = dst_code_write_ptr + 1; - uint8_t search_len = 1; - - if (src_len != 0) { - for (;;) { - if (dst_write_ptr >= dst_buf_end_ptr) { - result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); - break; - } - - uint8_t src_byte = *src_read_ptr++; - if (src_byte == 0) { - *dst_code_write_ptr = search_len; - dst_code_write_ptr = dst_write_ptr++; - search_len = 1; - if (src_read_ptr >= src_end_ptr) { - break; - } - } else { - *dst_write_ptr++ = src_byte; - search_len++; - if (src_read_ptr >= src_end_ptr) { - break; - } - if (search_len == 0xFF) { - *dst_code_write_ptr = search_len; - dst_code_write_ptr = dst_write_ptr++; - search_len = 1; - } - } - } - } - - if (dst_code_write_ptr >= dst_buf_end_ptr) { - result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); - dst_write_ptr = dst_buf_end_ptr; - } else { - *dst_code_write_ptr = search_len; - } - - result.out_len = dst_write_ptr - dst_buf_start_ptr; - - return result; -} - -cobs_decode_result cobs_decode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) -{ - cobs_decode_result result = {0, COBS_DECODE_OK}; - - if (!dst_buf_ptr || !src_ptr) { - result.status = COBS_DECODE_NULL_POINTER; - return result; - } - - const uint8_t *src_read_ptr = src_ptr; - const uint8_t *src_end_ptr = src_read_ptr + src_len; - uint8_t *dst_buf_start_ptr = dst_buf_ptr; - const uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; - uint8_t *dst_write_ptr = dst_buf_ptr; - - if (src_len != 0) { - for (;;) { - uint8_t len_code = *src_read_ptr++; - if (len_code == 0) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); - break; - } - len_code--; - - size_t remaining_bytes = src_end_ptr - src_read_ptr; - if (len_code > remaining_bytes) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_INPUT_TOO_SHORT); - len_code = remaining_bytes; - } - - remaining_bytes = dst_buf_end_ptr - dst_write_ptr; - if (len_code > remaining_bytes) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); - len_code = remaining_bytes; - } - - for (uint8_t i = len_code; i != 0; i--) { - uint8_t src_byte = *src_read_ptr++; - if (src_byte == 0) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); - } - *dst_write_ptr++ = src_byte; - } - - if (src_read_ptr >= src_end_ptr) { - break; - } - - if (len_code != 0xFE) { - if (dst_write_ptr >= dst_buf_end_ptr) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); - break; - } - *dst_write_ptr++ = 0; - } - } - } - - result.out_len = dst_write_ptr - dst_buf_start_ptr; - - return result; -} - -#endif \ No newline at end of file diff --git a/src/serialization/cobs.h b/src/serialization/cobs.h deleted file mode 100644 index f95e61f6261..00000000000 --- a/src/serialization/cobs.h +++ /dev/null @@ -1,75 +0,0 @@ -#ifndef COBS_H_ -#define COBS_H_ - -#include "configuration.h" - -#ifdef SENSECAP_INDICATOR - -#include -#include - -#define COBS_ENCODE_DST_BUF_LEN_MAX(SRC_LEN) ((SRC_LEN) + (((SRC_LEN) + 253u) / 254u)) -#define COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) (((SRC_LEN) == 0) ? 0u : ((SRC_LEN)-1u)) -#define COBS_ENCODE_SRC_OFFSET(SRC_LEN) (((SRC_LEN) + 253u) / 254u) - -typedef enum { - COBS_ENCODE_OK = 0x00, - COBS_ENCODE_NULL_POINTER = 0x01, - COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 -} cobs_encode_status; - -typedef struct { - size_t out_len; - cobs_encode_status status; -} cobs_encode_result; - -typedef enum { - COBS_DECODE_OK = 0x00, - COBS_DECODE_NULL_POINTER = 0x01, - COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, - COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, - COBS_DECODE_INPUT_TOO_SHORT = 0x08 -} cobs_decode_status; - -typedef struct { - size_t out_len; - cobs_decode_status status; -} cobs_decode_result; - -#ifdef __cplusplus -extern "C" { -#endif - -/* COBS-encode a string of input bytes. - * - * dst_buf_ptr: The buffer into which the result will be written - * dst_buf_len: Length of the buffer into which the result will be written - * src_ptr: The byte string to be encoded - * src_len Length of the byte string to be encoded - * - * returns: A struct containing the success status of the encoding - * operation and the length of the result (that was written to - * dst_buf_ptr) - */ -cobs_encode_result cobs_encode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len); - -/* Decode a COBS byte string. - * - * dst_buf_ptr: The buffer into which the result will be written - * dst_buf_len: Length of the buffer into which the result will be written - * src_ptr: The byte string to be decoded - * src_len Length of the byte string to be decoded - * - * returns: A struct containing the success status of the decoding - * operation and the length of the result (that was written to - * dst_buf_ptr) - */ -cobs_decode_result cobs_decode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len); - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -#endif /* SENSECAP_INDICATOR */ - -#endif /* COBS_H_ */ diff --git a/variants/esp32s3/seeed-sensecap-indicator/variant.h b/variants/esp32s3/seeed-sensecap-indicator/variant.h index b59e6e059a5..61505a24b48 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/variant.h +++ b/variants/esp32s3/seeed-sensecap-indicator/variant.h @@ -5,7 +5,7 @@ #define SENSOR_RP2040_TXD 19 #define SENSOR_RP2040_RXD 20 #define SENSOR_PORT_NUM UART_NUM_2 -#define SENSOR_BAUD_RATE 115200 +#define SENSOR_BAUD_RATE 2000000 #define BUTTON_PIN 38 #define BUTTON_ACTIVE_LOW true @@ -46,13 +46,7 @@ #define TOUCH_I2C_PORT 0 #define TOUCH_SLAVE_ADDRESS 0x48 -// in future, we may want to add a buzzer and add all sensors to the indicator via a data protocol for now only GPS is supported -// // Buzzer -// #define PIN_BUZZER 19 - #define GPS_DEFAULT_NOT_PRESENT 1 -#define GPS_RX_PIN 20 -#define GPS_TX_PIN 19 #define HAS_GPS 1 #define USE_SX1262 @@ -83,3 +77,7 @@ #define USE_VIRTUAL_KEYBOARD 1 #define DISPLAY_CLOCK_FRAME 1 + +// this powers the RP 2040 on boot. +#define SENSOR_POWER_CTRL_EXPANDER (8 | IO_EXPANDER) +#define SENSOR_POWER_ON_EXPANDER 1 From 082e9d81207ac1c15fe2d95d23144f0501680b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sun, 12 Jul 2026 20:18:44 +0200 Subject: [PATCH 02/28] indicator: address review Correlate responses with request ids, serialize the shared TX buffer, reject oversized frames, fix RX buffer overflow and NMEA truncation, full-length file paths. --- src/mesh/generated/meshtastic/interdevice.pb.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index c316c926663..e8f13d78735 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -46,7 +46,7 @@ typedef PB_BYTES_ARRAY_T(4096) meshtastic_FileTransfer_filedata_t; /* Message for file operations */ typedef struct _meshtastic_FileTransfer { meshtastic_FileOperation operation; /* File operation (GET, POST, PUT, DELETE) */ - char filepath[255]; /* Path of the file on the SD card */ + char filepath[256]; /* Path of the file on the SD card */ meshtastic_FileTransfer_filedata_t filedata; /* Chunk content (POST/PUT request, GET response) */ bool success; /* Response: Was the operation successful? */ char message[255]; /* Response: Status message */ @@ -60,7 +60,7 @@ typedef struct _meshtastic_FileTransfer { /* Message for structured directory listing */ typedef struct _meshtastic_DirectoryListing { - char directory[255]; /* Path of the directory */ + char directory[256]; /* Path of the directory */ /* One page of entry names, full FAT LFN length so they round-trip into FileTransfer.filepath. Page size is the max_count in interdevice.options; page through with offset and total_count. */ @@ -291,11 +291,11 @@ extern const pb_msgdesc_t meshtastic_InterdeviceMessage_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_INTERDEVICE_PB_H_MAX_SIZE meshtastic_InterdeviceMessage_size -#define meshtastic_DirectoryListing_size 4656 -#define meshtastic_FileTransfer_size 4645 +#define meshtastic_DirectoryListing_size 4657 +#define meshtastic_FileTransfer_size 4646 #define meshtastic_I2CResult_size 261 #define meshtastic_I2CTransaction_size 271 -#define meshtastic_InterdeviceMessage_size 4665 +#define meshtastic_InterdeviceMessage_size 4666 #define meshtastic_SdCardInfo_size 39 #ifdef __cplusplus From 50eb4ec489cbeca54879c1658885c455a2ee043b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sun, 12 Jul 2026 20:36:50 +0200 Subject: [PATCH 03/28] indicator: assign the GPS FakeUART at runtime Static initialization order across translation units is undefined, so createGps() assigns and null-checks the bridged serial instead. Bound the NMEA length defensively. --- src/gps/GPS.cpp | 10 ++++++++-- src/mesh/IndicatorSerial.cpp | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index d5e3d751c42..9aa2c29e6bf 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -47,7 +47,7 @@ template std::size_t array_count(const T (&)[N]) #endif #if defined(SENSECAP_INDICATOR) -FakeUART *GPS::_serial_gps = FakeSerial; +FakeUART *GPS::_serial_gps = nullptr; // assigned in createGps(), see there #elif defined(ARCH_NRF52) Uart *GPS::_serial_gps = &GPS_SERIAL_PORT; #elif defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32) @@ -1914,7 +1914,13 @@ std::unique_ptr GPS::createGps() } else return nullptr; #endif -#if !defined(SENSECAP_INDICATOR) +#if defined(SENSECAP_INDICATOR) + // assigned at runtime, static initialization order across translation + // units is undefined + _serial_gps = FakeSerial; + if (!_serial_gps) + return nullptr; +#else if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all return nullptr; #endif diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp index d3ad797cc22..391054989d6 100644 --- a/src/mesh/IndicatorSerial.cpp +++ b/src/mesh/IndicatorSerial.cpp @@ -288,7 +288,7 @@ bool SensecapIndicator::handle_packet(size_t payload_len) switch (message.which_data) { case meshtastic_InterdeviceMessage_nmea_tag: // send String to NMEA processing - FakeSerial->stuff_buffer(message.data.nmea, strlen(message.data.nmea)); + FakeSerial->stuff_buffer(message.data.nmea, strnlen(message.data.nmea, sizeof(message.data.nmea) - 1)); return true; case meshtastic_InterdeviceMessage_i2c_result_tag: // response for the transaction i2c_transact() is waiting on From 2c98c546317f3b9f80412747a48c8564781d903c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sun, 12 Jul 2026 21:09:27 +0200 Subject: [PATCH 04/28] indicator: bump device-ui pin to 27e6c0c --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 9dddcfde660..66a0eaabbfc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -127,7 +127,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/dcb45b7d5ab6df2fcc2d66e10df7c2c0fcf3ff51.zip + https://github.com/meshtastic/device-ui/archive/27e6c0c5a6f280e67b910a6c17fbcf90768011ca.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 76adb48128a4eca7278184cf71faf6654b3200d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sun, 12 Jul 2026 21:59:54 +0200 Subject: [PATCH 05/28] indicator: ping/pong link probe, non-blocking runOnce, FakeI2C locking The RP2040 sends nothing unsolicited without a GPS module attached, so wait_ready now probes with the new ping message instead of listening passively. runOnce skips its pump while a requester holds link_lock, keeping the main loop from blocking for a full request timeout. FakeI2C serializes transactions between the UI task and the main loop with an owner-tracked lock held from beginTransmission to transaction end. --- src/mesh/IndicatorSerial.cpp | 42 +++++++++++++++++++ src/mesh/IndicatorSerial.h | 15 ++++++- src/mesh/comms/FakeI2C.cpp | 30 +++++++++++-- src/mesh/comms/FakeI2C.h | 17 ++++++++ .../generated/meshtastic/interdevice.pb.h | 8 ++++ 5 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp index 391054989d6..bd3de48643a 100644 --- a/src/mesh/IndicatorSerial.cpp +++ b/src/mesh/IndicatorSerial.cpp @@ -26,6 +26,11 @@ SensecapIndicator::SensecapIndicator(HardwareSerial &serial) : OSThread("Senseca int32_t SensecapIndicator::runOnce() { if (running) { + // A requester is pumping the link itself and holds link_lock for up + // to its full request timeout; blocking on the lock here would stall + // every other thread of the cooperative main loop with it + if (request_in_flight) + return (10); concurrency::LockGuard guard(&link_lock); pump(); return (10); @@ -70,6 +75,7 @@ uint32_t SensecapIndicator::stamp_request(meshtastic_InterdeviceMessage &request bool SensecapIndicator::i2c_transact(meshtastic_InterdeviceMessage &request, meshtastic_I2CResult *result, uint32_t timeout_ms) { + InFlight busy(request_in_flight); concurrency::LockGuard guard(&link_lock); if (packets_received == 0) return false; // co-processor has never talked to us, fail fast instead of timing out @@ -88,6 +94,7 @@ bool SensecapIndicator::i2c_transact(meshtastic_InterdeviceMessage &request, mes bool SensecapIndicator::file_request(meshtastic_InterdeviceMessage &request, meshtastic_FileTransfer *out, uint32_t timeout_ms) { + InFlight busy(request_in_flight); concurrency::LockGuard guard(&link_lock); if (packets_received == 0) return false; @@ -143,6 +150,7 @@ bool SensecapIndicator::file_remove(const char *path, meshtastic_FileTransfer *o bool SensecapIndicator::list_directory(const char *path, uint32_t offset, meshtastic_DirectoryListing *out, uint32_t timeout_ms) { + InFlight busy(request_in_flight); concurrency::LockGuard guard(&link_lock); if (packets_received == 0) return false; @@ -165,6 +173,7 @@ bool SensecapIndicator::list_directory(const char *path, uint32_t offset, meshta bool SensecapIndicator::sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms) { + InFlight busy(request_in_flight); concurrency::LockGuard guard(&link_lock); if (packets_received == 0) return false; @@ -186,9 +195,26 @@ bool SensecapIndicator::sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms) bool SensecapIndicator::wait_ready(uint32_t timeout_ms) { + InFlight busy(request_in_flight); concurrency::LockGuard guard(&link_lock); uint32_t start = millis(); + uint32_t last_probe = 0; + bool probed = false; while (packets_received == 0) { + // The co-processor never sends anything unsolicited unless a GPS + // module is attached, so waiting passively would leave the bridge + // down forever on GPS-less units. Ping until any response marks + // the link up; pong touches no peripherals on the other side. + if (!probed || millis() - last_probe >= 250) { + meshtastic_InterdeviceMessage &msg = tx_message; + memset(&msg, 0, sizeof(msg)); + msg.which_data = meshtastic_InterdeviceMessage_ping_tag; + msg.data.ping = true; + stamp_request(msg); + send_uplink_unlocked(msg); + last_probe = millis(); + probed = true; + } pump(); if (packets_received > 0) break; @@ -317,6 +343,22 @@ bool SensecapIndicator::handle_packet(size_t payload_len) LOG_DEBUG("Drop stale listing response id=%u", message.id); } return true; + case meshtastic_InterdeviceMessage_ping_tag: { + // Liveness probe from the other side, answer regardless of state. + // tx_message is safe to reuse: a request in flight was already + // encoded into pb_tx_buf when it was sent + meshtastic_InterdeviceMessage &pong = tx_message; + memset(&pong, 0, sizeof(pong)); + pong.id = message.id; + pong.which_data = meshtastic_InterdeviceMessage_pong_tag; + pong.data.pong = true; + send_uplink_unlocked(pong); + return true; + } + case meshtastic_InterdeviceMessage_pong_tag: + // no payload to deliver: receiving it already counted the packet, + // which is all wait_ready() is looking for + return true; case meshtastic_InterdeviceMessage_sd_info_tag: if (pending_sd_info && message.id == expected_id) { *pending_sd_info = message.data.sd_info; diff --git a/src/mesh/IndicatorSerial.h b/src/mesh/IndicatorSerial.h index f84b2cc3d47..13aee0df396 100644 --- a/src/mesh/IndicatorSerial.h +++ b/src/mesh/IndicatorSerial.h @@ -54,8 +54,10 @@ class SensecapIndicator : public concurrency::OSThread bool sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms = 2000); // True once at least one valid packet was received from the RP2040. - // Pumps the link until then or until the timeout expires. Used to defer - // bridge traffic until the co-processor has booted. + // Actively probes the co-processor (it never speaks unsolicited unless a + // GPS module is attached) and pumps the link until a response arrives or + // the timeout expires. Used to defer bridge traffic until the + // co-processor has booted. bool wait_ready(uint32_t timeout_ms); private: @@ -87,6 +89,15 @@ class SensecapIndicator : public concurrency::OSThread // arrives after its request timed out cannot satisfy a later request uint32_t next_request_id = 0; uint32_t expected_id = 0; + // True while a requester holds link_lock for a full request/response + // round trip (up to the request timeout). runOnce checks it so the + // cooperative main loop is not blocked on the lock for that long. + volatile bool request_in_flight = false; + struct InFlight { + volatile bool &flag; + explicit InFlight(volatile bool &f) : flag(f) { flag = true; } + ~InFlight() { flag = false; } + }; uint32_t stamp_request(meshtastic_InterdeviceMessage &request); bool send_uplink_unlocked(const meshtastic_InterdeviceMessage &message); bool file_request(meshtastic_InterdeviceMessage &request, meshtastic_FileTransfer *out, uint32_t timeout_ms); diff --git a/src/mesh/comms/FakeI2C.cpp b/src/mesh/comms/FakeI2C.cpp index 956881acb84..f97e07d6354 100644 --- a/src/mesh/comms/FakeI2C.cpp +++ b/src/mesh/comms/FakeI2C.cpp @@ -5,8 +5,23 @@ FakeI2C *FakeWire = new FakeI2C(); +void FakeI2C::acquire() +{ + _lock.lock(); + _owner = xTaskGetCurrentTaskHandle(); +} + +void FakeI2C::release() +{ + _owner = nullptr; + _lock.unlock(); +} + void FakeI2C::beginTransmission(uint8_t address) { + // re-begin from the owning thread just restages, everyone else waits + if (_owner != xTaskGetCurrentTaskHandle()) + acquire(); _txAddress = address; _txLen = 0; _txPending = true; @@ -30,14 +45,18 @@ size_t FakeI2C::write(const uint8_t *data, size_t len) uint8_t FakeI2C::endTransmission(bool stopBit) { + if (_owner != xTaskGetCurrentTaskHandle()) + return 4; // endTransmission without beginTransmission if (!stopBit) { // Keep the buffered write pending, it is combined with the following - // requestFrom() into a single write+read transaction + // requestFrom() into a single write+read transaction; the lock stays + // held across the repeated start return 0; } uint8_t rv = transact(_txAddress, _txBuf, _txLen, 0); _txLen = 0; _txPending = false; + release(); return rv; } @@ -47,6 +66,10 @@ size_t FakeI2C::requestFrom(uint8_t address, size_t len, bool stopBit) if (len > MAX_READ) len = MAX_READ; + // standalone read without a preceding beginTransmission + if (_owner != xTaskGetCurrentTaskHandle()) + acquire(); + const uint8_t *wbuf = NULL; size_t wlen = 0; if (_txPending && _txAddress == address) { @@ -56,6 +79,7 @@ size_t FakeI2C::requestFrom(uint8_t address, size_t len, bool stopBit) uint8_t rv = transact(address, wbuf, wlen, len); _txLen = 0; _txPending = false; + release(); return rv == 0 ? _rxLen : 0; } @@ -84,8 +108,8 @@ uint8_t FakeI2C::transact(uint8_t address, const uint8_t *wbuf, size_t wlen, siz if (!sensecapIndicator) return 4; - // static: ~4.6KB struct, too large for task stacks; I2C transactions - // only originate from the cooperative main loop + // static: ~4.6KB struct, too large for task stacks. Safe because every + // caller holds _lock while the transaction executes static meshtastic_InterdeviceMessage msg; memset(&msg, 0, sizeof(msg)); msg.which_data = meshtastic_InterdeviceMessage_i2c_transaction_tag; diff --git a/src/mesh/comms/FakeI2C.h b/src/mesh/comms/FakeI2C.h index 1ba7a27d0b4..92ba3ea49b5 100644 --- a/src/mesh/comms/FakeI2C.h +++ b/src/mesh/comms/FakeI2C.h @@ -3,6 +3,7 @@ #ifdef SENSECAP_INDICATOR #include "../generated/meshtastic/interdevice.pb.h" +#include "concurrency/Lock.h" #include /** @@ -19,6 +20,13 @@ * Constructed on bus number 0: TwoWire::begin() is final and cannot be * intercepted, but it returns without touching hardware when the bus is * already initialized, which is always true for bus 0 (local touch panel). + * + * Thread safety: the bus is shared between the main loop (sensor drivers) + * and the UI task (keyboard scanner), so a transaction holds an internal + * lock from beginTransmission()/requestFrom() until the transaction + * executes, like the HAL lock of the real ESP32 TwoWire. As with the real + * TwoWire, the read buffer must be consumed before the next transaction + * from another thread replaces it. */ class FakeI2C : public TwoWire { @@ -59,6 +67,15 @@ class FakeI2C : public TwoWire size_t _rxLen = 0; size_t _rxPos = 0; + // Serializes staged transactions across threads. Held from + // beginTransmission() (or a standalone requestFrom()) until the + // transaction executes. The lock is a plain binary semaphore, so the + // owning task is tracked to keep re-entry from deadlocking. + concurrency::Lock _lock; + TaskHandle_t _owner = nullptr; + void acquire(); + void release(); + uint8_t transact(uint8_t address, const uint8_t *wbuf, size_t wlen, size_t rlen); }; diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index e8f13d78735..c383ea02a76 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -117,6 +117,10 @@ typedef struct _meshtastic_InterdeviceMessage { meshtastic_DirectoryListing directory_listing; bool get_sd_info; /* Request: SD card statistics */ meshtastic_SdCardInfo sd_info; /* Response */ + /* Link liveness probe. The receiver answers ping with pong, echoing the + id. Touches no peripherals, so it works with nothing attached. */ + bool ping; + bool pong; } data; /* Correlates a response with its request: responses echo the id of the request they answer. 0 for unsolicited messages (e.g. the nmea stream). */ @@ -206,6 +210,8 @@ extern "C" { #define meshtastic_InterdeviceMessage_directory_listing_tag 8 #define meshtastic_InterdeviceMessage_get_sd_info_tag 9 #define meshtastic_InterdeviceMessage_sd_info_tag 10 +#define meshtastic_InterdeviceMessage_ping_tag 11 +#define meshtastic_InterdeviceMessage_pong_tag 12 #define meshtastic_InterdeviceMessage_id_tag 15 /* Struct field encoding specification for nanopb */ @@ -265,6 +271,8 @@ X(a, STATIC, ONEOF, MESSAGE, (data,file_transfer,data.file_transfer), 7) X(a, STATIC, ONEOF, MESSAGE, (data,directory_listing,data.directory_listing), 8) \ X(a, STATIC, ONEOF, BOOL, (data,get_sd_info,data.get_sd_info), 9) \ X(a, STATIC, ONEOF, MESSAGE, (data,sd_info,data.sd_info), 10) \ +X(a, STATIC, ONEOF, BOOL, (data,ping,data.ping), 11) \ +X(a, STATIC, ONEOF, BOOL, (data,pong,data.pong), 12) \ X(a, STATIC, SINGULAR, UINT32, id, 15) #define meshtastic_InterdeviceMessage_CALLBACK NULL #define meshtastic_InterdeviceMessage_DEFAULT NULL From 4a7d8d55c73a866d0b4272d8789f129cb7d51e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sun, 12 Jul 2026 22:21:06 +0200 Subject: [PATCH 06/28] indicator: link resync, config-honoring GPS, bridged-bus routing, stats validity Frame resync scans to the next magic instead of flushing the RX buffer, and the pump handles all buffered frames per pass. The RX drain reads in bulk and the protobuf encoder gets the correct buffer bound. GPS honors the gps_mode setting on the Indicator instead of always running. RTC, I2C keyboard and motion sensor drivers resolve WIRE1 through ScanI2CTwoWire::fetchI2CBus so bridged buses reach the right transport. FakeUART implements flush/availableForWrite/const-write from the Stream contract and fences its cross-core ring buffer. SdCardInfo.stats_valid is passed through to device-ui, and the remote FS backend gains the remove operation used for cleanup of failed tile saves. --- src/gps/GPS.cpp | 16 ++++- src/gps/RTC.cpp | 9 +-- src/graphics/tftSetup.cpp | 9 +++ src/input/kbI2cBase.cpp | 10 +-- src/mesh/IndicatorSerial.cpp | 64 ++++++++++--------- src/mesh/IndicatorSerial.h | 2 +- src/mesh/comms/FakeUART.cpp | 2 + src/mesh/comms/FakeUART.h | 14 +++- .../generated/meshtastic/interdevice.pb.h | 22 +++++-- src/motion/BMI270Sensor.cpp | 9 ++- src/motion/BMM150Sensor.cpp | 9 ++- src/motion/ICM20948Sensor.cpp | 10 ++- 12 files changed, 111 insertions(+), 65 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 9aa2c29e6bf..6fd0d6dd527 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1430,7 +1430,21 @@ void GPS::publishUpdate() int32_t GPS::runOnce() { -#if !defined(SENSECAP_INDICATOR) +#if defined(SENSECAP_INDICATOR) + // No model probe on the bridged fake UART (the module only streams + // NMEA), but the user's GPS mode setting must still be honored + if (!GPSInitFinished) { + if (!_serial_gps || config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + LOG_INFO("GPS set to not-present. Skip probe"); + return disable(); + } + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + return disable(); + } + GPSInitFinished = true; + publishUpdate(); + } +#else if (!GPSInitFinished) { if (!_serial_gps || config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { LOG_INFO("GPS set to not-present. Skip probe"); diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index ad0bdec0407..43a6f7d87b1 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -1,6 +1,7 @@ #include "RTC.h" #include "configuration.h" #include "detect/ScanI2C.h" +#include "detect/ScanI2CTwoWire.h" #include "main.h" #include "modules/NodeInfoModule.h" #include @@ -93,7 +94,7 @@ RTCSetResult readFromRTC() uint32_t now = millis(); Melopero_RV3028 rtc; #if WIRE_INTERFACES_COUNT == 2 - rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + rtc.initI2C(*ScanI2CTwoWire::fetchI2CBus(rtc_found)); #else rtc.initI2C(); #endif @@ -142,7 +143,7 @@ RTCSetResult readFromRTC() uint32_t now = millis(); #if WIRE_INTERFACES_COUNT == 2 - rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + rtc.begin(*ScanI2CTwoWire::fetchI2CBus(rtc_found)); #else rtc.begin(Wire); #endif @@ -287,7 +288,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd if (rtc_found.address == RV3028_RTC) { Melopero_RV3028 rtc; #if WIRE_INTERFACES_COUNT == 2 - rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + rtc.initI2C(*ScanI2CTwoWire::fetchI2CBus(rtc_found)); #else rtc.initI2C(); #endif @@ -309,7 +310,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd #endif #if WIRE_INTERFACES_COUNT == 2 - rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + rtc.begin(*ScanI2CTwoWire::fetchI2CBus(rtc_found)); #else rtc.begin(Wire); #endif diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 1422246f0ee..f13256b33fc 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -64,9 +64,18 @@ class IndicatorRemoteFS : public IRemoteFS info.cardSize = result.card_size; info.usedBytes = result.used_bytes; info.freeBytes = result.free_bytes; + info.statsValid = result.stats_valid; return true; } + bool remove(const char *path) override + { + if (!sensecapIndicator) + return false; + memset(&result, 0, sizeof(result)); + return sensecapIndicator->file_remove(path, &result) && result.success; + } + bool listDir(const char *path, std::set &entries) override { if (!sensecapIndicator) diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 510fb1e31df..88386c5c9d0 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -59,16 +59,18 @@ int32_t KbI2cBase::runOnce() case ScanI2C::WIRE1: #if WIRE_INTERFACES_COUNT == 2 LOG_DEBUG("Use I2C Bus 1 (the second one)"); - i2cBus = &Wire1; + // resolved via the scanner: WIRE1 may be a bridged bus rather + // than the local Wire1 (e.g. SenseCAP Indicator) + i2cBus = ScanI2CTwoWire::fetchI2CBus(cardkb_found); if (cardkb_found.address == BBQ10_KB_ADDR) { - Q10keyboard.begin(BBQ10_KB_ADDR, &Wire1); + Q10keyboard.begin(BBQ10_KB_ADDR, i2cBus); Q10keyboard.setBacklight(0); } if (cardkb_found.address == MPR121_KB_ADDR) { - MPRkeyboard.begin(MPR121_KB_ADDR, &Wire1); + MPRkeyboard.begin(MPR121_KB_ADDR, i2cBus); } if (cardkb_found.address == TCA8418_KB_ADDR) { - TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire1); + TCAKeyboard.begin(TCA8418_KB_ADDR, i2cBus); } break; #endif diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp index bd3de48643a..63e05692fa9 100644 --- a/src/mesh/IndicatorSerial.cpp +++ b/src/mesh/IndicatorSerial.cpp @@ -237,7 +237,7 @@ bool SensecapIndicator::send_uplink_unlocked(const meshtastic_InterdeviceMessage pb_tx_buf[0] = MT_MAGIC_0; pb_tx_buf[1] = MT_MAGIC_1; - pb_ostream_t stream = pb_ostream_from_buffer(pb_tx_buf + MT_HEADER_SIZE, PB_BUFSIZE); + pb_ostream_t stream = pb_ostream_from_buffer(pb_tx_buf + MT_HEADER_SIZE, PB_BUFSIZE - MT_HEADER_SIZE); if (!pb_encode(&stream, meshtastic_InterdeviceMessage_fields, &message)) { LOG_DEBUG("pb_encode failed"); return false; @@ -254,43 +254,47 @@ bool SensecapIndicator::send_uplink_unlocked(const meshtastic_InterdeviceMessage size_t SensecapIndicator::serial_check(char *buf, size_t space_left) { - size_t bytes_read = 0; - while (bytes_read < space_left && _serial->available()) { - buf[bytes_read++] = _serial->read(); - } - return bytes_read; + int avail = _serial->available(); + if (avail <= 0) + return 0; + if ((size_t)avail > space_left) + avail = space_left; + // bulk copy out of the driver's RX buffer; only reads what is available + return _serial->read((uint8_t *)buf, avail); } -void SensecapIndicator::check_packet() +// Distance to the next byte that could start a frame. Skips the corrupt +// prefix while keeping anything that may be a frame queued behind it (a +// trailing lone MT_MAGIC_0 counts, its successor has not arrived yet). +static size_t scan_magic(const pb_byte_t *buf, size_t len) { - if (pb_rx_size < MT_HEADER_SIZE) { - // We don't even have a header yet - return; + for (size_t i = 1; i < len; i++) { + if (buf[i] == MT_MAGIC_0 && (i + 1 == len || buf[i + 1] == MT_MAGIC_1)) + return i; } + return len; +} - if (pb_rx_buf[0] != MT_MAGIC_0 || pb_rx_buf[1] != MT_MAGIC_1) { - LOG_DEBUG("Got bad magic"); - memset(pb_rx_buf, 0, PB_BUFSIZE); - pb_rx_size = 0; - return; - } +void SensecapIndicator::check_packet() +{ + // process everything buffered; one pump can deliver several frames + while (pb_rx_size >= MT_HEADER_SIZE) { + size_t payload_len = (size_t)(pb_rx_buf[2] << 8 | pb_rx_buf[3]); + if (pb_rx_buf[0] != MT_MAGIC_0 || pb_rx_buf[1] != MT_MAGIC_1 || payload_len + MT_HEADER_SIZE > PB_BUFSIZE) { + // Corrupt or false header: resync on the next magic instead of + // flushing, one bad byte must not cost the frames behind it + size_t skip = scan_magic(pb_rx_buf, pb_rx_size); + LOG_DEBUG("Bad frame header, dropping %u bytes", (unsigned)skip); + memmove(pb_rx_buf, pb_rx_buf + skip, pb_rx_size - skip); + pb_rx_size -= skip; + continue; + } - uint16_t payload_len = pb_rx_buf[2] << 8 | pb_rx_buf[3]; - if ((size_t)payload_len + MT_HEADER_SIZE > PB_BUFSIZE) { - // oversized frame can never complete, resync on the next magic - LOG_DEBUG("Got packet claiming to be ridiculous length"); - memset(pb_rx_buf, 0, PB_BUFSIZE); - pb_rx_size = 0; - return; - } + if (payload_len + MT_HEADER_SIZE > pb_rx_size) + return; // frame not complete yet - if ((size_t)(payload_len + 4) > pb_rx_size) { - // Packet not complete yet - return; + handle_packet(payload_len); } - - // We have a complete packet, handle it - handle_packet(payload_len); } bool SensecapIndicator::handle_packet(size_t payload_len) diff --git a/src/mesh/IndicatorSerial.h b/src/mesh/IndicatorSerial.h index 13aee0df396..f5640b5af39 100644 --- a/src/mesh/IndicatorSerial.h +++ b/src/mesh/IndicatorSerial.h @@ -21,7 +21,7 @@ // Wait this many msec if there's nothing new on the channel #define NO_NEWS_PAUSE 25 -#define PB_BUFSIZE meshtastic_InterdeviceMessage_size + MT_HEADER_SIZE +#define PB_BUFSIZE (meshtastic_InterdeviceMessage_size + MT_HEADER_SIZE) class SensecapIndicator : public concurrency::OSThread { diff --git a/src/mesh/comms/FakeUART.cpp b/src/mesh/comms/FakeUART.cpp index 94f9ee16839..c1a77f2fcc1 100644 --- a/src/mesh/comms/FakeUART.cpp +++ b/src/mesh/comms/FakeUART.cpp @@ -28,6 +28,7 @@ int FakeUART::peek() { if (buf_tail == buf_head) return -1; + __sync_synchronize(); // pair with the producer barrier in buf_push return buf[buf_tail]; } @@ -35,6 +36,7 @@ int FakeUART::read() { if (buf_tail == buf_head) return -1; + __sync_synchronize(); // pair with the producer barrier in buf_push uint8_t ret = buf[buf_tail]; buf_tail = (buf_tail + 1) % BUF_SIZE; return ret; diff --git a/src/mesh/comms/FakeUART.h b/src/mesh/comms/FakeUART.h index 4fad9ebb531..fbd9c8b841c 100644 --- a/src/mesh/comms/FakeUART.h +++ b/src/mesh/comms/FakeUART.h @@ -20,16 +20,23 @@ class FakeUART : public Stream int available(); int peek(); int read(); - void flush(bool wait = true); + void flush(bool wait); + // Stream/Print contract: flush() through a base pointer must behave + // like the HardwareSerial variant callers expect + void flush() override { flush(true); } + // writes are buffered into one uplink message, so this is its capacity + // (Print's default of 0 would make drivers back off forever) + int availableForWrite() override { return (int)sizeof(meshtastic_InterdeviceMessage{}.data.nmea) - 1; } uint32_t baudRate(); void updateBaudRate(unsigned long speed); size_t setRxBufferSize(size_t size); size_t write(const char *buffer); size_t write(char *buffer, size_t size); size_t write(uint8_t *buffer, size_t size); + size_t write(const uint8_t *buffer, size_t size) override { return write((char *)buffer, size); } size_t stuff_buffer(const char *buffer, size_t size); - virtual size_t write(uint8_t c) { return write(&c, 1); } + size_t write(uint8_t c) override { return write(&c, 1); } private: unsigned long baudrate = 115200; @@ -48,6 +55,9 @@ class FakeUART : public Stream if (next == buf_tail) return false; // full buf[buf_head] = c; + // producer and consumer run on different cores: the byte must be + // visible before the index says it is there + __sync_synchronize(); buf_head = next; return true; } diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index c383ea02a76..00a94e8bbb7 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -61,9 +61,11 @@ typedef struct _meshtastic_FileTransfer { /* Message for structured directory listing */ typedef struct _meshtastic_DirectoryListing { char directory[256]; /* Path of the directory */ - /* One page of entry names, full FAT LFN length so they round-trip into - FileTransfer.filepath. Page size is the max_count in - interdevice.options; page through with offset and total_count. */ + /* One page of entry names, full FAT LFN length. Subdirectories carry a + trailing slash. Note that a name whose directory prefix pushes the + combined path past the FileTransfer.filepath limit cannot round-trip. + Page size is the max_count in interdevice.options; page through with + offset and total_count. */ pb_size_t filenames_count; char filenames[16][256]; bool success; /* Response: Was the operation successful? */ @@ -93,6 +95,10 @@ typedef struct _meshtastic_SdCardInfo { uint64_t card_size; /* Filesystem size in bytes */ uint64_t used_bytes; /* Used bytes (may be expensive to compute on FAT32) */ uint64_t free_bytes; /* Free bytes */ + /* used_bytes/free_bytes are only meaningful when true: the scan behind + them runs in the background after mount and can take a while, and a + full card is otherwise indistinguishable from a scan in progress */ + bool stats_valid; } meshtastic_SdCardInfo; typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CResult_read_data_t; @@ -164,13 +170,13 @@ extern "C" { #define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, 0, "", 0, 0, 0} #define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, 0, "", 0, 0} #define meshtastic_I2CTransaction_init_default {0, {0, {0}}, 0} -#define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0} +#define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0} #define meshtastic_I2CResult_init_default {_meshtastic_I2CResult_Status_MIN, {0, {0}}} #define meshtastic_InterdeviceMessage_init_default {0, {""}, 0} #define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, 0, "", 0, 0, 0} #define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, 0, "", 0, 0} #define meshtastic_I2CTransaction_init_zero {0, {0, {0}}, 0} -#define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0} +#define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0} #define meshtastic_I2CResult_init_zero {_meshtastic_I2CResult_Status_MIN, {0, {0}}} #define meshtastic_InterdeviceMessage_init_zero {0, {""}, 0} @@ -198,6 +204,7 @@ extern "C" { #define meshtastic_SdCardInfo_card_size_tag 4 #define meshtastic_SdCardInfo_used_bytes_tag 5 #define meshtastic_SdCardInfo_free_bytes_tag 6 +#define meshtastic_SdCardInfo_stats_valid_tag 7 #define meshtastic_I2CResult_status_tag 1 #define meshtastic_I2CResult_read_data_tag 2 #define meshtastic_InterdeviceMessage_nmea_tag 1 @@ -250,7 +257,8 @@ X(a, STATIC, SINGULAR, UENUM, card_type, 2) \ X(a, STATIC, SINGULAR, UENUM, fat_type, 3) \ X(a, STATIC, SINGULAR, UINT64, card_size, 4) \ X(a, STATIC, SINGULAR, UINT64, used_bytes, 5) \ -X(a, STATIC, SINGULAR, UINT64, free_bytes, 6) +X(a, STATIC, SINGULAR, UINT64, free_bytes, 6) \ +X(a, STATIC, SINGULAR, BOOL, stats_valid, 7) #define meshtastic_SdCardInfo_CALLBACK NULL #define meshtastic_SdCardInfo_DEFAULT NULL @@ -304,7 +312,7 @@ extern const pb_msgdesc_t meshtastic_InterdeviceMessage_msg; #define meshtastic_I2CResult_size 261 #define meshtastic_I2CTransaction_size 271 #define meshtastic_InterdeviceMessage_size 4666 -#define meshtastic_SdCardInfo_size 39 +#define meshtastic_SdCardInfo_size 41 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/motion/BMI270Sensor.cpp b/src/motion/BMI270Sensor.cpp index bc547529da0..87785f1b883 100644 --- a/src/motion/BMI270Sensor.cpp +++ b/src/motion/BMI270Sensor.cpp @@ -2,6 +2,7 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMI270) +#include "detect/ScanI2CTwoWire.h" #include // BMI270 registers used @@ -426,11 +427,9 @@ static const uint8_t bmi270_config_file[] PROGMEM = { BMI270Sensor::BMI270Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) { if (foundDevice.address.port == ScanI2C::I2CPort::WIRE1) { -#ifdef I2C_SDA1 - wire = &Wire1; -#else - wire = &Wire; -#endif + // resolved via the scanner: WIRE1 may be a bridged bus rather than + // the local Wire1 (e.g. SenseCAP Indicator) + wire = ScanI2CTwoWire::fetchI2CBus(foundDevice.address); } else { wire = &Wire; } diff --git a/src/motion/BMM150Sensor.cpp b/src/motion/BMM150Sensor.cpp index f48d20288b1..a1a4ba3a020 100644 --- a/src/motion/BMM150Sensor.cpp +++ b/src/motion/BMM150Sensor.cpp @@ -1,6 +1,7 @@ #include "BMM150Sensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() +#include "detect/ScanI2CTwoWire.h" #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // screen is defined in main.cpp @@ -34,11 +35,9 @@ int32_t BMM150Sensor::runOnce() // Get a singleton wrapper for an Sparkfun BMM_150_I2C BMM150Singleton *BMM150Singleton::GetInstance(ScanI2C::FoundDevice device) { -#if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1) - TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); -#else - TwoWire &bus = Wire; // fallback if only one I2C interface -#endif + // resolved via the scanner: WIRE1 may be a bridged bus rather than the + // local Wire1 (e.g. SenseCAP Indicator) + TwoWire &bus = *ScanI2CTwoWire::fetchI2CBus(device.address); if (pinstance == nullptr) { pinstance = new BMM150Singleton(&bus, device.address.address); } diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index 3dd5a5e468f..503f0c1a0f5 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -1,6 +1,7 @@ #include "ICM20948Sensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() +#include "detect/ScanI2CTwoWire.h" #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // screen is defined in main.cpp @@ -169,12 +170,9 @@ bool ICM20948Singleton::init(ScanI2C::FoundDevice device) enableDebugging(); #endif - // startup -#if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1) - TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); -#else - TwoWire &bus = Wire; // fallback if only one I2C interface -#endif + // startup; the bus is resolved via the scanner: WIRE1 may be a bridged + // bus rather than the local Wire1 (e.g. SenseCAP Indicator) + TwoWire &bus = *ScanI2CTwoWire::fetchI2CBus(device.address); bool bAddr = (device.address.address == 0x69); delay(100); From ae839eb1cada827418fa00ad0c0b818bd742630f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sun, 12 Jul 2026 22:32:38 +0200 Subject: [PATCH 07/28] indicator: retry lost link round trips, I2CResult UNSPECIFIED Remote FS operations retry once on a transport timeout. Correlation ids drop late responses of the first attempt; a retried append whose first attempt landed is recognized by the offset conflict carrying the resulting file size. Definitive failures are not retried, missing-tile probes stay a single round trip. Regenerated bindings add the I2CResult.Status UNSPECIFIED zero value so an empty result cannot decode as success. --- src/graphics/tftSetup.cpp | 78 ++++++++++++++----- .../generated/meshtastic/interdevice.pb.h | 13 ++-- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index f13256b33fc..cea406c8fcb 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -21,34 +21,59 @@ #include "mesh/comms/FakeI2C.h" // Serves the UI map tiles from the SD card behind the RP2040, chunk-wise -// over the interdevice link +// over the interdevice link. +// +// Every operation retries once on a transport timeout: a frame lost to an +// overflow or resync must not fail the whole tile. Correlation ids make +// this safe, a late response to the first attempt is dropped as stale. +// Definitive answers (success=false from the co-processor) are never +// retried, missing-tile probes stay a single round trip. class IndicatorRemoteFS : public IRemoteFS { + static const int LINK_RETRIES = 1; + public: bool readChunk(const char *path, uint32_t offset, uint8_t *buf, uint32_t len, uint32_t *bytesRead, uint32_t *fileSize) override { if (!sensecapIndicator) return false; - memset(&result, 0, sizeof(result)); - if (!sensecapIndicator->file_read(path, offset, len, &result) || !result.success) - return false; - uint32_t n = result.filedata.size; - if (n > len) - n = len; - memcpy(buf, result.filedata.bytes, n); - *bytesRead = n; - // lv_fs positions are 32 bit, map tiles never come close - *fileSize = (uint32_t)result.file_size; - return true; + for (int attempt = 0; attempt <= LINK_RETRIES; attempt++) { + memset(&result, 0, sizeof(result)); + if (!sensecapIndicator->file_read(path, offset, len, &result)) + continue; // transport timeout, retry + if (!result.success) + return false; + uint32_t n = result.filedata.size; + if (n > len) + n = len; + memcpy(buf, result.filedata.bytes, n); + *bytesRead = n; + // lv_fs positions are 32 bit, map tiles never come close + *fileSize = (uint32_t)result.file_size; + return true; + } + return false; } bool writeChunk(const char *path, uint32_t offset, const uint8_t *buf, uint32_t len, bool create) override { if (!sensecapIndicator) return false; - memset(&result, 0, sizeof(result)); - return sensecapIndicator->file_write(path, offset, buf, len, create, &result) && result.success; + for (int attempt = 0; attempt <= LINK_RETRIES; attempt++) { + memset(&result, 0, sizeof(result)); + if (!sensecapIndicator->file_write(path, offset, buf, len, create, &result)) + continue; // transport timeout, retry (POST re-truncates, safe) + if (result.success) + return true; + // The retry of an append whose first attempt landed but whose + // response was lost is rejected as an offset conflict carrying + // the resulting file size + if (attempt > 0 && !create && result.file_size == (uint64_t)offset + len) + return true; + return false; + } + return false; } bool sdInfo(RemoteSdInfo &info) override @@ -56,7 +81,10 @@ class IndicatorRemoteFS : public IRemoteFS if (!sensecapIndicator) return false; meshtastic_SdCardInfo result = meshtastic_SdCardInfo_init_zero; - if (!sensecapIndicator->sd_info(&result)) + bool ok = false; + for (int attempt = 0; attempt <= LINK_RETRIES && !ok; attempt++) + ok = sensecapIndicator->sd_info(&result); + if (!ok) return false; info.present = result.present; info.cardType = (uint8_t)result.card_type; @@ -72,8 +100,16 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; - memset(&result, 0, sizeof(result)); - return sensecapIndicator->file_remove(path, &result) && result.success; + for (int attempt = 0; attempt <= LINK_RETRIES; attempt++) { + memset(&result, 0, sizeof(result)); + if (!sensecapIndicator->file_remove(path, &result)) + continue; // transport timeout, retry + if (result.success) + return true; + // after a lost response the retry finds the file already gone + return attempt > 0; + } + return false; } bool listDir(const char *path, std::set &entries) override @@ -82,8 +118,12 @@ class IndicatorRemoteFS : public IRemoteFS return false; uint32_t offset = 0; while (true) { - memset(&listing, 0, sizeof(listing)); - if (!sensecapIndicator->list_directory(path, offset, &listing) || !listing.success) + bool ok = false; + for (int attempt = 0; attempt <= LINK_RETRIES && !ok; attempt++) { + memset(&listing, 0, sizeof(listing)); + ok = sensecapIndicator->list_directory(path, offset, &listing); + } + if (!ok || !listing.success) return false; for (pb_size_t i = 0; i < listing.filenames_count; i++) entries.insert(listing.filenames[i]); diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index 00a94e8bbb7..02c9b2e3fe9 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -35,10 +35,13 @@ typedef enum _meshtastic_SdCardInfo_FatType { } meshtastic_SdCardInfo_FatType; typedef enum _meshtastic_I2CResult_Status { - meshtastic_I2CResult_Status_OK = 0, - meshtastic_I2CResult_Status_NACK_ADDRESS = 1, - meshtastic_I2CResult_Status_NACK_DATA = 2, - meshtastic_I2CResult_Status_ERROR = 3 + /* Never sent: an all-defaults (e.g. accidentally empty) message must + not decode as a successful transaction */ + meshtastic_I2CResult_Status_UNSPECIFIED = 0, + meshtastic_I2CResult_Status_OK = 1, + meshtastic_I2CResult_Status_NACK_ADDRESS = 2, + meshtastic_I2CResult_Status_NACK_DATA = 3, + meshtastic_I2CResult_Status_ERROR = 4 } meshtastic_I2CResult_Status; /* Struct definitions */ @@ -151,7 +154,7 @@ extern "C" { #define _meshtastic_SdCardInfo_FatType_MAX meshtastic_SdCardInfo_FatType_EXFAT #define _meshtastic_SdCardInfo_FatType_ARRAYSIZE ((meshtastic_SdCardInfo_FatType)(meshtastic_SdCardInfo_FatType_EXFAT+1)) -#define _meshtastic_I2CResult_Status_MIN meshtastic_I2CResult_Status_OK +#define _meshtastic_I2CResult_Status_MIN meshtastic_I2CResult_Status_UNSPECIFIED #define _meshtastic_I2CResult_Status_MAX meshtastic_I2CResult_Status_ERROR #define _meshtastic_I2CResult_Status_ARRAYSIZE ((meshtastic_I2CResult_Status)(meshtastic_I2CResult_Status_ERROR+1)) From 8ee2f705c9b8e3eaf79346cf8ebe74cd67eeb6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 09:20:00 +0200 Subject: [PATCH 08/28] indicator: nack responses, rename bridge classes to I2CProxy/UARTProxy A request the co-processor cannot decode or handle is nacked, so the requester fails fast instead of burning its timeout. All requests stage the shared tx_message under link_lock. FakeI2C and FakeUART are renamed to I2CProxy and UARTProxy after the pattern they implement, with their instances following suit. Drops dead code (unused NO_NEWS_PAUSE, unreachable not-running branches, doubled include guards) and the GPS pin log line that is meaningless on the tunneled port. --- src/configuration.h | 2 +- src/detect/ScanI2CTwoWire.cpp | 6 +- src/gps/GPS.cpp | 8 +- src/gps/GPS.h | 4 +- src/graphics/tftSetup.cpp | 4 +- src/main.cpp | 4 +- src/mesh/IndicatorSerial.cpp | 76 +++++++++++-------- src/mesh/IndicatorSerial.h | 24 +++--- src/mesh/comms/{FakeI2C.cpp => I2CProxy.cpp} | 26 +++---- src/mesh/comms/{FakeI2C.h => I2CProxy.h} | 6 +- .../comms/{FakeUART.cpp => UARTProxy.cpp} | 40 +++++----- src/mesh/comms/{FakeUART.h => UARTProxy.h} | 20 +++-- .../generated/meshtastic/interdevice.pb.h | 7 ++ 13 files changed, 126 insertions(+), 101 deletions(-) rename src/mesh/comms/{FakeI2C.cpp => I2CProxy.cpp} (85%) rename src/mesh/comms/{FakeI2C.h => I2CProxy.h} (97%) rename src/mesh/comms/{FakeUART.cpp => UARTProxy.cpp} (61%) rename src/mesh/comms/{FakeUART.h => UARTProxy.h} (82%) diff --git a/src/configuration.h b/src/configuration.h index fd4f148455e..253e2fad5ab 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -395,7 +395,7 @@ along with this program. If not, see . #ifndef WIRE_INTERFACES_COUNT // Officially an NRF52 macro // Repurposed cross-platform to identify devices using Wire1 -// The SenseCAP Indicator has a second bus bridged to the RP2040 (FakeI2C) +// The SenseCAP Indicator has a second bus bridged to the RP2040 (I2CProxy) #if defined(I2C_SDA1) || defined(PIN_WIRE1_SDA) || defined(SENSECAP_INDICATOR) #define WIRE_INTERFACES_COUNT 2 #elif HAS_WIRE diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index de9850a873f..b908134c1a4 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -9,7 +9,7 @@ #include "linux/LinuxHardwareI2C.h" #endif #if defined(SENSECAP_INDICATOR) -#include "mesh/comms/FakeI2C.h" +#include "mesh/comms/I2CProxy.h" #endif #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32) #include "meshUtils.h" // vformat @@ -249,7 +249,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) #if WIRE_INTERFACES_COUNT == 2 if (port == I2CPort::WIRE1) { #if defined(SENSECAP_INDICATOR) - i2cBus = FakeWire; // WIRE1 is bridged to the RP2040 + i2cBus = i2cProxy; // WIRE1 is bridged to the RP2040 #else i2cBus = &Wire1; #endif @@ -916,7 +916,7 @@ TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) return &Wire; } else { #if defined(SENSECAP_INDICATOR) - return FakeWire; // WIRE1 is bridged to the RP2040 + return i2cProxy; // WIRE1 is bridged to the RP2040 #elif WIRE_INTERFACES_COUNT == 2 return &Wire1; #else diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 6fd0d6dd527..4df2147e402 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -47,7 +47,7 @@ template std::size_t array_count(const T (&)[N]) #endif #if defined(SENSECAP_INDICATOR) -FakeUART *GPS::_serial_gps = nullptr; // assigned in createGps(), see there +UARTProxy *GPS::_serial_gps = nullptr; // assigned in createGps(), see there #elif defined(ARCH_NRF52) Uart *GPS::_serial_gps = &GPS_SERIAL_PORT; #elif defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32) @@ -1931,7 +1931,7 @@ std::unique_ptr GPS::createGps() #if defined(SENSECAP_INDICATOR) // assigned at runtime, static initialization order across translation // units is undefined - _serial_gps = FakeSerial; + _serial_gps = uartProxy; if (!_serial_gps) return nullptr; #else @@ -1998,8 +1998,12 @@ std::unique_ptr GPS::createGps() _serial_gps->setRxBufferSize(SERIAL_BUFFER_SIZE); // the default is 256 #endif +#if defined(SENSECAP_INDICATOR) + LOG_DEBUG("Use the RP2040 tunnel for GPS, no local pins"); +#else LOG_DEBUG("Use GPIO%d for GPS RX", new_gps->rx_gpio); LOG_DEBUG("Use GPIO%d for GPS TX", new_gps->tx_gpio); +#endif // ESP32 has a special set of parameters vs other arduino ports #if defined(ARCH_ESP32) diff --git a/src/gps/GPS.h b/src/gps/GPS.h index fd628bbb1cf..7e43dc8177a 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -14,7 +14,7 @@ #include "modules/PositionModule.h" #ifdef SENSECAP_INDICATOR -#include "mesh/comms/FakeUART.h" +#include "mesh/comms/UARTProxy.h" #endif // Allow defining the polarity of the ENABLE output. default is active high @@ -224,7 +224,7 @@ class GPS : private concurrency::OSThread /** If !NULL we will use this serial port to construct our GPS */ #if defined(SENSECAP_INDICATOR) - static FakeUART *_serial_gps; + static UARTProxy *_serial_gps; #elif defined(ARCH_RP2040) static SerialUART *_serial_gps; #elif defined(ARCH_NRF52) diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index cea406c8fcb..28e4b915da9 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -18,7 +18,7 @@ #include "graphics/map/RemoteSDService.h" #include "input/I2CKeyboardScanner.h" #include "mesh/IndicatorSerial.h" -#include "mesh/comms/FakeI2C.h" +#include "mesh/comms/I2CProxy.h" // Serves the UI map tiles from the SD card behind the RP2040, chunk-wise // over the interdevice link. @@ -172,7 +172,7 @@ void tftSetup(void) RemoteSDService::setBackend(new IndicatorRemoteFS()); // the second bus is bridged to the RP2040, keep the keyboard scan off the // uninitialized local Wire1 - I2CKeyboardScanner::setSecondaryBus(FakeWire); + I2CKeyboardScanner::setSecondaryBus(i2cProxy); #endif #ifndef ARCH_PORTDUINO deviceScreen = &DeviceScreen::create(); diff --git a/src/main.cpp b/src/main.cpp index 0a777062d32..a357824170f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -32,7 +32,7 @@ #ifdef SENSECAP_INDICATOR // on the indicator run the additional serial port for the RP2040 #include "IndicatorSerial.h" -#include "mesh/comms/FakeI2C.h" +#include "mesh/comms/I2CProxy.h" #endif #if !MESHTASTIC_EXCLUDE_I2C @@ -568,7 +568,7 @@ void setup() #if !MESHTASTIC_EXCLUDE_I2C #if defined(SENSECAP_INDICATOR) // The Sensecap Indicator has its second I2C bus on the RP2040, bridged - // over serial as FakeWire. No local interface to initialize. + // over serial as i2cProxy. No local interface to initialize. #elif defined(I2C_SDA1) && defined(ARCH_RP2040) Wire1.setSDA(I2C_SDA1); Wire1.setSCL(I2C_SCL1); diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp index 63e05692fa9..ba6c8813fab 100644 --- a/src/mesh/IndicatorSerial.cpp +++ b/src/mesh/IndicatorSerial.cpp @@ -2,7 +2,7 @@ #include "IndicatorSerial.h" #include "concurrency/LockGuard.h" -#include "mesh/comms/FakeUART.h" +#include "mesh/comms/UARTProxy.h" #include #include #include @@ -11,33 +11,25 @@ SensecapIndicator *sensecapIndicator; SensecapIndicator::SensecapIndicator(HardwareSerial &serial) : OSThread("SensecapIndicator") { - if (!running) { - _serial = &serial; - // Twice the largest frame: the pump runs from the cooperative main - // loop, which can stall for tens of ms while data keeps arriving - _serial->setRxBufferSize(2 * PB_BUFSIZE); - _serial->setPins(SENSOR_RP2040_RXD, SENSOR_RP2040_TXD); - _serial->begin(SENSOR_BAUD_RATE); - running = true; - LOG_DEBUG("Start indicator communication thread"); - } + _serial = &serial; + // Twice the largest frame: the pump runs from the cooperative main + // loop, which can stall for tens of ms while data keeps arriving + _serial->setRxBufferSize(2 * PB_BUFSIZE); + _serial->setPins(SENSOR_RP2040_RXD, SENSOR_RP2040_TXD); + _serial->begin(SENSOR_BAUD_RATE); + LOG_DEBUG("Start indicator communication thread"); } int32_t SensecapIndicator::runOnce() { - if (running) { - // A requester is pumping the link itself and holds link_lock for up - // to its full request timeout; blocking on the lock here would stall - // every other thread of the cooperative main loop with it - if (request_in_flight) - return (10); - concurrency::LockGuard guard(&link_lock); - pump(); + // A requester is pumping the link itself and holds link_lock for up + // to its full request timeout; blocking on the lock here would stall + // every other thread of the cooperative main loop with it + if (request_in_flight) return (10); - } else { - LOG_DEBUG("Not running"); - return (1000); - } + concurrency::LockGuard guard(&link_lock); + pump(); + return (10); } // Read whatever is available on the link and process complete packets @@ -48,7 +40,8 @@ void SensecapIndicator::pump() check_packet(); } -// Pump the link until `flag` goes true or the timeout expires +// Pump the link until `flag` goes true, a nack arrives, or the timeout +// expires bool SensecapIndicator::wait_response(bool &flag, uint32_t timeout_ms) { uint32_t start = millis(); @@ -56,6 +49,8 @@ bool SensecapIndicator::wait_response(bool &flag, uint32_t timeout_ms) pump(); if (flag) break; + if (request_nacked) + return false; // the other side could not handle the request if (millis() - start >= timeout_ms) return false; delay(1); @@ -70,6 +65,7 @@ uint32_t SensecapIndicator::stamp_request(meshtastic_InterdeviceMessage &request next_request_id = 1; request.id = next_request_id; expected_id = next_request_id; + request_nacked = false; return next_request_id; } @@ -94,11 +90,6 @@ bool SensecapIndicator::i2c_transact(meshtastic_InterdeviceMessage &request, mes bool SensecapIndicator::file_request(meshtastic_InterdeviceMessage &request, meshtastic_FileTransfer *out, uint32_t timeout_ms) { - InFlight busy(request_in_flight); - concurrency::LockGuard guard(&link_lock); - if (packets_received == 0) - return false; - stamp_request(request); file_response_ready = false; pending_file = out; @@ -112,6 +103,11 @@ bool SensecapIndicator::file_request(meshtastic_InterdeviceMessage &request, mes bool SensecapIndicator::file_read(const char *path, uint32_t offset, uint32_t length, meshtastic_FileTransfer *out, uint32_t timeout_ms) { + InFlight busy(request_in_flight); + concurrency::LockGuard guard(&link_lock); + if (packets_received == 0) + return false; + meshtastic_InterdeviceMessage &msg = tx_message; memset(&msg, 0, sizeof(msg)); msg.which_data = meshtastic_InterdeviceMessage_file_transfer_tag; @@ -125,6 +121,11 @@ bool SensecapIndicator::file_read(const char *path, uint32_t offset, uint32_t le bool SensecapIndicator::file_write(const char *path, uint32_t offset, const uint8_t *data, size_t len, bool create, meshtastic_FileTransfer *out, uint32_t timeout_ms) { + InFlight busy(request_in_flight); + concurrency::LockGuard guard(&link_lock); + if (packets_received == 0) + return false; + meshtastic_InterdeviceMessage &msg = tx_message; memset(&msg, 0, sizeof(msg)); msg.which_data = meshtastic_InterdeviceMessage_file_transfer_tag; @@ -140,6 +141,11 @@ bool SensecapIndicator::file_write(const char *path, uint32_t offset, const uint bool SensecapIndicator::file_remove(const char *path, meshtastic_FileTransfer *out, uint32_t timeout_ms) { + InFlight busy(request_in_flight); + concurrency::LockGuard guard(&link_lock); + if (packets_received == 0) + return false; + meshtastic_InterdeviceMessage &msg = tx_message; memset(&msg, 0, sizeof(msg)); msg.which_data = meshtastic_InterdeviceMessage_file_transfer_tag; @@ -318,7 +324,7 @@ bool SensecapIndicator::handle_packet(size_t payload_len) switch (message.which_data) { case meshtastic_InterdeviceMessage_nmea_tag: // send String to NMEA processing - FakeSerial->stuff_buffer(message.data.nmea, strnlen(message.data.nmea, sizeof(message.data.nmea) - 1)); + uartProxy->stuff_buffer(message.data.nmea, strnlen(message.data.nmea, sizeof(message.data.nmea) - 1)); return true; case meshtastic_InterdeviceMessage_i2c_result_tag: // response for the transaction i2c_transact() is waiting on @@ -363,6 +369,14 @@ bool SensecapIndicator::handle_packet(size_t payload_len) // no payload to deliver: receiving it already counted the packet, // which is all wait_ready() is looking for return true; + case meshtastic_InterdeviceMessage_nack_tag: + // id 0 means the request was undecodable over there; the link is + // single-outstanding, so that can only be the request in flight + if (message.id == expected_id || message.id == 0) { + LOG_WARN("Request %u nacked by the co-processor", expected_id); + request_nacked = true; + } + return true; case meshtastic_InterdeviceMessage_sd_info_tag: if (pending_sd_info && message.id == expected_id) { *pending_sd_info = message.data.sd_info; @@ -387,4 +401,4 @@ bool SensecapIndicator::send(const char *buf, size_t len) return false; } -#endif // SENSECAP_INDICATOR \ No newline at end of file +#endif // SENSECAP_INDICATOR diff --git a/src/mesh/IndicatorSerial.h b/src/mesh/IndicatorSerial.h index f5640b5af39..06ea50eb6a2 100644 --- a/src/mesh/IndicatorSerial.h +++ b/src/mesh/IndicatorSerial.h @@ -1,8 +1,5 @@ #pragma once -#ifndef INDICATORSERIAL_H -#define INDICATORSERIAL_H - #ifdef SENSECAP_INDICATOR #include "concurrency/Lock.h" @@ -18,9 +15,6 @@ // The header is the magic number plus a 16-bit payload-length field #define MT_HEADER_SIZE 4 -// Wait this many msec if there's nothing new on the channel -#define NO_NEWS_PAUSE 25 - #define PB_BUFSIZE (meshtastic_InterdeviceMessage_size + MT_HEADER_SIZE) class SensecapIndicator : public concurrency::OSThread @@ -28,7 +22,7 @@ class SensecapIndicator : public concurrency::OSThread public: SensecapIndicator(HardwareSerial &serial); int32_t runOnce() override; - // Standalone send (e.g. NMEA from FakeUART), takes link_lock to + // Standalone send (e.g. NMEA from UARTProxy), takes link_lock to // serialize the shared TX buffer against the request methods bool send_uplink(const meshtastic_InterdeviceMessage &message); @@ -50,7 +44,8 @@ class SensecapIndicator : public concurrency::OSThread // subdirectories get a trailing slash) bool list_directory(const char *path, uint32_t offset, meshtastic_DirectoryListing *out, uint32_t timeout_ms = 1000); // SD card statistics, answered from a cache on the co-processor. - // used/free read zero while its background FAT scan is still running + // used/free are only meaningful once stats_valid is set: the FAT scan + // behind them runs in the background after the card is mounted bool sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms = 2000); // True once at least one valid packet was received from the RP2040. @@ -67,15 +62,14 @@ class SensecapIndicator : public concurrency::OSThread pb_byte_t pb_tx_buf[PB_BUFSIZE]; pb_byte_t pb_rx_buf[PB_BUFSIZE]; size_t pb_rx_size = 0; // Number of bytes currently in the buffer - HardwareSerial *_serial = &Serial2; - bool running = false; + HardwareSerial *_serial = nullptr; uint32_t packets_received = 0; meshtastic_I2CResult i2c_result = meshtastic_I2CResult_init_zero; bool i2c_result_ready = false; // Statically allocated message structs: with 4KB file chunks an - // InterdeviceMessage is ~4.6KB, too large for task stacks. rx is used - // by handle_packet (under link_lock); tx only by the file/dir/sd - // requests, which all originate from the single UI task. + // InterdeviceMessage is ~4.6KB, too large for task stacks. Both are + // only touched while link_lock is held, so requests staged by one + // thread cannot be overwritten by another. meshtastic_InterdeviceMessage rx_message; meshtastic_InterdeviceMessage tx_message; // Response destinations for the file operation in flight @@ -89,6 +83,8 @@ class SensecapIndicator : public concurrency::OSThread // arrives after its request timed out cannot satisfy a later request uint32_t next_request_id = 0; uint32_t expected_id = 0; + // a nack response fails the request in flight without its timeout + bool request_nacked = false; // True while a requester holds link_lock for a full request/response // round trip (up to the request timeout). runOnce checks it so the // cooperative main loop is not blocked on the lock for that long. @@ -100,6 +96,7 @@ class SensecapIndicator : public concurrency::OSThread }; uint32_t stamp_request(meshtastic_InterdeviceMessage &request); bool send_uplink_unlocked(const meshtastic_InterdeviceMessage &message); + // callers hold link_lock (the request was staged in the shared tx_message) bool file_request(meshtastic_InterdeviceMessage &request, meshtastic_FileTransfer *out, uint32_t timeout_ms); bool wait_response(bool &flag, uint32_t timeout_ms); void pump(); @@ -112,4 +109,3 @@ class SensecapIndicator : public concurrency::OSThread extern SensecapIndicator *sensecapIndicator; #endif // SENSECAP_INDICATOR -#endif // INDICATORSERIAL_H \ No newline at end of file diff --git a/src/mesh/comms/FakeI2C.cpp b/src/mesh/comms/I2CProxy.cpp similarity index 85% rename from src/mesh/comms/FakeI2C.cpp rename to src/mesh/comms/I2CProxy.cpp index f97e07d6354..766ccfa5f17 100644 --- a/src/mesh/comms/FakeI2C.cpp +++ b/src/mesh/comms/I2CProxy.cpp @@ -1,23 +1,23 @@ #ifdef SENSECAP_INDICATOR -#include "FakeI2C.h" +#include "I2CProxy.h" #include "../IndicatorSerial.h" -FakeI2C *FakeWire = new FakeI2C(); +I2CProxy *i2cProxy = new I2CProxy(); -void FakeI2C::acquire() +void I2CProxy::acquire() { _lock.lock(); _owner = xTaskGetCurrentTaskHandle(); } -void FakeI2C::release() +void I2CProxy::release() { _owner = nullptr; _lock.unlock(); } -void FakeI2C::beginTransmission(uint8_t address) +void I2CProxy::beginTransmission(uint8_t address) { // re-begin from the owning thread just restages, everyone else waits if (_owner != xTaskGetCurrentTaskHandle()) @@ -27,7 +27,7 @@ void FakeI2C::beginTransmission(uint8_t address) _txPending = true; } -size_t FakeI2C::write(uint8_t data) +size_t I2CProxy::write(uint8_t data) { if (!_txPending || _txLen >= MAX_WRITE) return 0; @@ -35,7 +35,7 @@ size_t FakeI2C::write(uint8_t data) return 1; } -size_t FakeI2C::write(const uint8_t *data, size_t len) +size_t I2CProxy::write(const uint8_t *data, size_t len) { size_t n = 0; while (n < len && write(data[n])) @@ -43,7 +43,7 @@ size_t FakeI2C::write(const uint8_t *data, size_t len) return n; } -uint8_t FakeI2C::endTransmission(bool stopBit) +uint8_t I2CProxy::endTransmission(bool stopBit) { if (_owner != xTaskGetCurrentTaskHandle()) return 4; // endTransmission without beginTransmission @@ -60,7 +60,7 @@ uint8_t FakeI2C::endTransmission(bool stopBit) return rv; } -size_t FakeI2C::requestFrom(uint8_t address, size_t len, bool stopBit) +size_t I2CProxy::requestFrom(uint8_t address, size_t len, bool stopBit) { (void)stopBit; if (len > MAX_READ) @@ -83,24 +83,24 @@ size_t FakeI2C::requestFrom(uint8_t address, size_t len, bool stopBit) return rv == 0 ? _rxLen : 0; } -int FakeI2C::available() +int I2CProxy::available() { return (int)(_rxLen - _rxPos); } -int FakeI2C::read() +int I2CProxy::read() { return _rxPos < _rxLen ? _rxBuf[_rxPos++] : -1; } -int FakeI2C::peek() +int I2CProxy::peek() { return _rxPos < _rxLen ? _rxBuf[_rxPos] : -1; } // Run one tunneled transaction, returns TwoWire endTransmission error codes: // 0 success, 1 data too long, 2 NACK on address, 3 NACK on data, 4 other, 5 timeout -uint8_t FakeI2C::transact(uint8_t address, const uint8_t *wbuf, size_t wlen, size_t rlen) +uint8_t I2CProxy::transact(uint8_t address, const uint8_t *wbuf, size_t wlen, size_t rlen) { _rxLen = 0; _rxPos = 0; diff --git a/src/mesh/comms/FakeI2C.h b/src/mesh/comms/I2CProxy.h similarity index 97% rename from src/mesh/comms/FakeI2C.h rename to src/mesh/comms/I2CProxy.h index 92ba3ea49b5..9be0887de81 100644 --- a/src/mesh/comms/FakeI2C.h +++ b/src/mesh/comms/I2CProxy.h @@ -28,10 +28,10 @@ * TwoWire, the read buffer must be consumed before the next transaction * from another thread replaces it. */ -class FakeI2C : public TwoWire +class I2CProxy : public TwoWire { public: - FakeI2C() : TwoWire(0) {} + I2CProxy() : TwoWire(0) {} bool end() override { return true; } bool setClock(uint32_t) override { return true; } @@ -79,6 +79,6 @@ class FakeI2C : public TwoWire uint8_t transact(uint8_t address, const uint8_t *wbuf, size_t wlen, size_t rlen); }; -extern FakeI2C *FakeWire; +extern I2CProxy *i2cProxy; #endif // SENSECAP_INDICATOR diff --git a/src/mesh/comms/FakeUART.cpp b/src/mesh/comms/UARTProxy.cpp similarity index 61% rename from src/mesh/comms/FakeUART.cpp rename to src/mesh/comms/UARTProxy.cpp index c1a77f2fcc1..f576859badf 100644 --- a/src/mesh/comms/FakeUART.cpp +++ b/src/mesh/comms/UARTProxy.cpp @@ -1,30 +1,30 @@ -#include "FakeUART.h" +#include "UARTProxy.h" #ifdef SENSECAP_INDICATOR -FakeUART *FakeSerial = new FakeUART(); +UARTProxy *uartProxy = new UARTProxy(); -FakeUART::FakeUART() {} +UARTProxy::UARTProxy() {} -void FakeUART::begin(unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPin, bool invert, unsigned long timeout_ms, - uint8_t rxfifo_full_thrhd) +void UARTProxy::begin(unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPin, bool invert, unsigned long timeout_ms, + uint8_t rxfifo_full_thrhd) { baudrate = baud; buf_clear(); - LOG_DEBUG("FakeUART::begin(%lu)", baud); + LOG_DEBUG("UARTProxy::begin(%lu)", baud); } -void FakeUART::end() +void UARTProxy::end() { buf_clear(); } -int FakeUART::available() +int UARTProxy::available() { return buf_avail(); } -int FakeUART::peek() +int UARTProxy::peek() { if (buf_tail == buf_head) return -1; @@ -32,7 +32,7 @@ int FakeUART::peek() return buf[buf_tail]; } -int FakeUART::read() +int UARTProxy::read() { if (buf_tail == buf_head) return -1; @@ -42,37 +42,37 @@ int FakeUART::read() return ret; } -void FakeUART::flush(bool wait) +void UARTProxy::flush(bool wait) { buf_clear(); } -uint32_t FakeUART::baudRate() +uint32_t UARTProxy::baudRate() { return baudrate; } -void FakeUART::updateBaudRate(unsigned long speed) +void UARTProxy::updateBaudRate(unsigned long speed) { baudrate = speed; } -size_t FakeUART::setRxBufferSize(size_t size) +size_t UARTProxy::setRxBufferSize(size_t size) { return size; } -size_t FakeUART::write(const char *buffer) +size_t UARTProxy::write(const char *buffer) { return write((char *)buffer, strlen(buffer)); } -size_t FakeUART::write(uint8_t *buffer, size_t size) +size_t UARTProxy::write(uint8_t *buffer, size_t size) { return write((char *)buffer, size); } -size_t FakeUART::write(char *buffer, size_t size) +size_t UARTProxy::write(char *buffer, size_t size) { // static: ~4.6KB struct, too large for task stacks; only written from // the cooperative main loop (GPS thread) @@ -83,12 +83,12 @@ size_t FakeUART::write(char *buffer, size_t size) } memcpy(message.data.nmea, buffer, size); message.which_data = meshtastic_InterdeviceMessage_nmea_tag; - LOG_DEBUG("FakeUART::write(%s)", message.data.nmea); + LOG_DEBUG("UARTProxy::write %u bytes", (unsigned)size); sensecapIndicator->send_uplink(message); return size; } -size_t FakeUART::stuff_buffer(const char *buffer, size_t size) +size_t UARTProxy::stuff_buffer(const char *buffer, size_t size) { // push buffer byte-wise, stop when full for (size_t i = 0; i < size; i++) { @@ -99,4 +99,4 @@ size_t FakeUART::stuff_buffer(const char *buffer, size_t size) return size; } -#endif // SENSECAP_INDICATOR \ No newline at end of file +#endif // SENSECAP_INDICATOR diff --git a/src/mesh/comms/FakeUART.h b/src/mesh/comms/UARTProxy.h similarity index 82% rename from src/mesh/comms/FakeUART.h rename to src/mesh/comms/UARTProxy.h index fbd9c8b841c..b99fd9d8c4a 100644 --- a/src/mesh/comms/FakeUART.h +++ b/src/mesh/comms/UARTProxy.h @@ -1,18 +1,24 @@ #pragma once -#ifndef FAKEUART_H -#define FAKEUART_H - #ifdef SENSECAP_INDICATOR #include "../IndicatorSerial.h" #include #include -class FakeUART : public Stream +/** + * Stream implementation that proxies a serial port living on the RP2040 + * over the interdevice link, so the regular GPS driver of the main + * firmware can talk to the GNSS module attached to the secondary MCU. + * + * Reads are served from a ring buffer that the link fills with the NMEA + * sentences the co-processor forwards; writes are packed into a single + * uplink message each. + */ +class UARTProxy : public Stream { public: - FakeUART(); + UARTProxy(); void begin(unsigned long baud, uint32_t config = 0x800001c, int8_t rxPin = -1, int8_t txPin = -1, bool invert = false, unsigned long timeout_ms = 20000UL, uint8_t rxfifo_full_thrhd = 112); @@ -65,8 +71,6 @@ class FakeUART : public Stream size_t buf_avail() { return (buf_head + BUF_SIZE - buf_tail) % BUF_SIZE; } }; -extern FakeUART *FakeSerial; +extern UARTProxy *uartProxy; #endif // SENSECAP_INDICATOR - -#endif // FAKEUART_H \ No newline at end of file diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index 02c9b2e3fe9..3f2bb3e4354 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -130,6 +130,11 @@ typedef struct _meshtastic_InterdeviceMessage { id. Touches no peripherals, so it works with nothing attached. */ bool ping; bool pong; + /* Response: the request could not be decoded or is of an unhandled + type, so the requester fails fast instead of burning its timeout. + Echoes the id when known, 0 when the frame was undecodable. Never + sent in reaction to a nack. */ + bool nack; } data; /* Correlates a response with its request: responses echo the id of the request they answer. 0 for unsolicited messages (e.g. the nmea stream). */ @@ -222,6 +227,7 @@ extern "C" { #define meshtastic_InterdeviceMessage_sd_info_tag 10 #define meshtastic_InterdeviceMessage_ping_tag 11 #define meshtastic_InterdeviceMessage_pong_tag 12 +#define meshtastic_InterdeviceMessage_nack_tag 13 #define meshtastic_InterdeviceMessage_id_tag 15 /* Struct field encoding specification for nanopb */ @@ -284,6 +290,7 @@ X(a, STATIC, ONEOF, BOOL, (data,get_sd_info,data.get_sd_info), 9) \ X(a, STATIC, ONEOF, MESSAGE, (data,sd_info,data.sd_info), 10) \ X(a, STATIC, ONEOF, BOOL, (data,ping,data.ping), 11) \ X(a, STATIC, ONEOF, BOOL, (data,pong,data.pong), 12) \ +X(a, STATIC, ONEOF, BOOL, (data,nack,data.nack), 13) \ X(a, STATIC, SINGULAR, UINT32, id, 15) #define meshtastic_InterdeviceMessage_CALLBACK NULL #define meshtastic_InterdeviceMessage_DEFAULT NULL From e7224671cb5b366028ed35bfa9a02dde74fcc0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 09:28:53 +0200 Subject: [PATCH 09/28] indicator: refuse a co-processor that speaks another protocol version The ping/pong handshake now carries InterdeviceVersion. A pong reporting a version other than ours means the RP2040 runs firmware that does not match this build, so the bridge stays shut down for the session and the mismatch is logged with both versions. Requests fail fast instead of being misinterpreted by the other side. --- src/mesh/IndicatorSerial.cpp | 58 +++++++++++++------ src/mesh/IndicatorSerial.h | 16 +++-- .../generated/meshtastic/interdevice.pb.cpp | 2 + .../generated/meshtastic/interdevice.pb.h | 31 ++++++++-- 4 files changed, 79 insertions(+), 28 deletions(-) diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp index ba6c8813fab..2fab162f0a3 100644 --- a/src/mesh/IndicatorSerial.cpp +++ b/src/mesh/IndicatorSerial.cpp @@ -69,12 +69,19 @@ uint32_t SensecapIndicator::stamp_request(meshtastic_InterdeviceMessage &request return next_request_id; } +// callers hold link_lock: the co-processor has answered at least once and +// speaks our protocol version. Fails fast instead of timing out per request. +bool SensecapIndicator::link_ready() +{ + return packets_received > 0 && link_compatible; +} + bool SensecapIndicator::i2c_transact(meshtastic_InterdeviceMessage &request, meshtastic_I2CResult *result, uint32_t timeout_ms) { InFlight busy(request_in_flight); concurrency::LockGuard guard(&link_lock); - if (packets_received == 0) - return false; // co-processor has never talked to us, fail fast instead of timing out + if (!link_ready()) + return false; stamp_request(request); i2c_result_ready = false; @@ -105,7 +112,7 @@ bool SensecapIndicator::file_read(const char *path, uint32_t offset, uint32_t le { InFlight busy(request_in_flight); concurrency::LockGuard guard(&link_lock); - if (packets_received == 0) + if (!link_ready()) return false; meshtastic_InterdeviceMessage &msg = tx_message; @@ -123,7 +130,7 @@ bool SensecapIndicator::file_write(const char *path, uint32_t offset, const uint { InFlight busy(request_in_flight); concurrency::LockGuard guard(&link_lock); - if (packets_received == 0) + if (!link_ready()) return false; meshtastic_InterdeviceMessage &msg = tx_message; @@ -143,7 +150,7 @@ bool SensecapIndicator::file_remove(const char *path, meshtastic_FileTransfer *o { InFlight busy(request_in_flight); concurrency::LockGuard guard(&link_lock); - if (packets_received == 0) + if (!link_ready()) return false; meshtastic_InterdeviceMessage &msg = tx_message; @@ -158,7 +165,7 @@ bool SensecapIndicator::list_directory(const char *path, uint32_t offset, meshta { InFlight busy(request_in_flight); concurrency::LockGuard guard(&link_lock); - if (packets_received == 0) + if (!link_ready()) return false; meshtastic_InterdeviceMessage &msg = tx_message; @@ -181,7 +188,7 @@ bool SensecapIndicator::sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms) { InFlight busy(request_in_flight); concurrency::LockGuard guard(&link_lock); - if (packets_received == 0) + if (!link_ready()) return false; meshtastic_InterdeviceMessage &msg = tx_message; @@ -206,29 +213,36 @@ bool SensecapIndicator::wait_ready(uint32_t timeout_ms) uint32_t start = millis(); uint32_t last_probe = 0; bool probed = false; - while (packets_received == 0) { + while (!pong_received) { // The co-processor never sends anything unsolicited unless a GPS // module is attached, so waiting passively would leave the bridge - // down forever on GPS-less units. Ping until any response marks - // the link up; pong touches no peripherals on the other side. + // down forever on GPS-less units. Ping until it answers; the pong + // touches no peripherals on the other side and carries the + // protocol version it speaks. if (!probed || millis() - last_probe >= 250) { meshtastic_InterdeviceMessage &msg = tx_message; memset(&msg, 0, sizeof(msg)); msg.which_data = meshtastic_InterdeviceMessage_ping_tag; - msg.data.ping = true; + msg.data.ping = meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT; stamp_request(msg); send_uplink_unlocked(msg); last_probe = millis(); probed = true; } pump(); - if (packets_received > 0) + if (pong_received) break; - if (millis() - start >= timeout_ms) + if (millis() - start >= timeout_ms) { + if (packets_received > 0) { + // it talks (NMEA), it just did not answer the handshake + LOG_WARN("RP2040 did not answer the version handshake"); + return true; + } return false; + } delay(1); } - return true; + return link_compatible; } bool SensecapIndicator::send_uplink(const meshtastic_InterdeviceMessage &message) @@ -361,13 +375,23 @@ bool SensecapIndicator::handle_packet(size_t payload_len) memset(&pong, 0, sizeof(pong)); pong.id = message.id; pong.which_data = meshtastic_InterdeviceMessage_pong_tag; - pong.data.pong = true; + pong.data.pong = meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT; send_uplink_unlocked(pong); return true; } case meshtastic_InterdeviceMessage_pong_tag: - // no payload to deliver: receiving it already counted the packet, - // which is all wait_ready() is looking for + // The handshake: the co-processor reports the protocol version it + // speaks. Anything else than ours means its firmware does not match + // this build, and every request would be misinterpreted. + pong_received = true; + if (message.data.pong != meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT) { + link_compatible = false; + LOG_ERROR("RP2040 speaks interdevice protocol v%u, this firmware speaks v%u. Flash the matching " + "indicator_rp2040 firmware; sensors, GPS and SD card stay disabled", + (unsigned)message.data.pong, (unsigned)meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT); + } else { + LOG_INFO("RP2040 link up, interdevice protocol v%u", (unsigned)message.data.pong); + } return true; case meshtastic_InterdeviceMessage_nack_tag: // id 0 means the request was undecodable over there; the link is diff --git a/src/mesh/IndicatorSerial.h b/src/mesh/IndicatorSerial.h index 06ea50eb6a2..c2733738116 100644 --- a/src/mesh/IndicatorSerial.h +++ b/src/mesh/IndicatorSerial.h @@ -48,11 +48,12 @@ class SensecapIndicator : public concurrency::OSThread // behind them runs in the background after the card is mounted bool sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms = 2000); - // True once at least one valid packet was received from the RP2040. - // Actively probes the co-processor (it never speaks unsolicited unless a - // GPS module is attached) and pumps the link until a response arrives or - // the timeout expires. Used to defer bridge traffic until the - // co-processor has booted. + // True once the co-processor has answered and speaks our protocol + // version. Actively probes it (it never speaks unsolicited unless a GPS + // module is attached) and pumps the link until a pong arrives or the + // timeout expires. Used to defer bridge traffic until the co-processor + // has booted. A version mismatch fails permanently: no request is sent + // to a co-processor running incompatible firmware. bool wait_ready(uint32_t timeout_ms); private: @@ -64,6 +65,10 @@ class SensecapIndicator : public concurrency::OSThread size_t pb_rx_size = 0; // Number of bytes currently in the buffer HardwareSerial *_serial = nullptr; uint32_t packets_received = 0; + // cleared when a pong reports a protocol version we do not speak; the + // bridge stays shut down for the rest of the session + bool link_compatible = true; + bool pong_received = false; meshtastic_I2CResult i2c_result = meshtastic_I2CResult_init_zero; bool i2c_result_ready = false; // Statically allocated message structs: with 4KB file chunks an @@ -94,6 +99,7 @@ class SensecapIndicator : public concurrency::OSThread explicit InFlight(volatile bool &f) : flag(f) { flag = true; } ~InFlight() { flag = false; } }; + bool link_ready(); uint32_t stamp_request(meshtastic_InterdeviceMessage &request); bool send_uplink_unlocked(const meshtastic_InterdeviceMessage &message); // callers hold link_lock (the request was staged in the shared tx_message) diff --git a/src/mesh/generated/meshtastic/interdevice.pb.cpp b/src/mesh/generated/meshtastic/interdevice.pb.cpp index eb8f528d39f..6c49cf3fb1b 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.cpp +++ b/src/mesh/generated/meshtastic/interdevice.pb.cpp @@ -33,3 +33,5 @@ PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 4) + + diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index 3f2bb3e4354..4348e1f3109 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -10,6 +10,18 @@ #endif /* Enum definitions */ +/* Version of the interdevice protocol spoken on the link. Both sides send + theirs in the ping/pong handshake; a peer reporting a different one runs + firmware that does not match and is not talked to. + + On a change that breaks the other side (renumbered fields, changed + semantics, removed messages), raise the value of CURRENT. Do not add + another entry: this enum carries a single constant, not a history. */ +typedef enum _meshtastic_InterdeviceVersion { + meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_UNSPECIFIED = 0, + meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT = 1 +} meshtastic_InterdeviceVersion; + /* Defines the supported file operations */ typedef enum _meshtastic_FileOperation { meshtastic_FileOperation_GET = 0, @@ -126,10 +138,13 @@ typedef struct _meshtastic_InterdeviceMessage { meshtastic_DirectoryListing directory_listing; bool get_sd_info; /* Request: SD card statistics */ meshtastic_SdCardInfo sd_info; /* Response */ - /* Link liveness probe. The receiver answers ping with pong, echoing the - id. Touches no peripherals, so it works with nothing attached. */ - bool ping; - bool pong; + /* Link liveness probe and version handshake. The receiver answers ping + with pong, echoing the id. Touches no peripherals, so it works with + nothing attached. Both carry the sender's InterdeviceVersion; a peer + that answers with a different one speaks another protocol and must + not be used. */ + uint32_t ping; + uint32_t pong; /* Response: the request could not be decoded or is of an unhandled type, so the requester fails fast instead of burning its timeout. Echoes the id when known, 0 when the frame was undecodable. Never @@ -147,6 +162,10 @@ extern "C" { #endif /* Helper constants for enums */ +#define _meshtastic_InterdeviceVersion_MIN meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_UNSPECIFIED +#define _meshtastic_InterdeviceVersion_MAX meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT +#define _meshtastic_InterdeviceVersion_ARRAYSIZE ((meshtastic_InterdeviceVersion)(meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT+1)) + #define _meshtastic_FileOperation_MIN meshtastic_FileOperation_GET #define _meshtastic_FileOperation_MAX meshtastic_FileOperation_DELETE #define _meshtastic_FileOperation_ARRAYSIZE ((meshtastic_FileOperation)(meshtastic_FileOperation_DELETE+1)) @@ -288,8 +307,8 @@ X(a, STATIC, ONEOF, MESSAGE, (data,file_transfer,data.file_transfer), 7) X(a, STATIC, ONEOF, MESSAGE, (data,directory_listing,data.directory_listing), 8) \ X(a, STATIC, ONEOF, BOOL, (data,get_sd_info,data.get_sd_info), 9) \ X(a, STATIC, ONEOF, MESSAGE, (data,sd_info,data.sd_info), 10) \ -X(a, STATIC, ONEOF, BOOL, (data,ping,data.ping), 11) \ -X(a, STATIC, ONEOF, BOOL, (data,pong,data.pong), 12) \ +X(a, STATIC, ONEOF, UINT32, (data,ping,data.ping), 11) \ +X(a, STATIC, ONEOF, UINT32, (data,pong,data.pong), 12) \ X(a, STATIC, ONEOF, BOOL, (data,nack,data.nack), 13) \ X(a, STATIC, SINGULAR, UINT32, id, 15) #define meshtastic_InterdeviceMessage_CALLBACK NULL From 8057c611369f634552f7ecf4b022544b97e6aa2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 09:44:25 +0200 Subject: [PATCH 10/28] indicator: regen protos, interdevice protocol version 2 --- src/mesh/generated/meshtastic/interdevice.pb.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index 4348e1f3109..f5a67d0357a 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -19,7 +19,10 @@ another entry: this enum carries a single constant, not a history. */ typedef enum _meshtastic_InterdeviceVersion { meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_UNSPECIFIED = 0, - meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT = 1 + /* Never use 1: ping/pong were bools before the handshake existed, and a + bool true is the same varint on the wire as the number 1, so firmware + predating the handshake would pass it. */ + meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT = 2 } meshtastic_InterdeviceVersion; /* Defines the supported file operations */ From 57866fa7b5c6741b6850b1ab38c54f4b09cb0133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 10:05:33 +0200 Subject: [PATCH 11/28] indicator: per-task I2C contexts, gated handshake, retryable link failures The bridged I2C bus is shared between the main loop and the UI task, and TwoWire has no transaction bracket a lock can span: drivers drain the read buffer with available()/read() long after requestFrom() returned. Each calling task therefore gets its own staging and read buffers instead of a lock that could be left held (or that could not protect the read buffer anyway). The transaction is staged inside the link, under its lock. No request is sent before the co-processor has completed the version handshake, and runOnce keeps probing until it does, so a co-processor that boots slowly or reboots on its watchdog no longer leaves the bridge dead for the session. Requests in flight are counted, not flagged: two threads can be in a request and the first one out must not clear the other's state. File operations are retried on a lost frame and on a co-processor busy with card maintenance, but not on a refusal (nack) or a definitive failure, and they release the SPI lock while they wait so a slow link does not starve the radio. --- src/graphics/tftSetup.cpp | 156 +++++++++++++----- src/main.cpp | 3 +- src/mesh/IndicatorSerial.cpp | 119 +++++++------ src/mesh/IndicatorSerial.h | 47 ++++-- src/mesh/comms/I2CProxy.cpp | 112 ++++++------- src/mesh/comms/I2CProxy.h | 45 +++-- src/mesh/comms/UARTProxy.cpp | 4 + .../generated/meshtastic/interdevice.pb.cpp | 2 + .../generated/meshtastic/interdevice.pb.h | 49 ++++-- 9 files changed, 338 insertions(+), 199 deletions(-) diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 28e4b915da9..34ef0ebf4c2 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -20,17 +20,68 @@ #include "mesh/IndicatorSerial.h" #include "mesh/comms/I2CProxy.h" +// Set while the TFT task holds spiLock around the LVGL handler, so the file +// operations below (which run inside LVGL callbacks) can hand the SPI bus +// back to the radio while they wait on the interdevice link +static volatile TaskHandle_t spiLockHolder = nullptr; + +/** + * The remote FS round trips block for the request timeout, and the LVGL + * handler runs with spiLock held, which would starve the LoRa radio for + * that whole time. Nothing in a link round trip touches SPI, so give the + * bus back for its duration. + */ +class SpiLockBreak +{ + public: + SpiLockBreak() + { + held = spiLockHolder == xTaskGetCurrentTaskHandle(); + if (held) { + spiLockHolder = nullptr; + spiLock->unlock(); + } + } + ~SpiLockBreak() + { + if (held) { + spiLock->lock(); + spiLockHolder = xTaskGetCurrentTaskHandle(); + } + } + + private: + bool held; +}; + // Serves the UI map tiles from the SD card behind the RP2040, chunk-wise // over the interdevice link. // -// Every operation retries once on a transport timeout: a frame lost to an -// overflow or resync must not fail the whole tile. Correlation ids make -// this safe, a late response to the first attempt is dropped as stale. -// Definitive answers (success=false from the co-processor) are never -// retried, missing-tile probes stay a single round trip. +// Retry policy: a lost frame (timeout) and a co-processor busy with card +// maintenance (FILE_BUSY) are transient, so they are retried; a request the +// co-processor refused (nack) or answered definitively (missing file, IO +// error) is not. Correlation ids make retrying safe, a late response to the +// first attempt is dropped as stale. class IndicatorRemoteFS : public IRemoteFS { - static const int LINK_RETRIES = 1; + static const int LINK_ATTEMPTS = 3; + static const uint32_t BUSY_BACKOFF_MS = 150; + + // Returns true when the request should be sent again. `answered` is + // false when the link itself failed (timeout), true when the + // co-processor replied. + static bool retryable(bool answered, meshtastic_FileStatus status, int attempt) + { + if (attempt + 1 >= LINK_ATTEMPTS) + return false; + if (!answered) + return !sensecapIndicator->last_request_nacked(); // refused, not lost + if (status == meshtastic_FileStatus_FILE_BUSY) { + delay(BUSY_BACKOFF_MS); // card maintenance, give it a moment + return true; + } + return false; + } public: bool readChunk(const char *path, uint32_t offset, uint8_t *buf, uint32_t len, uint32_t *bytesRead, @@ -38,20 +89,22 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; - for (int attempt = 0; attempt <= LINK_RETRIES; attempt++) { + SpiLockBreak spiFree; + for (int attempt = 0; attempt < LINK_ATTEMPTS; attempt++) { memset(&result, 0, sizeof(result)); - if (!sensecapIndicator->file_read(path, offset, len, &result)) - continue; // transport timeout, retry - if (!result.success) + bool answered = sensecapIndicator->file_read(path, offset, len, &result); + if (answered && result.status == meshtastic_FileStatus_FILE_OK) { + uint32_t n = result.filedata.size; + if (n > len) + n = len; + memcpy(buf, result.filedata.bytes, n); + *bytesRead = n; + // lv_fs positions are 32 bit, map tiles never come close + *fileSize = (uint32_t)result.file_size; + return true; + } + if (!retryable(answered, result.status, attempt)) return false; - uint32_t n = result.filedata.size; - if (n > len) - n = len; - memcpy(buf, result.filedata.bytes, n); - *bytesRead = n; - // lv_fs positions are 32 bit, map tiles never come close - *fileSize = (uint32_t)result.file_size; - return true; } return false; } @@ -60,18 +113,22 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; - for (int attempt = 0; attempt <= LINK_RETRIES; attempt++) { + SpiLockBreak spiFree; + for (int attempt = 0; attempt < LINK_ATTEMPTS; attempt++) { memset(&result, 0, sizeof(result)); - if (!sensecapIndicator->file_write(path, offset, buf, len, create, &result)) - continue; // transport timeout, retry (POST re-truncates, safe) - if (result.success) - return true; - // The retry of an append whose first attempt landed but whose - // response was lost is rejected as an offset conflict carrying - // the resulting file size - if (attempt > 0 && !create && result.file_size == (uint64_t)offset + len) - return true; - return false; + bool answered = sensecapIndicator->file_write(path, offset, buf, len, create, &result); + if (answered) { + if (result.status == meshtastic_FileStatus_FILE_OK) + return true; + // An append whose first attempt landed but whose response was + // lost is refused as an offset conflict, and the file already + // holds this chunk: that is the outcome we wanted + if (attempt > 0 && !create && result.status == meshtastic_FileStatus_FILE_OFFSET_CONFLICT && + result.file_size == (uint64_t)offset + len) + return true; + } + if (!retryable(answered, result.status, attempt)) + return false; } return false; } @@ -80,10 +137,14 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; + SpiLockBreak spiFree; meshtastic_SdCardInfo result = meshtastic_SdCardInfo_init_zero; bool ok = false; - for (int attempt = 0; attempt <= LINK_RETRIES && !ok; attempt++) + for (int attempt = 0; attempt < LINK_ATTEMPTS && !ok; attempt++) { ok = sensecapIndicator->sd_info(&result); + if (!ok && sensecapIndicator->last_request_nacked()) + break; + } if (!ok) return false; info.present = result.present; @@ -100,14 +161,17 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; - for (int attempt = 0; attempt <= LINK_RETRIES; attempt++) { + SpiLockBreak spiFree; + for (int attempt = 0; attempt < LINK_ATTEMPTS; attempt++) { memset(&result, 0, sizeof(result)); - if (!sensecapIndicator->file_remove(path, &result)) - continue; // transport timeout, retry - if (result.success) + bool answered = sensecapIndicator->file_remove(path, &result); + // delete is idempotent on the co-processor: a file that is + // already gone reports OK, so a retry after a lost response does + // not have to be guessed at here + if (answered && result.status == meshtastic_FileStatus_FILE_OK) return true; - // after a lost response the retry finds the file already gone - return attempt > 0; + if (!retryable(answered, result.status, attempt)) + return false; } return false; } @@ -116,14 +180,19 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; + SpiLockBreak spiFree; uint32_t offset = 0; while (true) { - bool ok = false; - for (int attempt = 0; attempt <= LINK_RETRIES && !ok; attempt++) { + bool got_page = false; + for (int attempt = 0; attempt < LINK_ATTEMPTS && !got_page; attempt++) { memset(&listing, 0, sizeof(listing)); - ok = sensecapIndicator->list_directory(path, offset, &listing); + bool answered = sensecapIndicator->list_directory(path, offset, &listing); + if (answered && listing.status == meshtastic_FileStatus_FILE_OK) + got_page = true; + else if (!retryable(answered, listing.status, attempt)) + return false; } - if (!ok || !listing.success) + if (!got_page) return false; for (pb_size_t i = 0; i < listing.filenames_count; i++) entries.insert(listing.filenames[i]); @@ -160,7 +229,14 @@ void tft_task_handler(void *param = nullptr) { while (true) { spiLock->lock(); +#ifdef SENSECAP_INDICATOR + // lets the remote FS hand the bus back while it waits on the link + spiLockHolder = xTaskGetCurrentTaskHandle(); +#endif deviceScreen->task_handler(); +#ifdef SENSECAP_INDICATOR + spiLockHolder = nullptr; +#endif spiLock->unlock(); deviceScreen->sleep(); } diff --git a/src/main.cpp b/src/main.cpp index a357824170f..73334f2d094 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -641,7 +641,8 @@ void setup() #endif sensecapIndicator = new SensecapIndicator(Serial2); if (!sensecapIndicator->wait_ready(3000)) - LOG_WARN("RP2040 co-processor not answering, bridged I2C bus unavailable"); + LOG_WARN("RP2040 co-processor did not complete the handshake in time, bridged peripherals stay disabled until it " + "answers"); #endif #if !MESHTASTIC_EXCLUDE_I2C diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp index 2fab162f0a3..ecc7e3cbf79 100644 --- a/src/mesh/IndicatorSerial.cpp +++ b/src/mesh/IndicatorSerial.cpp @@ -25,13 +25,33 @@ int32_t SensecapIndicator::runOnce() // A requester is pumping the link itself and holds link_lock for up // to its full request timeout; blocking on the lock here would stall // every other thread of the cooperative main loop with it - if (request_in_flight) + if (requests_in_flight > 0) return (10); concurrency::LockGuard guard(&link_lock); pump(); + // Keep probing until the co-processor has answered the handshake: it + // may boot slower than we do, or reboot on its watchdog. Without this + // the bridge would stay dead for the rest of the session. + if (!handshake_done) + probe_link(); return (10); } +// Send a ping, rate limited. The co-processor answers with a pong carrying +// the protocol version it speaks. Caller holds link_lock. +void SensecapIndicator::probe_link() +{ + if (last_probe != 0 && millis() - last_probe < 250) + return; + meshtastic_InterdeviceMessage &msg = tx_message; + memset(&msg, 0, sizeof(msg)); + msg.which_data = meshtastic_InterdeviceMessage_ping_tag; + msg.data.ping = meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT; + stamp_request(msg); + send_uplink_unlocked(msg); + last_probe = millis(); +} + // Read whatever is available on the link and process complete packets void SensecapIndicator::pump() { @@ -69,23 +89,35 @@ uint32_t SensecapIndicator::stamp_request(meshtastic_InterdeviceMessage &request return next_request_id; } -// callers hold link_lock: the co-processor has answered at least once and +// callers hold link_lock: the co-processor has completed the handshake and // speaks our protocol version. Fails fast instead of timing out per request. bool SensecapIndicator::link_ready() { - return packets_received > 0 && link_compatible; + return handshake_done && link_compatible; } -bool SensecapIndicator::i2c_transact(meshtastic_InterdeviceMessage &request, meshtastic_I2CResult *result, uint32_t timeout_ms) +bool SensecapIndicator::i2c_transact(uint8_t address, const uint8_t *wbuf, size_t wlen, size_t rlen, meshtastic_I2CResult *result, + uint32_t timeout_ms) { - InFlight busy(request_in_flight); + InFlight busy(requests_in_flight); concurrency::LockGuard guard(&link_lock); if (!link_ready()) return false; - stamp_request(request); + meshtastic_InterdeviceMessage &msg = tx_message; + memset(&msg, 0, sizeof(msg)); + msg.which_data = meshtastic_InterdeviceMessage_i2c_transaction_tag; + msg.data.i2c_transaction.address = address; + msg.data.i2c_transaction.read_len = rlen; + if (wlen > sizeof(msg.data.i2c_transaction.write_data.bytes)) + return false; + msg.data.i2c_transaction.write_data.size = wlen; + if (wlen) + memcpy(msg.data.i2c_transaction.write_data.bytes, wbuf, wlen); + + stamp_request(msg); i2c_result_ready = false; - if (!send_uplink_unlocked(request)) + if (!send_uplink_unlocked(msg)) return false; if (!wait_response(i2c_result_ready, timeout_ms)) @@ -110,7 +142,7 @@ bool SensecapIndicator::file_request(meshtastic_InterdeviceMessage &request, mes bool SensecapIndicator::file_read(const char *path, uint32_t offset, uint32_t length, meshtastic_FileTransfer *out, uint32_t timeout_ms) { - InFlight busy(request_in_flight); + InFlight busy(requests_in_flight); concurrency::LockGuard guard(&link_lock); if (!link_ready()) return false; @@ -128,7 +160,7 @@ bool SensecapIndicator::file_read(const char *path, uint32_t offset, uint32_t le bool SensecapIndicator::file_write(const char *path, uint32_t offset, const uint8_t *data, size_t len, bool create, meshtastic_FileTransfer *out, uint32_t timeout_ms) { - InFlight busy(request_in_flight); + InFlight busy(requests_in_flight); concurrency::LockGuard guard(&link_lock); if (!link_ready()) return false; @@ -148,7 +180,7 @@ bool SensecapIndicator::file_write(const char *path, uint32_t offset, const uint bool SensecapIndicator::file_remove(const char *path, meshtastic_FileTransfer *out, uint32_t timeout_ms) { - InFlight busy(request_in_flight); + InFlight busy(requests_in_flight); concurrency::LockGuard guard(&link_lock); if (!link_ready()) return false; @@ -163,7 +195,7 @@ bool SensecapIndicator::file_remove(const char *path, meshtastic_FileTransfer *o bool SensecapIndicator::list_directory(const char *path, uint32_t offset, meshtastic_DirectoryListing *out, uint32_t timeout_ms) { - InFlight busy(request_in_flight); + InFlight busy(requests_in_flight); concurrency::LockGuard guard(&link_lock); if (!link_ready()) return false; @@ -186,7 +218,7 @@ bool SensecapIndicator::list_directory(const char *path, uint32_t offset, meshta bool SensecapIndicator::sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms) { - InFlight busy(request_in_flight); + InFlight busy(requests_in_flight); concurrency::LockGuard guard(&link_lock); if (!link_ready()) return false; @@ -208,38 +240,23 @@ bool SensecapIndicator::sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms) bool SensecapIndicator::wait_ready(uint32_t timeout_ms) { - InFlight busy(request_in_flight); + InFlight busy(requests_in_flight); concurrency::LockGuard guard(&link_lock); uint32_t start = millis(); - uint32_t last_probe = 0; - bool probed = false; - while (!pong_received) { + while (!handshake_done) { // The co-processor never sends anything unsolicited unless a GPS // module is attached, so waiting passively would leave the bridge // down forever on GPS-less units. Ping until it answers; the pong // touches no peripherals on the other side and carries the - // protocol version it speaks. - if (!probed || millis() - last_probe >= 250) { - meshtastic_InterdeviceMessage &msg = tx_message; - memset(&msg, 0, sizeof(msg)); - msg.which_data = meshtastic_InterdeviceMessage_ping_tag; - msg.data.ping = meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT; - stamp_request(msg); - send_uplink_unlocked(msg); - last_probe = millis(); - probed = true; - } + // protocol version it speaks. If it does not answer in time, + // runOnce keeps probing (a slow boot must not disable the bridge + // for the session), but no request is sent until it does. + probe_link(); pump(); - if (pong_received) + if (handshake_done) break; - if (millis() - start >= timeout_ms) { - if (packets_received > 0) { - // it talks (NMEA), it just did not answer the handshake - LOG_WARN("RP2040 did not answer the version handshake"); - return true; - } + if (millis() - start >= timeout_ms) return false; - } delay(1); } return link_compatible; @@ -379,26 +396,34 @@ bool SensecapIndicator::handle_packet(size_t payload_len) send_uplink_unlocked(pong); return true; } - case meshtastic_InterdeviceMessage_pong_tag: + case meshtastic_InterdeviceMessage_pong_tag: { // The handshake: the co-processor reports the protocol version it // speaks. Anything else than ours means its firmware does not match // this build, and every request would be misinterpreted. - pong_received = true; - if (message.data.pong != meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT) { - link_compatible = false; - LOG_ERROR("RP2040 speaks interdevice protocol v%u, this firmware speaks v%u. Flash the matching " - "indicator_rp2040 firmware; sensors, GPS and SD card stay disabled", - (unsigned)message.data.pong, (unsigned)meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT); - } else { - LOG_INFO("RP2040 link up, interdevice protocol v%u", (unsigned)message.data.pong); + bool compatible = message.data.pong == meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT; + if (!handshake_done || compatible != link_compatible) { + if (compatible) + LOG_INFO("RP2040 link up, interdevice protocol v%u", (unsigned)message.data.pong); + else + LOG_ERROR("RP2040 speaks interdevice protocol v%u, this firmware speaks v%u. Flash the matching " + "indicator_rp2040 firmware; sensors, GPS and SD card stay disabled", + (unsigned)message.data.pong, (unsigned)meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT); } + link_compatible = compatible; + handshake_done = true; return true; + } case meshtastic_InterdeviceMessage_nack_tag: - // id 0 means the request was undecodable over there; the link is - // single-outstanding, so that can only be the request in flight - if (message.id == expected_id || message.id == 0) { + // A nack for the request in flight is definitive: resending it would + // only be refused again. An id of 0 means the co-processor could not + // even decode the frame, which may just as well have been an + // unrelated NMEA uplink, so that one only ends the wait (the caller + // may retry it as the transport failure it is). + if (message.id == expected_id) { LOG_WARN("Request %u nacked by the co-processor", expected_id); request_nacked = true; + } else if (message.id == 0) { + LOG_WARN("Co-processor could not decode a frame"); } return true; case meshtastic_InterdeviceMessage_sd_info_tag: diff --git a/src/mesh/IndicatorSerial.h b/src/mesh/IndicatorSerial.h index c2733738116..b978463ed4a 100644 --- a/src/mesh/IndicatorSerial.h +++ b/src/mesh/IndicatorSerial.h @@ -7,6 +7,7 @@ #include "configuration.h" #include "mesh/generated/meshtastic/interdevice.pb.h" +#include // Magic number at the start of all MT packets #define MT_MAGIC_0 0x94 @@ -26,14 +27,17 @@ class SensecapIndicator : public concurrency::OSThread // serialize the shared TX buffer against the request methods bool send_uplink(const meshtastic_InterdeviceMessage &message); - // Send a request and pump the link until the matching response arrives. - // Cooperative threading means nothing else runs while we wait, so the - // response cannot be consumed behind our back. - bool i2c_transact(meshtastic_InterdeviceMessage &request, meshtastic_I2CResult *result, uint32_t timeout_ms = 100); + // Run one tunneled I2C transaction: an optional write of wlen bytes + // followed by an optional read of rlen bytes with repeated start. The + // request is staged under the link lock, so callers need no locking. + bool i2c_transact(uint8_t address, const uint8_t *wbuf, size_t wlen, size_t rlen, meshtastic_I2CResult *result, + uint32_t timeout_ms = 100); // Synchronous file operations against the SD card attached to the RP2040. // The response (chunk data, file size, status) is written to *out. - // Returns false when the link is down or the request timed out. + // Returns false when the link is down or the request timed out; a + // request the co-processor answered is true, with out->status carrying + // the outcome (FILE_BUSY is worth retrying, the other failures are not). bool file_read(const char *path, uint32_t offset, uint32_t length, meshtastic_FileTransfer *out, uint32_t timeout_ms = 1000); // Sequential chunked write: create=true starts a new file (offset must be 0), // create=false appends (offset must equal the current file size) @@ -48,6 +52,10 @@ class SensecapIndicator : public concurrency::OSThread // behind them runs in the background after the card is mounted bool sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms = 2000); + // True when the last request was refused by the co-processor rather than + // lost: retrying it would only be refused again + bool last_request_nacked() const { return request_nacked; } + // True once the co-processor has answered and speaks our protocol // version. Actively probes it (it never speaks unsolicited unless a GPS // module is attached) and pumps the link until a pong arrives or the @@ -65,10 +73,13 @@ class SensecapIndicator : public concurrency::OSThread size_t pb_rx_size = 0; // Number of bytes currently in the buffer HardwareSerial *_serial = nullptr; uint32_t packets_received = 0; - // cleared when a pong reports a protocol version we do not speak; the - // bridge stays shut down for the rest of the session - bool link_compatible = true; - bool pong_received = false; + // The handshake gates the bridge: no request is sent before the + // co-processor has answered a ping with the protocol version we speak. + // A mismatch is permanent, a missing answer is retried by runOnce (the + // co-processor may boot slower than we do, or reboot on its watchdog). + bool link_compatible = false; + bool handshake_done = false; + uint32_t last_probe = 0; meshtastic_I2CResult i2c_result = meshtastic_I2CResult_init_zero; bool i2c_result_ready = false; // Statically allocated message structs: with 4KB file chunks an @@ -90,16 +101,20 @@ class SensecapIndicator : public concurrency::OSThread uint32_t expected_id = 0; // a nack response fails the request in flight without its timeout bool request_nacked = false; - // True while a requester holds link_lock for a full request/response - // round trip (up to the request timeout). runOnce checks it so the - // cooperative main loop is not blocked on the lock for that long. - volatile bool request_in_flight = false; + // Number of requesters inside a request/response round trip, each + // holding link_lock for up to its timeout. runOnce skips its pump while + // this is nonzero so the cooperative main loop is not blocked on the + // lock for that long. A counter, not a flag: the UI task and the main + // loop can both be in a request, and the first one out must not clear + // the state of the other. + std::atomic requests_in_flight{0}; struct InFlight { - volatile bool &flag; - explicit InFlight(volatile bool &f) : flag(f) { flag = true; } - ~InFlight() { flag = false; } + std::atomic &count; + explicit InFlight(std::atomic &c) : count(c) { count++; } + ~InFlight() { count--; } }; bool link_ready(); + void probe_link(); // caller holds link_lock uint32_t stamp_request(meshtastic_InterdeviceMessage &request); bool send_uplink_unlocked(const meshtastic_InterdeviceMessage &message); // callers hold link_lock (the request was staged in the shared tx_message) diff --git a/src/mesh/comms/I2CProxy.cpp b/src/mesh/comms/I2CProxy.cpp index 766ccfa5f17..f2e5c4e580b 100644 --- a/src/mesh/comms/I2CProxy.cpp +++ b/src/mesh/comms/I2CProxy.cpp @@ -5,33 +5,40 @@ I2CProxy *i2cProxy = new I2CProxy(); -void I2CProxy::acquire() +// Transaction state of the calling task. Slots are claimed on first use and +// never released: the set of tasks touching the bridged bus is fixed (main +// loop, UI task). +I2CProxy::Context &I2CProxy::ctx() { - _lock.lock(); - _owner = xTaskGetCurrentTaskHandle(); -} - -void I2CProxy::release() -{ - _owner = nullptr; - _lock.unlock(); + TaskHandle_t self = xTaskGetCurrentTaskHandle(); + for (size_t i = 0; i < MAX_TASKS; i++) { + if (_ctx[i].task == self) + return _ctx[i]; + } + for (size_t i = 0; i < MAX_TASKS; i++) { + if (_ctx[i].task == nullptr) { + _ctx[i].task = self; + return _ctx[i]; + } + } + LOG_WARN("I2CProxy: more tasks than contexts, sharing the last one"); + return _ctx[MAX_TASKS - 1]; } void I2CProxy::beginTransmission(uint8_t address) { - // re-begin from the owning thread just restages, everyone else waits - if (_owner != xTaskGetCurrentTaskHandle()) - acquire(); - _txAddress = address; - _txLen = 0; - _txPending = true; + Context &c = ctx(); + c.txAddress = address; + c.txLen = 0; + c.txPending = true; } size_t I2CProxy::write(uint8_t data) { - if (!_txPending || _txLen >= MAX_WRITE) + Context &c = ctx(); + if (!c.txPending || c.txLen >= MAX_WRITE) return 0; - _txBuf[_txLen++] = data; + c.txBuf[c.txLen++] = data; return 1; } @@ -45,96 +52,81 @@ size_t I2CProxy::write(const uint8_t *data, size_t len) uint8_t I2CProxy::endTransmission(bool stopBit) { - if (_owner != xTaskGetCurrentTaskHandle()) - return 4; // endTransmission without beginTransmission + Context &c = ctx(); if (!stopBit) { // Keep the buffered write pending, it is combined with the following - // requestFrom() into a single write+read transaction; the lock stays - // held across the repeated start + // requestFrom() into a single write+read transaction return 0; } - uint8_t rv = transact(_txAddress, _txBuf, _txLen, 0); - _txLen = 0; - _txPending = false; - release(); + uint8_t rv = transact(c, c.txAddress, 0); + c.txLen = 0; + c.txPending = false; return rv; } size_t I2CProxy::requestFrom(uint8_t address, size_t len, bool stopBit) { (void)stopBit; + Context &c = ctx(); if (len > MAX_READ) len = MAX_READ; - // standalone read without a preceding beginTransmission - if (_owner != xTaskGetCurrentTaskHandle()) - acquire(); + // a write pending for this address is executed together with the read as + // one transaction with repeated start + if (!c.txPending || c.txAddress != address) + c.txLen = 0; - const uint8_t *wbuf = NULL; - size_t wlen = 0; - if (_txPending && _txAddress == address) { - wbuf = _txBuf; - wlen = _txLen; - } - uint8_t rv = transact(address, wbuf, wlen, len); - _txLen = 0; - _txPending = false; - release(); - return rv == 0 ? _rxLen : 0; + uint8_t rv = transact(c, address, len); + c.txLen = 0; + c.txPending = false; + return rv == 0 ? c.rxLen : 0; } int I2CProxy::available() { - return (int)(_rxLen - _rxPos); + Context &c = ctx(); + return (int)(c.rxLen - c.rxPos); } int I2CProxy::read() { - return _rxPos < _rxLen ? _rxBuf[_rxPos++] : -1; + Context &c = ctx(); + return c.rxPos < c.rxLen ? c.rxBuf[c.rxPos++] : -1; } int I2CProxy::peek() { - return _rxPos < _rxLen ? _rxBuf[_rxPos] : -1; + Context &c = ctx(); + return c.rxPos < c.rxLen ? c.rxBuf[c.rxPos] : -1; } // Run one tunneled transaction, returns TwoWire endTransmission error codes: // 0 success, 1 data too long, 2 NACK on address, 3 NACK on data, 4 other, 5 timeout -uint8_t I2CProxy::transact(uint8_t address, const uint8_t *wbuf, size_t wlen, size_t rlen) +uint8_t I2CProxy::transact(Context &c, uint8_t address, size_t rlen) { - _rxLen = 0; - _rxPos = 0; + c.rxLen = 0; + c.rxPos = 0; if (!sensecapIndicator) return 4; - - // static: ~4.6KB struct, too large for task stacks. Safe because every - // caller holds _lock while the transaction executes - static meshtastic_InterdeviceMessage msg; - memset(&msg, 0, sizeof(msg)); - msg.which_data = meshtastic_InterdeviceMessage_i2c_transaction_tag; - msg.data.i2c_transaction.address = address; - msg.data.i2c_transaction.read_len = rlen; - if (wlen > sizeof(msg.data.i2c_transaction.write_data.bytes)) + if (c.txLen > MAX_WRITE) return 1; - msg.data.i2c_transaction.write_data.size = wlen; - if (wlen) - memcpy(msg.data.i2c_transaction.write_data.bytes, wbuf, wlen); meshtastic_I2CResult result = meshtastic_I2CResult_init_zero; - if (!sensecapIndicator->i2c_transact(msg, &result)) + if (!sensecapIndicator->i2c_transact(address, c.txBuf, c.txLen, rlen, &result)) return 5; switch (result.status) { case meshtastic_I2CResult_Status_OK: - _rxLen = result.read_data.size > MAX_READ ? MAX_READ : result.read_data.size; - memcpy(_rxBuf, result.read_data.bytes, _rxLen); + c.rxLen = result.read_data.size > MAX_READ ? MAX_READ : result.read_data.size; + memcpy(c.rxBuf, result.read_data.bytes, c.rxLen); return 0; case meshtastic_I2CResult_Status_NACK_ADDRESS: return 2; case meshtastic_I2CResult_Status_NACK_DATA: return 3; default: + // includes UNSPECIFIED: an empty result must not read as success return 4; } } diff --git a/src/mesh/comms/I2CProxy.h b/src/mesh/comms/I2CProxy.h index 9be0887de81..22f069a3463 100644 --- a/src/mesh/comms/I2CProxy.h +++ b/src/mesh/comms/I2CProxy.h @@ -3,7 +3,6 @@ #ifdef SENSECAP_INDICATOR #include "../generated/meshtastic/interdevice.pb.h" -#include "concurrency/Lock.h" #include /** @@ -22,11 +21,12 @@ * already initialized, which is always true for bus 0 (local touch panel). * * Thread safety: the bus is shared between the main loop (sensor drivers) - * and the UI task (keyboard scanner), so a transaction holds an internal - * lock from beginTransmission()/requestFrom() until the transaction - * executes, like the HAL lock of the real ESP32 TwoWire. As with the real - * TwoWire, the read buffer must be consumed before the next transaction - * from another thread replaces it. + * and the UI task (keyboard scanner), and TwoWire has no transaction + * bracket a lock could safely span - drivers drain the read buffer with + * available()/read() long after requestFrom() returned. Each calling task + * therefore gets its own staging and read buffers; the tunneled round trip + * itself is serialized by the link. Tasks beyond MAX_TASKS share the last + * context, which is only correct if they do not run concurrently. */ class I2CProxy : public TwoWire { @@ -58,25 +58,24 @@ class I2CProxy : public TwoWire static const size_t MAX_WRITE = sizeof(meshtastic_I2CTransaction{}.write_data.bytes); static const size_t MAX_READ = sizeof(meshtastic_I2CResult{}.read_data.bytes); - uint8_t _txAddress = 0; - uint8_t _txBuf[MAX_WRITE]; - size_t _txLen = 0; - bool _txPending = false; + // main loop, UI task, and one spare + static const size_t MAX_TASKS = 3; - uint8_t _rxBuf[MAX_READ]; - size_t _rxLen = 0; - size_t _rxPos = 0; + struct Context { + TaskHandle_t task = nullptr; + uint8_t txAddress = 0; + uint8_t txBuf[MAX_WRITE]; + size_t txLen = 0; + bool txPending = false; + uint8_t rxBuf[MAX_READ]; + size_t rxLen = 0; + size_t rxPos = 0; + }; + Context _ctx[MAX_TASKS]; - // Serializes staged transactions across threads. Held from - // beginTransmission() (or a standalone requestFrom()) until the - // transaction executes. The lock is a plain binary semaphore, so the - // owning task is tracked to keep re-entry from deadlocking. - concurrency::Lock _lock; - TaskHandle_t _owner = nullptr; - void acquire(); - void release(); - - uint8_t transact(uint8_t address, const uint8_t *wbuf, size_t wlen, size_t rlen); + // per-task transaction state, claimed on first use + Context &ctx(); + uint8_t transact(Context &c, uint8_t address, size_t rlen); }; extern I2CProxy *i2cProxy; diff --git a/src/mesh/comms/UARTProxy.cpp b/src/mesh/comms/UARTProxy.cpp index f576859badf..894da8952d1 100644 --- a/src/mesh/comms/UARTProxy.cpp +++ b/src/mesh/comms/UARTProxy.cpp @@ -38,6 +38,8 @@ int UARTProxy::read() return -1; __sync_synchronize(); // pair with the producer barrier in buf_push uint8_t ret = buf[buf_tail]; + // the slot must be read before the producer is told it is free again + __sync_synchronize(); buf_tail = (buf_tail + 1) % BUF_SIZE; return ret; } @@ -74,6 +76,8 @@ size_t UARTProxy::write(uint8_t *buffer, size_t size) size_t UARTProxy::write(char *buffer, size_t size) { + if (!sensecapIndicator) + return 0; // the link is constructed after this global // static: ~4.6KB struct, too large for task stacks; only written from // the cooperative main loop (GPS thread) static meshtastic_InterdeviceMessage message; diff --git a/src/mesh/generated/meshtastic/interdevice.pb.cpp b/src/mesh/generated/meshtastic/interdevice.pb.cpp index 6c49cf3fb1b..2ab13406a67 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.cpp +++ b/src/mesh/generated/meshtastic/interdevice.pb.cpp @@ -35,3 +35,5 @@ PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 4) + + diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index f5a67d0357a..e927dc5f50e 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -33,6 +33,25 @@ typedef enum _meshtastic_FileOperation { meshtastic_FileOperation_DELETE = 3 } meshtastic_FileOperation; +/* Outcome of a file or directory operation. The requester must be able to + tell a transient condition from a definitive one: BUSY is worth another + try, NOT_FOUND is not. */ +typedef enum _meshtastic_FileStatus { + meshtastic_FileStatus_FILE_UNSPECIFIED = 0, + meshtastic_FileStatus_FILE_OK = 1, + /* Retry later: the co-processor is doing card maintenance (mount, + free space scan) and cannot serve the request right now */ + meshtastic_FileStatus_FILE_BUSY = 2, + meshtastic_FileStatus_FILE_NO_CARD = 3, + meshtastic_FileStatus_FILE_NOT_FOUND = 4, + /* PUT only: offset did not match the current end of the file. file_size + carries the size the file actually has, so the writer can resync (or + recognize its own chunk as already written after a lost response). */ + meshtastic_FileStatus_FILE_OFFSET_CONFLICT = 5, + meshtastic_FileStatus_FILE_IO_ERROR = 6, + meshtastic_FileStatus_FILE_NOT_A_FILE = 7 /* path is a directory (GET) or not one (listing) */ +} meshtastic_FileStatus; + typedef enum _meshtastic_SdCardInfo_CardType { meshtastic_SdCardInfo_CardType_NONE = 0, meshtastic_SdCardInfo_CardType_MMC = 1, @@ -66,8 +85,8 @@ typedef struct _meshtastic_FileTransfer { meshtastic_FileOperation operation; /* File operation (GET, POST, PUT, DELETE) */ char filepath[256]; /* Path of the file on the SD card */ meshtastic_FileTransfer_filedata_t filedata; /* Chunk content (POST/PUT request, GET response) */ - bool success; /* Response: Was the operation successful? */ - char message[255]; /* Response: Status message */ + meshtastic_FileStatus status; /* Response: outcome of the operation */ + char message[255]; /* Response: human readable detail, may be empty */ uint64_t offset; /* Byte offset of this chunk within the file (ranged GET/PUT) */ /* GET request: number of bytes to read, 0 = max chunk size. A response carries at most the filedata max_size (see interdevice.options) per @@ -86,8 +105,8 @@ typedef struct _meshtastic_DirectoryListing { offset and total_count. */ pb_size_t filenames_count; char filenames[16][256]; - bool success; /* Response: Was the operation successful? */ - char message[255]; /* Response: Status message */ + meshtastic_FileStatus status; /* Response: outcome of the operation */ + char message[255]; /* Response: human readable detail, may be empty */ uint32_t offset; /* Request: skip this many entries (paging) */ uint32_t total_count; /* Response: total number of entries in the directory */ } meshtastic_DirectoryListing; @@ -173,6 +192,10 @@ extern "C" { #define _meshtastic_FileOperation_MAX meshtastic_FileOperation_DELETE #define _meshtastic_FileOperation_ARRAYSIZE ((meshtastic_FileOperation)(meshtastic_FileOperation_DELETE+1)) +#define _meshtastic_FileStatus_MIN meshtastic_FileStatus_FILE_UNSPECIFIED +#define _meshtastic_FileStatus_MAX meshtastic_FileStatus_FILE_NOT_A_FILE +#define _meshtastic_FileStatus_ARRAYSIZE ((meshtastic_FileStatus)(meshtastic_FileStatus_FILE_NOT_A_FILE+1)) + #define _meshtastic_SdCardInfo_CardType_MIN meshtastic_SdCardInfo_CardType_NONE #define _meshtastic_SdCardInfo_CardType_MAX meshtastic_SdCardInfo_CardType_UNKNOWN_CARD #define _meshtastic_SdCardInfo_CardType_ARRAYSIZE ((meshtastic_SdCardInfo_CardType)(meshtastic_SdCardInfo_CardType_UNKNOWN_CARD+1)) @@ -186,7 +209,9 @@ extern "C" { #define _meshtastic_I2CResult_Status_ARRAYSIZE ((meshtastic_I2CResult_Status)(meshtastic_I2CResult_Status_ERROR+1)) #define meshtastic_FileTransfer_operation_ENUMTYPE meshtastic_FileOperation +#define meshtastic_FileTransfer_status_ENUMTYPE meshtastic_FileStatus +#define meshtastic_DirectoryListing_status_ENUMTYPE meshtastic_FileStatus #define meshtastic_SdCardInfo_card_type_ENUMTYPE meshtastic_SdCardInfo_CardType @@ -197,14 +222,14 @@ extern "C" { /* Initializer values for message structs */ -#define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, 0, "", 0, 0, 0} -#define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, 0, "", 0, 0} +#define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0} +#define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0} #define meshtastic_I2CTransaction_init_default {0, {0, {0}}, 0} #define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0} #define meshtastic_I2CResult_init_default {_meshtastic_I2CResult_Status_MIN, {0, {0}}} #define meshtastic_InterdeviceMessage_init_default {0, {""}, 0} -#define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, 0, "", 0, 0, 0} -#define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, 0, "", 0, 0} +#define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0} +#define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0} #define meshtastic_I2CTransaction_init_zero {0, {0, {0}}, 0} #define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0} #define meshtastic_I2CResult_init_zero {_meshtastic_I2CResult_Status_MIN, {0, {0}}} @@ -214,14 +239,14 @@ extern "C" { #define meshtastic_FileTransfer_operation_tag 1 #define meshtastic_FileTransfer_filepath_tag 2 #define meshtastic_FileTransfer_filedata_tag 3 -#define meshtastic_FileTransfer_success_tag 4 +#define meshtastic_FileTransfer_status_tag 4 #define meshtastic_FileTransfer_message_tag 5 #define meshtastic_FileTransfer_offset_tag 6 #define meshtastic_FileTransfer_length_tag 7 #define meshtastic_FileTransfer_file_size_tag 8 #define meshtastic_DirectoryListing_directory_tag 1 #define meshtastic_DirectoryListing_filenames_tag 2 -#define meshtastic_DirectoryListing_success_tag 3 +#define meshtastic_DirectoryListing_status_tag 3 #define meshtastic_DirectoryListing_message_tag 4 #define meshtastic_DirectoryListing_offset_tag 5 #define meshtastic_DirectoryListing_total_count_tag 6 @@ -257,7 +282,7 @@ extern "C" { X(a, STATIC, SINGULAR, UENUM, operation, 1) \ X(a, STATIC, SINGULAR, STRING, filepath, 2) \ X(a, STATIC, SINGULAR, BYTES, filedata, 3) \ -X(a, STATIC, SINGULAR, BOOL, success, 4) \ +X(a, STATIC, SINGULAR, UENUM, status, 4) \ X(a, STATIC, SINGULAR, STRING, message, 5) \ X(a, STATIC, SINGULAR, UINT64, offset, 6) \ X(a, STATIC, SINGULAR, UINT32, length, 7) \ @@ -268,7 +293,7 @@ X(a, STATIC, SINGULAR, UINT64, file_size, 8) #define meshtastic_DirectoryListing_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, directory, 1) \ X(a, STATIC, REPEATED, STRING, filenames, 2) \ -X(a, STATIC, SINGULAR, BOOL, success, 3) \ +X(a, STATIC, SINGULAR, UENUM, status, 3) \ X(a, STATIC, SINGULAR, STRING, message, 4) \ X(a, STATIC, SINGULAR, UINT32, offset, 5) \ X(a, STATIC, SINGULAR, UINT32, total_count, 6) From 8b95f44b26a1299236bf657288434bd0289b7f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 10:36:44 +0200 Subject: [PATCH 12/28] indicator: fail safe on a peer mismatch, wait out card maintenance FileStatus moved to a fresh tag: reusing the tag of the removed success flag made every failure status decode as success on a peer that predates it. A card being mounted (busy) is retried rather than reported as an empty slot, and a co-processor busy with card maintenance is waited out: mounting takes seconds and the free space scan of a large card walks its whole FAT, which is not a reason to report a missing tile. The bridged I2C bus releases the SPI lock as well, so the keyboard scan on the UI task cannot starve the radio either. Slot claims in the I2C proxy are atomic, NMEA is not sent to a peer we refuse to talk to, and the handshake is completed by the unsolicited ping the co-processor sends when it has booted, which also reports a reboot. --- src/graphics/tftSetup.cpp | 101 ++++++++---------- src/main.cpp | 10 +- src/mesh/IndicatorSerial.cpp | 56 ++++++---- src/mesh/IndicatorSerial.h | 16 +-- src/mesh/comms/I2CProxy.cpp | 19 +++- src/mesh/comms/I2CProxy.h | 1 + src/mesh/comms/LinkSpiLock.h | 46 ++++++++ .../generated/meshtastic/interdevice.pb.h | 42 +++++--- 8 files changed, 185 insertions(+), 106 deletions(-) create mode 100644 src/mesh/comms/LinkSpiLock.h diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 34ef0ebf4c2..1f5904f6d2e 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -19,40 +19,7 @@ #include "input/I2CKeyboardScanner.h" #include "mesh/IndicatorSerial.h" #include "mesh/comms/I2CProxy.h" - -// Set while the TFT task holds spiLock around the LVGL handler, so the file -// operations below (which run inside LVGL callbacks) can hand the SPI bus -// back to the radio while they wait on the interdevice link -static volatile TaskHandle_t spiLockHolder = nullptr; - -/** - * The remote FS round trips block for the request timeout, and the LVGL - * handler runs with spiLock held, which would starve the LoRa radio for - * that whole time. Nothing in a link round trip touches SPI, so give the - * bus back for its duration. - */ -class SpiLockBreak -{ - public: - SpiLockBreak() - { - held = spiLockHolder == xTaskGetCurrentTaskHandle(); - if (held) { - spiLockHolder = nullptr; - spiLock->unlock(); - } - } - ~SpiLockBreak() - { - if (held) { - spiLock->lock(); - spiLockHolder = xTaskGetCurrentTaskHandle(); - } - } - - private: - bool held; -}; +#include "mesh/comms/LinkSpiLock.h" // Serves the UI map tiles from the SD card behind the RP2040, chunk-wise // over the interdevice link. @@ -64,20 +31,28 @@ class SpiLockBreak // first attempt is dropped as stale. class IndicatorRemoteFS : public IRemoteFS { + // A lost frame is retried a couple of times. A co-processor busy with + // card maintenance is a different story: mounting a card takes seconds, + // and the free space scan of a large card walks its whole FAT, so wait + // that out rather than reporting a missing tile. static const int LINK_ATTEMPTS = 3; - static const uint32_t BUSY_BACKOFF_MS = 150; + static const int BUSY_ATTEMPTS = 20; + static const uint32_t BUSY_BACKOFF_MS = 250; // Returns true when the request should be sent again. `answered` is // false when the link itself failed (timeout), true when the // co-processor replied. - static bool retryable(bool answered, meshtastic_FileStatus status, int attempt) + static bool retryable(bool answered, meshtastic_FileStatus status, int &attempts_left) { - if (attempt + 1 >= LINK_ATTEMPTS) + if (--attempts_left <= 0) return false; if (!answered) return !sensecapIndicator->last_request_nacked(); // refused, not lost if (status == meshtastic_FileStatus_FILE_BUSY) { - delay(BUSY_BACKOFF_MS); // card maintenance, give it a moment + // it answered, so nothing was lost: only the wait counts + if (attempts_left < BUSY_ATTEMPTS) + attempts_left = BUSY_ATTEMPTS; + delay(BUSY_BACKOFF_MS); return true; } return false; @@ -90,7 +65,8 @@ class IndicatorRemoteFS : public IRemoteFS if (!sensecapIndicator) return false; SpiLockBreak spiFree; - for (int attempt = 0; attempt < LINK_ATTEMPTS; attempt++) { + int attempts_left = LINK_ATTEMPTS; + do { memset(&result, 0, sizeof(result)); bool answered = sensecapIndicator->file_read(path, offset, len, &result); if (answered && result.status == meshtastic_FileStatus_FILE_OK) { @@ -103,10 +79,9 @@ class IndicatorRemoteFS : public IRemoteFS *fileSize = (uint32_t)result.file_size; return true; } - if (!retryable(answered, result.status, attempt)) + if (!retryable(answered, result.status, attempts_left)) return false; - } - return false; + } while (true); } bool writeChunk(const char *path, uint32_t offset, const uint8_t *buf, uint32_t len, bool create) override @@ -114,7 +89,9 @@ class IndicatorRemoteFS : public IRemoteFS if (!sensecapIndicator) return false; SpiLockBreak spiFree; - for (int attempt = 0; attempt < LINK_ATTEMPTS; attempt++) { + int attempts_left = LINK_ATTEMPTS; + bool retried = false; + do { memset(&result, 0, sizeof(result)); bool answered = sensecapIndicator->file_write(path, offset, buf, len, create, &result); if (answered) { @@ -123,14 +100,14 @@ class IndicatorRemoteFS : public IRemoteFS // An append whose first attempt landed but whose response was // lost is refused as an offset conflict, and the file already // holds this chunk: that is the outcome we wanted - if (attempt > 0 && !create && result.status == meshtastic_FileStatus_FILE_OFFSET_CONFLICT && + if (retried && !create && result.status == meshtastic_FileStatus_FILE_OFFSET_CONFLICT && result.file_size == (uint64_t)offset + len) return true; } - if (!retryable(answered, result.status, attempt)) + if (!retryable(answered, result.status, attempts_left)) return false; - } - return false; + retried = true; + } while (true); } bool sdInfo(RemoteSdInfo &info) override @@ -139,14 +116,19 @@ class IndicatorRemoteFS : public IRemoteFS return false; SpiLockBreak spiFree; meshtastic_SdCardInfo result = meshtastic_SdCardInfo_init_zero; - bool ok = false; - for (int attempt = 0; attempt < LINK_ATTEMPTS && !ok; attempt++) { - ok = sensecapIndicator->sd_info(&result); - if (!ok && sensecapIndicator->last_request_nacked()) + int attempts_left = LINK_ATTEMPTS; + while (true) { + result = meshtastic_SdCardInfo_init_zero; + bool answered = sensecapIndicator->sd_info(&result); + // `busy` means a card is being mounted: whether one is there is + // not decided yet, and reporting an empty slot would stick for + // the session + if (answered && !result.busy) break; + if (!retryable(answered, answered ? meshtastic_FileStatus_FILE_BUSY : meshtastic_FileStatus_FILE_UNSPECIFIED, + attempts_left)) + return false; } - if (!ok) - return false; info.present = result.present; info.cardType = (uint8_t)result.card_type; info.fatType = (uint8_t)result.fat_type; @@ -162,7 +144,8 @@ class IndicatorRemoteFS : public IRemoteFS if (!sensecapIndicator) return false; SpiLockBreak spiFree; - for (int attempt = 0; attempt < LINK_ATTEMPTS; attempt++) { + int attempts_left = LINK_ATTEMPTS; + do { memset(&result, 0, sizeof(result)); bool answered = sensecapIndicator->file_remove(path, &result); // delete is idempotent on the co-processor: a file that is @@ -170,10 +153,9 @@ class IndicatorRemoteFS : public IRemoteFS // not have to be guessed at here if (answered && result.status == meshtastic_FileStatus_FILE_OK) return true; - if (!retryable(answered, result.status, attempt)) + if (!retryable(answered, result.status, attempts_left)) return false; - } - return false; + } while (true); } bool listDir(const char *path, std::set &entries) override @@ -184,12 +166,13 @@ class IndicatorRemoteFS : public IRemoteFS uint32_t offset = 0; while (true) { bool got_page = false; - for (int attempt = 0; attempt < LINK_ATTEMPTS && !got_page; attempt++) { + int attempts_left = LINK_ATTEMPTS; + while (!got_page) { memset(&listing, 0, sizeof(listing)); bool answered = sensecapIndicator->list_directory(path, offset, &listing); if (answered && listing.status == meshtastic_FileStatus_FILE_OK) got_page = true; - else if (!retryable(answered, listing.status, attempt)) + else if (!retryable(answered, listing.status, attempts_left)) return false; } if (!got_page) diff --git a/src/main.cpp b/src/main.cpp index 73334f2d094..c924824c608 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -640,9 +640,13 @@ void setup() digitalWrite(SENSOR_POWER_CTRL_EXPANDER, SENSOR_POWER_ON_EXPANDER); #endif sensecapIndicator = new SensecapIndicator(Serial2); - if (!sensecapIndicator->wait_ready(3000)) - LOG_WARN("RP2040 co-processor did not complete the handshake in time, bridged peripherals stay disabled until it " - "answers"); + // The bus behind the co-processor is scanned right below and the devices + // found there are registered once, so the link has to be up by then: a + // co-processor that only answers later would leave its sensors + // undiscovered for the rest of the session. It announces itself when it + // has booted, so this normally returns as soon as that message arrives. + if (!sensecapIndicator->wait_ready(5000)) + LOG_ERROR("RP2040 co-processor did not answer, its sensors, GPS and SD card are unavailable this session"); #endif #if !MESHTASTIC_EXCLUDE_I2C diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp index ecc7e3cbf79..a7080a5353d 100644 --- a/src/mesh/IndicatorSerial.cpp +++ b/src/mesh/IndicatorSerial.cpp @@ -96,6 +96,26 @@ bool SensecapIndicator::link_ready() return handshake_done && link_compatible; } +// The co-processor reported the protocol version it speaks, in a pong or in +// the unsolicited ping it sends when it has booted. Anything else than ours +// means its firmware does not match this build, and every request would be +// misinterpreted. Caller holds link_lock. +void SensecapIndicator::note_handshake(uint32_t peer_version) +{ + bool compatible = peer_version == meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT; + bool changed = !handshake_done || compatible != link_compatible; + if (changed) { + if (compatible) + LOG_INFO("RP2040 link up, interdevice protocol v%u", (unsigned)peer_version); + else + LOG_ERROR("RP2040 speaks interdevice protocol v%u, this firmware speaks v%u. Flash the matching " + "indicator_rp2040 firmware; sensors, GPS and SD card stay disabled", + (unsigned)peer_version, (unsigned)meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT); + } + link_compatible = compatible; + handshake_done = true; +} + bool SensecapIndicator::i2c_transact(uint8_t address, const uint8_t *wbuf, size_t wlen, size_t rlen, meshtastic_I2CResult *result, uint32_t timeout_ms) { @@ -264,7 +284,10 @@ bool SensecapIndicator::wait_ready(uint32_t timeout_ms) bool SensecapIndicator::send_uplink(const meshtastic_InterdeviceMessage &message) { + InFlight busy(requests_in_flight); concurrency::LockGuard guard(&link_lock); + if (!link_ready()) + return false; // nothing is sent to a peer we do not speak the same protocol with return send_uplink_unlocked(message); } @@ -385,9 +408,17 @@ bool SensecapIndicator::handle_packet(size_t payload_len) } return true; case meshtastic_InterdeviceMessage_ping_tag: { - // Liveness probe from the other side, answer regardless of state. - // tx_message is safe to reuse: a request in flight was already - // encoded into pb_tx_buf when it was sent + // The co-processor pings us unsolicited (id 0) when it has booted, + // which is also how a reboot after its watchdog is noticed. It + // reports the version it speaks, so this completes the handshake + // just like a pong does. + if (message.id == 0) { + if (handshake_done) + LOG_WARN("RP2040 rebooted"); + note_handshake(message.data.ping); + } + // answer regardless of state. tx_message is safe to reuse: a request + // in flight was already encoded into pb_tx_buf when it was sent meshtastic_InterdeviceMessage &pong = tx_message; memset(&pong, 0, sizeof(pong)); pong.id = message.id; @@ -396,23 +427,10 @@ bool SensecapIndicator::handle_packet(size_t payload_len) send_uplink_unlocked(pong); return true; } - case meshtastic_InterdeviceMessage_pong_tag: { - // The handshake: the co-processor reports the protocol version it - // speaks. Anything else than ours means its firmware does not match - // this build, and every request would be misinterpreted. - bool compatible = message.data.pong == meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT; - if (!handshake_done || compatible != link_compatible) { - if (compatible) - LOG_INFO("RP2040 link up, interdevice protocol v%u", (unsigned)message.data.pong); - else - LOG_ERROR("RP2040 speaks interdevice protocol v%u, this firmware speaks v%u. Flash the matching " - "indicator_rp2040 firmware; sensors, GPS and SD card stay disabled", - (unsigned)message.data.pong, (unsigned)meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT); - } - link_compatible = compatible; - handshake_done = true; + case meshtastic_InterdeviceMessage_pong_tag: + // the answer to our probe, carrying the version it speaks + note_handshake(message.data.pong); return true; - } case meshtastic_InterdeviceMessage_nack_tag: // A nack for the request in flight is definitive: resending it would // only be refused again. An id of 0 means the co-processor could not diff --git a/src/mesh/IndicatorSerial.h b/src/mesh/IndicatorSerial.h index b978463ed4a..d5e50ce8d09 100644 --- a/src/mesh/IndicatorSerial.h +++ b/src/mesh/IndicatorSerial.h @@ -46,11 +46,14 @@ class SensecapIndicator : public concurrency::OSThread bool file_remove(const char *path, meshtastic_FileTransfer *out, uint32_t timeout_ms = 1000); // List directory entries starting at entry number `offset` (paged, // subdirectories get a trailing slash) - bool list_directory(const char *path, uint32_t offset, meshtastic_DirectoryListing *out, uint32_t timeout_ms = 1000); - // SD card statistics, answered from a cache on the co-processor. - // used/free are only meaningful once stats_valid is set: the FAT scan - // behind them runs in the background after the card is mounted - bool sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms = 2000); + // the first page of a large directory has to walk it to the end to count + // the entries, so this one gets a longer budget + bool list_directory(const char *path, uint32_t offset, meshtastic_DirectoryListing *out, uint32_t timeout_ms = 5000); + // SD card statistics, answered from state the co-processor cached at + // mount time, so this never waits on the card. used/free are only + // meaningful once stats_valid is set: the FAT scan behind them runs in + // the background. `busy` means a card is being mounted right now. + bool sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms = 500); // True when the last request was refused by the co-processor rather than // lost: retrying it would only be refused again @@ -114,7 +117,8 @@ class SensecapIndicator : public concurrency::OSThread ~InFlight() { count--; } }; bool link_ready(); - void probe_link(); // caller holds link_lock + void probe_link(); // caller holds link_lock + void note_handshake(uint32_t peer_version); // caller holds link_lock uint32_t stamp_request(meshtastic_InterdeviceMessage &request); bool send_uplink_unlocked(const meshtastic_InterdeviceMessage &message); // callers hold link_lock (the request was staged in the shared tx_message) diff --git a/src/mesh/comms/I2CProxy.cpp b/src/mesh/comms/I2CProxy.cpp index f2e5c4e580b..e8d0308e368 100644 --- a/src/mesh/comms/I2CProxy.cpp +++ b/src/mesh/comms/I2CProxy.cpp @@ -2,9 +2,13 @@ #include "I2CProxy.h" #include "../IndicatorSerial.h" +#include "LinkSpiLock.h" I2CProxy *i2cProxy = new I2CProxy(); +// the TFT task publishes itself here while it holds spiLock, see LinkSpiLock.h +volatile TaskHandle_t spiLockHolder = nullptr; + // Transaction state of the calling task. Slots are claimed on first use and // never released: the set of tasks touching the bridged bus is fixed (main // loop, UI task). @@ -15,12 +19,19 @@ I2CProxy::Context &I2CProxy::ctx() if (_ctx[i].task == self) return _ctx[i]; } - for (size_t i = 0; i < MAX_TASKS; i++) { + // claiming a slot must be atomic: two tasks that pick the same one would + // interleave their transactions in one buffer set + Context *claimed = nullptr; + portENTER_CRITICAL(&_claim_mux); + for (size_t i = 0; i < MAX_TASKS && !claimed; i++) { if (_ctx[i].task == nullptr) { _ctx[i].task = self; - return _ctx[i]; + claimed = &_ctx[i]; } } + portEXIT_CRITICAL(&_claim_mux); + if (claimed) + return *claimed; LOG_WARN("I2CProxy: more tasks than contexts, sharing the last one"); return _ctx[MAX_TASKS - 1]; } @@ -112,6 +123,10 @@ uint8_t I2CProxy::transact(Context &c, uint8_t address, size_t rlen) if (c.txLen > MAX_WRITE) return 1; + // the keyboard scanner polls this bus from the LVGL task, which holds the + // SPI lock the radio needs + SpiLockBreak spiFree; + meshtastic_I2CResult result = meshtastic_I2CResult_init_zero; if (!sensecapIndicator->i2c_transact(address, c.txBuf, c.txLen, rlen, &result)) return 5; diff --git a/src/mesh/comms/I2CProxy.h b/src/mesh/comms/I2CProxy.h index 22f069a3463..3dde7cc18cb 100644 --- a/src/mesh/comms/I2CProxy.h +++ b/src/mesh/comms/I2CProxy.h @@ -72,6 +72,7 @@ class I2CProxy : public TwoWire size_t rxPos = 0; }; Context _ctx[MAX_TASKS]; + portMUX_TYPE _claim_mux = portMUX_INITIALIZER_UNLOCKED; // per-task transaction state, claimed on first use Context &ctx(); diff --git a/src/mesh/comms/LinkSpiLock.h b/src/mesh/comms/LinkSpiLock.h new file mode 100644 index 00000000000..2101f110337 --- /dev/null +++ b/src/mesh/comms/LinkSpiLock.h @@ -0,0 +1,46 @@ +#pragma once + +#ifdef SENSECAP_INDICATOR + +#include "SPILock.h" +#include +#include + +// Set by the TFT task while it holds spiLock around the LVGL handler. The +// link round trips below run inside LVGL callbacks (map tiles, keyboard +// scan) and would otherwise hold the SPI bus - which the LoRa radio shares +// with the display - for the duration of a request. +extern volatile TaskHandle_t spiLockHolder; + +/** + * Hands the SPI bus back for the duration of an interdevice link round + * trip. Nothing in a round trip touches SPI, and a slow or unresponsive + * co-processor would otherwise starve the radio for the request timeout. + * + * A no-op unless the current task is the one that holds spiLock, so it is + * safe to use from any context (and nests harmlessly). + */ +class SpiLockBreak +{ + public: + SpiLockBreak() + { + held = spiLockHolder == xTaskGetCurrentTaskHandle(); + if (held) { + spiLockHolder = nullptr; + spiLock->unlock(); + } + } + ~SpiLockBreak() + { + if (held) { + spiLock->lock(); + spiLockHolder = xTaskGetCurrentTaskHandle(); + } + } + + private: + bool held; +}; + +#endif // SENSECAP_INDICATOR diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index e927dc5f50e..fdc48e796e8 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -85,7 +85,6 @@ typedef struct _meshtastic_FileTransfer { meshtastic_FileOperation operation; /* File operation (GET, POST, PUT, DELETE) */ char filepath[256]; /* Path of the file on the SD card */ meshtastic_FileTransfer_filedata_t filedata; /* Chunk content (POST/PUT request, GET response) */ - meshtastic_FileStatus status; /* Response: outcome of the operation */ char message[255]; /* Response: human readable detail, may be empty */ uint64_t offset; /* Byte offset of this chunk within the file (ranged GET/PUT) */ /* GET request: number of bytes to read, 0 = max chunk size. A response @@ -93,6 +92,7 @@ typedef struct _meshtastic_FileTransfer { chunk; larger requests are truncated, visible in the filedata length. */ uint32_t length; uint64_t file_size; /* GET response: total size of the file */ + meshtastic_FileStatus status; /* Response: outcome of the operation */ } meshtastic_FileTransfer; /* Message for structured directory listing */ @@ -105,10 +105,10 @@ typedef struct _meshtastic_DirectoryListing { offset and total_count. */ pb_size_t filenames_count; char filenames[16][256]; - meshtastic_FileStatus status; /* Response: outcome of the operation */ char message[255]; /* Response: human readable detail, may be empty */ uint32_t offset; /* Request: skip this many entries (paging) */ uint32_t total_count; /* Response: total number of entries in the directory */ + meshtastic_FileStatus status; /* Response: outcome of the operation */ } meshtastic_DirectoryListing; typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CTransaction_write_data_t; @@ -126,7 +126,9 @@ typedef struct _meshtastic_I2CTransaction { /* SD card statistics */ typedef struct _meshtastic_SdCardInfo { - bool present; /* Card initialized and usable */ + /* Card initialized and usable. False while `busy` is set does not mean + there is no card: the co-processor does not know yet. */ + bool present; meshtastic_SdCardInfo_CardType card_type; meshtastic_SdCardInfo_FatType fat_type; uint64_t card_size; /* Filesystem size in bytes */ @@ -136,6 +138,10 @@ typedef struct _meshtastic_SdCardInfo { them runs in the background after mount and can take a while, and a full card is otherwise indistinguishable from a scan in progress */ bool stats_valid; + /* The co-processor is mounting a card right now, so whether one is + present is not decided yet. Ask again rather than concluding the slot + is empty. */ + bool busy; } meshtastic_SdCardInfo; typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CResult_read_data_t; @@ -222,16 +228,16 @@ extern "C" { /* Initializer values for message structs */ -#define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0} -#define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0} +#define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, "", 0, 0, 0, _meshtastic_FileStatus_MIN} +#define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, "", 0, 0, _meshtastic_FileStatus_MIN} #define meshtastic_I2CTransaction_init_default {0, {0, {0}}, 0} -#define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0} +#define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0} #define meshtastic_I2CResult_init_default {_meshtastic_I2CResult_Status_MIN, {0, {0}}} #define meshtastic_InterdeviceMessage_init_default {0, {""}, 0} -#define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0} -#define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0} +#define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, "", 0, 0, 0, _meshtastic_FileStatus_MIN} +#define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, "", 0, 0, _meshtastic_FileStatus_MIN} #define meshtastic_I2CTransaction_init_zero {0, {0, {0}}, 0} -#define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0} +#define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0} #define meshtastic_I2CResult_init_zero {_meshtastic_I2CResult_Status_MIN, {0, {0}}} #define meshtastic_InterdeviceMessage_init_zero {0, {""}, 0} @@ -239,17 +245,17 @@ extern "C" { #define meshtastic_FileTransfer_operation_tag 1 #define meshtastic_FileTransfer_filepath_tag 2 #define meshtastic_FileTransfer_filedata_tag 3 -#define meshtastic_FileTransfer_status_tag 4 #define meshtastic_FileTransfer_message_tag 5 #define meshtastic_FileTransfer_offset_tag 6 #define meshtastic_FileTransfer_length_tag 7 #define meshtastic_FileTransfer_file_size_tag 8 +#define meshtastic_FileTransfer_status_tag 9 #define meshtastic_DirectoryListing_directory_tag 1 #define meshtastic_DirectoryListing_filenames_tag 2 -#define meshtastic_DirectoryListing_status_tag 3 #define meshtastic_DirectoryListing_message_tag 4 #define meshtastic_DirectoryListing_offset_tag 5 #define meshtastic_DirectoryListing_total_count_tag 6 +#define meshtastic_DirectoryListing_status_tag 7 #define meshtastic_I2CTransaction_address_tag 1 #define meshtastic_I2CTransaction_write_data_tag 2 #define meshtastic_I2CTransaction_read_len_tag 3 @@ -260,6 +266,7 @@ extern "C" { #define meshtastic_SdCardInfo_used_bytes_tag 5 #define meshtastic_SdCardInfo_free_bytes_tag 6 #define meshtastic_SdCardInfo_stats_valid_tag 7 +#define meshtastic_SdCardInfo_busy_tag 8 #define meshtastic_I2CResult_status_tag 1 #define meshtastic_I2CResult_read_data_tag 2 #define meshtastic_InterdeviceMessage_nmea_tag 1 @@ -282,21 +289,21 @@ extern "C" { X(a, STATIC, SINGULAR, UENUM, operation, 1) \ X(a, STATIC, SINGULAR, STRING, filepath, 2) \ X(a, STATIC, SINGULAR, BYTES, filedata, 3) \ -X(a, STATIC, SINGULAR, UENUM, status, 4) \ X(a, STATIC, SINGULAR, STRING, message, 5) \ X(a, STATIC, SINGULAR, UINT64, offset, 6) \ X(a, STATIC, SINGULAR, UINT32, length, 7) \ -X(a, STATIC, SINGULAR, UINT64, file_size, 8) +X(a, STATIC, SINGULAR, UINT64, file_size, 8) \ +X(a, STATIC, SINGULAR, UENUM, status, 9) #define meshtastic_FileTransfer_CALLBACK NULL #define meshtastic_FileTransfer_DEFAULT NULL #define meshtastic_DirectoryListing_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, directory, 1) \ X(a, STATIC, REPEATED, STRING, filenames, 2) \ -X(a, STATIC, SINGULAR, UENUM, status, 3) \ X(a, STATIC, SINGULAR, STRING, message, 4) \ X(a, STATIC, SINGULAR, UINT32, offset, 5) \ -X(a, STATIC, SINGULAR, UINT32, total_count, 6) +X(a, STATIC, SINGULAR, UINT32, total_count, 6) \ +X(a, STATIC, SINGULAR, UENUM, status, 7) #define meshtastic_DirectoryListing_CALLBACK NULL #define meshtastic_DirectoryListing_DEFAULT NULL @@ -314,7 +321,8 @@ X(a, STATIC, SINGULAR, UENUM, fat_type, 3) \ X(a, STATIC, SINGULAR, UINT64, card_size, 4) \ X(a, STATIC, SINGULAR, UINT64, used_bytes, 5) \ X(a, STATIC, SINGULAR, UINT64, free_bytes, 6) \ -X(a, STATIC, SINGULAR, BOOL, stats_valid, 7) +X(a, STATIC, SINGULAR, BOOL, stats_valid, 7) \ +X(a, STATIC, SINGULAR, BOOL, busy, 8) #define meshtastic_SdCardInfo_CALLBACK NULL #define meshtastic_SdCardInfo_DEFAULT NULL @@ -369,7 +377,7 @@ extern const pb_msgdesc_t meshtastic_InterdeviceMessage_msg; #define meshtastic_I2CResult_size 261 #define meshtastic_I2CTransaction_size 271 #define meshtastic_InterdeviceMessage_size 4666 -#define meshtastic_SdCardInfo_size 41 +#define meshtastic_SdCardInfo_size 43 #ifdef __cplusplus } /* extern "C" */ From ec544bfc0beddb834716773be8a7b44ab3bcc4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 11:37:55 +0200 Subject: [PATCH 13/28] indicator: regen protos, FileStatus back on the original tags --- .../generated/meshtastic/interdevice.pb.h | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index fdc48e796e8..61487b9e6c6 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -85,6 +85,7 @@ typedef struct _meshtastic_FileTransfer { meshtastic_FileOperation operation; /* File operation (GET, POST, PUT, DELETE) */ char filepath[256]; /* Path of the file on the SD card */ meshtastic_FileTransfer_filedata_t filedata; /* Chunk content (POST/PUT request, GET response) */ + meshtastic_FileStatus status; /* Response: outcome of the operation */ char message[255]; /* Response: human readable detail, may be empty */ uint64_t offset; /* Byte offset of this chunk within the file (ranged GET/PUT) */ /* GET request: number of bytes to read, 0 = max chunk size. A response @@ -92,7 +93,6 @@ typedef struct _meshtastic_FileTransfer { chunk; larger requests are truncated, visible in the filedata length. */ uint32_t length; uint64_t file_size; /* GET response: total size of the file */ - meshtastic_FileStatus status; /* Response: outcome of the operation */ } meshtastic_FileTransfer; /* Message for structured directory listing */ @@ -105,10 +105,10 @@ typedef struct _meshtastic_DirectoryListing { offset and total_count. */ pb_size_t filenames_count; char filenames[16][256]; + meshtastic_FileStatus status; /* Response: outcome of the operation */ char message[255]; /* Response: human readable detail, may be empty */ uint32_t offset; /* Request: skip this many entries (paging) */ uint32_t total_count; /* Response: total number of entries in the directory */ - meshtastic_FileStatus status; /* Response: outcome of the operation */ } meshtastic_DirectoryListing; typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CTransaction_write_data_t; @@ -228,14 +228,14 @@ extern "C" { /* Initializer values for message structs */ -#define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, "", 0, 0, 0, _meshtastic_FileStatus_MIN} -#define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, "", 0, 0, _meshtastic_FileStatus_MIN} +#define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0} +#define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0} #define meshtastic_I2CTransaction_init_default {0, {0, {0}}, 0} #define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0} #define meshtastic_I2CResult_init_default {_meshtastic_I2CResult_Status_MIN, {0, {0}}} #define meshtastic_InterdeviceMessage_init_default {0, {""}, 0} -#define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, "", 0, 0, 0, _meshtastic_FileStatus_MIN} -#define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, "", 0, 0, _meshtastic_FileStatus_MIN} +#define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0} +#define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0} #define meshtastic_I2CTransaction_init_zero {0, {0, {0}}, 0} #define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0} #define meshtastic_I2CResult_init_zero {_meshtastic_I2CResult_Status_MIN, {0, {0}}} @@ -245,17 +245,17 @@ extern "C" { #define meshtastic_FileTransfer_operation_tag 1 #define meshtastic_FileTransfer_filepath_tag 2 #define meshtastic_FileTransfer_filedata_tag 3 +#define meshtastic_FileTransfer_status_tag 4 #define meshtastic_FileTransfer_message_tag 5 #define meshtastic_FileTransfer_offset_tag 6 #define meshtastic_FileTransfer_length_tag 7 #define meshtastic_FileTransfer_file_size_tag 8 -#define meshtastic_FileTransfer_status_tag 9 #define meshtastic_DirectoryListing_directory_tag 1 #define meshtastic_DirectoryListing_filenames_tag 2 +#define meshtastic_DirectoryListing_status_tag 3 #define meshtastic_DirectoryListing_message_tag 4 #define meshtastic_DirectoryListing_offset_tag 5 #define meshtastic_DirectoryListing_total_count_tag 6 -#define meshtastic_DirectoryListing_status_tag 7 #define meshtastic_I2CTransaction_address_tag 1 #define meshtastic_I2CTransaction_write_data_tag 2 #define meshtastic_I2CTransaction_read_len_tag 3 @@ -289,21 +289,21 @@ extern "C" { X(a, STATIC, SINGULAR, UENUM, operation, 1) \ X(a, STATIC, SINGULAR, STRING, filepath, 2) \ X(a, STATIC, SINGULAR, BYTES, filedata, 3) \ +X(a, STATIC, SINGULAR, UENUM, status, 4) \ X(a, STATIC, SINGULAR, STRING, message, 5) \ X(a, STATIC, SINGULAR, UINT64, offset, 6) \ X(a, STATIC, SINGULAR, UINT32, length, 7) \ -X(a, STATIC, SINGULAR, UINT64, file_size, 8) \ -X(a, STATIC, SINGULAR, UENUM, status, 9) +X(a, STATIC, SINGULAR, UINT64, file_size, 8) #define meshtastic_FileTransfer_CALLBACK NULL #define meshtastic_FileTransfer_DEFAULT NULL #define meshtastic_DirectoryListing_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, directory, 1) \ X(a, STATIC, REPEATED, STRING, filenames, 2) \ +X(a, STATIC, SINGULAR, UENUM, status, 3) \ X(a, STATIC, SINGULAR, STRING, message, 4) \ X(a, STATIC, SINGULAR, UINT32, offset, 5) \ -X(a, STATIC, SINGULAR, UINT32, total_count, 6) \ -X(a, STATIC, SINGULAR, UENUM, status, 7) +X(a, STATIC, SINGULAR, UINT32, total_count, 6) #define meshtastic_DirectoryListing_CALLBACK NULL #define meshtastic_DirectoryListing_DEFAULT NULL From d70bf246ccddf301d0bd7e910469afd9247834f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 11:49:44 +0200 Subject: [PATCH 14/28] indicator: regen protos, ping/pong carry the InterdeviceVersion enum --- src/mesh/generated/meshtastic/interdevice.pb.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index 61487b9e6c6..7f2ec4ec185 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -168,11 +168,11 @@ typedef struct _meshtastic_InterdeviceMessage { meshtastic_SdCardInfo sd_info; /* Response */ /* Link liveness probe and version handshake. The receiver answers ping with pong, echoing the id. Touches no peripherals, so it works with - nothing attached. Both carry the sender's InterdeviceVersion; a peer + nothing attached. Both carry the version the sender speaks; a peer that answers with a different one speaks another protocol and must not be used. */ - uint32_t ping; - uint32_t pong; + meshtastic_InterdeviceVersion ping; + meshtastic_InterdeviceVersion pong; /* Response: the request could not be decoded or is of an unhandled type, so the requester fails fast instead of burning its timeout. Echoes the id when known, 0 when the frame was undecodable. Never @@ -225,6 +225,8 @@ extern "C" { #define meshtastic_I2CResult_status_ENUMTYPE meshtastic_I2CResult_Status +#define meshtastic_InterdeviceMessage_data_ping_ENUMTYPE meshtastic_InterdeviceVersion +#define meshtastic_InterdeviceMessage_data_pong_ENUMTYPE meshtastic_InterdeviceVersion /* Initializer values for message structs */ @@ -343,8 +345,8 @@ X(a, STATIC, ONEOF, MESSAGE, (data,file_transfer,data.file_transfer), 7) X(a, STATIC, ONEOF, MESSAGE, (data,directory_listing,data.directory_listing), 8) \ X(a, STATIC, ONEOF, BOOL, (data,get_sd_info,data.get_sd_info), 9) \ X(a, STATIC, ONEOF, MESSAGE, (data,sd_info,data.sd_info), 10) \ -X(a, STATIC, ONEOF, UINT32, (data,ping,data.ping), 11) \ -X(a, STATIC, ONEOF, UINT32, (data,pong,data.pong), 12) \ +X(a, STATIC, ONEOF, UENUM, (data,ping,data.ping), 11) \ +X(a, STATIC, ONEOF, UENUM, (data,pong,data.pong), 12) \ X(a, STATIC, ONEOF, BOOL, (data,nack,data.nack), 13) \ X(a, STATIC, SINGULAR, UINT32, id, 15) #define meshtastic_InterdeviceMessage_CALLBACK NULL From 4fe753889f89716c61a9f2c289284de1d0b094c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 12:03:10 +0200 Subject: [PATCH 15/28] indicator: point the protobufs submodule at the merged interdevice protos --- protobufs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protobufs b/protobufs index 9d589c13214..da2fc413931 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 9d589c1321478193885d6cecf852bf02d14fc92b +Subproject commit da2fc413931c81c1d2bc9b6a838d739e2bf35bc1 From a13bff56c35550d2530fe97f6e706e24f2d8c358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 12:09:12 +0200 Subject: [PATCH 16/28] indicator: pin device-ui to the branch with the remote SD support --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 66a0eaabbfc..5381a21485f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -127,7 +127,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/27e6c0c5a6f280e67b910a6c17fbcf90768011ca.zip + https://github.com/meshtastic/device-ui/archive/f74a67f2ae97f83d1d36db3fc1dfbe766c0cf5b1.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 3225430dc93115c436ed1da5d694f625d2ceace8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 12:53:31 +0200 Subject: [PATCH 17/28] indicator: honor the txOnly flag of flush, report dropped GPS writes flush() through a Stream pointer discarded the receive buffer: the flag is txOnly, and HardwareSerial::flush() keeps what has been received. write() reported bytes as written even when the link refused to send them. The link probe uses Throttle for its rate limit. --- src/main.cpp | 7 ++----- src/mesh/IndicatorSerial.cpp | 4 +++- src/mesh/comms/LinkSpiLock.h | 13 +++---------- src/mesh/comms/UARTProxy.cpp | 14 +++++++++++--- src/mesh/comms/UARTProxy.h | 16 +++++++--------- 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index c924824c608..dbdd60903cd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -640,11 +640,8 @@ void setup() digitalWrite(SENSOR_POWER_CTRL_EXPANDER, SENSOR_POWER_ON_EXPANDER); #endif sensecapIndicator = new SensecapIndicator(Serial2); - // The bus behind the co-processor is scanned right below and the devices - // found there are registered once, so the link has to be up by then: a - // co-processor that only answers later would leave its sensors - // undiscovered for the rest of the session. It announces itself when it - // has booted, so this normally returns as soon as that message arrives. + // the bus behind it is scanned right below and its devices are registered + // once, so the link has to be up by then if (!sensecapIndicator->wait_ready(5000)) LOG_ERROR("RP2040 co-processor did not answer, its sensors, GPS and SD card are unavailable this session"); #endif diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp index a7080a5353d..f39409e8b1f 100644 --- a/src/mesh/IndicatorSerial.cpp +++ b/src/mesh/IndicatorSerial.cpp @@ -4,6 +4,7 @@ #include "concurrency/LockGuard.h" #include "mesh/comms/UARTProxy.h" #include +#include #include #include @@ -41,7 +42,8 @@ int32_t SensecapIndicator::runOnce() // the protocol version it speaks. Caller holds link_lock. void SensecapIndicator::probe_link() { - if (last_probe != 0 && millis() - last_probe < 250) + // last_probe 0 means we have not probed at all yet, which must not throttle + if (last_probe != 0 && Throttle::isWithinTimespanMs(last_probe, 250)) return; meshtastic_InterdeviceMessage &msg = tx_message; memset(&msg, 0, sizeof(msg)); diff --git a/src/mesh/comms/LinkSpiLock.h b/src/mesh/comms/LinkSpiLock.h index 2101f110337..34fdf9c6d62 100644 --- a/src/mesh/comms/LinkSpiLock.h +++ b/src/mesh/comms/LinkSpiLock.h @@ -6,19 +6,12 @@ #include #include -// Set by the TFT task while it holds spiLock around the LVGL handler. The -// link round trips below run inside LVGL callbacks (map tiles, keyboard -// scan) and would otherwise hold the SPI bus - which the LoRa radio shares -// with the display - for the duration of a request. +// set by the TFT task while it holds spiLock around the LVGL handler extern volatile TaskHandle_t spiLockHolder; /** - * Hands the SPI bus back for the duration of an interdevice link round - * trip. Nothing in a round trip touches SPI, and a slow or unresponsive - * co-processor would otherwise starve the radio for the request timeout. - * - * A no-op unless the current task is the one that holds spiLock, so it is - * safe to use from any context (and nests harmlessly). + * Hands the SPI bus (shared with the LoRa radio) back for the duration of a + * link round trip. No-op unless the current task holds spiLock. */ class SpiLockBreak { diff --git a/src/mesh/comms/UARTProxy.cpp b/src/mesh/comms/UARTProxy.cpp index 894da8952d1..34170ec41b2 100644 --- a/src/mesh/comms/UARTProxy.cpp +++ b/src/mesh/comms/UARTProxy.cpp @@ -44,9 +44,13 @@ int UARTProxy::read() return ret; } -void UARTProxy::flush(bool wait) +void UARTProxy::flush(bool txOnly) { - buf_clear(); + // writes are packed into one uplink message each, so there is never + // anything in flight to wait for. Dropping the unread receive buffer is + // what flush(false) is for (GPS::clearBuffer uses it). + if (!txOnly) + buf_clear(); } uint32_t UARTProxy::baudRate() @@ -88,7 +92,11 @@ size_t UARTProxy::write(char *buffer, size_t size) memcpy(message.data.nmea, buffer, size); message.which_data = meshtastic_InterdeviceMessage_nmea_tag; LOG_DEBUG("UARTProxy::write %u bytes", (unsigned)size); - sensecapIndicator->send_uplink(message); + // the link refuses to send before the handshake, and to a co-processor + // running incompatible firmware: report the bytes as not written rather + // than pretending a GPS command was delivered + if (!sensecapIndicator->send_uplink(message)) + return 0; return size; } diff --git a/src/mesh/comms/UARTProxy.h b/src/mesh/comms/UARTProxy.h index b99fd9d8c4a..1171bec1e7a 100644 --- a/src/mesh/comms/UARTProxy.h +++ b/src/mesh/comms/UARTProxy.h @@ -7,12 +7,8 @@ #include /** - * Stream implementation that proxies a serial port living on the RP2040 - * over the interdevice link, so the regular GPS driver of the main - * firmware can talk to the GNSS module attached to the secondary MCU. - * - * Reads are served from a ring buffer that the link fills with the NMEA - * sentences the co-processor forwards; writes are packed into a single + * Stream that proxies the GPS serial port of the RP2040 over the interdevice + * link: reads come from a ring buffer the link fills, writes go out as one * uplink message each. */ class UARTProxy : public Stream @@ -26,9 +22,11 @@ class UARTProxy : public Stream int available(); int peek(); int read(); - void flush(bool wait); - // Stream/Print contract: flush() through a base pointer must behave - // like the HardwareSerial variant callers expect + // HardwareSerial semantics: the flag is txOnly, not "wait". Only a + // flush(false) discards what has been received but not read yet. + void flush(bool txOnly); + // HardwareSerial::flush() is uartFlushTxOnly(txOnly = true): it waits + // for the transmitter and keeps the receive buffer void flush() override { flush(true); } // writes are buffered into one uplink message, so this is its capacity // (Print's default of 0 would make drivers back off forever) From 33ae81aabacb0664958022454eba7f97e72a08a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 13:01:12 +0200 Subject: [PATCH 18/28] indicator: decide the log tag on the formatted message, hex request ids The thread tag was suppressed based on the printf template, which disagrees with the rendered message it is compared against: a format starting with a conversion could produce two tags, and one without a trailing bracket-space lost the tag entirely. vprintf now receives the thread name and picks. Also shifts only the bytes actually buffered after a frame, throttles with Throttle and logs request ids as hex. --- src/RedirectablePrint.cpp | 25 +++++++++++-------------- src/RedirectablePrint.h | 5 +++-- src/mesh/IndicatorSerial.cpp | 19 ++++++++++--------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index cb078a71a76..14dc3a8e3d8 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -48,7 +48,7 @@ size_t RedirectablePrint::write(uint8_t c) // serial port said (which could be zero) } -size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg) +size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg, const char *threadName) { va_list copy; #if ARCH_PORTDUINO @@ -78,9 +78,8 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l if (!std::isprint(static_cast(printBuf[f])) && printBuf[f] != '\n') printBuf[f] = '#'; } - // A leading "[Tag] " in the message simulates the thread name tag (used - // by contexts without an OSThread, e.g. the device-ui task). Print it - // before applying the level color so it renders like a real thread name. + // A message with its own "[Tag] " (the device-ui task has no OSThread) + // uses it instead of the thread name, printed uncolored like a real one. size_t tagLen = 0; if (printBuf[0] == '[') { const char *end = (const char *)memchr(printBuf, ']', len < 24 ? len : 24); @@ -89,6 +88,11 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l } if (tagLen) Print::write(printBuf, tagLen); + else if (threadName) { + Print::write("[", 1); + Print::write(threadName, strlen(threadName)); + Print::write("] ", 2); + } if (color && logLevel != nullptr) { if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) Print::write("\u001b[34m", 5); @@ -172,15 +176,8 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, #endif } auto thread = concurrency::OSThread::currentThread; - // A message starting with "[Tag] " brings its own tag (e.g. device-ui - // logging from the UI task, where currentThread is unrelated) - if (thread && format[0] != '[') { - print("["); - // printf("%p ", thread); - // assert(thread->ThreadName.length()); - print(thread->ThreadName); - print("] "); - } + // the tag is printed by vprintf, which knows whether the formatted + // message already carries one of its own #ifdef DEBUG_HEAP // Add heap free space bytes prefix before every log message @@ -191,7 +188,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, #endif #endif // DEBUG_HEAP - r += vprintf(logLevel, format, arg); + r += vprintf(logLevel, format, arg, thread ? thread->ThreadName.c_str() : nullptr); } void RedirectablePrint::log_to_syslog(const char *logLevel, const char *format, va_list arg) diff --git a/src/RedirectablePrint.h b/src/RedirectablePrint.h index 8535933fc64..216315464ac 100644 --- a/src/RedirectablePrint.h +++ b/src/RedirectablePrint.h @@ -41,8 +41,9 @@ class RedirectablePrint : public Print */ void log(const char *logLevel, const char *format, ...) __attribute__((format(printf, 3, 4))); - /** like printf but va_list based */ - size_t vprintf(const char *logLevel, const char *format, va_list arg); + /** like printf but va_list based. threadName is prefixed unless the + * message already starts with a "[Tag] " of its own */ + size_t vprintf(const char *logLevel, const char *format, va_list arg, const char *threadName = nullptr); void hexDump(const char *logLevel, const unsigned char *buf, uint16_t len); diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp index f39409e8b1f..26e447a867e 100644 --- a/src/mesh/IndicatorSerial.cpp +++ b/src/mesh/IndicatorSerial.cpp @@ -73,7 +73,7 @@ bool SensecapIndicator::wait_response(bool &flag, uint32_t timeout_ms) break; if (request_nacked) return false; // the other side could not handle the request - if (millis() - start >= timeout_ms) + if (!Throttle::isWithinTimespanMs(start, timeout_ms)) return false; delay(1); } @@ -277,7 +277,7 @@ bool SensecapIndicator::wait_ready(uint32_t timeout_ms) pump(); if (handshake_done) break; - if (millis() - start >= timeout_ms) + if (!Throttle::isWithinTimespanMs(start, timeout_ms)) return false; delay(1); } @@ -369,8 +369,9 @@ bool SensecapIndicator::handle_packet(size_t payload_len) // next loop) pb_istream_t stream = pb_istream_from_buffer(pb_rx_buf + MT_HEADER_SIZE, payload_len); bool status = pb_decode(&stream, meshtastic_InterdeviceMessage_fields, &message); - memmove(pb_rx_buf, pb_rx_buf + MT_HEADER_SIZE + payload_len, PB_BUFSIZE - MT_HEADER_SIZE - payload_len); - pb_rx_size -= MT_HEADER_SIZE + payload_len; + size_t remaining = pb_rx_size - MT_HEADER_SIZE - payload_len; + memmove(pb_rx_buf, pb_rx_buf + MT_HEADER_SIZE + payload_len, remaining); + pb_rx_size = remaining; if (!status) { LOG_DEBUG("Decoding failed"); @@ -388,7 +389,7 @@ bool SensecapIndicator::handle_packet(size_t payload_len) i2c_result = message.data.i2c_result; i2c_result_ready = true; } else { - LOG_DEBUG("Drop stale i2c response id=%u", message.id); + LOG_DEBUG("Drop stale i2c response id=0x%08x", message.id); } return true; case meshtastic_InterdeviceMessage_file_transfer_tag: @@ -397,7 +398,7 @@ bool SensecapIndicator::handle_packet(size_t payload_len) pending_file = NULL; file_response_ready = true; } else { - LOG_DEBUG("Drop stale file response id=%u", message.id); + LOG_DEBUG("Drop stale file response id=0x%08x", message.id); } return true; case meshtastic_InterdeviceMessage_directory_listing_tag: @@ -406,7 +407,7 @@ bool SensecapIndicator::handle_packet(size_t payload_len) pending_dir = NULL; dir_response_ready = true; } else { - LOG_DEBUG("Drop stale listing response id=%u", message.id); + LOG_DEBUG("Drop stale listing response id=0x%08x", message.id); } return true; case meshtastic_InterdeviceMessage_ping_tag: { @@ -440,7 +441,7 @@ bool SensecapIndicator::handle_packet(size_t payload_len) // unrelated NMEA uplink, so that one only ends the wait (the caller // may retry it as the transport failure it is). if (message.id == expected_id) { - LOG_WARN("Request %u nacked by the co-processor", expected_id); + LOG_WARN("Request 0x%08x nacked by the co-processor", expected_id); request_nacked = true; } else if (message.id == 0) { LOG_WARN("Co-processor could not decode a frame"); @@ -452,7 +453,7 @@ bool SensecapIndicator::handle_packet(size_t payload_len) pending_sd_info = NULL; sd_info_ready = true; } else { - LOG_DEBUG("Drop stale sd info response id=%u", message.id); + LOG_DEBUG("Drop stale sd info response id=0x%08x", message.id); } return true; default: From a92e3c759cc8d0dd3b990553cb4035d2c067d7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Tue, 14 Jul 2026 16:59:23 +0200 Subject: [PATCH 19/28] indicator: SD mount, eject and format commands over the link --- src/graphics/tftSetup.cpp | 19 +++++++++++ src/mesh/IndicatorSerial.cpp | 22 +++++++++++++ src/mesh/IndicatorSerial.h | 3 ++ .../generated/meshtastic/interdevice.pb.cpp | 2 ++ .../generated/meshtastic/interdevice.pb.h | 33 ++++++++++++++++--- 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 1f5904f6d2e..2f2cf0112f4 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -136,9 +136,19 @@ class IndicatorRemoteFS : public IRemoteFS info.usedBytes = result.used_bytes; info.freeBytes = result.free_bytes; info.statsValid = result.stats_valid; + info.unformatted = result.unformatted; return true; } + bool sdEject(void) override { return sdCommand(meshtastic_SdCommand_SD_EJECT); } + bool sdMount(void) override { return sdCommand(meshtastic_SdCommand_SD_MOUNT); } + bool sdFormat(void) override + { + // wiping the card takes seconds; the co-processor answers right away + // and mounts the fresh filesystem afterwards + return sdCommand(meshtastic_SdCommand_SD_FORMAT); + } + bool remove(const char *path) override { if (!sensecapIndicator) @@ -187,6 +197,15 @@ class IndicatorRemoteFS : public IRemoteFS } private: + bool sdCommand(meshtastic_SdCommand command) + { + if (!sensecapIndicator) + return false; + SpiLockBreak spiFree; + meshtastic_SdCardInfo state = meshtastic_SdCardInfo_init_zero; + return sensecapIndicator->sd_command(command, &state); + } + // Several KB each, kept off the UI task stack. All file operations // originate from the single UI task. meshtastic_FileTransfer result; diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp index 26e447a867e..9ad9a0db71a 100644 --- a/src/mesh/IndicatorSerial.cpp +++ b/src/mesh/IndicatorSerial.cpp @@ -260,6 +260,28 @@ bool SensecapIndicator::sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms) return true; } +bool SensecapIndicator::sd_command(meshtastic_SdCommand command, meshtastic_SdCardInfo *out, uint32_t timeout_ms) +{ + InFlight busy(requests_in_flight); + concurrency::LockGuard guard(&link_lock); + if (!link_ready()) + return false; + + meshtastic_InterdeviceMessage &msg = tx_message; + memset(&msg, 0, sizeof(msg)); + msg.which_data = meshtastic_InterdeviceMessage_sd_command_tag; + msg.data.sd_command = command; + + stamp_request(msg); + sd_info_ready = false; + pending_sd_info = out; + if (!send_uplink_unlocked(msg) || !wait_response(sd_info_ready, timeout_ms)) { + pending_sd_info = NULL; + return false; + } + return true; +} + bool SensecapIndicator::wait_ready(uint32_t timeout_ms) { InFlight busy(requests_in_flight); diff --git a/src/mesh/IndicatorSerial.h b/src/mesh/IndicatorSerial.h index d5e50ce8d09..dcbfd04e879 100644 --- a/src/mesh/IndicatorSerial.h +++ b/src/mesh/IndicatorSerial.h @@ -54,6 +54,9 @@ class SensecapIndicator : public concurrency::OSThread // meaningful once stats_valid is set: the FAT scan behind them runs in // the background. `busy` means a card is being mounted right now. bool sd_info(meshtastic_SdCardInfo *out, uint32_t timeout_ms = 500); + // Mount the card, or release it so it can be pulled safely. Answered with + // the card state as it is right now (a mount is still busy at that point). + bool sd_command(meshtastic_SdCommand command, meshtastic_SdCardInfo *out, uint32_t timeout_ms = 500); // True when the last request was refused by the co-processor rather than // lost: retrying it would only be refused again diff --git a/src/mesh/generated/meshtastic/interdevice.pb.cpp b/src/mesh/generated/meshtastic/interdevice.pb.cpp index 2ab13406a67..d17d62d4404 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.cpp +++ b/src/mesh/generated/meshtastic/interdevice.pb.cpp @@ -37,3 +37,5 @@ PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 4) + + diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index 7f2ec4ec185..e955a9f364f 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.h +++ b/src/mesh/generated/meshtastic/interdevice.pb.h @@ -52,6 +52,14 @@ typedef enum _meshtastic_FileStatus { meshtastic_FileStatus_FILE_NOT_A_FILE = 7 /* path is a directory (GET) or not one (listing) */ } meshtastic_FileStatus; +/* What to do with the SD card of the co-processor */ +typedef enum _meshtastic_SdCommand { + meshtastic_SdCommand_SD_COMMAND_UNSPECIFIED = 0, + meshtastic_SdCommand_SD_MOUNT = 1, /* mount a card that is in the slot, also after an eject */ + meshtastic_SdCommand_SD_EJECT = 2, /* flush and release the card so it can be pulled safely */ + meshtastic_SdCommand_SD_FORMAT = 3 /* wipe the card and put a fresh FAT on it, then mount it */ +} meshtastic_SdCommand; + typedef enum _meshtastic_SdCardInfo_CardType { meshtastic_SdCardInfo_CardType_NONE = 0, meshtastic_SdCardInfo_CardType_MMC = 1, @@ -142,6 +150,9 @@ typedef struct _meshtastic_SdCardInfo { present is not decided yet. Ask again rather than concluding the slot is empty. */ bool busy; + /* A card answers in the slot but carries no filesystem that could be + mounted (present is false then). Formatting it makes it usable. */ + bool unformatted; } meshtastic_SdCardInfo; typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CResult_read_data_t; @@ -178,6 +189,11 @@ typedef struct _meshtastic_InterdeviceMessage { Echoes the id when known, 0 when the frame was undecodable. Never sent in reaction to a nack. */ bool nack; + /* Request: mount the card, or release it so it can be pulled safely. The + co-processor answers with sd_info. Without an eject the card is mounted + on its own and kept mounted; after one it stays released until a mount + is asked for. */ + meshtastic_SdCommand sd_command; } data; /* Correlates a response with its request: responses echo the id of the request they answer. 0 for unsolicited messages (e.g. the nmea stream). */ @@ -202,6 +218,10 @@ extern "C" { #define _meshtastic_FileStatus_MAX meshtastic_FileStatus_FILE_NOT_A_FILE #define _meshtastic_FileStatus_ARRAYSIZE ((meshtastic_FileStatus)(meshtastic_FileStatus_FILE_NOT_A_FILE+1)) +#define _meshtastic_SdCommand_MIN meshtastic_SdCommand_SD_COMMAND_UNSPECIFIED +#define _meshtastic_SdCommand_MAX meshtastic_SdCommand_SD_FORMAT +#define _meshtastic_SdCommand_ARRAYSIZE ((meshtastic_SdCommand)(meshtastic_SdCommand_SD_FORMAT+1)) + #define _meshtastic_SdCardInfo_CardType_MIN meshtastic_SdCardInfo_CardType_NONE #define _meshtastic_SdCardInfo_CardType_MAX meshtastic_SdCardInfo_CardType_UNKNOWN_CARD #define _meshtastic_SdCardInfo_CardType_ARRAYSIZE ((meshtastic_SdCardInfo_CardType)(meshtastic_SdCardInfo_CardType_UNKNOWN_CARD+1)) @@ -227,19 +247,20 @@ extern "C" { #define meshtastic_InterdeviceMessage_data_ping_ENUMTYPE meshtastic_InterdeviceVersion #define meshtastic_InterdeviceMessage_data_pong_ENUMTYPE meshtastic_InterdeviceVersion +#define meshtastic_InterdeviceMessage_data_sd_command_ENUMTYPE meshtastic_SdCommand /* Initializer values for message structs */ #define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0} #define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0} #define meshtastic_I2CTransaction_init_default {0, {0, {0}}, 0} -#define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0} +#define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0, 0} #define meshtastic_I2CResult_init_default {_meshtastic_I2CResult_Status_MIN, {0, {0}}} #define meshtastic_InterdeviceMessage_init_default {0, {""}, 0} #define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0} #define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0} #define meshtastic_I2CTransaction_init_zero {0, {0, {0}}, 0} -#define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0} +#define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0, 0} #define meshtastic_I2CResult_init_zero {_meshtastic_I2CResult_Status_MIN, {0, {0}}} #define meshtastic_InterdeviceMessage_init_zero {0, {""}, 0} @@ -269,6 +290,7 @@ extern "C" { #define meshtastic_SdCardInfo_free_bytes_tag 6 #define meshtastic_SdCardInfo_stats_valid_tag 7 #define meshtastic_SdCardInfo_busy_tag 8 +#define meshtastic_SdCardInfo_unformatted_tag 9 #define meshtastic_I2CResult_status_tag 1 #define meshtastic_I2CResult_read_data_tag 2 #define meshtastic_InterdeviceMessage_nmea_tag 1 @@ -284,6 +306,7 @@ extern "C" { #define meshtastic_InterdeviceMessage_ping_tag 11 #define meshtastic_InterdeviceMessage_pong_tag 12 #define meshtastic_InterdeviceMessage_nack_tag 13 +#define meshtastic_InterdeviceMessage_sd_command_tag 14 #define meshtastic_InterdeviceMessage_id_tag 15 /* Struct field encoding specification for nanopb */ @@ -324,7 +347,8 @@ X(a, STATIC, SINGULAR, UINT64, card_size, 4) \ X(a, STATIC, SINGULAR, UINT64, used_bytes, 5) \ X(a, STATIC, SINGULAR, UINT64, free_bytes, 6) \ X(a, STATIC, SINGULAR, BOOL, stats_valid, 7) \ -X(a, STATIC, SINGULAR, BOOL, busy, 8) +X(a, STATIC, SINGULAR, BOOL, busy, 8) \ +X(a, STATIC, SINGULAR, BOOL, unformatted, 9) #define meshtastic_SdCardInfo_CALLBACK NULL #define meshtastic_SdCardInfo_DEFAULT NULL @@ -348,6 +372,7 @@ X(a, STATIC, ONEOF, MESSAGE, (data,sd_info,data.sd_info), 10) \ X(a, STATIC, ONEOF, UENUM, (data,ping,data.ping), 11) \ X(a, STATIC, ONEOF, UENUM, (data,pong,data.pong), 12) \ X(a, STATIC, ONEOF, BOOL, (data,nack,data.nack), 13) \ +X(a, STATIC, ONEOF, UENUM, (data,sd_command,data.sd_command), 14) \ X(a, STATIC, SINGULAR, UINT32, id, 15) #define meshtastic_InterdeviceMessage_CALLBACK NULL #define meshtastic_InterdeviceMessage_DEFAULT NULL @@ -379,7 +404,7 @@ extern const pb_msgdesc_t meshtastic_InterdeviceMessage_msg; #define meshtastic_I2CResult_size 261 #define meshtastic_I2CTransaction_size 271 #define meshtastic_InterdeviceMessage_size 4666 -#define meshtastic_SdCardInfo_size 43 +#define meshtastic_SdCardInfo_size 45 #ifdef __cplusplus } /* extern "C" */ From 04d2a9b0d43cb4f5e3bc66afc24187a43fd73400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Tue, 14 Jul 2026 22:39:39 +0200 Subject: [PATCH 20/28] indicator: bound how long a busy card state blocks the UI task --- src/graphics/tftSetup.cpp | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 2f2cf0112f4..986aa922222 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -37,6 +37,9 @@ class IndicatorRemoteFS : public IRemoteFS // that out rather than reporting a missing tile. static const int LINK_ATTEMPTS = 3; static const int BUSY_ATTEMPTS = 20; + // card state is answered from a cache, so it is only ever busy across a + // mount: a much shorter wait than a file operation has to sit out + static const int INFO_BUSY_ATTEMPTS = 10; static const uint32_t BUSY_BACKOFF_MS = 250; // Returns true when the request should be sent again. `answered` is @@ -116,17 +119,24 @@ class IndicatorRemoteFS : public IRemoteFS return false; SpiLockBreak spiFree; meshtastic_SdCardInfo result = meshtastic_SdCardInfo_init_zero; + // A mount takes up to two seconds. Waiting it out here is worth it (an + // empty slot reported once sticks in the UI), but this runs on the UI + // task, so the wait is bounded well below what a user would call a + // hang, and a card that stays busy longer is simply asked again later. + int busy_left = INFO_BUSY_ATTEMPTS; int attempts_left = LINK_ATTEMPTS; while (true) { result = meshtastic_SdCardInfo_init_zero; bool answered = sensecapIndicator->sd_info(&result); - // `busy` means a card is being mounted: whether one is there is - // not decided yet, and reporting an empty slot would stick for - // the session if (answered && !result.busy) break; - if (!retryable(answered, answered ? meshtastic_FileStatus_FILE_BUSY : meshtastic_FileStatus_FILE_UNSPECIFIED, - attempts_left)) + if (answered && result.busy) { + if (--busy_left <= 0) + return false; + delay(BUSY_BACKOFF_MS); + continue; + } + if (!retryable(answered, meshtastic_FileStatus_FILE_UNSPECIFIED, attempts_left)) return false; } info.present = result.present; From e5ee49fab8c86249a2b59c2970b0417b63cd1260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Tue, 14 Jul 2026 22:47:35 +0200 Subject: [PATCH 21/28] indicator: a busy co-processor must not block the UI task for ever The busy retry re-armed its own budget on every busy answer, so a co-processor that stayed busy kept the caller in the loop with no way out. Transport retries and the wait for a busy card are now separate budgets that only count down. --- src/graphics/tftSetup.cpp | 51 +++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 986aa922222..2ccff034a30 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -42,19 +42,27 @@ class IndicatorRemoteFS : public IRemoteFS static const int INFO_BUSY_ATTEMPTS = 10; static const uint32_t BUSY_BACKOFF_MS = 250; + // Both budgets are separate and only ever count down: a co-processor that + // stays busy must not be able to keep a caller here for ever. This runs on + // the UI task, an unbounded wait is a frozen screen. + struct Budget { + int attempts = LINK_ATTEMPTS; + int busy = BUSY_ATTEMPTS; + }; + // Returns true when the request should be sent again. `answered` is // false when the link itself failed (timeout), true when the // co-processor replied. - static bool retryable(bool answered, meshtastic_FileStatus status, int &attempts_left) + static bool retryable(bool answered, meshtastic_FileStatus status, Budget &budget) { - if (--attempts_left <= 0) - return false; - if (!answered) + if (!answered) { + if (--budget.attempts <= 0) + return false; return !sensecapIndicator->last_request_nacked(); // refused, not lost + } if (status == meshtastic_FileStatus_FILE_BUSY) { - // it answered, so nothing was lost: only the wait counts - if (attempts_left < BUSY_ATTEMPTS) - attempts_left = BUSY_ATTEMPTS; + if (--budget.busy <= 0) + return false; delay(BUSY_BACKOFF_MS); return true; } @@ -68,7 +76,7 @@ class IndicatorRemoteFS : public IRemoteFS if (!sensecapIndicator) return false; SpiLockBreak spiFree; - int attempts_left = LINK_ATTEMPTS; + Budget budget; do { memset(&result, 0, sizeof(result)); bool answered = sensecapIndicator->file_read(path, offset, len, &result); @@ -82,7 +90,7 @@ class IndicatorRemoteFS : public IRemoteFS *fileSize = (uint32_t)result.file_size; return true; } - if (!retryable(answered, result.status, attempts_left)) + if (!retryable(answered, result.status, budget)) return false; } while (true); } @@ -92,7 +100,7 @@ class IndicatorRemoteFS : public IRemoteFS if (!sensecapIndicator) return false; SpiLockBreak spiFree; - int attempts_left = LINK_ATTEMPTS; + Budget budget; bool retried = false; do { memset(&result, 0, sizeof(result)); @@ -107,7 +115,7 @@ class IndicatorRemoteFS : public IRemoteFS result.file_size == (uint64_t)offset + len) return true; } - if (!retryable(answered, result.status, attempts_left)) + if (!retryable(answered, result.status, budget)) return false; retried = true; } while (true); @@ -123,20 +131,15 @@ class IndicatorRemoteFS : public IRemoteFS // empty slot reported once sticks in the UI), but this runs on the UI // task, so the wait is bounded well below what a user would call a // hang, and a card that stays busy longer is simply asked again later. - int busy_left = INFO_BUSY_ATTEMPTS; - int attempts_left = LINK_ATTEMPTS; + Budget budget; + budget.busy = INFO_BUSY_ATTEMPTS; // a mount, not a whole FAT scan while (true) { result = meshtastic_SdCardInfo_init_zero; bool answered = sensecapIndicator->sd_info(&result); if (answered && !result.busy) break; - if (answered && result.busy) { - if (--busy_left <= 0) - return false; - delay(BUSY_BACKOFF_MS); - continue; - } - if (!retryable(answered, meshtastic_FileStatus_FILE_UNSPECIFIED, attempts_left)) + meshtastic_FileStatus status = answered ? meshtastic_FileStatus_FILE_BUSY : meshtastic_FileStatus_FILE_UNSPECIFIED; + if (!retryable(answered, status, budget)) return false; } info.present = result.present; @@ -164,7 +167,7 @@ class IndicatorRemoteFS : public IRemoteFS if (!sensecapIndicator) return false; SpiLockBreak spiFree; - int attempts_left = LINK_ATTEMPTS; + Budget budget; do { memset(&result, 0, sizeof(result)); bool answered = sensecapIndicator->file_remove(path, &result); @@ -173,7 +176,7 @@ class IndicatorRemoteFS : public IRemoteFS // not have to be guessed at here if (answered && result.status == meshtastic_FileStatus_FILE_OK) return true; - if (!retryable(answered, result.status, attempts_left)) + if (!retryable(answered, result.status, budget)) return false; } while (true); } @@ -186,13 +189,13 @@ class IndicatorRemoteFS : public IRemoteFS uint32_t offset = 0; while (true) { bool got_page = false; - int attempts_left = LINK_ATTEMPTS; + Budget budget; while (!got_page) { memset(&listing, 0, sizeof(listing)); bool answered = sensecapIndicator->list_directory(path, offset, &listing); if (answered && listing.status == meshtastic_FileStatus_FILE_OK) got_page = true; - else if (!retryable(answered, listing.status, attempts_left)) + else if (!retryable(answered, listing.status, budget)) return false; } if (!got_page) From 3793c0995a3bee6920618860c055e01e5caa7d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Wed, 15 Jul 2026 08:31:14 +0200 Subject: [PATCH 22/28] indicator: start each request from an aligned receive buffer A byte run lost mid-response (a UART overflow during a 4KB tile chunk, when the display starves the RX interrupt) misaligns the assembly buffer. The buffer was never reset, so the poison outlived the request and cascaded into the following chunks of the same tile: one glitch dropped a whole multi-chunk tile, while single-chunk tiles resynced in the idle gap and survived. Each request now flushes the buffer first, bounding a glitch to the one chunk it hit. Adds resync/decode/timeout counters, logged rarely, to see the rate. --- src/mesh/IndicatorSerial.cpp | 24 +++++++++++++++++++++++- src/mesh/IndicatorSerial.h | 5 +++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/mesh/IndicatorSerial.cpp b/src/mesh/IndicatorSerial.cpp index 9ad9a0db71a..9afa39b0924 100644 --- a/src/mesh/IndicatorSerial.cpp +++ b/src/mesh/IndicatorSerial.cpp @@ -35,6 +35,16 @@ int32_t SensecapIndicator::runOnce() // the bridge would stay dead for the rest of the session. if (!handshake_done) probe_link(); + // Diagnostics for the map-tile transfers: a resync or a decode failure + // means a response was corrupted on the wire, a timeout means it never + // arrived. Printed at most once every two seconds, and only when + // something went wrong. + if ((link_resyncs || link_decode_fail || link_timeouts) && !Throttle::isWithinTimespanMs(last_link_report, 2000)) { + last_link_report = millis(); + LOG_WARN("link: resync=%u decode_fail=%u timeout=%u", (unsigned)link_resyncs, (unsigned)link_decode_fail, + (unsigned)link_timeouts); + link_resyncs = link_decode_fail = link_timeouts = 0; + } return (10); } @@ -73,8 +83,10 @@ bool SensecapIndicator::wait_response(bool &flag, uint32_t timeout_ms) break; if (request_nacked) return false; // the other side could not handle the request - if (!Throttle::isWithinTimespanMs(start, timeout_ms)) + if (!Throttle::isWithinTimespanMs(start, timeout_ms)) { + link_timeouts++; return false; + } delay(1); } return true; @@ -88,6 +100,14 @@ uint32_t SensecapIndicator::stamp_request(meshtastic_InterdeviceMessage &request request.id = next_request_id; expected_id = next_request_id; request_nacked = false; + // Start the response for this request from an aligned buffer. A byte run + // lost mid-response (a UART overflow during a 4KB chunk) leaves the + // assembly misaligned, and without this that poison would outlive the + // request and cascade into the following chunks of the same tile. The + // link is single-outstanding and stale responses are dropped by id, so + // nothing worth keeping is ever pending here (at most a partial NMEA + // sentence, which self-heals). + pb_rx_size = 0; return next_request_id; } @@ -368,6 +388,7 @@ void SensecapIndicator::check_packet() // Corrupt or false header: resync on the next magic instead of // flushing, one bad byte must not cost the frames behind it size_t skip = scan_magic(pb_rx_buf, pb_rx_size); + link_resyncs++; LOG_DEBUG("Bad frame header, dropping %u bytes", (unsigned)skip); memmove(pb_rx_buf, pb_rx_buf + skip, pb_rx_size - skip); pb_rx_size -= skip; @@ -396,6 +417,7 @@ bool SensecapIndicator::handle_packet(size_t payload_len) pb_rx_size = remaining; if (!status) { + link_decode_fail++; LOG_DEBUG("Decoding failed"); return false; } diff --git a/src/mesh/IndicatorSerial.h b/src/mesh/IndicatorSerial.h index dcbfd04e879..c357d6ec256 100644 --- a/src/mesh/IndicatorSerial.h +++ b/src/mesh/IndicatorSerial.h @@ -86,6 +86,11 @@ class SensecapIndicator : public concurrency::OSThread bool link_compatible = false; bool handshake_done = false; uint32_t last_probe = 0; + // link health diagnostics for the map-tile transfers, reported rarely + uint32_t link_resyncs = 0; + uint32_t link_decode_fail = 0; + uint32_t link_timeouts = 0; + uint32_t last_link_report = 0; meshtastic_I2CResult i2c_result = meshtastic_I2CResult_init_zero; bool i2c_result_ready = false; // Statically allocated message structs: with 4KB file chunks an From 87362bf08c3242eb9c761497eddf0c3f43c6e75d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Wed, 15 Jul 2026 13:58:05 +0200 Subject: [PATCH 23/28] indicator: enlarge the LVGL heap for low-zoom map tiles The heap was 3MB and the image cache reserves 1.5MB of it, so a low-zoom map tile could not find a large enough contiguous block to decode and rendered white. 5MB of the 8MB PSRAM fixes it with room to spare. --- variants/esp32s3/seeed-sensecap-indicator/platformio.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini index b750e7decb0..02caeceff26 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini +++ b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini @@ -84,7 +84,11 @@ build_flags = -D HAS_SCREEN=1 -D HAS_TFT=1 -D DISPLAY_SET_RESOLUTION - -D RAM_SIZE=3072 + # LVGL heap (RAM_SIZE * 1024). The image cache reserves 1.5MB of it, and a + # low-zoom map tile needs a large contiguous realloc to decode that the + # remainder could not satisfy, so those tiles rendered white. 5MB of the + # 8MB PSRAM leaves comfortable headroom for the decode plus the cache. + -D RAM_SIZE=5120 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE -D LV_COMP_CONF_INCLUDE_SIMPLE From 4a5111d3420de8a09236a132f3ef9f62352dd827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Wed, 15 Jul 2026 17:01:19 +0200 Subject: [PATCH 24/28] indicator: advance the device-ui and protobufs pins to the merged commits Point the protobufs submodule at the merged SD command protos (protobufs #986) so it matches the checked in interdevice sources, and bump the device-ui archive to the current indicator branch tip that carries the SD button and format UI. --- platformio.ini | 2 +- protobufs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 5381a21485f..7826265fd64 100644 --- a/platformio.ini +++ b/platformio.ini @@ -127,7 +127,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/f74a67f2ae97f83d1d36db3fc1dfbe766c0cf5b1.zip + https://github.com/meshtastic/device-ui/archive/e07779ab4433ce5db7d66fa1c9824275d740e1d9.zip ; Common libs for environmental measurements in telemetry module [environmental_base] diff --git a/protobufs b/protobufs index da2fc413931..821d89b2874 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit da2fc413931c81c1d2bc9b6a838d739e2bf35bc1 +Subproject commit 821d89b2874105a788b1d57be706afb428f1f4a6 From cc6c3c23fcad59ec94163e337f809415ccf25969 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Fri, 31 Jul 2026 19:31:22 +0200 Subject: [PATCH 25/28] Update device-ui library dependency URL --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 65e3620f52d..dddffd777ec 100644 --- a/platformio.ini +++ b/platformio.ini @@ -128,7 +128,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/6968b23e5270dccb2fc8531a7a37687131354fa8.zip + https://github.com/meshtastic/device-ui/archive/3d4f718cfb68e2d1052e1d1127b91fd74a753574.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 9af1ec34f43c500cba9e20eac4f96e9ac8218c30 Mon Sep 17 00:00:00 2001 From: mverch67 Date: Fri, 31 Jul 2026 19:36:48 +0200 Subject: [PATCH 26/28] remove cutom sdkconfig --- .../seeed-sensecap-indicator/platformio.ini | 38 +------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini index a7e69af9a91..db27b029497 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini +++ b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini @@ -12,7 +12,6 @@ custom_meshtastic_partition_scheme = 8MB custom_meshtastic_has_mui = true extends = esp32s3_base -board_level = release platform_packages = ; Version needs to match the pioarduino version used in esp32_common.ini platformio/framework-arduinoespressif32 @ https://github.com/mverch67/arduino-esp32#6dde0f3b58e32d9f884b94b4685b0f3a267c4624 ; 3.3.11 @@ -42,42 +41,11 @@ lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 -custom_sdkconfig = - ${esp32s3_base.custom_sdkconfig} - CONFIG_AUTOSTART_ARDUINO=y - CONFIG_LOG_DEFAULT_LEVEL_INFO=y - CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y - CONFIG_SPIRAM_MODE_OCT=y -; CONFIG_SPIRAM_SPEED_120M=y -; CONFIG_SPIRAM_SPEED=120 - CONFIG_SPIRAM_SPEED_80M=y - CONFIG_SPIRAM_SPEED=80 - CONFIG_SPIRAM_XIP_FROM_PSRAM=y - CONFIG_LCD_RGB_ISR_IRAM_SAFE=y - CONFIG_GDMA_CTRL_FUNC_IN_IRAM=y - CONFIG_I2S_ISR_IRAM_SAFE=y - CONFIG_GDMA_ISR_IRAM_SAFE=y - CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y - CONFIG_ESP32S3_DATA_CACHE_64KB=y - CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y - CONFIG_I2C_SKIP_LEGACY_CONFLICT_CHECK=y - CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=0 - CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y - CONFIG_ESPTOOLPY_FLASHSIZE="8MB" -; CONFIG_ESPTOOLPY_FLASHFREQ_120M=y -; CONFIG_ESPTOOLPY_FLASHFREQ="120m" - '# CONFIG_ESPTOOLPY_FLASHFREQ_120M is not set' - CONFIG_ESPTOOLPY_FLASHFREQ_80M=y - CONFIG_ESPTOOLPY_FLASHFREQ="80m" - CONFIG_COMPILER_OPTIMIZATION_PERF=y - CONFIG_BOOTLOADER_LOG_LEVEL_NONE=n - CONFIG_BOOTLOADER_LOG_LEVEL_INFO=y - [env:seeed-sensecap-indicator-tft] extends = env:seeed-sensecap-indicator board_level = pr board_check = true -upload_speed = 460800 +upload_speed = 921600 build_flags = ${env:seeed-sensecap-indicator.build_flags} @@ -86,10 +54,6 @@ build_flags = -D HAS_SCREEN=1 -D HAS_TFT=1 -D DISPLAY_SET_RESOLUTION - # LVGL heap (RAM_SIZE * 1024). The image cache reserves 1.5MB of it, and a - # low-zoom map tile needs a large contiguous realloc to decode that the - # remainder could not satisfy, so those tiles rendered white. 5MB of the - # 8MB PSRAM leaves comfortable headroom for the decode plus the cache. -D RAM_SIZE=5120 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE From d3bb4f0347605e9974e21f22330cd635dc2bb868 Mon Sep 17 00:00:00 2001 From: mverch67 Date: Fri, 31 Jul 2026 21:04:30 +0200 Subject: [PATCH 27/28] remove duplicated synchronisation (after PR11278 is in place) --- src/graphics/tftSetup.cpp | 14 ------------- src/mesh/comms/I2CProxy.cpp | 8 -------- src/mesh/comms/LinkSpiLock.h | 39 ------------------------------------ 3 files changed, 61 deletions(-) delete mode 100644 src/mesh/comms/LinkSpiLock.h diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index e764ad1355b..de4e0e66c87 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -25,7 +25,6 @@ #include "input/I2CKeyboardScanner.h" #include "mesh/IndicatorSerial.h" #include "mesh/comms/I2CProxy.h" -#include "mesh/comms/LinkSpiLock.h" // Serves the UI map tiles from the SD card behind the RP2040, chunk-wise // over the interdevice link. @@ -81,7 +80,6 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; - SpiLockBreak spiFree; Budget budget; do { memset(&result, 0, sizeof(result)); @@ -105,7 +103,6 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; - SpiLockBreak spiFree; Budget budget; bool retried = false; do { @@ -131,7 +128,6 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; - SpiLockBreak spiFree; meshtastic_SdCardInfo result = meshtastic_SdCardInfo_init_zero; // A mount takes up to two seconds. Waiting it out here is worth it (an // empty slot reported once sticks in the UI), but this runs on the UI @@ -172,7 +168,6 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; - SpiLockBreak spiFree; Budget budget; do { memset(&result, 0, sizeof(result)); @@ -191,7 +186,6 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; - SpiLockBreak spiFree; uint32_t offset = 0; while (true) { bool got_page = false; @@ -220,7 +214,6 @@ class IndicatorRemoteFS : public IRemoteFS { if (!sensecapIndicator) return false; - SpiLockBreak spiFree; meshtastic_SdCardInfo state = meshtastic_SdCardInfo_init_zero; return sensecapIndicator->sd_command(command, &state); } @@ -280,18 +273,11 @@ class ReentrantSpiLock : public ISpiLock spiLock->lock(); owner = self; depth = 1; -#ifdef SENSECAP_INDICATOR - // lets the remote FS hand the bus back while it waits on the link - spiLockHolder = self; -#endif } void unlock(void) override { if (--depth == 0) { -#ifdef SENSECAP_INDICATOR - spiLockHolder = nullptr; -#endif owner = ThreadId(); spiLock->unlock(); } diff --git a/src/mesh/comms/I2CProxy.cpp b/src/mesh/comms/I2CProxy.cpp index e8d0308e368..3f9367e6874 100644 --- a/src/mesh/comms/I2CProxy.cpp +++ b/src/mesh/comms/I2CProxy.cpp @@ -2,13 +2,9 @@ #include "I2CProxy.h" #include "../IndicatorSerial.h" -#include "LinkSpiLock.h" I2CProxy *i2cProxy = new I2CProxy(); -// the TFT task publishes itself here while it holds spiLock, see LinkSpiLock.h -volatile TaskHandle_t spiLockHolder = nullptr; - // Transaction state of the calling task. Slots are claimed on first use and // never released: the set of tasks touching the bridged bus is fixed (main // loop, UI task). @@ -123,10 +119,6 @@ uint8_t I2CProxy::transact(Context &c, uint8_t address, size_t rlen) if (c.txLen > MAX_WRITE) return 1; - // the keyboard scanner polls this bus from the LVGL task, which holds the - // SPI lock the radio needs - SpiLockBreak spiFree; - meshtastic_I2CResult result = meshtastic_I2CResult_init_zero; if (!sensecapIndicator->i2c_transact(address, c.txBuf, c.txLen, rlen, &result)) return 5; diff --git a/src/mesh/comms/LinkSpiLock.h b/src/mesh/comms/LinkSpiLock.h deleted file mode 100644 index 34fdf9c6d62..00000000000 --- a/src/mesh/comms/LinkSpiLock.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#ifdef SENSECAP_INDICATOR - -#include "SPILock.h" -#include -#include - -// set by the TFT task while it holds spiLock around the LVGL handler -extern volatile TaskHandle_t spiLockHolder; - -/** - * Hands the SPI bus (shared with the LoRa radio) back for the duration of a - * link round trip. No-op unless the current task holds spiLock. - */ -class SpiLockBreak -{ - public: - SpiLockBreak() - { - held = spiLockHolder == xTaskGetCurrentTaskHandle(); - if (held) { - spiLockHolder = nullptr; - spiLock->unlock(); - } - } - ~SpiLockBreak() - { - if (held) { - spiLock->lock(); - spiLockHolder = xTaskGetCurrentTaskHandle(); - } - } - - private: - bool held; -}; - -#endif // SENSECAP_INDICATOR From 042bb31a439c023a9338cf8235223fa077d24f89 Mon Sep 17 00:00:00 2001 From: mverch67 Date: Fri, 31 Jul 2026 21:09:52 +0200 Subject: [PATCH 28/28] set commit reference to updated RemoteSDService class --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index dddffd777ec..0c8564b6dd5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -128,7 +128,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/3d4f718cfb68e2d1052e1d1127b91fd74a753574.zip + https://github.com/meshtastic/device-ui/archive/34707ae74cbb73242fcba7614c305db901ec131b.zip ; Common libs for environmental measurements in telemetry module [environmental_base]