diff --git a/platformio.ini b/platformio.ini index 2fea569674a..65e3620f52d 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/ef573c368767625ffbe8c32cf921ea7366f2dd53.zip + https://github.com/meshtastic/device-ui/archive/6968b23e5270dccb2fc8531a7a37687131354fa8.zip ; Common libs for environmental measurements in telemetry module [environmental_base] diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index e53806ac77a..4603eb2617b 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 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/configuration.h b/src/configuration.h index 672d8b6f190..ee5f55cbe29 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 c543aac856e..b12624aa784 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; @@ -968,7 +975,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 389fa600606..b6cd6fbaf00 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 328a9a316c3..9924f73fc18 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 94288529e16..c741e8d1c4a 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 "modules/NodeInfoModule.h" #include @@ -97,7 +98,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 @@ -146,7 +147,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 @@ -315,7 +316,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 @@ -340,7 +341,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 708cd896747..2ccff034a30 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -14,6 +14,218 @@ #include #endif +#ifdef SENSECAP_INDICATOR +#include "graphics/map/RemoteSDService.h" +#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. +// +// 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: + 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; + SpiLockBreak spiFree; + 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; + SpiLockBreak spiFree; + 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; + 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. + 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; + meshtastic_FileStatus status = answered ? meshtastic_FileStatus_FILE_BUSY : meshtastic_FileStatus_FILE_UNSPECIFIED; + if (!retryable(answered, status, budget)) + 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; + 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) + return false; + SpiLockBreak spiFree; + 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; + SpiLockBreak spiFree; + 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; + 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; + meshtastic_DirectoryListing listing; +}; +#endif + DeviceScreen *deviceScreen = nullptr; #ifndef TFT_TASK_STACK_SIZE @@ -32,7 +244,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(); } @@ -40,6 +259,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(); PacketAPI::create(PacketServer::init()); 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/main.cpp b/src/main.cpp index 77beb839631..4c7f95ff8bc 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" @@ -565,7 +570,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(); @@ -628,6 +636,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 @@ -636,7 +658,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..9afa39b0924 --- /dev/null +++ b/src/mesh/IndicatorSerial.cpp @@ -0,0 +1,518 @@ +#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(bool &flag, uint32_t timeout_ms) +{ + uint32_t start = millis(); + while (!flag) { + pump(); + if (flag) + break; + 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 00000000000..c357d6ec256 --- /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: + 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 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; + // 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(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 00000000000..e8d0308e368 --- /dev/null +++ b/src/mesh/comms/I2CProxy.cpp @@ -0,0 +1,149 @@ +#ifdef SENSECAP_INDICATOR + +#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). +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() +{ + 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() +{ + 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; + + // 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; + + 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 00000000000..3dde7cc18cb --- /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) {} + + 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/LinkSpiLock.h b/src/mesh/comms/LinkSpiLock.h new file mode 100644 index 00000000000..34fdf9c6d62 --- /dev/null +++ b/src/mesh/comms/LinkSpiLock.h @@ -0,0 +1,39 @@ +#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 diff --git a/src/mesh/comms/UARTProxy.cpp b/src/mesh/comms/UARTProxy.cpp new file mode 100644 index 00000000000..34170ec41b2 --- /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 00000000000..1171bec1e7a --- /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/mesh/generated/meshtastic/interdevice.pb.cpp b/src/mesh/generated/meshtastic/interdevice.pb.cpp index bc795b2a7ea..d17d62d4404 100644 --- a/src/mesh/generated/meshtastic/interdevice.pb.cpp +++ b/src/mesh/generated/meshtastic/interdevice.pb.cpp @@ -1,5 +1,5 @@ /* 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 diff --git a/src/mesh/generated/meshtastic/interdevice.pb.h b/src/mesh/generated/meshtastic/interdevice.pb.h index 92a1670ee31..e955a9f364f 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 diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 659ed327817..b343b7b462f 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/motion/BMI270Sensor.cpp b/src/motion/BMI270Sensor.cpp index 9f3911956a1..79e3d358b01 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 4f4bf26c0db..299770ede82 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 e0fc2c5a880..8238b3dbc92 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); 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/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 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