From 43440dbb9bda06e8c905bd85b62a003970ebd20d Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:09:50 -0400 Subject: [PATCH] InkHUD: GPS improvements (#10846) * GPS improvements * Update * Update MenuApplet.cpp * Remove fixed position * Update MenuApplet.cpp --- .../InkHUD/Applets/System/Menu/MenuAction.h | 5 + .../InkHUD/Applets/System/Menu/MenuApplet.cpp | 258 ++++++++++++++++-- .../InkHUD/Applets/System/Menu/MenuPage.h | 4 + src/graphics/niche/InkHUD/docs/README.md | 7 + 4 files changed, 248 insertions(+), 26 deletions(-) diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index e3011b0e2..d92bb30c8 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -26,6 +26,11 @@ enum MenuAction { NEXT_TILE, TOGGLE_BACKLIGHT, TOGGLE_GPS, + TOGGLE_SMART_POSITION, + SET_POSITION_BROADCAST_INTERVAL, + SET_SMART_BROADCAST_INTERVAL, + SET_SMART_BROADCAST_DISTANCE, + SET_GPS_UPDATE_INTERVAL, ENABLE_BLUETOOTH, TOGGLE_APPLET, TOGGLE_AUTOSHOW_APPLET, diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 5923f6b93..657cc667c 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -36,11 +36,36 @@ struct DisplayTimeoutOption { const char *label; }; +struct UInt32Option { + uint32_t value; + const char *label; +}; + static constexpr DisplayTimeoutOption DISPLAY_TIMEOUT_OPTIONS[] = { {0, "Forever"}, {30, "30 secs"}, {60, "1 min"}, {5 * 60, "5 min"}, {15 * 60, "15 min"}, {30 * 60, "30 min"}, {60 * 60, "1 hr"}, }; +static constexpr UInt32Option POSITION_BROADCAST_OPTIONS[] = { + {60, "1 min"}, {90, "90 sec"}, {5 * 60, "5 min"}, {15 * 60, "15 min"}, + {60 * 60, "1 hr"}, {2 * 60 * 60, "2 hr"}, {3 * 60 * 60, "3 hr"}, {4 * 60 * 60, "4 hr"}, + {5 * 60 * 60, "5 hr"}, {6 * 60 * 60, "6 hr"}, {12 * 60 * 60, "12 hr"}, {18 * 60 * 60, "18 hr"}, + {24 * 60 * 60, "24 hr"}, {36 * 60 * 60, "36 hr"}, {48 * 60 * 60, "48 hr"}, {72 * 60 * 60, "72 hr"}, +}; + +static constexpr UInt32Option GPS_UPDATE_INTERVAL_OPTIONS[] = { + {8, "8 sec"}, {20, "20 sec"}, {40, "40 sec"}, {60, "1 min"}, {80, "80 sec"}, + {2 * 60, "2 min"}, {5 * 60, "5 min"}, {10 * 60, "10 min"}, {15 * 60, "15 min"}, {30 * 60, "30 min"}, + {60 * 60, "1 hr"}, {6 * 60 * 60, "6 hr"}, {12 * 60 * 60, "12 hr"}, {24 * 60 * 60, "24 hr"}, {2147483647UL, "At Boot"}, +}; + +static constexpr UInt32Option SMART_INTERVAL_OPTIONS[] = { + {5 * 60, "5 min"}, {10 * 60, "10 min"}, {15 * 60, "15 min"}, {30 * 60, "30 min"}, {60 * 60, "1 hr"}, + {2 * 60 * 60, "2 hr"}, {6 * 60 * 60, "6 hr"}, {12 * 60 * 60, "12 hr"}, {24 * 60 * 60, "24 hr"}, +}; + +static constexpr uint32_t SMART_DISTANCE_OPTIONS[] = {20, 50, 100, 250, 500, 1000, 2000, 5000}; + struct PositionPrecisionOption { uint8_t value; // proto value const char *metric; @@ -65,6 +90,30 @@ static const char *getDisplayTimeoutLabel(uint32_t timeoutSeconds) return "Custom"; } +static std::string getUInt32OptionLabel(const UInt32Option *options, uint8_t optionCount, uint32_t value, + const char *zeroLabel = nullptr) +{ + for (uint8_t i = 0; i < optionCount; i++) { + if (options[i].value == value) { + return options[i].label; + } + } + + if (value == 0) { + return zeroLabel ? zeroLabel : "0 sec"; + } + if (value == 2147483647UL) { + return "At Boot"; + } + if (value % (60 * 60) == 0) { + return std::to_string(value / (60 * 60)) + " hr"; + } + if (value % 60 == 0) { + return std::to_string(value / 60) + " min"; + } + return std::to_string(value) + " sec"; +} + static bool supportsFreeTextKeyboard(const InkHUD::InkHUD *inkhud, const InkHUD::Persistence::Settings *settings) { return !inkhud->twoWayRocker && (settings->joystick.enabled || inkhud->hasTouchEnabledProvider()); @@ -333,6 +382,17 @@ static void applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset preset) rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } +static void applyConfigReload(uint32_t changes = SEGMENT_CONFIG, bool reboot = false) +{ + nodeDB->saveToDisk(changes); + service->reloadConfig(changes); + + if (reboot) { + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } +} + static const char *getTimezoneLabelFromValue(const char *tzdef) { if (!tzdef || !*tzdef) @@ -559,6 +619,51 @@ void InkHUD::MenuApplet::execute(MenuItem item) #endif break; + case TOGGLE_SMART_POSITION: + config.position.position_broadcast_smart_enabled = !config.position.position_broadcast_smart_enabled; + applyConfigReload(SEGMENT_CONFIG, true); + break; + + case SET_POSITION_BROADCAST_INTERVAL: { + const uint8_t index = cursor - 1; + constexpr uint8_t optionCount = sizeof(POSITION_BROADCAST_OPTIONS) / sizeof(POSITION_BROADCAST_OPTIONS[0]); + if (index < optionCount && config.position.position_broadcast_secs != POSITION_BROADCAST_OPTIONS[index].value) { + config.position.position_broadcast_secs = POSITION_BROADCAST_OPTIONS[index].value; + applyConfigReload(SEGMENT_CONFIG, true); + } + break; + } + + case SET_SMART_BROADCAST_INTERVAL: { + const uint8_t index = cursor - 1; + constexpr uint8_t optionCount = sizeof(SMART_INTERVAL_OPTIONS) / sizeof(SMART_INTERVAL_OPTIONS[0]); + if (index < optionCount && config.position.broadcast_smart_minimum_interval_secs != SMART_INTERVAL_OPTIONS[index].value) { + config.position.broadcast_smart_minimum_interval_secs = SMART_INTERVAL_OPTIONS[index].value; + applyConfigReload(SEGMENT_CONFIG, true); + } + break; + } + + case SET_SMART_BROADCAST_DISTANCE: { + const uint8_t index = cursor - 1; + constexpr uint8_t optionCount = sizeof(SMART_DISTANCE_OPTIONS) / sizeof(SMART_DISTANCE_OPTIONS[0]); + if (index < optionCount && config.position.broadcast_smart_minimum_distance != SMART_DISTANCE_OPTIONS[index]) { + config.position.broadcast_smart_minimum_distance = SMART_DISTANCE_OPTIONS[index]; + applyConfigReload(SEGMENT_CONFIG, true); + } + break; + } + + case SET_GPS_UPDATE_INTERVAL: { + const uint8_t index = cursor - 1; + constexpr uint8_t optionCount = sizeof(GPS_UPDATE_INTERVAL_OPTIONS) / sizeof(GPS_UPDATE_INTERVAL_OPTIONS[0]); + if (index < optionCount && config.position.gps_update_interval != GPS_UPDATE_INTERVAL_OPTIONS[index].value) { + config.position.gps_update_interval = GPS_UPDATE_INTERVAL_OPTIONS[index].value; + applyConfigReload(SEGMENT_CONFIG, true); + } + break; + } + case ENABLE_BLUETOOTH: // This helps users recover from a bad wifi config LOG_INFO("Enabling Bluetooth"); @@ -1199,6 +1304,9 @@ void InkHUD::MenuApplet::showPage(MenuPage page) case NODE_CONFIG_POSITION: { previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); + + items.push_back(MenuItem::Header("Device GPS")); + #if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS const auto mode = config.position.gps_mode; if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { @@ -1206,12 +1314,93 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } else { gpsEnabled = (mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED); items.push_back(MenuItem("GPS", MenuAction::TOGGLE_GPS, MenuPage::NODE_CONFIG_POSITION, &gpsEnabled)); + + nodeConfigLabels.emplace_back( + "GPS Poll: " + getUInt32OptionLabel(GPS_UPDATE_INTERVAL_OPTIONS, + sizeof(GPS_UPDATE_INTERVAL_OPTIONS) / sizeof(GPS_UPDATE_INTERVAL_OPTIONS[0]), + config.position.gps_update_interval, "Default")); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, + MenuPage::NODE_CONFIG_POSITION_GPS_UPDATE_INTERVAL)); } #endif + + items.push_back(MenuItem::Header("Position Packet")); + + nodeConfigLabels.emplace_back( + "Broadcast: " + getUInt32OptionLabel(POSITION_BROADCAST_OPTIONS, + sizeof(POSITION_BROADCAST_OPTIONS) / sizeof(POSITION_BROADCAST_OPTIONS[0]), + config.position.position_broadcast_secs)); + items.push_back( + MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_POSITION_BROADCAST_INTERVAL)); + + items.push_back(MenuItem("Smart Pos", MenuAction::TOGGLE_SMART_POSITION, MenuPage::NODE_CONFIG_POSITION, + &config.position.position_broadcast_smart_enabled)); + + if (config.position.position_broadcast_smart_enabled) { + nodeConfigLabels.emplace_back("Smart Int: " + + getUInt32OptionLabel(SMART_INTERVAL_OPTIONS, + sizeof(SMART_INTERVAL_OPTIONS) / sizeof(SMART_INTERVAL_OPTIONS[0]), + config.position.broadcast_smart_minimum_interval_secs)); + items.push_back( + MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_POSITION_SMART_INTERVAL)); + + nodeConfigLabels.emplace_back("Smart Dist: " + localizeDistance(config.position.broadcast_smart_minimum_distance)); + items.push_back( + MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_POSITION_SMART_DISTANCE)); + } + items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } + case NODE_CONFIG_POSITION_BROADCAST_INTERVAL: + previousPage = MenuPage::NODE_CONFIG_POSITION; + items.push_back(MenuItem("Back", previousPage)); + items.push_back(MenuItem::Header("Max time between sends")); + for (const auto &option : POSITION_BROADCAST_OPTIONS) { + nodeConfigLabels.emplace_back(option.label); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::SET_POSITION_BROADCAST_INTERVAL, + MenuPage::NODE_CONFIG_POSITION)); + } + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + case NODE_CONFIG_POSITION_SMART_INTERVAL: + previousPage = MenuPage::NODE_CONFIG_POSITION; + items.push_back(MenuItem("Back", previousPage)); + items.push_back(MenuItem::Header("Fastest smart resend")); + for (const auto &option : SMART_INTERVAL_OPTIONS) { + nodeConfigLabels.emplace_back(option.label); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::SET_SMART_BROADCAST_INTERVAL, + MenuPage::NODE_CONFIG_POSITION)); + } + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + case NODE_CONFIG_POSITION_SMART_DISTANCE: + previousPage = MenuPage::NODE_CONFIG_POSITION; + items.push_back(MenuItem("Back", previousPage)); + items.push_back(MenuItem::Header("Move this far to send")); + for (const auto &option : SMART_DISTANCE_OPTIONS) { + nodeConfigLabels.emplace_back(localizeDistance(option)); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::SET_SMART_BROADCAST_DISTANCE, + MenuPage::NODE_CONFIG_POSITION)); + } + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + case NODE_CONFIG_POSITION_GPS_UPDATE_INTERVAL: + previousPage = MenuPage::NODE_CONFIG_POSITION; + items.push_back(MenuItem("Back", previousPage)); + items.push_back(MenuItem::Header("GPS poll cadence")); + for (const auto &option : GPS_UPDATE_INTERVAL_OPTIONS) { + nodeConfigLabels.emplace_back(option.label); + items.push_back( + MenuItem(nodeConfigLabels.back().c_str(), MenuAction::SET_GPS_UPDATE_INTERVAL, MenuPage::NODE_CONFIG_POSITION)); + } + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + case NODE_CONFIG_POWER: { previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); @@ -2324,26 +2513,29 @@ void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t const int16_t divY = top + height; height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item) - // Create a variable number of columns - // Either 3 or 4, depending on whether we have GPS - // Todo - constexpr uint8_t N_COL = 3; - int16_t colL[N_COL]; - int16_t colC[N_COL]; - int16_t colR[N_COL]; - for (uint8_t i = 0; i < N_COL; i++) { - colL[i] = left + ((width / N_COL) * i); - colC[i] = colL[i] + ((width / N_COL) / 2); - colR[i] = colL[i] + (width / N_COL); + // Create a variable number of columns. +#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS + const bool showGpsInfo = config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT; +#else + const bool showGpsInfo = false; +#endif + const uint8_t columnCount = showGpsInfo ? 4 : 3; + int16_t colL[4] = {}; + int16_t colC[4] = {}; + int16_t colR[4] = {}; + for (uint8_t i = 0; i < columnCount; i++) { + colL[i] = left + ((width / columnCount) * i); + colC[i] = colL[i] + ((width / columnCount) / 2); + colR[i] = colL[i] + (width / columnCount); } // Info blocks, left to right // Voltage float voltage = powerStatus->getBatteryVoltageMv() / 1000.0; - char voltageStr[6]; // "XX.XV" - sprintf(voltageStr, "%.2fV", voltage); - printAt(colC[0], labelT, "Bat", CENTER, TOP); + char voltageStr[8]; // e.g. "4.12" + snprintf(voltageStr, sizeof(voltageStr), "%.2f", voltage); + printAt(colC[0], labelT, "Bat V", CENTER, TOP); printAt(colC[0], valT, voltageStr, CENTER, TOP); // Divider @@ -2351,8 +2543,8 @@ void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t drawPixel(colR[0], y, BLACK); // Channel Util - char chUtilStr[4]; // "XX%" - sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent()); + char chUtilStr[8]; // e.g. "100%" + snprintf(chUtilStr, sizeof(chUtilStr), "%2.f%%", airTime->channelUtilizationPercent()); printAt(colC[1], labelT, "Ch", CENTER, TOP); printAt(colC[1], valT, chUtilStr, CENTER, TOP); @@ -2361,20 +2553,34 @@ void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t drawPixel(colR[1], y, BLACK); // Duty Cycle (AirTimeTx) - char dutyUtilStr[4]; // "XX%" - sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent()); + char dutyUtilStr[8]; // e.g. "100%" + snprintf(dutyUtilStr, sizeof(dutyUtilStr), "%2.f%%", airTime->utilizationTXPercent()); printAt(colC[2], labelT, "Duty", CENTER, TOP); printAt(colC[2], valT, dutyUtilStr, CENTER, TOP); - /* - // Divider - for (int16_t y = valT; y <= divY; y += 3) - drawPixel(colR[2], y, BLACK); + if (showGpsInfo) { + // Divider + for (int16_t y = valT; y <= divY; y += 3) + drawPixel(colR[2], y, BLACK); - // GPS satellites - todo - printAt(colC[3], labelT, "Sats", CENTER, TOP); - printAt(colC[3], valT, "ToDo", CENTER, TOP); - */ + const bool gpsDisabled = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED; + const char *gpsLabel = "GPS"; + printAt(colC[3], labelT, gpsLabel, CENTER, TOP); + + if (gpsDisabled) { + const int16_t labelW = getTextWidth(gpsLabel); + const int16_t strikeY = labelT + (fontSmall.lineHeight() / 2); + drawLine(colC[3] - (labelW / 2), strikeY, colC[3] + ((labelW - 1) / 2), strikeY, BLACK); + } + + if (!gpsDisabled && gpsStatus != nullptr && gpsStatus->getIsConnected()) { + char satsStr[12]; + snprintf(satsStr, sizeof(satsStr), "%lu", (unsigned long)gpsStatus->getNumSatellites()); + printAt(colC[3], valT, satsStr, CENTER, TOP); + } else { + printAt(colC[3], valT, "--", CENTER, TOP); + } + } // Horizontal divider, at bottom of system info panel for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h index 275bb97f2..925aa7087 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h @@ -35,6 +35,10 @@ enum MenuPage : uint8_t { NODE_CONFIG_DISPLAY_TIMEOUT, NODE_CONFIG_BLUETOOTH, NODE_CONFIG_POSITION, + NODE_CONFIG_POSITION_BROADCAST_INTERVAL, + NODE_CONFIG_POSITION_SMART_INTERVAL, + NODE_CONFIG_POSITION_SMART_DISTANCE, + NODE_CONFIG_POSITION_GPS_UPDATE_INTERVAL, NODE_CONFIG_ADMIN_RESET, NODE_CONFIG_ADMIN_MESSAGES, TIMEZONE, diff --git a/src/graphics/niche/InkHUD/docs/README.md b/src/graphics/niche/InkHUD/docs/README.md index 1e67ef943..bea373b7a 100644 --- a/src/graphics/niche/InkHUD/docs/README.md +++ b/src/graphics/niche/InkHUD/docs/README.md @@ -268,6 +268,13 @@ Tiles are 256×256 pixels, 1-bit (column-major bit packing), compressed per tile When the menu is opened from a map applet, zoom controls appear automatically. Zoom In and Zoom Out step through the available tile zoom levels. Reset Zoom returns to the default auto-fit behavior, where the map scales to show all visible nodes. +##### Position Menu + +InkHUD's `Node Config -> Position` page now exposes the common position controls directly in the menu applet, using the same compact selector pattern as other InkHUD config pages. + +- Device GPS controls: GPS enable/disable, GPS polling interval +- Position packet controls: broadcast interval, smart position toggle, smart minimum interval, smart minimum distance + #### System Applets So far, we have been talking about "user applets". We also recognize a separate category of "system applets". These handle things like the menu, and the boot screen. These often need special handling, and need to be implemented manually.