diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 5660810e6..f5ca6ed03 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -470,18 +470,49 @@ bool isAllowedPunctuation(char c) return allowed.find(c) != std::string::npos; } +static void replaceAll(std::string &s, const std::string &from, const std::string &to) +{ + if (from.empty()) + return; + size_t pos = 0; + while ((pos = s.find(from, pos)) != std::string::npos) { + s.replace(pos, from.size(), to); + pos += to.size(); + } +} + std::string sanitizeString(const std::string &input) { std::string output; bool inReplacement = false; - for (char c : input) { - if (std::isalnum(static_cast(c)) || isAllowedPunctuation(c)) { + // Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first. + std::string s = input; + + // Curly single quotes: ‘ ’ + replaceAll(s, "\xE2\x80\x98", "'"); // U+2018 + replaceAll(s, "\xE2\x80\x99", "'"); // U+2019 + + // Curly double quotes: “ ” + replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C + replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D + + // En dash / Em dash: – — + replaceAll(s, "\xE2\x80\x93", "-"); // U+2013 + replaceAll(s, "\xE2\x80\x94", "-"); // U+2014 + + // Non-breaking space + replaceAll(s, "\xC2\xA0", " "); // U+00A0 + + // Now do your original sanitize pass over the normalized string. + for (unsigned char uc : s) { + char c = static_cast(uc); + if (std::isalnum(uc) || isAllowedPunctuation(c)) { output += c; inReplacement = false; } else { if (!inReplacement) { - output += 0xbf; // ISO-8859-1 for inverted question mark + output += static_cast(0xBF); // ISO-8859-1 for inverted question mark inReplacement = true; } } diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index ac877e150..d9db84c35 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1092,11 +1092,23 @@ void menuHandler::favoriteBaseMenu() void menuHandler::positionBaseMenu() { - enum optionsNumbers { Back, GPSToggle, GPSFormat, CompassMenu, CompassCalibrate, enumEnd }; + enum optionsNumbers { + Back, + GPSToggle, + GPSFormat, + CompassMenu, + CompassCalibrate, + GPSSmartPosition, + GPSUpdateInterval, + GPSPositionBroadcast, + enumEnd + }; - static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "GPS Format", "Compass"}; - static int optionsEnumArray[enumEnd] = {Back, GPSToggle, GPSFormat, CompassMenu}; - int options = 4; + static const char *optionsArray[enumEnd] = { + "Back", "On/Off Toggle", "Format", "Smart Position", "Update Interval", "Broadcast Interval", "Compass"}; + static int optionsEnumArray[enumEnd] = { + Back, GPSToggle, GPSFormat, GPSSmartPosition, GPSUpdateInterval, GPSPositionBroadcast, CompassMenu}; + int options = 7; if (accelerometerThread) { optionsArray[options] = "Compass Calibrate"; @@ -1104,7 +1116,7 @@ void menuHandler::positionBaseMenu() } BannerOverlayOptions bannerOptions; - bannerOptions.message = "Position Action"; + bannerOptions.message = "GPS Action"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; @@ -1120,6 +1132,15 @@ void menuHandler::positionBaseMenu() screen->runNow(); } else if (selected == CompassCalibrate) { accelerometerThread->calibrate(30); + } else if (selected == GPSSmartPosition) { + menuQueue = gps_smart_position_menu; + screen->runNow(); + } else if (selected == GPSUpdateInterval) { + menuQueue = gps_update_interval_menu; + screen->runNow(); + } else if (selected == GPSPositionBroadcast) { + menuQueue = gps_position_broadcast_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -1346,6 +1367,217 @@ void menuHandler::GPSFormatMenu() bannerOptions.InitialSelected = uiconfig.gps_format + 1; screen->showOverlayBanner(bannerOptions); } + +void menuHandler::GPSSmartPositionMenu() +{ + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Toggle Smart Position"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Smrt Postn"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.position_broadcast_smart_enabled = true; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 2) { + config.position.position_broadcast_smart_enabled = false; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + bannerOptions.InitialSelected = config.position.position_broadcast_smart_enabled ? 1 : 2; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSUpdateIntervalMenu() +{ + static const char *optionsArray[] = {"Back", "8 seconds", "20 seconds", "40 seconds", "1 minute", "80 seconds", + "2 minutes", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", + "6 hours", "12 hours", "24 hours", "At Boot Only"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Update Interval"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 16; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.gps_update_interval = 8; + } else if (selected == 2) { + config.position.gps_update_interval = 20; + } else if (selected == 3) { + config.position.gps_update_interval = 40; + } else if (selected == 4) { + config.position.gps_update_interval = 60; + } else if (selected == 5) { + config.position.gps_update_interval = 80; + } else if (selected == 6) { + config.position.gps_update_interval = 120; + } else if (selected == 7) { + config.position.gps_update_interval = 300; + } else if (selected == 8) { + config.position.gps_update_interval = 600; + } else if (selected == 9) { + config.position.gps_update_interval = 900; + } else if (selected == 10) { + config.position.gps_update_interval = 1800; + } else if (selected == 11) { + config.position.gps_update_interval = 3600; + } else if (selected == 12) { + config.position.gps_update_interval = 21600; + } else if (selected == 13) { + config.position.gps_update_interval = 43200; + } else if (selected == 14) { + config.position.gps_update_interval = 86400; + } else if (selected == 15) { + config.position.gps_update_interval = 2147483647; // At Boot Only + } + + if (selected != 0) { + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + + if (config.position.gps_update_interval == 8) { + bannerOptions.InitialSelected = 1; + } else if (config.position.gps_update_interval == 20) { + bannerOptions.InitialSelected = 2; + } else if (config.position.gps_update_interval == 40) { + bannerOptions.InitialSelected = 3; + } else if (config.position.gps_update_interval == 60) { + bannerOptions.InitialSelected = 4; + } else if (config.position.gps_update_interval == 80) { + bannerOptions.InitialSelected = 5; + } else if (config.position.gps_update_interval == 120) { + bannerOptions.InitialSelected = 6; + } else if (config.position.gps_update_interval == 300) { + bannerOptions.InitialSelected = 7; + } else if (config.position.gps_update_interval == 600) { + bannerOptions.InitialSelected = 8; + } else if (config.position.gps_update_interval == 900) { + bannerOptions.InitialSelected = 9; + } else if (config.position.gps_update_interval == 1800) { + bannerOptions.InitialSelected = 10; + } else if (config.position.gps_update_interval == 3600) { + bannerOptions.InitialSelected = 11; + } else if (config.position.gps_update_interval == 21600) { + bannerOptions.InitialSelected = 12; + } else if (config.position.gps_update_interval == 43200) { + bannerOptions.InitialSelected = 13; + } else if (config.position.gps_update_interval == 86400) { + bannerOptions.InitialSelected = 14; + } else if (config.position.gps_update_interval == 2147483647) { // At Boot Only + bannerOptions.InitialSelected = 15; + } else { + bannerOptions.InitialSelected = 0; + } + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSPositionBroadcastMenu() +{ + static const char *optionsArray[] = {"Back", "1 minute", "90 seconds", "5 minutes", "15 minutes", "1 hour", + "2 hours", "3 hours", "4 hours", "5 hours", "6 hours", "12 hours", + "18 hours", "24 hours", "36 hours", "48 hours", "72 hours"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Broadcast Interval"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 17; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.position_broadcast_secs = 60; + } else if (selected == 2) { + config.position.position_broadcast_secs = 90; + } else if (selected == 3) { + config.position.position_broadcast_secs = 300; + } else if (selected == 4) { + config.position.position_broadcast_secs = 900; + } else if (selected == 5) { + config.position.position_broadcast_secs = 3600; + } else if (selected == 6) { + config.position.position_broadcast_secs = 7200; + } else if (selected == 7) { + config.position.position_broadcast_secs = 10800; + } else if (selected == 8) { + config.position.position_broadcast_secs = 14400; + } else if (selected == 9) { + config.position.position_broadcast_secs = 18000; + } else if (selected == 10) { + config.position.position_broadcast_secs = 21600; + } else if (selected == 11) { + config.position.position_broadcast_secs = 43200; + } else if (selected == 12) { + config.position.position_broadcast_secs = 64800; + } else if (selected == 13) { + config.position.position_broadcast_secs = 86400; + } else if (selected == 14) { + config.position.position_broadcast_secs = 129600; + } else if (selected == 15) { + config.position.position_broadcast_secs = 172800; + } else if (selected == 16) { + config.position.position_broadcast_secs = 259200; + } + + if (selected != 0) { + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + + if (config.position.position_broadcast_secs == 60) { + bannerOptions.InitialSelected = 1; + } else if (config.position.position_broadcast_secs == 90) { + bannerOptions.InitialSelected = 2; + } else if (config.position.position_broadcast_secs == 300) { + bannerOptions.InitialSelected = 3; + } else if (config.position.position_broadcast_secs == 900) { + bannerOptions.InitialSelected = 4; + } else if (config.position.position_broadcast_secs == 3600) { + bannerOptions.InitialSelected = 5; + } else if (config.position.position_broadcast_secs == 7200) { + bannerOptions.InitialSelected = 6; + } else if (config.position.position_broadcast_secs == 10800) { + bannerOptions.InitialSelected = 7; + } else if (config.position.position_broadcast_secs == 14400) { + bannerOptions.InitialSelected = 8; + } else if (config.position.position_broadcast_secs == 18000) { + bannerOptions.InitialSelected = 9; + } else if (config.position.position_broadcast_secs == 21600) { + bannerOptions.InitialSelected = 10; + } else if (config.position.position_broadcast_secs == 43200) { + bannerOptions.InitialSelected = 11; + } else if (config.position.position_broadcast_secs == 64800) { + bannerOptions.InitialSelected = 12; + } else if (config.position.position_broadcast_secs == 86400) { + bannerOptions.InitialSelected = 13; + } else if (config.position.position_broadcast_secs == 129600) { + bannerOptions.InitialSelected = 14; + } else if (config.position.position_broadcast_secs == 172800) { + bannerOptions.InitialSelected = 15; + } else if (config.position.position_broadcast_secs == 259200) { + bannerOptions.InitialSelected = 16; + } else { + bannerOptions.InitialSelected = 0; + } + screen->showOverlayBanner(bannerOptions); +} + #endif void menuHandler::BluetoothToggleMenu() @@ -2126,6 +2358,15 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case gps_format_menu: GPSFormatMenu(); break; + case gps_smart_position_menu: + GPSSmartPositionMenu(); + break; + case gps_update_interval_menu: + GPSUpdateIntervalMenu(); + break; + case gps_position_broadcast_menu: + GPSPositionBroadcastMenu(); + break; #endif case compass_point_north_menu: compassNorthMenu(); diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index e53b4baf7..bda744a66 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -22,6 +22,9 @@ class menuHandler node_base_menu, gps_toggle_menu, gps_format_menu, + gps_smart_position_menu, + gps_update_interval_menu, + gps_position_broadcast_menu, compass_point_north_menu, reset_node_db_menu, buzzermodemenupicker, @@ -77,6 +80,9 @@ class menuHandler static void compassNorthMenu(); static void GPSToggleMenu(); static void GPSFormatMenu(); + static void GPSSmartPositionMenu(); + static void GPSUpdateIntervalMenu(); + static void GPSPositionBroadcastMenu(); static void BuzzerModeMenu(); static void switchToMUIMenu(); static void TFTColorPickerMenu(OLEDDisplay *display); diff --git a/src/main.cpp b/src/main.cpp index d28eb13f0..9ac060d37 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1186,11 +1186,6 @@ void setup() #endif #endif -#ifdef PIN_PWR_DELAY_MS - // This may be required to give the peripherals time to power up. - delay(PIN_PWR_DELAY_MS); -#endif - #ifdef ARCH_PORTDUINO // as one can't use a function pointer to the class constructor: auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index c63e6d2d2..e1037f789 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -195,15 +195,13 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p) p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone -#if HAS_SCREEN - if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && p.to != NODENUM_BROADCAST && - p.to != 0) // DM only - { - perhapsDecode(&p); - const StoredMessage &sm = messageStore.addFromPacket(p); - graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI - } -#endif + IF_SCREEN(if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && + p.to != NODENUM_BROADCAST && p.to != 0) // DM only + { + perhapsDecode(&p); + const StoredMessage &sm = messageStore.addFromPacket(p); + graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI + }) // Send the packet into the mesh DEBUG_HEAP_BEFORE; auto a = packetPool.allocCopy(p); diff --git a/src/modules/TextMessageModule.cpp b/src/modules/TextMessageModule.cpp index 76e063436..7f889e087 100644 --- a/src/modules/TextMessageModule.cpp +++ b/src/modules/TextMessageModule.cpp @@ -21,18 +21,17 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp // We only store/display messages destined for us. devicestate.rx_text_message = mp; devicestate.has_rx_text_message = true; -#if HAS_SCREEN - // Guard against running in MeshtasticUI - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - // Store in the central message history - const StoredMessage &sm = messageStore.addFromPacket(mp); + IF_SCREEN( + // Guard against running in MeshtasticUI or with no screen + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + // Store in the central message history + const StoredMessage &sm = messageStore.addFromPacket(mp); - // Pass message to renderer (banner + thread switching + scroll reset) - // Use the global Screen singleton to retrieve the current OLED display - auto *display = screen ? screen->getDisplayDevice() : nullptr; - graphics::MessageRenderer::handleNewMessage(display, sm, mp); - } -#endif + // Pass message to renderer (banner + thread switching + scroll reset) + // Use the global Screen singleton to retrieve the current OLED display + auto *display = screen ? screen->getDisplayDevice() : nullptr; + graphics::MessageRenderer::handleNewMessage(display, sm, mp); + }) // Only trigger screen wake if configuration allows it if (shouldWakeOnReceivedMessage()) { powerFSM.trigger(EVENT_RECEIVED_MSG); diff --git a/variants/nrf52840/canaryone/variant.h b/variants/nrf52840/canaryone/variant.h index 204ca6306..61d1e8df9 100644 --- a/variants/nrf52840/canaryone/variant.h +++ b/variants/nrf52840/canaryone/variant.h @@ -103,7 +103,7 @@ static const uint8_t A0 = PIN_A0; #define EXTERNAL_FLASH_USE_QSPI // Add a delay on startup to allow LoRa and GPS to power up -#define PIN_PWR_DELAY_MS 100 +#define PERIPHERAL_WARMUP_MS 100 /* * Lora radio @@ -178,4 +178,4 @@ static const uint8_t A0 = PIN_A0; * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif