From 4b4e82bd72335f40b1325adfacb07f54a3b84167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sat, 1 Aug 2026 18:23:30 +0200 Subject: [PATCH] SenseCAP Indicator: RP2040 peripherals for the main firmware (#6220) * 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. * 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. * 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. * indicator: bump device-ui pin to 27e6c0c * 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. * 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. * 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. * 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. * 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. * indicator: regen protos, interdevice protocol version 2 * 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. * 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. * indicator: regen protos, FileStatus back on the original tags * indicator: regen protos, ping/pong carry the InterdeviceVersion enum * indicator: point the protobufs submodule at the merged interdevice protos * indicator: pin device-ui to the branch with the remote SD support * 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. * 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. * indicator: SD mount, eject and format commands over the link * indicator: bound how long a busy card state blocks the UI task * 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. * 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. * 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. * 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. * Update device-ui library dependency URL * remove cutom sdkconfig * remove duplicated synchronisation (after PR11278 is in place) * set commit reference to updated RemoteSDService class * Add board_level configuration for release * fix cppcheck errors --------- Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit Co-authored-by: mverch67 --- src/RedirectablePrint.cpp | 30 +- src/RedirectablePrint.h | 5 +- src/configuration.h | 3 +- src/detect/ScanI2CTwoWire.cpp | 11 +- src/gps/GPS.cpp | 32 +- src/gps/GPS.h | 8 +- src/gps/RTC.cpp | 9 +- src/graphics/tftSetup.cpp | 213 ++++++++ src/input/kbI2cBase.cpp | 10 +- src/main.cpp | 26 +- src/mesh/IndicatorSerial.cpp | 516 ++++++++++++++++++ src/mesh/IndicatorSerial.h | 144 +++++ src/mesh/comms/I2CProxy.cpp | 141 +++++ src/mesh/comms/I2CProxy.h | 84 +++ src/mesh/comms/UARTProxy.cpp | 114 ++++ src/mesh/comms/UARTProxy.h | 74 +++ .../Telemetry/EnvironmentTelemetry.cpp | 8 - .../Telemetry/Sensor/IndicatorSensor.cpp | 166 ------ .../Telemetry/Sensor/IndicatorSensor.h | 19 - src/motion/BMI270Sensor.cpp | 9 +- src/motion/BMM150Sensor.cpp | 9 +- src/motion/ICM20948Sensor.cpp | 10 +- src/serialization/cobs.cpp | 129 ----- src/serialization/cobs.h | 75 --- .../seeed-sensecap-indicator/platformio.ini | 37 +- .../seeed-sensecap-indicator/variant.h | 12 +- 26 files changed, 1414 insertions(+), 480 deletions(-) create mode 100644 src/mesh/IndicatorSerial.cpp create mode 100644 src/mesh/IndicatorSerial.h create mode 100644 src/mesh/comms/I2CProxy.cpp create mode 100644 src/mesh/comms/I2CProxy.h create mode 100644 src/mesh/comms/UARTProxy.cpp create mode 100644 src/mesh/comms/UARTProxy.h delete mode 100644 src/modules/Telemetry/Sensor/IndicatorSensor.cpp delete mode 100644 src/modules/Telemetry/Sensor/IndicatorSensor.h mode change 100755 => 100644 src/motion/ICM20948Sensor.cpp delete mode 100644 src/serialization/cobs.cpp delete mode 100644 src/serialization/cobs.h diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index f6641fdbdc..f0ebbcc208 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,6 +78,21 @@ 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 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); + if (end && (size_t)(end - printBuf) + 1 < len && end[1] == ' ') + tagLen = end - printBuf + 2; + } + 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); @@ -88,7 +103,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,13 +176,8 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, #endif } auto thread = concurrency::OSThread::currentThread; - if (thread) { - 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 @@ -178,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 8535933fc6..216315464a 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/configuration.h b/src/configuration.h index 672d8b6f19..ee5f55cbe2 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -403,7 +403,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 (I2CProxy) +#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 be326f4dbd..3ad42ab042 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/I2CProxy.h" +#endif #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32) #include "meshUtils.h" // vformat @@ -283,7 +286,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 = i2cProxy; // WIRE1 is bridged to the RP2040 +#else i2cBus = &Wire1; +#endif } else { #endif i2cBus = &Wire; @@ -972,7 +979,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 i2cProxy; // 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 389fa60060..b6cd6fbaf0 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) +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) HardwareSerial *GPS::_serial_gps = &GPS_SERIAL_PORT; @@ -1428,6 +1430,21 @@ void GPS::publishUpdate() int32_t GPS::runOnce() { +#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"); @@ -1448,6 +1465,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 +1928,16 @@ std::unique_ptr GPS::createGps() } else return nullptr; #endif +#if defined(SENSECAP_INDICATOR) + // assigned at runtime, static initialization order across translation + // units is undefined + _serial_gps = uartProxy; + if (!_serial_gps) + return nullptr; +#else 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; @@ -1972,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 328a9a316c..9924f73fc1 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/UARTProxy.h" +#endif + // Allow defining the polarity of the ENABLE output. default is active high #ifndef GPS_EN_ACTIVE #define GPS_EN_ACTIVE 1 @@ -216,7 +220,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 UARTProxy *_serial_gps; +#elif defined(ARCH_RP2040) static SerialUART *_serial_gps; #elif defined(ARCH_NRF52) static Uart *_serial_gps; diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 217bbb4c39..1559e5e276 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -1,6 +1,7 @@ #include "gps/RTC.h" #include "configuration.h" #include "detect/ScanI2C.h" +#include "detect/ScanI2CTwoWire.h" #include "main.h" #include "mesh/MeshService.h" #include "modules/NodeInfoModule.h" @@ -102,7 +103,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 @@ -151,7 +152,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 @@ -320,7 +321,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 @@ -345,7 +346,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 0ab868f94e..0526f4a343 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -20,6 +20,213 @@ #include #endif +#ifdef SENSECAP_INDICATOR +#include "graphics/map/RemoteSDService.h" +#include "input/I2CKeyboardScanner.h" +#include "mesh/IndicatorSerial.h" +#include "mesh/comms/I2CProxy.h" + +// Serves the UI map tiles from the SD card behind the RP2040, chunk-wise +// over the interdevice link. +// +// 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 +{ + // 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 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; + + // 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, Budget &budget) + { + if (!answered) { + if (--budget.attempts <= 0) + return false; + return !sensecapIndicator->last_request_nacked(); // refused, not lost + } + if (status == meshtastic_FileStatus_FILE_BUSY) { + if (--budget.busy <= 0) + return false; + delay(BUSY_BACKOFF_MS); + return true; + } + return false; + } + + public: + IndicatorRemoteFS() : result(meshtastic_FileTransfer_init_zero), listing(meshtastic_DirectoryListing_init_zero) {} + + 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; + Budget budget; + do { + memset(&result, 0, sizeof(result)); + 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, budget)) + return false; + } while (true); + } + + bool writeChunk(const char *path, uint32_t offset, const uint8_t *buf, uint32_t len, bool create) override + { + if (!sensecapIndicator) + return false; + Budget budget; + bool retried = false; + do { + memset(&result, 0, sizeof(result)); + 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 (retried && !create && result.status == meshtastic_FileStatus_FILE_OFFSET_CONFLICT && + result.file_size == (uint64_t)offset + len) + return true; + } + if (!retryable(answered, result.status, budget)) + return false; + retried = true; + } while (true); + } + + bool sdInfo(RemoteSdInfo &info) override + { + if (!sensecapIndicator) + return false; + meshtastic_SdCardInfo sdState = 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. + Budget budget; + budget.busy = INFO_BUSY_ATTEMPTS; // a mount, not a whole FAT scan + while (true) { + sdState = meshtastic_SdCardInfo_init_zero; + bool answered = sensecapIndicator->sd_info(&sdState); + if (answered && !sdState.busy) + break; + meshtastic_FileStatus status = answered ? meshtastic_FileStatus_FILE_BUSY : meshtastic_FileStatus_FILE_UNSPECIFIED; + if (!retryable(answered, status, budget)) + return false; + } + info.present = sdState.present; + info.cardType = (uint8_t)sdState.card_type; + info.fatType = (uint8_t)sdState.fat_type; + info.cardSize = sdState.card_size; + info.usedBytes = sdState.used_bytes; + info.freeBytes = sdState.free_bytes; + info.statsValid = sdState.stats_valid; + info.unformatted = sdState.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) + return false; + Budget budget; + do { + memset(&result, 0, sizeof(result)); + 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; + if (!retryable(answered, result.status, budget)) + return false; + } while (true); + } + + bool listDir(const char *path, std::set &entries) override + { + if (!sensecapIndicator) + return false; + uint32_t offset = 0; + while (true) { + bool got_page = false; + 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, budget)) + return false; + } + if (!got_page) + 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: + bool sdCommand(meshtastic_SdCommand command) + { + if (!sensecapIndicator) + return false; + 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; + meshtastic_DirectoryListing listing; +}; +#endif + DeviceScreen *deviceScreen = nullptr; #ifndef TFT_TASK_STACK_SIZE @@ -106,6 +313,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(i2cProxy); +#endif #ifndef ARCH_PORTDUINO deviceScreen = &DeviceScreen::create(reentrantSpiLock); PacketAPI::create(PacketServer::init()); diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 510fb1e31d..88386c5c9d 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/main.cpp b/src/main.cpp index bdac9c2f80..7989778cfe 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,6 +30,11 @@ #include "error.h" #include "gps/RTC.h" +#ifdef SENSECAP_INDICATOR // on the indicator run the additional serial port for the RP2040 +#include "IndicatorSerial.h" +#include "mesh/comms/I2CProxy.h" +#endif + #if !MESHTASTIC_EXCLUDE_I2C #include "detect/ScanI2CConsumer.h" #include "detect/ScanI2CTwoWire.h" @@ -568,7 +573,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 i2cProxy. No local interface to initialize. +#elif defined(I2C_SDA1) && defined(ARCH_RP2040) Wire1.setSDA(I2C_SDA1); Wire1.setSCL(I2C_SCL1); Wire1.begin(); @@ -631,6 +639,20 @@ 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); + // 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 + #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 @@ -639,7 +661,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 0000000000..14f60d9212 --- /dev/null +++ b/src/mesh/IndicatorSerial.cpp @@ -0,0 +1,516 @@ +#ifdef SENSECAP_INDICATOR + +#include "IndicatorSerial.h" +#include "concurrency/LockGuard.h" +#include "mesh/comms/UARTProxy.h" +#include +#include +#include +#include + +SensecapIndicator *sensecapIndicator; + +SensecapIndicator::SensecapIndicator(HardwareSerial &serial) : OSThread("SensecapIndicator") +{ + _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() +{ + // 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 (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(); + // 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); +} + +// 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() +{ + // 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)); + 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() +{ + 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, a nack arrives, or the timeout +// expires +bool SensecapIndicator::wait_response(const bool &flag, uint32_t timeout_ms) +{ + uint32_t start = millis(); + while (!flag) { + pump(); + if (request_nacked) + return false; // the other side could not handle the request + if (!Throttle::isWithinTimespanMs(start, timeout_ms)) { + link_timeouts++; + 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; + 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; +} + +// 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 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) +{ + 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_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(msg)) + 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) +{ + 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) +{ + 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_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) +{ + 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_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) +{ + 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_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) +{ + 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_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) +{ + 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_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::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); + concurrency::LockGuard guard(&link_lock); + uint32_t start = millis(); + 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 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 (handshake_done) + break; + if (!Throttle::isWithinTimespanMs(start, timeout_ms)) + return false; + delay(1); + } + return link_compatible; +} + +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); +} + +// 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 - MT_HEADER_SIZE); + 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) +{ + 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); +} + +// 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) +{ + 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; +} + +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); + 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; + continue; + } + + if (payload_len + MT_HEADER_SIZE > pb_rx_size) + return; // frame not complete yet + + 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); + 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) { + link_decode_fail++; + LOG_DEBUG("Decoding failed"); + return false; + } + packets_received++; + switch (message.which_data) { + case meshtastic_InterdeviceMessage_nmea_tag: + // send String to NMEA processing + 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 + if (message.id == expected_id) { + i2c_result = message.data.i2c_result; + i2c_result_ready = true; + } else { + LOG_DEBUG("Drop stale i2c response id=0x%08x", 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=0x%08x", 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=0x%08x", message.id); + } + return true; + case meshtastic_InterdeviceMessage_ping_tag: { + // 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; + pong.which_data = meshtastic_InterdeviceMessage_pong_tag; + pong.data.pong = meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT; + send_uplink_unlocked(pong); + return 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 + // 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 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"); + } + 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=0x%08x", 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 diff --git a/src/mesh/IndicatorSerial.h b/src/mesh/IndicatorSerial.h new file mode 100644 index 0000000000..5c9f6379bd --- /dev/null +++ b/src/mesh/IndicatorSerial.h @@ -0,0 +1,144 @@ +#pragma once + +#ifdef SENSECAP_INDICATOR + +#include "concurrency/Lock.h" +#include "concurrency/OSThread.h" +#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 +#define MT_MAGIC_1 0xc3 + +// The header is the magic number plus a 16-bit payload-length field +#define MT_HEADER_SIZE 4 + +#define PB_BUFSIZE (meshtastic_InterdeviceMessage_size + MT_HEADER_SIZE) + +class SensecapIndicator : public concurrency::OSThread +{ + public: + explicit SensecapIndicator(HardwareSerial &serial); + int32_t runOnce() override; + // 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); + + // 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; 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) + 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) + // 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); + // 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 + 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 + // 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: + // 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 = nullptr; + uint32_t packets_received = 0; + // 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; + // 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 + // 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_init_zero; + meshtastic_InterdeviceMessage tx_message = meshtastic_InterdeviceMessage_init_zero; + // 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; + // a nack response fails the request in flight without its timeout + bool request_nacked = 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 { + std::atomic &count; + explicit InFlight(std::atomic &c) : count(c) { count++; } + ~InFlight() { count--; } + }; + bool link_ready(); + 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) + bool file_request(meshtastic_InterdeviceMessage &request, meshtastic_FileTransfer *out, uint32_t timeout_ms); + bool wait_response(const 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 diff --git a/src/mesh/comms/I2CProxy.cpp b/src/mesh/comms/I2CProxy.cpp new file mode 100644 index 0000000000..37fd652e73 --- /dev/null +++ b/src/mesh/comms/I2CProxy.cpp @@ -0,0 +1,141 @@ +#ifdef SENSECAP_INDICATOR + +#include "I2CProxy.h" +#include "../IndicatorSerial.h" + +I2CProxy *i2cProxy = new I2CProxy(); + +// 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() +{ + TaskHandle_t self = xTaskGetCurrentTaskHandle(); + for (size_t i = 0; i < MAX_TASKS; i++) { + if (_ctx[i].task == self) + return _ctx[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; + 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]; +} + +void I2CProxy::beginTransmission(uint8_t address) +{ + Context &c = ctx(); + c.txAddress = address; + c.txLen = 0; + c.txPending = true; +} + +size_t I2CProxy::write(uint8_t data) +{ + Context &c = ctx(); + if (!c.txPending || c.txLen >= MAX_WRITE) + return 0; + c.txBuf[c.txLen++] = data; + return 1; +} + +size_t I2CProxy::write(const uint8_t *data, size_t len) +{ + size_t n = 0; + while (n < len && write(data[n])) + n++; + return n; +} + +uint8_t I2CProxy::endTransmission(bool stopBit) +{ + Context &c = ctx(); + 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(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; + + // 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; + + uint8_t rv = transact(c, address, len); + c.txLen = 0; + c.txPending = false; + return rv == 0 ? c.rxLen : 0; +} + +int I2CProxy::available() +{ + const Context &c = ctx(); + return (int)(c.rxLen - c.rxPos); +} + +int I2CProxy::read() +{ + Context &c = ctx(); + return c.rxPos < c.rxLen ? c.rxBuf[c.rxPos++] : -1; +} + +int I2CProxy::peek() +{ + const 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(Context &c, uint8_t address, size_t rlen) +{ + c.rxLen = 0; + c.rxPos = 0; + + if (!sensecapIndicator) + return 4; + if (c.txLen > MAX_WRITE) + return 1; + + meshtastic_I2CResult result = meshtastic_I2CResult_init_zero; + if (!sensecapIndicator->i2c_transact(address, c.txBuf, c.txLen, rlen, &result)) + return 5; + + switch (result.status) { + case meshtastic_I2CResult_Status_OK: + 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; + } +} + +#endif // SENSECAP_INDICATOR diff --git a/src/mesh/comms/I2CProxy.h b/src/mesh/comms/I2CProxy.h new file mode 100644 index 0000000000..d66fe144a5 --- /dev/null +++ b/src/mesh/comms/I2CProxy.h @@ -0,0 +1,84 @@ +#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). + * + * Thread safety: the bus is shared between the main loop (sensor drivers) + * 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 +{ + public: + I2CProxy() : TwoWire(0), _ctx{} {} + + 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); + + // main loop, UI task, and one spare + static const size_t MAX_TASKS = 3; + + 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]; + portMUX_TYPE _claim_mux = portMUX_INITIALIZER_UNLOCKED; + + // per-task transaction state, claimed on first use + Context &ctx(); + uint8_t transact(Context &c, uint8_t address, size_t rlen); +}; + +extern I2CProxy *i2cProxy; + +#endif // SENSECAP_INDICATOR diff --git a/src/mesh/comms/UARTProxy.cpp b/src/mesh/comms/UARTProxy.cpp new file mode 100644 index 0000000000..34170ec41b --- /dev/null +++ b/src/mesh/comms/UARTProxy.cpp @@ -0,0 +1,114 @@ +#include "UARTProxy.h" + +#ifdef SENSECAP_INDICATOR + +UARTProxy *uartProxy = new UARTProxy(); + +UARTProxy::UARTProxy() {} + +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("UARTProxy::begin(%lu)", baud); +} + +void UARTProxy::end() +{ + buf_clear(); +} + +int UARTProxy::available() +{ + return buf_avail(); +} + +int UARTProxy::peek() +{ + if (buf_tail == buf_head) + return -1; + __sync_synchronize(); // pair with the producer barrier in buf_push + return buf[buf_tail]; +} + +int UARTProxy::read() +{ + if (buf_tail == buf_head) + 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; +} + +void UARTProxy::flush(bool txOnly) +{ + // 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() +{ + return baudrate; +} + +void UARTProxy::updateBaudRate(unsigned long speed) +{ + baudrate = speed; +} + +size_t UARTProxy::setRxBufferSize(size_t size) +{ + return size; +} + +size_t UARTProxy::write(const char *buffer) +{ + return write((char *)buffer, strlen(buffer)); +} + +size_t UARTProxy::write(uint8_t *buffer, size_t size) +{ + return write((char *)buffer, 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; + 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("UARTProxy::write %u bytes", (unsigned)size); + // 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; +} + +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++) { + if (!buf_push(buffer[i])) { + return i; + } + } + return size; +} + +#endif // SENSECAP_INDICATOR diff --git a/src/mesh/comms/UARTProxy.h b/src/mesh/comms/UARTProxy.h new file mode 100644 index 0000000000..292a8965ad --- /dev/null +++ b/src/mesh/comms/UARTProxy.h @@ -0,0 +1,74 @@ +#pragma once + +#ifdef SENSECAP_INDICATOR + +#include "../IndicatorSerial.h" +#include +#include + +/** + * 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 +{ + public: + 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); + void end(); + int available(); + int peek(); + int read(); + // 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) + 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); + size_t write(uint8_t c) override { 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; + // 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; + } + void buf_clear() { buf_tail = buf_head; } + size_t buf_avail() { return (buf_head + BUF_SIZE - buf_tail) % BUF_SIZE; } +}; + +extern UARTProxy *uartProxy; + +#endif // SENSECAP_INDICATOR diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 659ed32781..b343b7b462 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 26a4bc5fc5..0000000000 --- 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 22a0d9c833..0000000000 --- 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/motion/BMI270Sensor.cpp b/src/motion/BMI270Sensor.cpp index 9f3911956a..79e3d358b0 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 4f4bf26c0d..299770ede8 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 old mode 100755 new mode 100644 index e0fc2c5a88..8238b3dbc9 --- 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); diff --git a/src/serialization/cobs.cpp b/src/serialization/cobs.cpp deleted file mode 100644 index afb868f50f..0000000000 --- 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 f95e61f626..0000000000 --- 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/platformio.ini b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini index 4c5d4c4d60..6850943cf7 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini +++ b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini @@ -12,12 +12,12 @@ 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 board = seeed-sensecap-indicator +board_level = release board_check = true board_build.partitions = partition-table-8MB.csv upload_protocol = esptool @@ -42,42 +42,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,7 +55,7 @@ build_flags = -D HAS_SCREEN=1 -D HAS_TFT=1 -D DISPLAY_SET_RESOLUTION - -D RAM_SIZE=3072 + -D RAM_SIZE=5120 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE -D LV_COMP_CONF_INCLUDE_SIMPLE diff --git a/variants/esp32s3/seeed-sensecap-indicator/variant.h b/variants/esp32s3/seeed-sensecap-indicator/variant.h index b59e6e059a..61505a24b4 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