diff --git a/.vscode/settings.json b/.vscode/settings.json index 07e198f0a7..bf9b82111d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ "trunk.enableWindows": true, "files.insertFinalNewline": false, "files.trimFinalNewlines": false, - "cmake.configureOnOpen": false + "cmake.configureOnOpen": false, + "[cpp]": { + "editor.defaultFormatter": "trunk.io" + } } diff --git a/boards/wiscore_rak4631.json b/boards/wiscore_rak4631.json index 6dec3f7cb4..c783f33a69 100644 --- a/boards/wiscore_rak4631.json +++ b/boards/wiscore_rak4631.json @@ -35,7 +35,7 @@ "svd_path": "nrf52840.svd", "openocd_target": "nrf52840-mdk-rs" }, - "frameworks": ["arduino"], + "frameworks": ["arduino", "freertos"], "name": "WisCore RAK4631 Board", "upload": { "maximum_ram_size": 248832, diff --git a/platformio.ini b/platformio.ini index 23ff53a102..bcdcc0034b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -77,10 +77,11 @@ build_flags = -Wno-missing-field-initializers -DMESHTASTIC_EXCLUDE_DROPZONE=1 monitor_speed = 115200 +monitor_filters = direct lib_deps = jgromes/RadioLib@~6.6.0 - https://github.com/meshtastic/esp8266-oled-ssd1306.git#2b40affbe7f7dc63b6c00fa88e7e12ed1f8e1719 ; ESP8266_SSD1306 + https://github.com/meshtastic/esp8266-oled-ssd1306.git#e16cee124fe26490cb14880c679321ad8ac89c95 ; ESP8266_SSD1306 mathertel/OneButton@^2.5.0 ; OneButton library for non-blocking button debounce https://github.com/meshtastic/arduino-fsm.git#7db3702bf0cfe97b783d6c72595e3f38e0b19159 https://github.com/meshtastic/TinyGPSPlus.git#71a82db35f3b973440044c476d4bcdc673b104f4 @@ -150,5 +151,4 @@ lib_deps = mprograms/QMC5883LCompass@^1.2.0 - https://github.com/meshtastic/DFRobot_LarkWeatherStation#dee914270dc7cb3e43fbf034edd85a63a16a12ee - + https://github.com/meshtastic/DFRobot_LarkWeatherStation#dee914270dc7cb3e43fbf034edd85a63a16a12ee \ No newline at end of file diff --git a/protobufs b/protobufs index 4da558d0f7..1198b7dbab 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 4da558d0f73c46ef91b74431facee73c09affbfc +Subproject commit 1198b7dbabf9768cb0143d2897707b4c7a51a5da diff --git a/pyocd.yaml b/pyocd.yaml new file mode 100644 index 0000000000..84bd9336b9 --- /dev/null +++ b/pyocd.yaml @@ -0,0 +1,7 @@ +# This is a config file to control pyocd ICE debugger probe options (only used for NRF52 targets with hardware debugging connections) +# for more info see FIXMEURL + +# console or telnet +semihost_console_type: telnet +enable_semihosting: True +telnet_port: 4444 diff --git a/src/AccelerometerThread.h b/src/AccelerometerThread.h index f45511cca3..c2910007e3 100644 --- a/src/AccelerometerThread.h +++ b/src/AccelerometerThread.h @@ -16,6 +16,8 @@ #include #ifdef RAK_4631 #include "Fusion/Fusion.h" +#include "graphics/Screen.h" +#include "graphics/ScreenFonts.h" #include #endif @@ -101,7 +103,11 @@ class AccelerometerThread : public concurrency::OSThread bmx160.getAllData(&magAccel, NULL, &gAccel); // expirimental calibrate routine. Limited to between 10 and 30 seconds after boot - if (millis() > 10 * 1000 && millis() < 30 * 1000) { + if (millis() > 12 * 1000 && millis() < 30 * 1000) { + if (!showingScreen) { + showingScreen = true; + screen->startAlert((FrameCallback)drawFrameCalibration); + } if (magAccel.x > highestX) highestX = magAccel.x; if (magAccel.x < lowestX) @@ -114,6 +120,9 @@ class AccelerometerThread : public concurrency::OSThread highestZ = magAccel.z; if (magAccel.z < lowestZ) lowestZ = magAccel.z; + } else if (showingScreen && millis() >= 30 * 1000) { + showingScreen = false; + screen->endAlert(); } int highestRealX = highestX - (highestX + lowestX) / 2; @@ -255,11 +264,34 @@ class AccelerometerThread : public concurrency::OSThread Adafruit_LIS3DH lis; Adafruit_LSM6DS3TRC lsm; SensorBMA423 bmaSensor; + bool BMA_IRQ = false; #ifdef RAK_4631 + bool showingScreen = false; RAK_BMX160 bmx160; float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; + + static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) + { + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 32; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_MEDIUM); + display->drawString(x, y, "Calibrating\nCompass"); + int16_t compassX = 0, compassY = 0; + uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight()); + + // coordinates for the center of the compass/circle + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + compassX = x + display->getWidth() - compassDiam / 2 - 5; + compassY = y + display->getHeight() / 2; + } else { + compassX = x + display->getWidth() - compassDiam / 2 - 5; + compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; + } + display->drawCircle(compassX, compassY, compassDiam / 2); + screen->drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180); + } #endif - bool BMA_IRQ = false; }; #endif \ No newline at end of file diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index 4b3bb3fbc5..1b85166d21 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -181,8 +181,9 @@ int32_t ButtonThread::runOnce() case BUTTON_EVENT_LONG_PRESSED: { LOG_BUTTON("Long press!\n"); powerFSM.trigger(EVENT_PRESS); - if (screen) - screen->startShutdownScreen(); + if (screen) { + screen->startAlert("Shutting down..."); + } playBeep(); break; } @@ -322,4 +323,4 @@ void ButtonThread::userButtonPressedLongStop() if (millis() > c_holdOffTime) { btnEvent = BUTTON_EVENT_LONG_RELEASED; } -} +} \ No newline at end of file diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h index cd8301274c..ebe9da8d44 100644 --- a/src/DebugConfiguration.h +++ b/src/DebugConfiguration.h @@ -26,6 +26,14 @@ #include "SerialConsole.h" +// If defined we will include support for ARM ICE "semihosting" for a virtual +// console over the JTAG port (to replace the normal serial port) +// Note: Normally this flag is passed into the gcc commandline by platformio.ini. +// for an example see env:rak4631_dap. +// #ifndef USE_SEMIHOSTING +// #define USE_SEMIHOSTING +// #endif + #define DEBUG_PORT (*console) // Serial debug port #ifdef USE_SEGGER diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 96aad1a9a4..7d3788c4d9 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -84,6 +84,58 @@ bool renameFile(const char *pathFrom, const char *pathTo) #endif } +#include + +/** + * @brief Get the list of files in a directory. + * + * This function returns a list of files in a directory. The list includes the full path of each file. + * + * @param dirname The name of the directory. + * @param levels The number of levels of subdirectories to list. + * @return A vector of strings containing the full path of each file in the directory. + */ +std::vector getFiles(const char *dirname, uint8_t levels) +{ + std::vector filenames = {}; +#ifdef FSCom + File root = FSCom.open(dirname, FILE_O_READ); + if (!root) + return filenames; + if (!root.isDirectory()) + return filenames; + + File file = root.openNextFile(); + while (file) { + if (file.isDirectory() && !String(file.name()).endsWith(".")) { + if (levels) { +#ifdef ARCH_ESP32 + std::vector subDirFilenames = getFiles(file.path(), levels - 1); +#else + std::vector subDirFilenames = getFiles(file.name(), levels - 1); +#endif + filenames.insert(filenames.end(), subDirFilenames.begin(), subDirFilenames.end()); + file.close(); + } + } else { + meshtastic_FileInfo fileInfo = {"", file.size()}; +#ifdef ARCH_ESP32 + strcpy(fileInfo.file_name, file.path()); +#else + strcpy(fileInfo.file_name, file.name()); +#endif + if (!String(fileInfo.file_name).endsWith(".")) { + filenames.push_back(fileInfo); + } + file.close(); + } + file = root.openNextFile(); + } + root.close(); +#endif + return filenames; +} + /** * Lists the contents of a directory. * diff --git a/src/FSCommon.h b/src/FSCommon.h index ef1d3e4c17..8fbabd9526 100644 --- a/src/FSCommon.h +++ b/src/FSCommon.h @@ -1,6 +1,7 @@ #pragma once #include "configuration.h" +#include // Cross platform filesystem API @@ -49,6 +50,7 @@ using namespace Adafruit_LittleFS_Namespace; void fsInit(); bool copyFile(const char *from, const char *to); bool renameFile(const char *pathFrom, const char *pathTo); +std::vector getFiles(const char *dirname, uint8_t levels); void listDir(const char *dirname, uint8_t levels, bool del); void rmDir(const char *dirname); void setupSDCard(); \ No newline at end of file diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index f27a8950e4..4949c86570 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -15,11 +15,6 @@ #include "platform/portduino/PortduinoGlue.h" #endif -/** - * A printer that doesn't go anywhere - */ -NoopPrint noopPrint; - #if HAS_NETWORKING extern Syslog syslog; #endif @@ -39,7 +34,7 @@ void RedirectablePrint::setDestination(Print *_dest) size_t RedirectablePrint::write(uint8_t c) { // Always send the characters to our segger JTAG debugger -#ifdef SEGGER_STDOUT_CH +#ifdef USE_SEGGER SEGGER_RTT_PutChar(SEGGER_STDOUT_CH, c); #endif @@ -50,7 +45,7 @@ size_t RedirectablePrint::write(uint8_t c) // serial port said (which could be zero) } -size_t RedirectablePrint::vprintf(const char *format, va_list arg) +size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg) { va_list copy; static char printBuf[160]; @@ -66,143 +61,181 @@ size_t RedirectablePrint::vprintf(const char *format, va_list arg) len = sizeof(printBuf) - 1; printBuf[sizeof(printBuf) - 2] = '\n'; } - + for (size_t f = 0; f < len; f++) { + if (!std::isprint(static_cast(printBuf[f])) && printBuf[f] != '\n') + printBuf[f] = '#'; + } + if (logLevel != nullptr) { + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) + Print::write("\u001b[34m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) + Print::write("\u001b[32m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) + Print::write("\u001b[33m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) + Print::write("\u001b[31m", 6); + } len = Print::write(printBuf, len); + Print::write("\u001b[0m", 5); return len; } -size_t RedirectablePrint::log(const char *logLevel, const char *format, ...) +void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, va_list arg) { -#ifdef ARCH_PORTDUINO - if (settingsMap[logoutputlevel] < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) - return 0; - else if (settingsMap[logoutputlevel] < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) - return 0; - else if (settingsMap[logoutputlevel] < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) - return 0; -#endif - if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { - return 0; - } size_t r = 0; -#ifdef HAS_FREE_RTOS - if (inDebugPrint != nullptr && xSemaphoreTake(inDebugPrint, portMAX_DELAY) == pdTRUE) { -#else - if (!inDebugPrint) { - inDebugPrint = true; -#endif - va_list arg; - va_start(arg, format); - - // Cope with 0 len format strings, but look for new line terminator - bool hasNewline = *format && format[strlen(format) - 1] == '\n'; + // Cope with 0 len format strings, but look for new line terminator + bool hasNewline = *format && format[strlen(format) - 1] == '\n'; - // If we are the first message on a report, include the header - if (!isContinuationMessage) { - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // display local time on logfile - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - // hms += tz.tz_dsttime * SEC_PER_HOUR; - // hms -= tz.tz_minuteswest * SEC_PER_MIN; - // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + // If we are the first message on a report, include the header + if (!isContinuationMessage) { + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) + Print::write("\u001b[34m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) + Print::write("\u001b[32m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) + Print::write("\u001b[33m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) + Print::write("\u001b[31m", 6); + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // display local time on logfile + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + // hms += tz.tz_dsttime * SEC_PER_HOUR; + // hms -= tz.tz_minuteswest * SEC_PER_MIN; + // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + // Tear apart hms into h:m:s + int hour = hms / SEC_PER_HOUR; + int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN #ifdef ARCH_PORTDUINO - r += ::printf("%s | %02d:%02d:%02d %u ", logLevel, hour, min, sec, millis() / 1000); + ::printf("%s \u001b[0m| %02d:%02d:%02d %u ", logLevel, hour, min, sec, millis() / 1000); #else - r += printf("%s | %02d:%02d:%02d %u ", logLevel, hour, min, sec, millis() / 1000); + printf("%s \u001b[0m| %02d:%02d:%02d %u ", logLevel, hour, min, sec, millis() / 1000); #endif - } else + } else #ifdef ARCH_PORTDUINO - r += ::printf("%s | ??:??:?? %u ", logLevel, millis() / 1000); + ::printf("%s \u001b[0m| ??:??:?? %u ", logLevel, millis() / 1000); #else - r += printf("%s | ??:??:?? %u ", logLevel, millis() / 1000); + printf("%s \u001b[0m| ??:??:?? %u ", logLevel, millis() / 1000); #endif - auto thread = concurrency::OSThread::currentThread; - if (thread) { - print("["); - // printf("%p ", thread); - // assert(thread->ThreadName.length()); - print(thread->ThreadName); - print("] "); - } + auto thread = concurrency::OSThread::currentThread; + if (thread) { + print("["); + // printf("%p ", thread); + // assert(thread->ThreadName.length()); + print(thread->ThreadName); + print("] "); } - r += vprintf(format, arg); + } + r += vprintf(logLevel, format, arg); + isContinuationMessage = !hasNewline; +} + +void RedirectablePrint::log_to_syslog(const char *logLevel, const char *format, va_list arg) +{ #if HAS_NETWORKING && !defined(ARCH_PORTDUINO) - // if syslog is in use, collect the log messages and send them to syslog - if (syslog.isEnabled()) { - int ll = 0; - switch (logLevel[0]) { - case 'D': - ll = SYSLOG_DEBUG; - break; - case 'I': - ll = SYSLOG_INFO; - break; - case 'W': - ll = SYSLOG_WARN; - break; - case 'E': - ll = SYSLOG_ERR; - break; - case 'C': - ll = SYSLOG_CRIT; - break; - default: - ll = 0; - } - auto thread = concurrency::OSThread::currentThread; - if (thread) { - syslog.vlogf(ll, thread->ThreadName.c_str(), format, arg); - } else { - syslog.vlogf(ll, format, arg); - } + // if syslog is in use, collect the log messages and send them to syslog + if (syslog.isEnabled()) { + int ll = 0; + switch (logLevel[0]) { + case 'D': + ll = SYSLOG_DEBUG; + break; + case 'I': + ll = SYSLOG_INFO; + break; + case 'W': + ll = SYSLOG_WARN; + break; + case 'E': + ll = SYSLOG_ERR; + break; + case 'C': + ll = SYSLOG_CRIT; + break; + default: + ll = 0; + } + auto thread = concurrency::OSThread::currentThread; + if (thread) { + syslog.vlogf(ll, thread->ThreadName.c_str(), format, arg); + } else { + syslog.vlogf(ll, format, arg); } + } #endif +} - isContinuationMessage = !hasNewline; - - if (config.bluetooth.device_logging_enabled && !pauseBluetoothLogging) { - bool isBleConnected = false; +void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_list arg) +{ + if (config.bluetooth.device_logging_enabled && !pauseBluetoothLogging) { + bool isBleConnected = false; #ifdef ARCH_ESP32 - isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected(); + isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected(); #elif defined(ARCH_NRF52) - isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected(); + isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected(); #endif - if (isBleConnected) { - char *message; - size_t initialLen; - size_t len; - initialLen = strlen(format); - message = new char[initialLen + 1]; - len = vsnprintf(message, initialLen + 1, format, arg); - if (len > initialLen) { - delete[] message; - message = new char[len + 1]; - vsnprintf(message, len + 1, format, arg); - } - auto thread = concurrency::OSThread::currentThread; + if (isBleConnected) { + char *message; + size_t initialLen; + size_t len; + initialLen = strlen(format); + message = new char[initialLen + 1]; + len = vsnprintf(message, initialLen + 1, format, arg); + if (len > initialLen) { + delete[] message; + message = new char[len + 1]; + vsnprintf(message, len + 1, format, arg); + } + auto thread = concurrency::OSThread::currentThread; #ifdef ARCH_ESP32 - if (thread) - nimbleBluetooth->sendLog(mt_sprintf("%s | [%s] %s", logLevel, thread->ThreadName.c_str(), message).c_str()); - else - nimbleBluetooth->sendLog(mt_sprintf("%s | %s", logLevel, message).c_str()); + if (thread) + nimbleBluetooth->sendLog(mt_sprintf("%s | [%s] %s", logLevel, thread->ThreadName.c_str(), message).c_str()); + else + nimbleBluetooth->sendLog(mt_sprintf("%s | %s", logLevel, message).c_str()); #elif defined(ARCH_NRF52) - if (thread) - nrf52Bluetooth->sendLog(mt_sprintf("%s | [%s] %s", logLevel, thread->ThreadName.c_str(), message).c_str()); - else - nrf52Bluetooth->sendLog(mt_sprintf("%s | %s", logLevel, message).c_str()); + if (thread) + nrf52Bluetooth->sendLog(mt_sprintf("%s | [%s] %s", logLevel, thread->ThreadName.c_str(), message).c_str()); + else + nrf52Bluetooth->sendLog(mt_sprintf("%s | %s", logLevel, message).c_str()); #endif - delete[] message; - } + delete[] message; } + } +} + +void RedirectablePrint::log(const char *logLevel, const char *format, ...) +{ +#ifdef ARCH_PORTDUINO + if (settingsMap[logoutputlevel] < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) + return; + else if (settingsMap[logoutputlevel] < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) + return; + else if (settingsMap[logoutputlevel] < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) + return; +#endif + if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { + return; + } + +#ifdef HAS_FREE_RTOS + if (inDebugPrint != nullptr && xSemaphoreTake(inDebugPrint, portMAX_DELAY) == pdTRUE) { +#else + if (!inDebugPrint) { + inDebugPrint = true; +#endif + + va_list arg; + va_start(arg, format); + + log_to_serial(logLevel, format, arg); + log_to_syslog(logLevel, format, arg); + log_to_ble(logLevel, format, arg); + va_end(arg); #ifdef HAS_FREE_RTOS xSemaphoreGive(inDebugPrint); @@ -211,7 +244,7 @@ size_t RedirectablePrint::log(const char *logLevel, const char *format, ...) #endif } - return r; + return; } void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16_t len) diff --git a/src/RedirectablePrint.h b/src/RedirectablePrint.h index 31cc1b6ef7..3f20c894cc 100644 --- a/src/RedirectablePrint.h +++ b/src/RedirectablePrint.h @@ -41,23 +41,20 @@ class RedirectablePrint : public Print * log message. Otherwise we assume more prints will come before the log message ends. This * allows you to call logDebug a few times to build up a single log message line if you wish. */ - size_t log(const char *logLevel, const char *format, ...) __attribute__((format(printf, 3, 4))); + void log(const char *logLevel, const char *format, ...) __attribute__((format(printf, 3, 4))); /** like printf but va_list based */ - size_t vprintf(const char *format, va_list arg); + size_t vprintf(const char *logLevel, const char *format, va_list arg); void hexDump(const char *logLevel, unsigned char *buf, uint16_t len); std::string mt_sprintf(const std::string fmt_str, ...); -}; -class NoopPrint : public Print -{ - public: - virtual size_t write(uint8_t c) { return 1; } -}; + protected: + /// Subclasses can override if they need to change how we format over the serial port + virtual void log_to_serial(const char *logLevel, const char *format, va_list arg); -/** - * A printer that doesn't go anywhere - */ -extern NoopPrint noopPrint; \ No newline at end of file + private: + void log_to_syslog(const char *logLevel, const char *format, va_list arg); + void log_to_ble(const char *logLevel, const char *format, va_list arg); +}; \ No newline at end of file diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index 53ece0fa3d..12b9d2bd06 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -24,7 +24,7 @@ void consolePrintf(const char *format, ...) { va_list arg; va_start(arg, format); - console->vprintf(format, arg); + console->vprintf(nullptr, format, arg); va_end(arg); console->flush(); } @@ -34,7 +34,6 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con assert(!console); console = this; canWrite = false; // We don't send packets to our port until it has talked to us first - // setDestination(&noopPrint); for testing, try turning off 'all' debug output and see what leaks #ifdef RP2040_SLOW_CLOCK Port.setTX(SERIAL2_TX); @@ -81,13 +80,40 @@ bool SerialConsole::handleToRadio(const uint8_t *buf, size_t len) { // only talk to the API once the configuration has been loaded and we're sure the serial port is not disabled. if (config.has_lora && config.device.serial_enabled) { - // Turn off debug serial printing once the API is activated, because other threads could print and corrupt packets - if (!config.device.debug_log_enabled) - setDestination(&noopPrint); + // Switch to protobufs for log messages + usingProtobufs = true; canWrite = true; return StreamAPI::handleToRadio(buf, len); } else { return false; } +} + +void SerialConsole::log_to_serial(const char *logLevel, const char *format, va_list arg) +{ + if (usingProtobufs) { + meshtastic_LogRecord_Level ll = meshtastic_LogRecord_Level_UNSET; // default to unset + switch (logLevel[0]) { + case 'D': + ll = meshtastic_LogRecord_Level_DEBUG; + break; + case 'I': + ll = meshtastic_LogRecord_Level_INFO; + break; + case 'W': + ll = meshtastic_LogRecord_Level_WARNING; + break; + case 'E': + ll = meshtastic_LogRecord_Level_ERROR; + break; + case 'C': + ll = meshtastic_LogRecord_Level_CRITICAL; + break; + } + + auto thread = concurrency::OSThread::currentThread; + emitLogRecord(ll, thread ? thread->ThreadName.c_str() : "", format, arg); + } else + RedirectablePrint::log_to_serial(logLevel, format, arg); } \ No newline at end of file diff --git a/src/SerialConsole.h b/src/SerialConsole.h index f8891ba14f..f1e636c9de 100644 --- a/src/SerialConsole.h +++ b/src/SerialConsole.h @@ -8,6 +8,11 @@ */ class SerialConsole : public StreamAPI, public RedirectablePrint, private concurrency::OSThread { + /** + * If true we are talking to a smart host and all messages (including log messages) must be framed as protobufs. + */ + bool usingProtobufs = false; + public: SerialConsole(); @@ -31,10 +36,13 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur protected: /// Check the current underlying physical link to see if the client is currently connected virtual bool checkIsConnected() override; + + /// Possibly switch to protobufs if we see a valid protobuf message + virtual void log_to_serial(const char *logLevel, const char *format, va_list arg); }; // A simple wrapper to allow non class aware code write to the console void consolePrintf(const char *format, ...); void consoleInit(); -extern SerialConsole *console; +extern SerialConsole *console; \ No newline at end of file diff --git a/src/commands.h b/src/commands.h index 03ede5982e..f2b7830105 100644 --- a/src/commands.h +++ b/src/commands.h @@ -8,13 +8,11 @@ enum class Cmd { SET_ON, SET_OFF, ON_PRESS, - START_BLUETOOTH_PIN_SCREEN, + START_ALERT_FRAME, + STOP_ALERT_FRAME, START_FIRMWARE_UPDATE_SCREEN, - STOP_BLUETOOTH_PIN_SCREEN, STOP_BOOT_SCREEN, PRINT, - START_SHUTDOWN_SCREEN, - START_REBOOT_SCREEN, SHOW_PREV_FRAME, SHOW_NEXT_FRAME }; \ No newline at end of file diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 86408b8d2e..8738e2722d 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -314,7 +314,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) case SHT31_4x_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2); - if (registerValue == 0x11a2) { + if (registerValue == 0x11a2 || registerValue == 0x11da) { type = SHT4X; LOG_INFO("SHT4X sensor found\n"); } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { @@ -402,4 +402,4 @@ TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) const size_t ScanI2CTwoWire::countDevices() const { return foundDevices.size(); -} \ No newline at end of file +} diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 60168cffcf..f724ddd3d4 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -75,7 +75,6 @@ namespace graphics // A text message frame + debug frame + all the node infos FrameCallback *normalFrames; static uint32_t targetFramerate = IDLE_FRAMERATE; -static char btPIN[16] = "888888"; uint32_t logo_timeout = 5000; // 4 seconds for EACH logo @@ -108,15 +107,39 @@ GeoCoord geoCoord; static bool heartbeat = false; #endif -static uint16_t displayWidth, displayHeight; - -#define SCREEN_WIDTH displayWidth -#define SCREEN_HEIGHT displayHeight +// Quick access to screen dimensions from static drawing functions +// DEPRECATED. To-do: move static functions inside Screen class +#define SCREEN_WIDTH display->getWidth() +#define SCREEN_HEIGHT display->getHeight() #include "graphics/ScreenFonts.h" #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) +/// Check if the display can render a string (detect special chars; emoji) +static bool haveGlyphs(const char *str) +{ +#if defined(OLED_UA) || defined(OLED_RU) + // Don't want to make any assumptions about custom language support + return true; +#endif + + // Check each character with the lookup function for the OLED library + // We're not really meant to use this directly.. + bool have = true; + for (uint16_t i = 0; i < strlen(str); i++) { + uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); + // If font doesn't support a character, it is substituted for ¿ + if (result == 191 && (uint8_t)str[i] != 191) { + have = false; + break; + } + } + + LOG_DEBUG("haveGlyphs=%d\n", have); + return have; +} + /** * Draw the icon with extra info printed around the corners */ @@ -140,13 +163,15 @@ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDispl if (upperMsg) display->drawString(x + 0, y + 0, upperMsg); - // Draw version in upper right - char buf[16]; - snprintf(buf, sizeof(buf), "%s", - xstr(APP_VERSION_SHORT)); // Note: we don't bother printing region or now, it makes the string too long - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(buf), y + 0, buf); + // Draw version and short name in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); + + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); screen->forceDisplay(); - // FIXME - draw serial # somewhere? + + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code } static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) @@ -181,14 +206,15 @@ static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDi if (upperMsg) display->drawString(x + 0, y + 0, upperMsg); - // Draw version in upper right - char buf[16]; - snprintf(buf, sizeof(buf), "%s", - xstr(APP_VERSION_SHORT)); // Note: we don't bother printing region or now, it makes the string too long - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(buf), y + 0, buf); + // Draw version and shortname in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); + + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); screen->forceDisplay(); - // FIXME - draw serial # somewhere? + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code } static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) @@ -198,7 +224,7 @@ static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i drawOEMIconScreen(region, display, state, x, y); } -static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) +void Screen::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) { uint16_t x_offset = display->width() / 2; display->setTextAlignment(TEXT_ALIGN_CENTER); @@ -206,20 +232,6 @@ static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->drawString(x_offset + x, 26 + y, message); } -static void drawBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ -#ifdef ARCH_ESP32 - if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) { - drawFrameText(display, state, x, y, "Resuming..."); - } else -#endif - { - // Draw region in upper left - const char *region = myRegion ? myRegion->name : NULL; - drawIconScreen(region, display, state, x, y); - } -} - // Used on boot when a certificate is being created static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -277,40 +289,19 @@ static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) } } -/// Check if the display can render a string (detect special chars; emoji) -static bool haveGlyphs(const char *str) -{ -#if defined(OLED_UA) || defined(OLED_RU) - // Don't want to make any assumptions about custom language support - return true; -#endif - - // Check each character with the lookup function for the OLED library - // We're not really meant to use this directly.. - bool have = true; - for (uint16_t i = 0; i < strlen(str); i++) { - uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); - // If font doesn't support a character, it is substituted for ¿ - if (result == 191 && (uint8_t)str[i] != 191) { - have = false; - break; - } - } - - LOG_DEBUG("haveGlyphs=%d\n", have); - return have; -} - #ifdef USE_EINK /// Used on eink displays while in deep sleep static void drawDeepSleepScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // Next frame should use full-refresh, and block while running, else device will sleep before async callback EINK_ADD_FRAMEFLAG(display, COSMETIC); EINK_ADD_FRAMEFLAG(display, BLOCKING); LOG_DEBUG("Drawing deep sleep screen\n"); - drawIconScreen("Sleeping...", display, state, x, y); + + // Display displayStr on the screen + drawIconScreen("Sleeping", display, state, x, y); } /// Used on eink displays when screen updates are paused @@ -375,7 +366,7 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int // in the array of "drawScreen" functions; however, // the passed-state doesn't quite reflect the "current" // screen, so we have to detect it. - if (state->frameState == IN_TRANSITION && state->transitionFrameRelationship == INCOMING) { + if (state->frameState == IN_TRANSITION && state->transitionFrameRelationship == TransitionRelationship_INCOMING) { // if we're transitioning from the end of the frame list back around to the first // frame, then we want this to be `0` module_frame = state->transitionFrameTarget; @@ -389,31 +380,6 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int pi.drawFrame(display, state, x, y); } -static void drawFrameBluetooth(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - int x_offset = display->width() / 2; - int y_offset = display->height() <= 80 ? 0 : 32; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Bluetooth"); - - display->setFont(FONT_SMALL); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; - display->drawString(x_offset + x, y_offset + y, "Enter this code"); - - display->setFont(FONT_LARGE); - String displayPin(btPIN); - String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; - display->drawString(x_offset + x, y_offset + y, pin); - - display->setFont(FONT_SMALL); - String deviceName = "Name: "; - deviceName.concat(getDeviceName()); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; - display->drawString(x_offset + x, y_offset + y, deviceName); -} - static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->setTextAlignment(TEXT_ALIGN_CENTER); @@ -1091,45 +1057,8 @@ static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state #endif } -/// Draw the last waypoint we received -static void drawWaypointFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - static char tempBuf[237]; - - meshtastic_MeshPacket &mp = devicestate.rx_waypoint; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - - uint32_t seconds = sinceReceived(&mp); - uint32_t minutes = seconds / 60; - uint32_t hours = minutes / 60; - uint32_t days = hours / 24; - - if (config.display.heading_bold) { - display->drawStringf(1 + x, 0 + y, tempBuf, "%s ago from %s", - screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), - (node && node->has_user) ? node->user.short_name : "???"); - } - display->drawStringf(0 + x, 0 + y, tempBuf, "%s ago from %s", screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), - (node && node->has_user) ? node->user.short_name : "???"); - - display->setColor(WHITE); - meshtastic_Waypoint scratch; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { - snprintf(tempBuf, sizeof(tempBuf), "Received waypoint: %s", scratch.name); - display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf); - } -} - /// Draw a series of fields in a column, wrapping to multiple columns if needed -static void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) +void Screen::drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) { // The coordinates define the left starting point of the text display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -1309,56 +1238,13 @@ static void drawGPScoordinates(OLEDDisplay *display, int16_t x, int16_t y, const } } #endif -namespace -{ - -/// A basic 2D point class for drawing -class Point -{ - public: - float x, y; - - Point(float _x, float _y) : x(_x), y(_y) {} - - /// Apply a rotation around zero (standard rotation matrix math) - void rotate(float radian) - { - float cos = cosf(radian), sin = sinf(radian); - float rx = x * cos + y * sin, ry = -x * sin + y * cos; - - x = rx; - y = ry; - } - - void translate(int16_t dx, int dy) - { - x += dx; - y += dy; - } - - void scale(float f) - { - // We use -f here to counter the flip that happens - // on the y axis when drawing and rotating on screen - x *= f; - y *= -f; - } -}; - -} // namespace - -static void drawLine(OLEDDisplay *d, const Point &p1, const Point &p2) -{ - d->drawLine(p1.x, p1.y, p2.x, p2.y); -} - /** * Given a recent lat/lon return a guess of the heading the user is walking on. * * We keep a series of "after you've gone 10 meters, what is your heading since * the last reference point?" */ -static float estimatedHeading(double lat, double lon) +float Screen::estimatedHeading(double lat, double lon) { static double oldLat, oldLon; static float b; @@ -1382,38 +1268,13 @@ static float estimatedHeading(double lat, double lon) return b; } -static uint16_t getCompassDiam(OLEDDisplay *display) -{ - uint16_t diam = 0; - uint16_t offset = 0; - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) - offset = FONT_HEIGHT_SMALL; - - // get the smaller of the 2 dimensions and subtract 20 - if (display->getWidth() > (display->getHeight() - offset)) { - diam = display->getHeight() - offset; - // if 2/3 of the other size would be smaller, use that - if (diam > (display->getWidth() * 2 / 3)) { - diam = display->getWidth() * 2 / 3; - } - } else { - diam = display->getWidth(); - if (diam > ((display->getHeight() - offset) * 2 / 3)) { - diam = (display->getHeight() - offset) * 2 / 3; - } - } - - return diam - 20; -}; - /// We will skip one node - the one for us, so we just blindly loop over all /// nodes static size_t nodeIndex; static int8_t prevFrame = -1; // Draw the arrow pointing to a node's location -static void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, float headingRadian) +void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) { Point tip(0.0f, 0.5f), tail(0.0f, -0.5f); // pointing up initially float arrowOffsetX = 0.2f, arrowOffsetY = 0.2f; @@ -1423,16 +1284,45 @@ static void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t comp for (int i = 0; i < 4; i++) { arrowPoints[i]->rotate(headingRadian); - arrowPoints[i]->scale(getCompassDiam(display) * 0.6); + arrowPoints[i]->scale(compassDiam * 0.6); arrowPoints[i]->translate(compassX, compassY); } - drawLine(display, tip, tail); - drawLine(display, leftArrow, tip); - drawLine(display, rightArrow, tip); + display->drawLine(tip.x, tip.y, tail.x, tail.y); + display->drawLine(leftArrow.x, leftArrow.y, tip.x, tip.y); + display->drawLine(rightArrow.x, rightArrow.y, tip.x, tip.y); +} + +// Get a string representation of the time passed since something happened +void Screen::getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) +{ + // Use an absolute timestamp in some cases. + // Particularly useful with E-Ink displays. Static UI, fewer refreshes. + uint8_t timestampHours, timestampMinutes; + int32_t daysAgo; + bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); + + if (agoSecs < 120) // last 2 mins? + snprintf(timeStr, maxLength, "%u seconds ago", agoSecs); + // -- if suitable for timestamp -- + else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes + snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); + else if (useTimestamp && daysAgo == 0) // Today + snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); + else if (useTimestamp && daysAgo == 1) // Yesterday + snprintf(timeStr, maxLength, "Seen yesterday"); + else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) + snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo); + // -- if using time delta instead -- + else if (agoSecs < 120 * 60) // last 2 hrs + snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60); + // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. + else if ((agoSecs / 60 / 60) < (hours_in_month * 6)) + snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); + else + snprintf(timeStr, maxLength, "unknown age"); } -// Draw north -static void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading) +void Screen::drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading) { // If north is supposed to be at the top of the compass we want rotation to be +0 if (config.display.compass_north_top) @@ -1442,19 +1332,43 @@ static void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t com Point N3(-0.04f, 0.55f), N4(0.04f, 0.55f); Point *rosePoints[] = {&N1, &N2, &N3, &N4}; + uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); + for (int i = 0; i < 4; i++) { // North on compass will be negative of heading rosePoints[i]->rotate(-myHeading); - rosePoints[i]->scale(getCompassDiam(display)); + rosePoints[i]->scale(compassDiam); rosePoints[i]->translate(compassX, compassY); } - drawLine(display, N1, N3); - drawLine(display, N2, N4); - drawLine(display, N1, N4); + display->drawLine(N1.x, N1.y, N3.x, N3.y); + display->drawLine(N2.x, N2.y, N4.x, N4.y); + display->drawLine(N1.x, N1.y, N4.x, N4.y); } -/// Convert an integer GPS coords to a floating point -#define DegD(i) (i * 1e-7) +uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) +{ + uint16_t diam = 0; + uint16_t offset = 0; + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) + offset = FONT_HEIGHT_SMALL; + + // get the smaller of the 2 dimensions and subtract 20 + if (displayWidth > (displayHeight - offset)) { + diam = displayHeight - offset; + // if 2/3 of the other size would be smaller, use that + if (diam > (displayWidth * 2 / 3)) { + diam = displayWidth * 2 / 3; + } + } else { + diam = displayWidth; + if (diam > ((displayHeight - offset) * 2 / 3)) { + diam = (displayHeight - offset) * 2 / 3; + } + } + + return diam - 20; +}; static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -1494,34 +1408,8 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ snprintf(signalStr, sizeof(signalStr), "Signal: %d%%", clamp((int)((node->snr + 10) * 5), 0, 100)); } - uint32_t agoSecs = sinceLastSeen(node); static char lastStr[20]; - - // Use an absolute timestamp in some cases. - // Particularly useful with E-Ink displays. Static UI, fewer refreshes. - uint8_t timestampHours, timestampMinutes; - int32_t daysAgo; - bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); - - if (agoSecs < 120) // last 2 mins? - snprintf(lastStr, sizeof(lastStr), "%u seconds ago", agoSecs); - // -- if suitable for timestamp -- - else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes - snprintf(lastStr, sizeof(lastStr), "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); - else if (useTimestamp && daysAgo == 0) // Today - snprintf(lastStr, sizeof(lastStr), "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); - else if (useTimestamp && daysAgo == 1) // Yesterday - snprintf(lastStr, sizeof(lastStr), "Seen yesterday"); - else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) - snprintf(lastStr, sizeof(lastStr), "%li days ago", (long)daysAgo); - // -- if using time delta instead -- - else if (agoSecs < 120 * 60) // last 2 hrs - snprintf(lastStr, sizeof(lastStr), "%u minutes ago", agoSecs / 60); - // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. - else if ((agoSecs / 60 / 60) < (hours_in_month * 6)) - snprintf(lastStr, sizeof(lastStr), "%u hours ago", agoSecs / 60 / 60); - else - snprintf(lastStr, sizeof(lastStr), "unknown age"); + screen->getTimeAgoStr(sinceLastSeen(node), lastStr, sizeof(lastStr)); static char distStr[20]; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { @@ -1532,13 +1420,14 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const char *fields[] = {username, lastStr, signalStr, distStr, NULL}; int16_t compassX = 0, compassY = 0; + uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); // coordinates for the center of the compass/circle if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - compassX = x + SCREEN_WIDTH - getCompassDiam(display) / 2 - 5; + compassX = x + SCREEN_WIDTH - compassDiam / 2 - 5; compassY = y + SCREEN_HEIGHT / 2; } else { - compassX = x + SCREEN_WIDTH - getCompassDiam(display) / 2 - 5; + compassX = x + SCREEN_WIDTH - compassDiam / 2 - 5; compassY = y + FONT_HEIGHT_SMALL + (SCREEN_HEIGHT - FONT_HEIGHT_SMALL) / 2; } bool hasNodeHeading = false; @@ -1549,8 +1438,8 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ if (screen->hasHeading()) myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians else - myHeading = estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - drawCompassNorth(display, compassX, compassY, myHeading); + myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + screen->drawCompassNorth(display, compassX, compassY, myHeading); if (hasValidPosition(node)) { // display direction toward node @@ -1577,7 +1466,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ // If the top of the compass is not a static north we need adjust bearingToOther based on heading if (!config.display.compass_north_top) bearingToOther -= myHeading; - drawNodeHeading(display, compassX, compassY, bearingToOther); + screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); } } if (!hasNodeHeading) { @@ -1587,13 +1476,13 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ // hasValidPosition(node)); display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); } - display->drawCircle(compassX, compassY, getCompassDiam(display) / 2); + display->drawCircle(compassX, compassY, compassDiam / 2); if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->setColor(BLACK); } // Must be after distStr is populated - drawColumns(display, x, y, fields); + screen->drawColumns(display, x, y, fields); } Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_OledType screenType, OLEDDISPLAY_GEOMETRY geometry) @@ -1741,9 +1630,19 @@ void Screen::setup() // Add frames. EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); - static FrameCallback bootFrames[] = {drawBootScreen}; - static const int bootFrameCount = sizeof(bootFrames) / sizeof(bootFrames[0]); - ui->setFrames(bootFrames, bootFrameCount); + alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { +#ifdef ARCH_ESP32 + if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) { + drawFrameText(display, state, x, y, "Resuming..."); + } else +#endif + { + // Draw region in upper left + const char *region = myRegion ? myRegion->name : NULL; + drawIconScreen(region, display, state, x, y); + } + }; + ui->setFrames(alertFrames, 1); // No overlays. ui->setOverlays(nullptr, 0); @@ -1916,13 +1815,22 @@ int32_t Screen::runOnce() case Cmd::SHOW_NEXT_FRAME: handleShowNextFrame(); break; - case Cmd::START_BLUETOOTH_PIN_SCREEN: - handleStartBluetoothPinScreen(cmd.bluetooth_pin); + case Cmd::START_ALERT_FRAME: { + showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away + showingNormalScreen = false; + alertFrames[0] = alertFrame; +#ifdef USE_EINK + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please + EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update + handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) +#endif + setFrameImmediateDraw(alertFrames); break; + } case Cmd::START_FIRMWARE_UPDATE_SCREEN: handleStartFirmwareUpdateScreen(); break; - case Cmd::STOP_BLUETOOTH_PIN_SCREEN: + case Cmd::STOP_ALERT_FRAME: case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame setFrames(); @@ -1931,12 +1839,6 @@ int32_t Screen::runOnce() handlePrint(cmd.print_text); free(cmd.print_text); break; - case Cmd::START_SHUTDOWN_SCREEN: - handleShutdownScreen(); - break; - case Cmd::START_REBOOT_SCREEN: - handleRebootScreen(); - break; default: LOG_ERROR("Invalid screen cmd\n"); } @@ -2133,10 +2035,6 @@ void Screen::setFrames() if (devicestate.has_rx_text_message && shouldDrawMessage(&devicestate.rx_text_message)) { normalFrames[numframes++] = drawTextMessageFrame; } - // If we have a waypoint - show it next, unless it's a phone message and we aren't using any special modules - if (devicestate.has_rx_waypoint && shouldDrawMessage(&devicestate.rx_waypoint)) { - normalFrames[numframes++] = drawWaypointFrame; - } // then all the nodes // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens @@ -2176,17 +2074,6 @@ void Screen::setFrames() setFastFramerate(); // Draw ASAP } -void Screen::handleStartBluetoothPinScreen(uint32_t pin) -{ - LOG_DEBUG("showing bluetooth screen\n"); - showingNormalScreen = false; - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame - - static FrameCallback frames[] = {drawFrameBluetooth}; - snprintf(btPIN, sizeof(btPIN), "%06u", pin); - setFrameImmediateDraw(frames); -} - void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) { ui->disableAllIndicators(); @@ -2194,41 +2081,6 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) setFastFramerate(); } -void Screen::handleShutdownScreen() -{ - LOG_DEBUG("showing shutdown screen\n"); - showingNormalScreen = false; -#ifdef USE_EINK - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please - EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update - handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) -#endif - - auto frame = [](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - drawFrameText(display, state, x, y, "Shutting down..."); - }; - static FrameCallback frames[] = {frame}; - - setFrameImmediateDraw(frames); -} - -void Screen::handleRebootScreen() -{ - LOG_DEBUG("showing reboot screen\n"); - showingNormalScreen = false; -#ifdef USE_EINK - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please - EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update - handleSetOn(true); // Power-on to show rebooting screen (PowerFSM should handle?) -#endif - - auto frame = [](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - drawFrameText(display, state, x, y, "Rebooting..."); - }; - static FrameCallback frames[] = {frame}; - setFrameImmediateDraw(frames); -} - void Screen::handleStartFirmwareUpdateScreen() { LOG_DEBUG("showing firmware screen\n"); @@ -2245,7 +2097,7 @@ void Screen::blink() uint8_t count = 10; dispdev->setBrightness(254); while (count > 0) { - dispdev->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight()); dispdev->display(); delay(50); dispdev->clear(); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index f4d7197152..e80581d6d9 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -21,11 +21,9 @@ class Screen void print(const char *) {} void doDeepSleep() {} void forceDisplay(bool forceUiUpdate = false) {} - void startBluetoothPinScreen(uint32_t pin) {} - void stopBluetoothPinScreen() {} - void startRebootScreen() {} - void startShutdownScreen() {} void startFirmwareUpdateScreen() {} + void startAlert(const char *) {} + void endAlert() {} }; } // namespace graphics #else @@ -34,6 +32,8 @@ class Screen #include #include "../configuration.h" +#include "gps/GeoCoord.h" +#include "graphics/ScreenFonts.h" #ifdef USE_ST7567 #include @@ -82,6 +82,46 @@ class Screen #define SEGMENT_WIDTH 16 #define SEGMENT_HEIGHT 4 +/// Convert an integer GPS coords to a floating point +#define DegD(i) (i * 1e-7) + +namespace +{ +/// A basic 2D point class for drawing +class Point +{ + public: + float x, y; + + Point(float _x, float _y) : x(_x), y(_y) {} + + /// Apply a rotation around zero (standard rotation matrix math) + void rotate(float radian) + { + float cos = cosf(radian), sin = sinf(radian); + float rx = x * cos + y * sin, ry = -x * sin + y * cos; + + x = rx; + y = ry; + } + + void translate(int16_t dx, int dy) + { + x += dx; + y += dy; + } + + void scale(float f) + { + // We use -f here to counter the flip that happens + // on the y axis when drawing and rotating on screen + x *= f; + y *= -f; + } +}; + +} // namespace + namespace graphics { @@ -166,41 +206,56 @@ class Screen : public concurrency::OSThread void blink(); + void drawFrameText(OLEDDisplay *, OLEDDisplayUiState *, int16_t, int16_t, const char *); + + void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength); + + // Draw north + void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading); + + static uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); + + float estimatedHeading(double lat, double lon); + + void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); + + void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); + /// Handle button press, trackball or swipe action) void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); } - /// Starts showing the Bluetooth PIN screen. - // - // Switches over to a static frame showing the Bluetooth pairing screen - // with the PIN. - void startBluetoothPinScreen(uint32_t pin) + // generic alert start + void startAlert(FrameCallback _alertFrame) { + alertFrame = _alertFrame; ScreenCmd cmd; - cmd.cmd = Cmd::START_BLUETOOTH_PIN_SCREEN; - cmd.bluetooth_pin = pin; + cmd.cmd = Cmd::START_ALERT_FRAME; enqueueCmd(cmd); } - void startFirmwareUpdateScreen() + void startAlert(const char *_alertMessage) { - ScreenCmd cmd; - cmd.cmd = Cmd::START_FIRMWARE_UPDATE_SCREEN; - enqueueCmd(cmd); + startAlert([_alertMessage](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + uint16_t x_offset = display->width() / 2; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, 26 + y, _alertMessage); + }); } - void startShutdownScreen() + void endAlert() { ScreenCmd cmd; - cmd.cmd = Cmd::START_SHUTDOWN_SCREEN; + cmd.cmd = Cmd::STOP_ALERT_FRAME; enqueueCmd(cmd); } - void startRebootScreen() + void startFirmwareUpdateScreen() { ScreenCmd cmd; - cmd.cmd = Cmd::START_REBOOT_SCREEN; + cmd.cmd = Cmd::START_FIRMWARE_UPDATE_SCREEN; enqueueCmd(cmd); } @@ -222,9 +277,6 @@ class Screen : public concurrency::OSThread void setFunctionSymbal(std::string sym); void removeFunctionSymbal(std::string sym); - /// Stops showing the bluetooth PIN screen. - void stopBluetoothPinScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BLUETOOTH_PIN_SCREEN}); } - /// Stops showing the boot screen. void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } @@ -358,7 +410,13 @@ class Screen : public concurrency::OSThread bool isAUTOOled = false; + // Screen dimensions (for convenience) + // Defined during Screen::setup + uint16_t displayWidth = 0; + uint16_t displayHeight = 0; + private: + FrameCallback alertFrames[1]; struct ScreenCmd { Cmd cmd; union { @@ -384,11 +442,8 @@ class Screen : public concurrency::OSThread void handleOnPress(); void handleShowNextFrame(); void handleShowPrevFrame(); - void handleStartBluetoothPinScreen(uint32_t pin); void handlePrint(const char *text); void handleStartFirmwareUpdateScreen(); - void handleShutdownScreen(); - void handleRebootScreen(); /// Rebuilds our list of frames (screens) to default ones. void setFrames(); @@ -426,6 +481,9 @@ class Screen : public concurrency::OSThread bool digitalWatchFace = true; #endif + /// callback for current alert frame + FrameCallback alertFrame; + /// Queue of commands to execute in doTask. TypedQueue cmdQueue; /// Whether we are using a display @@ -452,4 +510,5 @@ class Screen : public concurrency::OSThread }; } // namespace graphics + #endif \ No newline at end of file diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index 4b34563f70..8a48d053e9 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -28,8 +28,8 @@ #define FONT_LARGE ArialMT_Plain_24 // Height: 28 #endif -#define fontHeight(font) ((font)[1] + 1) // height is position 1 +#define _fontHeight(font) ((font)[1] + 1) // height is position 1 -#define FONT_HEIGHT_SMALL fontHeight(FONT_SMALL) -#define FONT_HEIGHT_MEDIUM fontHeight(FONT_MEDIUM) -#define FONT_HEIGHT_LARGE fontHeight(FONT_LARGE) +#define FONT_HEIGHT_SMALL _fontHeight(FONT_SMALL) +#define FONT_HEIGHT_MEDIUM _fontHeight(FONT_MEDIUM) +#define FONT_HEIGHT_LARGE _fontHeight(FONT_LARGE) \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 9ec4fa82de..196eae525b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -221,7 +221,7 @@ void setup() meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64; -#ifdef SEGGER_STDOUT_CH +#ifdef USE_SEGGER auto mode = false ? SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL : SEGGER_RTT_MODE_NO_BLOCK_TRIM; #ifdef NRF52840_XXAA auto buflen = 4096; // this board has a fair amount of ram @@ -930,7 +930,7 @@ void setup() nodeDB->saveToDisk(SEGMENT_CONFIG); if (!rIf->reconfigure()) { LOG_WARN("Reconfigure failed, rebooting\n"); - screen->startRebootScreen(); + screen->startAlert("Rebooting..."); rebootAtMsec = millis() + 5000; } } diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index 7866fa444e..0fdde52772 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -22,7 +22,6 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) if (wasSeenRecently(p)) { // Note: this will also add a recent packet record printPacket("Ignoring incoming msg we've already seen", p); if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && - config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT && config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER) { // cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! Router::cancelSending(p->from, p->id); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 923bf309ca..84872e4714 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -180,7 +180,7 @@ bool NodeDB::resetRadioConfig(bool factory_reset) if (didFactoryReset) { LOG_INFO("Rebooting due to factory reset"); - screen->startRebootScreen(); + screen->startAlert("Rebooting..."); rebootAtMsec = millis() + (5 * 1000); } diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 404666877c..0f69b21f9c 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -5,6 +5,7 @@ #include "Channels.h" #include "Default.h" +#include "FSCommon.h" #include "MeshService.h" #include "NodeDB.h" #include "PhoneAPI.h" @@ -47,6 +48,8 @@ void PhoneAPI::handleStartConfig() // even if we were already connected - restart our state machine state = STATE_SEND_MY_INFO; pauseBluetoothLogging = true; + filesManifest = getFiles("/", 10); + LOG_DEBUG("Got %d files in manifest\n", filesManifest.size()); LOG_INFO("Starting API client config\n"); nodeInfoForPhone.num = 0; // Don't keep returning old nodeinfos @@ -149,6 +152,7 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) STATE_SEND_CONFIG, STATE_SEND_MODULE_CONFIG, STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to the client + STATE_SEND_FILEMANIFEST, STATE_SEND_COMPLETE_ID, STATE_SEND_PACKETS // send packets or debug strings */ @@ -324,7 +328,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) // Advance when we have sent all of our ModuleConfig objects if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) { // Clients sending special nonce don't want to see other nodeinfos - state = config_nonce == SPECIAL_NONCE ? STATE_SEND_COMPLETE_ID : STATE_SEND_OTHER_NODEINFOS; + state = config_nonce == SPECIAL_NONCE ? STATE_SEND_FILEMANIFEST : STATE_SEND_OTHER_NODEINFOS; config_state = 0; } break; @@ -340,20 +344,32 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) nodeInfoForPhone.num = 0; // We just consumed a nodeinfo, will need a new one next time } else { LOG_INFO("Done sending nodeinfos\n"); - state = STATE_SEND_COMPLETE_ID; + state = STATE_SEND_FILEMANIFEST; // Go ahead and send that ID right now return getFromRadio(buf); } break; } + case STATE_SEND_FILEMANIFEST: { + LOG_INFO("getFromRadio=STATE_SEND_FILEMANIFEST\n"); + // last element + if (config_state == filesManifest.size()) { // also handles an empty filesManifest + config_state = 0; + filesManifest.clear(); + // Skip to complete packet + sendConfigComplete(); + } else { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_fileInfo_tag; + fromRadioScratch.fileInfo = filesManifest.at(config_state); + LOG_DEBUG("File: %s (%d) bytes\n", fromRadioScratch.fileInfo.file_name, fromRadioScratch.fileInfo.size_bytes); + config_state++; + } + break; + } + case STATE_SEND_COMPLETE_ID: - LOG_INFO("getFromRadio=STATE_SEND_COMPLETE_ID\n"); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag; - fromRadioScratch.config_complete_id = config_nonce; - config_nonce = 0; - state = STATE_SEND_PACKETS; - pauseBluetoothLogging = false; + sendConfigComplete(); break; case STATE_SEND_PACKETS: @@ -391,7 +407,9 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) // Encapsulate as a FromRadio packet size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); - LOG_DEBUG("encoding toPhone packet to phone variant=%d, %d bytes\n", fromRadioScratch.which_payload_variant, numbytes); + // VERY IMPORTANT to not print debug messages while writing to fromRadioScratch - because we use that same buffer + // for logging (when we are encapsulating with protobufs) + // LOG_DEBUG("encoding toPhone packet to phone variant=%d, %d bytes\n", fromRadioScratch.which_payload_variant, numbytes); return numbytes; } @@ -399,8 +417,19 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) return 0; } +void PhoneAPI::sendConfigComplete() +{ + LOG_INFO("getFromRadio=STATE_SEND_COMPLETE_ID\n"); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag; + fromRadioScratch.config_complete_id = config_nonce; + config_nonce = 0; + state = STATE_SEND_PACKETS; + pauseBluetoothLogging = false; +} + void PhoneAPI::handleDisconnect() { + filesManifest.clear(); pauseBluetoothLogging = false; LOG_INFO("PhoneAPI disconnect\n"); } @@ -443,6 +472,7 @@ bool PhoneAPI::available() case STATE_SEND_MODULECONFIG: case STATE_SEND_METADATA: case STATE_SEND_OWN_NODEINFO: + case STATE_SEND_FILEMANIFEST: case STATE_SEND_COMPLETE_ID: return true; @@ -457,7 +487,6 @@ bool PhoneAPI::available() } } return true; // Always say we have something, because we might need to advance our state machine - case STATE_SEND_PACKETS: { if (!queueStatusPacketForPhone) queueStatusPacketForPhone = service.getQueueStatusForPhone(); diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index 668f9c1f3c..3c3668300a 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -2,10 +2,20 @@ #include "Observer.h" #include "mesh-pb-constants.h" +#include #include +#include // Make sure that we never let our packets grow too large for one BLE packet #define MAX_TO_FROM_RADIO_SIZE 512 + +#if meshtastic_FromRadio_size > MAX_TO_FROM_RADIO_SIZE +#error "meshtastic_FromRadio_size is too large for our BLE packets" +#endif +#if meshtastic_ToRadio_size > MAX_TO_FROM_RADIO_SIZE +#error "meshtastic_ToRadio_size is too large for our BLE packets" +#endif + #define SPECIAL_NONCE 69420 /** @@ -29,6 +39,7 @@ class PhoneAPI STATE_SEND_CONFIG, // Replacement for the old Radioconfig STATE_SEND_MODULECONFIG, // Send Module specific config STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client + STATE_SEND_FILEMANIFEST, // Send file manifest STATE_SEND_COMPLETE_ID, STATE_SEND_PACKETS // send packets or debug strings }; @@ -65,6 +76,8 @@ class PhoneAPI uint32_t config_nonce = 0; uint32_t readIndex = 0; + std::vector filesManifest = {}; + void resetReadIndex() { readIndex = 0; } public: @@ -91,6 +104,8 @@ class PhoneAPI */ size_t getFromRadio(uint8_t *buf); + void sendConfigComplete(); + /** * Return true if we have data available to send to the phone */ diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 78228c077c..343b7f2008 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -261,7 +261,6 @@ uint32_t RadioInterface::getTxDelayMsecWeighted(float snr) uint8_t CWsize = map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax); // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d\n", snr, CWsize); if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || - config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT || config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { delay = random(0, 2 * CWsize) * slotTimeMsec; LOG_DEBUG("rx_snr found in packet. As a router, setting tx delay:%d\n", delay); @@ -522,7 +521,7 @@ void RadioInterface::applyModemConfig() LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f\n", freq, loraConfig.frequency_offset); LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d\n", myRegion->name, channelName, loraConfig.modem_preset, channel_num, power); - LOG_INFO("Radio myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f mhz)\n", myRegion->freqStart, myRegion->freqEnd, + LOG_INFO("Radio myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f MHz)\n", myRegion->freqStart, myRegion->freqEnd, myRegion->freqEnd - myRegion->freqStart); LOG_INFO("Radio myRegion->numChannels: %d x %.3fkHz\n", numChannels, bw); LOG_INFO("Radio channel_num: %d\n", channel_num + 1); diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 4d04dffe48..9f59aa971c 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -1,5 +1,6 @@ #include "StreamAPI.h" #include "PowerFSM.h" +#include "RTC.h" #include "configuration.h" #define START1 0x94 @@ -96,7 +97,6 @@ void StreamAPI::writeStream() void StreamAPI::emitTxBuffer(size_t len) { if (len != 0) { - // LOG_DEBUG("emit tx %d\n", len); txBuf[0] = START1; txBuf[1] = START2; txBuf[2] = (len >> 8) & 0xff; @@ -119,6 +119,25 @@ void StreamAPI::emitRebooted() emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch)); } +void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg) +{ + // In case we send a FromRadio packet + memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_log_record_tag; + fromRadioScratch.log_record.level = level; + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + fromRadioScratch.log_record.time = rtc_sec; + strncpy(fromRadioScratch.log_record.source, src, sizeof(fromRadioScratch.log_record.source) - 1); + + auto num_printed = + vsnprintf(fromRadioScratch.log_record.message, sizeof(fromRadioScratch.log_record.message) - 1, format, arg); + if (num_printed > 0 && fromRadioScratch.log_record.message[num_printed - 1] == + '\n') // Strip any ending newline, because we have records for framing instead. + fromRadioScratch.log_record.message[num_printed - 1] = '\0'; + emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch)); +} + /// Hookable to find out when connection changes void StreamAPI::onConnectionChanged(bool connected) { @@ -131,4 +150,4 @@ void StreamAPI::onConnectionChanged(bool connected) // received a packet in a while powerFSM.trigger(EVENT_SERIAL_DISCONNECTED); } -} +} \ No newline at end of file diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index 3196e96f8b..45cbb231c7 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -82,4 +82,7 @@ class StreamAPI : public PhoneAPI /// Subclasses can use this scratch buffer if they wish uint8_t txBuf[MAX_STREAM_BUF_SIZE] = {0}; -}; + + /// Low level function to emit a protobuf encapsulated log record + void emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg); +}; \ No newline at end of file diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 2da4b86e6a..e3037c910d 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -22,7 +22,6 @@ typedef enum _meshtastic_Config_DeviceConfig_Role { The wifi radio and the oled screen will be put to sleep. This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. */ meshtastic_Config_DeviceConfig_Role_ROUTER = 2, - /* Description: Combination of both ROUTER and CLIENT. Not for mobile devices. */ meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT = 3, /* Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list. Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index 100972c1eb..fc7bea53a4 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -8,7 +8,6 @@ #include "meshtastic/channel.pb.h" #include "meshtastic/localonly.pb.h" #include "meshtastic/mesh.pb.h" -#include "meshtastic/module_config.pb.h" #include "meshtastic/telemetry.pb.h" #if PB_PROTO_HEADER_VERSION != 40 diff --git a/src/mesh/generated/meshtastic/mesh.pb.cpp b/src/mesh/generated/meshtastic/mesh.pb.cpp index 46d59d6094..3fa81e1312 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.cpp +++ b/src/mesh/generated/meshtastic/mesh.pb.cpp @@ -36,7 +36,7 @@ PB_BIND(meshtastic_NodeInfo, meshtastic_NodeInfo, AUTO) PB_BIND(meshtastic_MyNodeInfo, meshtastic_MyNodeInfo, AUTO) -PB_BIND(meshtastic_LogRecord, meshtastic_LogRecord, AUTO) +PB_BIND(meshtastic_LogRecord, meshtastic_LogRecord, 2) PB_BIND(meshtastic_QueueStatus, meshtastic_QueueStatus, AUTO) @@ -45,6 +45,9 @@ PB_BIND(meshtastic_QueueStatus, meshtastic_QueueStatus, AUTO) PB_BIND(meshtastic_FromRadio, meshtastic_FromRadio, 2) +PB_BIND(meshtastic_FileInfo, meshtastic_FileInfo, AUTO) + + PB_BIND(meshtastic_ToRadio, meshtastic_ToRadio, 2) diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 0641158155..dbe9281ec0 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -167,6 +167,15 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_RADIOMASTER_900_BANDIT_NANO = 64, /* Heltec Capsule Sensor V3 with ESP32-S3 CPU, Portable LoRa device that can replace GNSS modules or sensors */ meshtastic_HardwareModel_HELTEC_CAPSULE_SENSOR_V3 = 65, + /* Heltec Vision Master T190 with ESP32-S3 CPU, and a 1.90 inch TFT display */ + meshtastic_HardwareModel_HELTEC_VISION_MASTER_T190 = 66, + /* Heltec Vision Master E213 with ESP32-S3 CPU, and a 2.13 inch E-Ink display */ + meshtastic_HardwareModel_HELTEC_VISION_MASTER_E213 = 67, + /* Heltec Vision Master E290 with ESP32-S3 CPU, and a 2.9 inch E-Ink display */ + meshtastic_HardwareModel_HELTEC_VISION_MASTER_E290 = 68, + /* Heltec Mesh Node T114 board with nRF52840 CPU, and a 1.14 inch TFT display, Ultimate low-power design, + specifically adapted for the Meshtatic project */ + meshtastic_HardwareModel_HELTEC_MESH_NODE_T114 = 69, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ @@ -691,11 +700,11 @@ typedef struct _meshtastic_MyNodeInfo { and then extend as needed by emitting multiple records. */ typedef struct _meshtastic_LogRecord { /* Log levels, chosen to match python logging conventions. */ - char message[64]; + char message[384]; /* Seconds since 1970 - or 0 for unknown/unset */ uint32_t time; /* Usually based on thread name - if known */ - char source[8]; + char source[32]; /* Not yet set */ meshtastic_LogRecord_Level level; } meshtastic_LogRecord; @@ -711,6 +720,14 @@ typedef struct _meshtastic_QueueStatus { uint32_t mesh_packet_id; } meshtastic_QueueStatus; +/* Individual File info for the device */ +typedef struct _meshtastic_FileInfo { + /* The fully qualified path of the file */ + char file_name[228]; + /* The size of the file in bytes */ + uint32_t size_bytes; +} meshtastic_FileInfo; + typedef PB_BYTES_ARRAY_T(237) meshtastic_Compressed_data_t; /* Compressed message payload */ typedef struct _meshtastic_Compressed { @@ -815,6 +832,8 @@ typedef struct _meshtastic_FromRadio { meshtastic_DeviceMetadata metadata; /* MQTT Client Proxy Message (device sending to client / phone for publishing to MQTT) */ meshtastic_MqttClientProxyMessage mqttClientProxyMessage; + /* File system manifest messages */ + meshtastic_FileInfo fileInfo; }; } meshtastic_FromRadio; @@ -958,6 +977,7 @@ extern "C" { + #define meshtastic_Compressed_portnum_ENUMTYPE meshtastic_PortNum @@ -985,6 +1005,7 @@ extern "C" { #define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_default {0, 0, 0, 0} #define meshtastic_FromRadio_init_default {0, 0, {meshtastic_MeshPacket_init_default}} +#define meshtastic_FileInfo_init_default {"", 0} #define meshtastic_ToRadio_init_default {0, {meshtastic_MeshPacket_init_default}} #define meshtastic_Compressed_init_default {_meshtastic_PortNum_MIN, {0, {0}}} #define meshtastic_NeighborInfo_init_default {0, 0, 0, 0, {meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default}} @@ -1008,6 +1029,7 @@ extern "C" { #define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_zero {0, 0, 0, 0} #define meshtastic_FromRadio_init_zero {0, 0, {meshtastic_MeshPacket_init_zero}} +#define meshtastic_FileInfo_init_zero {"", 0} #define meshtastic_ToRadio_init_zero {0, {meshtastic_MeshPacket_init_zero}} #define meshtastic_Compressed_init_zero {_meshtastic_PortNum_MIN, {0, {0}}} #define meshtastic_NeighborInfo_init_zero {0, 0, 0, 0, {meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero}} @@ -1110,6 +1132,8 @@ extern "C" { #define meshtastic_QueueStatus_free_tag 2 #define meshtastic_QueueStatus_maxlen_tag 3 #define meshtastic_QueueStatus_mesh_packet_id_tag 4 +#define meshtastic_FileInfo_file_name_tag 1 +#define meshtastic_FileInfo_size_bytes_tag 2 #define meshtastic_Compressed_portnum_tag 1 #define meshtastic_Compressed_data_tag 2 #define meshtastic_Neighbor_node_id_tag 1 @@ -1144,6 +1168,7 @@ extern "C" { #define meshtastic_FromRadio_xmodemPacket_tag 12 #define meshtastic_FromRadio_metadata_tag 13 #define meshtastic_FromRadio_mqttClientProxyMessage_tag 14 +#define meshtastic_FromRadio_fileInfo_tag 15 #define meshtastic_ToRadio_packet_tag 1 #define meshtastic_ToRadio_want_config_id_tag 3 #define meshtastic_ToRadio_disconnect_tag 4 @@ -1321,7 +1346,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,channel,channel), 10) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,queueStatus,queueStatus), 11) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,xmodemPacket,xmodemPacket), 12) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,metadata,metadata), 13) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,mqttClientProxyMessage,mqttClientProxyMessage), 14) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,mqttClientProxyMessage,mqttClientProxyMessage), 14) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,fileInfo,fileInfo), 15) #define meshtastic_FromRadio_CALLBACK NULL #define meshtastic_FromRadio_DEFAULT NULL #define meshtastic_FromRadio_payload_variant_packet_MSGTYPE meshtastic_MeshPacket @@ -1335,6 +1361,13 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,mqttClientProxyMessage,mqttC #define meshtastic_FromRadio_payload_variant_xmodemPacket_MSGTYPE meshtastic_XModem #define meshtastic_FromRadio_payload_variant_metadata_MSGTYPE meshtastic_DeviceMetadata #define meshtastic_FromRadio_payload_variant_mqttClientProxyMessage_MSGTYPE meshtastic_MqttClientProxyMessage +#define meshtastic_FromRadio_payload_variant_fileInfo_MSGTYPE meshtastic_FileInfo + +#define meshtastic_FileInfo_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, file_name, 1) \ +X(a, STATIC, SINGULAR, UINT32, size_bytes, 2) +#define meshtastic_FileInfo_CALLBACK NULL +#define meshtastic_FileInfo_DEFAULT NULL #define meshtastic_ToRadio_FIELDLIST(X, a) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,packet,packet), 1) \ @@ -1434,6 +1467,7 @@ extern const pb_msgdesc_t meshtastic_MyNodeInfo_msg; extern const pb_msgdesc_t meshtastic_LogRecord_msg; extern const pb_msgdesc_t meshtastic_QueueStatus_msg; extern const pb_msgdesc_t meshtastic_FromRadio_msg; +extern const pb_msgdesc_t meshtastic_FileInfo_msg; extern const pb_msgdesc_t meshtastic_ToRadio_msg; extern const pb_msgdesc_t meshtastic_Compressed_msg; extern const pb_msgdesc_t meshtastic_NeighborInfo_msg; @@ -1459,6 +1493,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_LogRecord_fields &meshtastic_LogRecord_msg #define meshtastic_QueueStatus_fields &meshtastic_QueueStatus_msg #define meshtastic_FromRadio_fields &meshtastic_FromRadio_msg +#define meshtastic_FileInfo_fields &meshtastic_FileInfo_msg #define meshtastic_ToRadio_fields &meshtastic_ToRadio_msg #define meshtastic_Compressed_fields &meshtastic_Compressed_msg #define meshtastic_NeighborInfo_fields &meshtastic_NeighborInfo_msg @@ -1478,9 +1513,10 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_Compressed_size 243 #define meshtastic_Data_size 270 #define meshtastic_DeviceMetadata_size 46 +#define meshtastic_FileInfo_size 236 #define meshtastic_FromRadio_size 510 #define meshtastic_Heartbeat_size 0 -#define meshtastic_LogRecord_size 81 +#define meshtastic_LogRecord_size 426 #define meshtastic_MeshPacket_size 326 #define meshtastic_MqttClientProxyMessage_size 501 #define meshtastic_MyNodeInfo_size 18 diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index 233e8d6534..6cc82352ab 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -124,6 +124,8 @@ typedef enum _meshtastic_PortNum { meshtastic_PortNum_ATAK_PLUGIN = 72, /* Provides unencrypted information about a node for consumption by a map via MQTT */ meshtastic_PortNum_MAP_REPORT_APP = 73, + /* PowerStress based monitoring support (for automated power consumption testing) */ + meshtastic_PortNum_POWERSTRESS_APP = 74, /* Private applications should use portnums >= 256. To simplify initial development and testing you can use "PRIVATE_APP" in your code without needing to rebuild protobuf files (via [regen-protos.sh](https://github.com/meshtastic/firmware/blob/master/bin/regen-protos.sh)) */ diff --git a/src/mesh/generated/meshtastic/powermon.pb.cpp b/src/mesh/generated/meshtastic/powermon.pb.cpp index 4d798e9a39..ce41ea0217 100644 --- a/src/mesh/generated/meshtastic/powermon.pb.cpp +++ b/src/mesh/generated/meshtastic/powermon.pb.cpp @@ -9,5 +9,9 @@ PB_BIND(meshtastic_PowerMon, meshtastic_PowerMon, AUTO) +PB_BIND(meshtastic_PowerStressMessage, meshtastic_PowerStressMessage, AUTO) + + + diff --git a/src/mesh/generated/meshtastic/powermon.pb.h b/src/mesh/generated/meshtastic/powermon.pb.h index 88e80bb559..7de0618e9b 100644 --- a/src/mesh/generated/meshtastic/powermon.pb.h +++ b/src/mesh/generated/meshtastic/powermon.pb.h @@ -38,6 +38,33 @@ See GPSPowerState for more details */ meshtastic_PowerMon_State_GPS_Active = 2048 } meshtastic_PowerMon_State; +/* What operation would we like the UUT to perform. +note: senders should probably set want_response in their request packets, so that they can know when the state +machine has started processing their request */ +typedef enum _meshtastic_PowerStressMessage_Opcode { + /* Unset/unused */ + meshtastic_PowerStressMessage_Opcode_UNSET = 0, + meshtastic_PowerStressMessage_Opcode_PRINT_INFO = 1, /* Print board version slog and send an ack that we are alive and ready to process commands */ + meshtastic_PowerStressMessage_Opcode_FORCE_QUIET = 2, /* Try to turn off all automatic processing of packets, screen, sleeping, etc (to make it easier to measure in isolation) */ + meshtastic_PowerStressMessage_Opcode_END_QUIET = 3, /* Stop powerstress processing - probably by just rebooting the board */ + meshtastic_PowerStressMessage_Opcode_SCREEN_ON = 16, /* Turn the screen on */ + meshtastic_PowerStressMessage_Opcode_SCREEN_OFF = 17, /* Turn the screen off */ + meshtastic_PowerStressMessage_Opcode_CPU_IDLE = 32, /* Let the CPU run but we assume mostly idling for num_seconds */ + meshtastic_PowerStressMessage_Opcode_CPU_DEEPSLEEP = 33, /* Force deep sleep for FIXME seconds */ + meshtastic_PowerStressMessage_Opcode_CPU_FULLON = 34, /* Spin the CPU as fast as possible for num_seconds */ + meshtastic_PowerStressMessage_Opcode_LED_ON = 48, /* Turn the LED on for num_seconds (and leave it on - for baseline power measurement purposes) */ + meshtastic_PowerStressMessage_Opcode_LED_OFF = 49, /* Force the LED off for num_seconds */ + meshtastic_PowerStressMessage_Opcode_LORA_OFF = 64, /* Completely turn off the LORA radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_LORA_TX = 65, /* Send Lora packets for num_seconds */ + meshtastic_PowerStressMessage_Opcode_LORA_RX = 66, /* Receive Lora packets for num_seconds (node will be mostly just listening, unless an external agent is helping stress this by sending packets on the current channel) */ + meshtastic_PowerStressMessage_Opcode_BT_OFF = 80, /* Turn off the BT radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_BT_ON = 81, /* Turn on the BT radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_WIFI_OFF = 96, /* Turn off the WIFI radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_WIFI_ON = 97, /* Turn on the WIFI radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_GPS_OFF = 112, /* Turn off the GPS radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_GPS_ON = 113 /* Turn on the GPS radio for num_seconds */ +} meshtastic_PowerStressMessage_Opcode; + /* Struct definitions */ /* Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) */ @@ -45,6 +72,13 @@ typedef struct _meshtastic_PowerMon { char dummy_field; } meshtastic_PowerMon; +/* PowerStress testing support via the C++ PowerStress module */ +typedef struct _meshtastic_PowerStressMessage { + /* What type of HardwareMessage is this? */ + meshtastic_PowerStressMessage_Opcode cmd; + float num_seconds; +} meshtastic_PowerStressMessage; + #ifdef __cplusplus extern "C" { @@ -55,13 +89,23 @@ extern "C" { #define _meshtastic_PowerMon_State_MAX meshtastic_PowerMon_State_GPS_Active #define _meshtastic_PowerMon_State_ARRAYSIZE ((meshtastic_PowerMon_State)(meshtastic_PowerMon_State_GPS_Active+1)) +#define _meshtastic_PowerStressMessage_Opcode_MIN meshtastic_PowerStressMessage_Opcode_UNSET +#define _meshtastic_PowerStressMessage_Opcode_MAX meshtastic_PowerStressMessage_Opcode_GPS_ON +#define _meshtastic_PowerStressMessage_Opcode_ARRAYSIZE ((meshtastic_PowerStressMessage_Opcode)(meshtastic_PowerStressMessage_Opcode_GPS_ON+1)) + + +#define meshtastic_PowerStressMessage_cmd_ENUMTYPE meshtastic_PowerStressMessage_Opcode /* Initializer values for message structs */ #define meshtastic_PowerMon_init_default {0} +#define meshtastic_PowerStressMessage_init_default {_meshtastic_PowerStressMessage_Opcode_MIN, 0} #define meshtastic_PowerMon_init_zero {0} +#define meshtastic_PowerStressMessage_init_zero {_meshtastic_PowerStressMessage_Opcode_MIN, 0} /* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_PowerStressMessage_cmd_tag 1 +#define meshtastic_PowerStressMessage_num_seconds_tag 2 /* Struct field encoding specification for nanopb */ #define meshtastic_PowerMon_FIELDLIST(X, a) \ @@ -69,14 +113,23 @@ extern "C" { #define meshtastic_PowerMon_CALLBACK NULL #define meshtastic_PowerMon_DEFAULT NULL +#define meshtastic_PowerStressMessage_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, cmd, 1) \ +X(a, STATIC, SINGULAR, FLOAT, num_seconds, 2) +#define meshtastic_PowerStressMessage_CALLBACK NULL +#define meshtastic_PowerStressMessage_DEFAULT NULL + extern const pb_msgdesc_t meshtastic_PowerMon_msg; +extern const pb_msgdesc_t meshtastic_PowerStressMessage_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_PowerMon_fields &meshtastic_PowerMon_msg +#define meshtastic_PowerStressMessage_fields &meshtastic_PowerStressMessage_msg /* Maximum encoded size of messages (where known) */ -#define MESHTASTIC_MESHTASTIC_POWERMON_PB_H_MAX_SIZE meshtastic_PowerMon_size +#define MESHTASTIC_MESHTASTIC_POWERMON_PB_H_MAX_SIZE meshtastic_PowerStressMessage_size #define meshtastic_PowerMon_size 0 +#define meshtastic_PowerStressMessage_size 7 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 8146866090..11821a0a3e 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -137,7 +137,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH if (BleOta::getOtaAppVersion().isEmpty()) { LOG_INFO("No OTA firmware available, scheduling regular reboot in %d seconds\n", s); - screen->startRebootScreen(); + screen->startAlert("Rebooting..."); } else { screen->startFirmwareUpdateScreen(); BleOta::switchToOtaApp(); @@ -145,7 +145,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta } #else LOG_INFO("Not on ESP32, scheduling regular reboot in %d seconds\n", s); - screen->startRebootScreen(); + screen->startAlert("Rebooting..."); #endif rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000); break; @@ -388,6 +388,10 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) LOG_DEBUG("Tried to set node_info_broadcast_secs too low, setting to %d\n", min_node_info_broadcast_secs); config.device.node_info_broadcast_secs = min_node_info_broadcast_secs; } + // Router Client is deprecated; Set it to client + if (c.payload_variant.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } break; case meshtastic_Config_position_tag: LOG_INFO("Setting config: Position\n"); @@ -811,7 +815,7 @@ void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t ch void AdminModule::reboot(int32_t seconds) { LOG_INFO("Rebooting in %d seconds\n", seconds); - screen->startRebootScreen(); + screen->startAlert("Rebooting..."); rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index f513e045f4..be414dce13 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -597,14 +597,14 @@ int32_t CannedMessageModule::runOnce() // handle fn+s for shutdown case 0x9b: if (screen) - screen->startShutdownScreen(); + screen->startAlert("Shutting down..."); shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; break; // and fn+r for reboot case 0x90: if (screen) - screen->startRebootScreen(); + screen->startAlert("Rebooting..."); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; break; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 8f899401b9..d37bb754de 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -188,7 +188,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt if (lastMeasurementPacket == nullptr) { // If there's no valid packet, display "Environment" display->drawString(x, y, "Environment"); - display->drawString(x, y += fontHeight(FONT_SMALL), "No measurement"); + display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); return; } @@ -213,31 +213,31 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt } // Continue with the remaining details - display->drawString(x, y += fontHeight(FONT_SMALL), + display->drawString(x, y += _fontHeight(FONT_SMALL), "Temp/Hum: " + last_temp + " / " + String(lastMeasurement.variant.environment_metrics.relative_humidity, 0) + "%"); if (lastMeasurement.variant.environment_metrics.barometric_pressure != 0) { - display->drawString(x, y += fontHeight(FONT_SMALL), + display->drawString(x, y += _fontHeight(FONT_SMALL), "Press: " + String(lastMeasurement.variant.environment_metrics.barometric_pressure, 0) + "hPA"); } if (lastMeasurement.variant.environment_metrics.voltage != 0) { - display->drawString(x, y += fontHeight(FONT_SMALL), + display->drawString(x, y += _fontHeight(FONT_SMALL), "Volt/Cur: " + String(lastMeasurement.variant.environment_metrics.voltage, 0) + "V / " + String(lastMeasurement.variant.environment_metrics.current, 0) + "mA"); } if (lastMeasurement.variant.environment_metrics.iaq != 0) { - display->drawString(x, y += fontHeight(FONT_SMALL), "IAQ: " + String(lastMeasurement.variant.environment_metrics.iaq)); + display->drawString(x, y += _fontHeight(FONT_SMALL), "IAQ: " + String(lastMeasurement.variant.environment_metrics.iaq)); } if (lastMeasurement.variant.environment_metrics.distance != 0) - display->drawString(x, y += fontHeight(FONT_SMALL), + display->drawString(x, y += _fontHeight(FONT_SMALL), "Water Level: " + String(lastMeasurement.variant.environment_metrics.distance, 0) + "mm"); if (lastMeasurement.variant.environment_metrics.weight != 0) - display->drawString(x, y += fontHeight(FONT_SMALL), + display->drawString(x, y += _fontHeight(FONT_SMALL), "Weight: " + String(lastMeasurement.variant.environment_metrics.weight, 0) + "kg"); } @@ -285,6 +285,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m valid = valid && sht31Sensor.getMetrics(m); hasSensor = true; } + if (sht4xSensor.hasSensor()) { + valid = valid && sht4xSensor.getMetrics(m); + hasSensor = true; + } if (lps22hbSensor.hasSensor()) { valid = valid && lps22hbSensor.getMetrics(m); hasSensor = true; @@ -533,4 +537,4 @@ AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule return result; } -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index cb864f4f3c..fb5aee375b 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -108,7 +108,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s display->drawString(x, y, "Power Telemetry"); if (lastMeasurementPacket == nullptr) { display->setFont(FONT_SMALL); - display->drawString(x, y += fontHeight(FONT_MEDIUM), "No measurement"); + display->drawString(x, y += _fontHeight(FONT_MEDIUM), "No measurement"); return; } @@ -120,22 +120,22 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s auto &p = lastMeasurementPacket->decoded; if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { display->setFont(FONT_SMALL); - display->drawString(x, y += fontHeight(FONT_MEDIUM), "Measurement Error"); + display->drawString(x, y += _fontHeight(FONT_MEDIUM), "Measurement Error"); LOG_ERROR("Unable to decode last packet"); return; } display->setFont(FONT_SMALL); String last_temp = String(lastMeasurement.variant.environment_metrics.temperature, 0) + "°C"; - display->drawString(x, y += fontHeight(FONT_MEDIUM) - 2, "From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + display->drawString(x, y += _fontHeight(FONT_MEDIUM) - 2, "From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); if (lastMeasurement.variant.power_metrics.ch1_voltage != 0) { - display->drawString(x, y += fontHeight(FONT_SMALL), + display->drawString(x, y += _fontHeight(FONT_SMALL), "Ch 1 Volt/Cur: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 0) + "V / " + String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA"); - display->drawString(x, y += fontHeight(FONT_SMALL), + display->drawString(x, y += _fontHeight(FONT_SMALL), "Ch 2 Volt/Cur: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 0) + "V / " + String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA"); - display->drawString(x, y += fontHeight(FONT_SMALL), + display->drawString(x, y += _fontHeight(FONT_SMALL), "Ch 3 Volt/Cur: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 0) + "V / " + String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA"); } diff --git a/src/modules/Telemetry/Sensor/INA3221Sensor.cpp b/src/modules/Telemetry/Sensor/INA3221Sensor.cpp index ea2cb4ea8c..edd29682e0 100644 --- a/src/modules/Telemetry/Sensor/INA3221Sensor.cpp +++ b/src/modules/Telemetry/Sensor/INA3221Sensor.cpp @@ -16,8 +16,7 @@ int32_t INA3221Sensor::runOnce() return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } if (!status) { - ina3221.setAddr(INA3221_ADDR42_SDA); // i2c address 0x42 - ina3221.begin(); + ina3221.begin(nodeTelemetrySensorsMap[sensorType].second); ina3221.setShuntRes(100, 100, 100); // 0.1 Ohm shunt resistors status = true; } else { diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 83485c8eee..d5b7d29ee5 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -2,6 +2,11 @@ #include "NodeDB.h" #include "PowerFSM.h" #include "configuration.h" +#if HAS_SCREEN +#include "gps/RTC.h" +#include "graphics/Screen.h" +#include "main.h" +#endif WaypointModule *waypointModule; @@ -11,14 +16,155 @@ ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) auto &p = mp.decoded; LOG_INFO("Received waypoint msg from=0x%0x, id=0x%x, msg=%.*s\n", mp.from, mp.id, p.payload.size, p.payload.bytes); #endif - + UIFrameEvent e = {true, true}; // We only store/display messages destined for us. // Keep a copy of the most recent text message. devicestate.rx_waypoint = mp; devicestate.has_rx_waypoint = true; powerFSM.trigger(EVENT_RECEIVED_MSG); - notifyObservers(&mp); + notifyObservers(&e); return ProcessMessage::CONTINUE; // Let others look at this message also if they want } + +#if HAS_SCREEN +bool WaypointModule::shouldDraw() +{ +#if !MESHTASTIC_EXCLUDE_WAYPOINT + // If no waypoint to show + if (!devicestate.has_rx_waypoint) + return false; + + // Decode the message, to find the expiration time (is waypoint still valid) + // This handles "deletion" as well as expiration + meshtastic_Waypoint wp; + memset(&wp, 0, sizeof(wp)); + if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size, + &meshtastic_Waypoint_msg, &wp)) { + // Valid waypoint + if (wp.expire > getTime()) + return devicestate.has_rx_waypoint = true; + + // Expired, or deleted + else + return devicestate.has_rx_waypoint = false; + } + + // If decoding failed + LOG_ERROR("Failed to decode waypoint\n"); + devicestate.has_rx_waypoint = false; + return false; +#else + return false; +#endif +} + +/// Draw the last waypoint we received +void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Prepare to draw + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // Handle inverted display + // Unsure of expected behavior: for now, copy drawNodeInfo + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + + // Decode the waypoint + meshtastic_MeshPacket &mp = devicestate.rx_waypoint; + meshtastic_Waypoint wp; + memset(&wp, 0, sizeof(wp)); + if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { + // This *should* be caught by shouldDrawWaypoint, but we'll short-circuit here just in case + display->drawStringMaxWidth(0 + x, 0 + y, x + display->getWidth(), "Couldn't decode waypoint"); + devicestate.has_rx_waypoint = false; + return; + } + + // Get timestamp info. Will pass as a field to drawColumns + static char lastStr[20]; + screen->getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); + + // Will contain distance information, passed as a field to drawColumns + static char distStr[20]; + + // Get our node, to use our own position + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + + // Text fields to draw (left of compass) + // Last element must be NULL. This signals the end of the char*[] to drawColumns + const char *fields[] = {"Waypoint", lastStr, wp.name, distStr, NULL}; + + // Dimensions / co-ordinates for the compass/circle + int16_t compassX = 0, compassY = 0; + uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight()); + + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + compassX = x + display->getWidth() - compassDiam / 2 - 5; + compassY = y + display->getHeight() / 2; + } else { + compassX = x + display->getWidth() - compassDiam / 2 - 5; + compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; + } + + // If our node has a position: + if (ourNode && (hasValidPosition(ourNode) || screen->hasHeading())) { + const meshtastic_PositionLite &op = ourNode->position; + float myHeading; + if (screen->hasHeading()) + myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians + else + myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + screen->drawCompassNorth(display, compassX, compassY, myHeading); + + // Distance to Waypoint + float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + if (d < (2 * MILES_TO_FEET)) + snprintf(distStr, sizeof(distStr), "%.0f ft", d * METERS_TO_FEET); + else + snprintf(distStr, sizeof(distStr), "%.1f mi", d * METERS_TO_FEET / MILES_TO_FEET); + } else { + if (d < 2000) + snprintf(distStr, sizeof(distStr), "%.0f m", d); + else + snprintf(distStr, sizeof(distStr), "%.1f km", d / 1000); + } + + // Compass bearing to waypoint + float bearingToOther = + GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); + // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly + // If the top of the compass is not a static north we need adjust bearingToOther based on heading + if (!config.display.compass_north_top) + bearingToOther -= myHeading; + screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + } + + // If our node doesn't have position + else { + // ? in the compass + display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); + + // ? in the distance field + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + strncpy(distStr, "? mi", sizeof(distStr)); + else + strncpy(distStr, "? km", sizeof(distStr)); + } + + // Undo color-inversion, if set prior to drawing header + // Unsure of expected behavior? For now: copy drawNodeInfo + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + + // Draw compass circle + display->drawCircle(compassX, compassY, compassDiam / 2); + + // Must be after distStr is populated + screen->drawColumns(display, x, y, fields); +} +#endif \ No newline at end of file diff --git a/src/modules/WaypointModule.h b/src/modules/WaypointModule.h index ddbabf4deb..4c9c7b86b0 100644 --- a/src/modules/WaypointModule.h +++ b/src/modules/WaypointModule.h @@ -5,21 +5,29 @@ /** * Waypoint message handling for meshtastic */ -class WaypointModule : public SinglePortModule, public Observable +class WaypointModule : public SinglePortModule, public Observable { public: /** Constructor * name is for debugging output */ WaypointModule() : SinglePortModule("waypoint", meshtastic_PortNum_WAYPOINT_APP) {} - +#if HAS_SCREEN + bool shouldDraw(); +#endif protected: /** Called to handle a particular incoming message @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for it */ + + virtual Observable *getUIFrameObservable() override { return this; } +#if HAS_SCREEN + virtual bool wantUIFrame() override { return this->shouldDraw(); } + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; +#endif virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; }; -extern WaypointModule *waypointModule; +extern WaypointModule *waypointModule; \ No newline at end of file diff --git a/src/modules/esp32/StoreForwardModule.cpp b/src/modules/esp32/StoreForwardModule.cpp index 12cddc5202..dc8650ad0b 100644 --- a/src/modules/esp32/StoreForwardModule.cpp +++ b/src/modules/esp32/StoreForwardModule.cpp @@ -319,8 +319,8 @@ ProcessMessage StoreForwardModule::handleReceived(const meshtastic_MeshPacket &m #ifdef ARCH_ESP32 if (moduleConfig.store_forward.enabled) { - // The router node should not be sending messages as a client. Unless he is a ROUTER_CLIENT - if ((getFrom(&mp) != nodeDB->getNodeNum()) || (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT)) { + // The router node should not be sending messages as a client + if ((getFrom(&mp) != nodeDB->getNodeNum())) { if ((mp.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) && is_server) { auto &p = mp.decoded; diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index b70420c8a0..78ef5a1d3e 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -82,7 +82,33 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks LOG_INFO("*** Enter passkey %d on the peer side ***\n", passkey); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - screen->startBluetoothPinScreen(passkey); +#if HAS_SCREEN + screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + char btPIN[16] = "888888"; + snprintf(btPIN, sizeof(btPIN), "%06u", passkey); + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 32; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Bluetooth"); + + display->setFont(FONT_SMALL); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; + display->drawString(x_offset + x, y_offset + y, "Enter this code"); + + display->setFont(FONT_LARGE); + String displayPin(btPIN); + String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; + display->drawString(x_offset + x, y_offset + y, pin); + + display->setFont(FONT_SMALL); + String deviceName = "Name: "; + deviceName.concat(getDeviceName()); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; + display->drawString(x_offset + x, y_offset + y, deviceName); + }); +#endif passkeyShowing = true; return passkey; @@ -94,7 +120,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks if (passkeyShowing) { passkeyShowing = false; - screen->stopBluetoothPinScreen(); + screen->endAlert(); } } @@ -180,7 +206,8 @@ void NimbleBluetooth::setupService() ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE); FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ); fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ); - logRadioCharacteristic = bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ); + logRadioCharacteristic = + bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U); } else { ToRadioCharacteristic = bleService->createCharacteristic( TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC); @@ -189,9 +216,10 @@ void NimbleBluetooth::setupService() fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); - logRadioCharacteristic = - bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | - NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); + logRadioCharacteristic = bleService->createCharacteristic( + LOGRADIO_UUID, + NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC, 512U); + logRadioCharacteristic->setValue("Init"); } bluetoothPhoneAPI = new BluetoothPhoneAPI(); @@ -245,7 +273,7 @@ void NimbleBluetooth::sendLog(const char *logMessage) if (!bleServer || !isConnected() || strlen(logMessage) > 512) { return; } - logRadioCharacteristic->notify(reinterpret_cast(logMessage), strlen(logMessage)); + logRadioCharacteristic->notify(reinterpret_cast(logMessage), strlen(logMessage), true); } void clearNVS() diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index a14829285b..56d7ed167d 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -290,7 +290,31 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke { LOG_INFO("BLE pairing process started with passkey %.3s %.3s\n", passkey, passkey + 3); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - screen->startBluetoothPinScreen(configuredPasskey); + screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + char btPIN[16] = "888888"; + snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 32; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Bluetooth"); + + display->setFont(FONT_SMALL); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; + display->drawString(x_offset + x, y_offset + y, "Enter this code"); + + display->setFont(FONT_LARGE); + String displayPin(btPIN); + String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; + display->drawString(x_offset + x, y_offset + y, pin); + + display->setFont(FONT_SMALL); + String deviceName = "Name: "; + deviceName.concat(getDeviceName()); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; + display->drawString(x_offset + x, y_offset + y, deviceName); + }); if (match_request) { uint32_t start_time = millis(); while (millis() < start_time + 30000) { @@ -307,7 +331,7 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu LOG_INFO("BLE pairing success\n"); else LOG_INFO("BLE pairing failed\n"); - screen->stopBluetoothPinScreen(); + screen->endAlert(); } void NRF52Bluetooth::sendLog(const char *logMessage) diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 1f2c6867d5..b79f28f139 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -63,7 +63,8 @@ static void initBrownout() // We don't bother with setting up brownout if soft device is disabled - because during production we always use softdevice } -static const bool useSoftDevice = true; // Set to false for easier debugging +// This is a public global so that the debugger can set it to false automatically from our gdbinit +bool useSoftDevice = true; // Set to false for easier debugging #if !MESHTASTIC_EXCLUDE_BLUETOOTH void setBluetoothEnable(bool enable) @@ -149,13 +150,43 @@ void nrf52Loop() checkSDEvents(); } +#ifdef USE_SEMIHOSTING +#include + +/** + * Note: this variable is in BSS and therfore false by default. But the gdbinit + * file will be installing a temporary breakpoint that changes wantSemihost to true. + */ +bool wantSemihost; + +/** + * Turn on semihosting if the ICE debugger wants it. + */ +void nrf52InitSemiHosting() +{ + if (wantSemihost) { + static SemihostingStream semiStream; + // We must dynamically alloc because the constructor does semihost operations which + // would crash any load not talking to a debugger + semiStream.open(); + semiStream.println("Semihosting starts!"); + // Redirect our serial output to instead go via the ICE port + console->setDestination(&semiStream); + } +} +#endif + void nrf52Setup() { - auto why = NRF_POWER->RESETREAS; + uint32_t why = NRF_POWER->RESETREAS; // per // https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.nrf52832.ps.v1.1%2Fpower.html LOG_DEBUG("Reset reason: 0x%x\n", why); +#ifdef USE_SEMIHOSTING + nrf52InitSemiHosting(); +#endif + // Per // https://devzone.nordicsemi.com/nordic/nordic-blog/b/blog/posts/monitor-mode-debugging-with-j-link-and-gdbeclipse // This is the recommended setting for Monitor Mode Debugging diff --git a/src/shutdown.h b/src/shutdown.h index 54fb3071b7..3f191eea88 100644 --- a/src/shutdown.h +++ b/src/shutdown.h @@ -38,7 +38,7 @@ void powerCommandsCheck() #if defined(ARCH_ESP32) || defined(ARCH_NRF52) if (shutdownAtMsec) { - screen->startShutdownScreen(); + screen->startAlert("Shutting down..."); } #endif diff --git a/src/sleep.cpp b/src/sleep.cpp index 7d85f1e732..735ebcf6ad 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -62,14 +62,14 @@ void setCPUFast(bool on) /* * * There's a newly introduced bug in the espressif framework where WiFi is - * unstable when the frequency is less than 240mhz. + * unstable when the frequency is less than 240MHz. * * This mostly impacts WiFi AP mode but we'll bump the frequency for * all WiFi use cases. * (Added: Dec 23, 2021 by Jm Casler) */ #ifndef CONFIG_IDF_TARGET_ESP32C3 - LOG_DEBUG("Setting CPU to 240mhz because WiFi is in use.\n"); + LOG_DEBUG("Setting CPU to 240MHz because WiFi is in use.\n"); setCpuFrequencyMhz(240); #endif return; diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index ef3e5a6458..6a67b00835 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -20,6 +20,7 @@ lib_deps = debug_tool = jlink + ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds ;upload_protocol = jlink @@ -27,26 +28,93 @@ debug_tool = jlink ; Allows programming and debug via the RAK NanoDAP as the default debugger tool for the RAK4631 (it is only $10!) ; programming time is about the same as the bootloader version. ; For information on this see the meshtastic developers documentation for "Development on the NRF52" -[env:rak4631_dap] +[env:rak4631_dbg] extends = env:rak4631 board_level = extra -; pyocd pack --i nrf52840 + +; if the builtin version of openocd has a buggy version of semihosting, so use the external version +; platform_packages = platformio/tool-openocd@^3.1200.0 + +build_flags = + ${env:rak4631.build_flags} + -D USE_SEMIHOSTING + +lib_deps = + ${env:rak4631.lib_deps} + https://github.com/geeksville/Armduino-Semihosting.git#35b538fdf208c3530c1434cd099a08e486672ee4 + +; NOTE: the pyocd support for semihosting is buggy. So I switched to using the builtin platformio support for the stlink adapter which worked much better. +; However the built in openocd version in platformio has buggy support for TCP to semihosting. +; +; So I'm now trying the external openocd - but the openocd scripts for nrf52.cfg assume you are using a DAP adapter not an STLINK adapter. +; In theory I could change those scripts. But for now I'm trying going back to a DAP adapter but with the external openocd. + +upload_protocol = stlink ; eventually use platformio/tool-pyocd@^2.3600.0 instad -upload_protocol = custom -upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE +;upload_protocol = custom +;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE + +; We want the initial breakpoint at setup() instead of main(). Also we want to enable semihosting at that point so instead of +; debug_init_break = tbreak setup +; we just turn off the platformio tbreak and do it in .gdbinit (where we have more flexibility for scripting) +; also we use a permanent breakpoint so it gets reused each time we restart the debugging session? +debug_init_break = tbreak setup + +; Note: add "monitor arm semihosting_redirect tcp 4444 all" if you want the stdout from the device to go to that port number instead +; (for use by meshtastic command line) +; monitor arm semihosting disable +; monitor debug_level 3 +; +; IMPORTANT: fileio must be disabled before using port 5555 - openocd ver 0.12 has a bug where if enabled it never properly parses the special :tt name +; for stdio access. +; monitor arm semihosting_redirect tcp 5555 stdio + +; Also note: it is _impossible_ to do non blocking reads on the semihost console port (an oversight when ARM specified the semihost API). +; So we'll neve be able to general purpose bi-directional communication with the device over semihosting. +debug_extra_cmds = + echo Running .gdbinit script + monitor arm semihosting enable + monitor arm semihosting_fileio enable + monitor arm semihosting_redirect disable + commands 1 + echo Breakpoint at setup() has semihosting console, connect to it with "telnet localhost 5555" + set wantSemihost = true + set useSoftDevice = false + end + ; Only reprogram the board if the code has changed debug_load_mode = modified ;debug_load_mode = manual -debug_tool = custom +debug_tool = stlink +;debug_tool = custom +; debug_server = +; openocd +; -f +; /usr/local/share/openocd/scripts/interface/stlink.cfg +; -f +; /usr/local/share/openocd/scripts/target/nrf52.cfg +; $PLATFORMIO_CORE_DIR/packages/tool-openocd/openocd/scripts/interface/cmsis-dap.cfg + +; Allows programming and debug via the RAK NanoDAP as the default debugger tool for the RAK4631 (it is only $10!) +; programming time is about the same as the bootloader version. +; For information on this see the meshtastic developers documentation for "Development on the NRF52" ; We manually pass in the elf file so that pyocd can reverse engineer FreeRTOS data (running threads, etc...) -debug_server = - pyocd - gdbserver - -t - nrf52840 - --elf - ${platformio.build_dir}/${this.__env__}/firmware.elf +;debug_server = +; pyocd +; gdbserver +; -j +; ${platformio.workspace_dir}/.. +; -t +; nrf52840 +; --semihosting +; --elf +; ${platformio.build_dir}/${this.__env__}/firmware.elf + +; If you want to debug the semihosting support you can turn on extra logging in pyocd with +; -L +; pyocd.debug.semihost.trace=debug + ; The following is not needed because it automatically tries do this ;debug_server_ready_pattern = -.*GDB server started on port \d+.* ;debug_port = localhost:3333 \ No newline at end of file diff --git a/variants/wio-sdk-wm1110/platformio.ini b/variants/wio-sdk-wm1110/platformio.ini index 7ca82e4c68..4a23e7a119 100644 --- a/variants/wio-sdk-wm1110/platformio.ini +++ b/variants/wio-sdk-wm1110/platformio.ini @@ -15,5 +15,19 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/wio-sdk-wm1110> lib_deps = ${nrf52840_base.lib_deps} debug_tool = jlink +;debug_tool = stlink +;debug_speed = 4000 +; No need to reflash if the binary hasn't changed +debug_load_mode = modified ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) upload_protocol = jlink +;upload_protocol = stlink +; we prefer to stop in setup() because we are an 'ardiuno' app +debug_init_break = tbreak setup + +; we need to turn off BLE/soft device if we are debugging otherwise it will watchdog reset us. +debug_extra_cmds = + echo Running .gdbinit script + commands 1 + set useSoftDevice = false + end \ No newline at end of file diff --git a/variants/xiao_ble/platformio.ini b/variants/xiao_ble/platformio.ini index 9d533c0ada..76e91e8444 100644 --- a/variants/xiao_ble/platformio.ini +++ b/variants/xiao_ble/platformio.ini @@ -3,7 +3,7 @@ extends = nrf52840_base board = xiao_ble_sense board_level = extra -build_flags = ${nrf52840_base.build_flags} -Ivariants/xiao_ble -Ivariants/xiao_ble/softdevice -Ivariants/xiao_ble/softdevice/nrf52 -D EBYTE_E22 -DEBYTE_E22_900M30S -DPRIVATE_HW +build_flags = ${nrf52840_base.build_flags} -Ivariants/xiao_ble -Ivariants/xiao_ble/softdevice -Ivariants/xiao_ble/softdevice/nrf52 -DEBYTE_E22 -DEBYTE_E22_900M30S -DPRIVATE_HW -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" board_build.ldscript = variants/xiao_ble/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/xiao_ble> @@ -11,4 +11,4 @@ lib_deps = ${nrf52840_base.lib_deps} debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) -;upload_protocol = jlink \ No newline at end of file +;upload_protocol = jlink diff --git a/version.properties b/version.properties index 268987418b..1cb93ac2bf 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 3 -build = 14 +build = 15