InkHUD: GPS improvements (#10846)

* GPS improvements

* Update

* Update MenuApplet.cpp

* Remove fixed position

* Update MenuApplet.cpp
This commit is contained in:
HarukiToreda
2026-07-02 17:09:50 -05:00
committed by GitHub
co-authored by GitHub
parent d6bd63ecfb
commit 43440dbb9b
4 changed files with 248 additions and 26 deletions
@@ -26,6 +26,11 @@ enum MenuAction {
NEXT_TILE, NEXT_TILE,
TOGGLE_BACKLIGHT, TOGGLE_BACKLIGHT,
TOGGLE_GPS, TOGGLE_GPS,
TOGGLE_SMART_POSITION,
SET_POSITION_BROADCAST_INTERVAL,
SET_SMART_BROADCAST_INTERVAL,
SET_SMART_BROADCAST_DISTANCE,
SET_GPS_UPDATE_INTERVAL,
ENABLE_BLUETOOTH, ENABLE_BLUETOOTH,
TOGGLE_APPLET, TOGGLE_APPLET,
TOGGLE_AUTOSHOW_APPLET, TOGGLE_AUTOSHOW_APPLET,
@@ -36,11 +36,36 @@ struct DisplayTimeoutOption {
const char *label; const char *label;
}; };
struct UInt32Option {
uint32_t value;
const char *label;
};
static constexpr DisplayTimeoutOption DISPLAY_TIMEOUT_OPTIONS[] = { static constexpr DisplayTimeoutOption DISPLAY_TIMEOUT_OPTIONS[] = {
{0, "Forever"}, {30, "30 secs"}, {60, "1 min"}, {5 * 60, "5 min"}, {0, "Forever"}, {30, "30 secs"}, {60, "1 min"}, {5 * 60, "5 min"},
{15 * 60, "15 min"}, {30 * 60, "30 min"}, {60 * 60, "1 hr"}, {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 { struct PositionPrecisionOption {
uint8_t value; // proto value uint8_t value; // proto value
const char *metric; const char *metric;
@@ -65,6 +90,30 @@ static const char *getDisplayTimeoutLabel(uint32_t timeoutSeconds)
return "Custom"; 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) static bool supportsFreeTextKeyboard(const InkHUD::InkHUD *inkhud, const InkHUD::Persistence::Settings *settings)
{ {
return !inkhud->twoWayRocker && (settings->joystick.enabled || inkhud->hasTouchEnabledProvider()); 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; 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) static const char *getTimezoneLabelFromValue(const char *tzdef)
{ {
if (!tzdef || !*tzdef) if (!tzdef || !*tzdef)
@@ -559,6 +619,51 @@ void InkHUD::MenuApplet::execute(MenuItem item)
#endif #endif
break; 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: case ENABLE_BLUETOOTH:
// This helps users recover from a bad wifi config // This helps users recover from a bad wifi config
LOG_INFO("Enabling Bluetooth"); LOG_INFO("Enabling Bluetooth");
@@ -1199,6 +1304,9 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
case NODE_CONFIG_POSITION: { case NODE_CONFIG_POSITION: {
previousPage = MenuPage::NODE_CONFIG; previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem::Header("Device GPS"));
#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS #if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS
const auto mode = config.position.gps_mode; const auto mode = config.position.gps_mode;
if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
@@ -1206,12 +1314,93 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
} else { } else {
gpsEnabled = (mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED); gpsEnabled = (mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED);
items.push_back(MenuItem("GPS", MenuAction::TOGGLE_GPS, MenuPage::NODE_CONFIG_POSITION, &gpsEnabled)); 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 #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)); items.push_back(MenuItem("Exit", MenuPage::EXIT));
break; 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: { case NODE_CONFIG_POWER: {
previousPage = MenuPage::NODE_CONFIG; previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage)); 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; const int16_t divY = top + height;
height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item) height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item)
// Create a variable number of columns // Create a variable number of columns.
// Either 3 or 4, depending on whether we have GPS #if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS
// Todo const bool showGpsInfo = config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT;
constexpr uint8_t N_COL = 3; #else
int16_t colL[N_COL]; const bool showGpsInfo = false;
int16_t colC[N_COL]; #endif
int16_t colR[N_COL]; const uint8_t columnCount = showGpsInfo ? 4 : 3;
for (uint8_t i = 0; i < N_COL; i++) { int16_t colL[4] = {};
colL[i] = left + ((width / N_COL) * i); int16_t colC[4] = {};
colC[i] = colL[i] + ((width / N_COL) / 2); int16_t colR[4] = {};
colR[i] = colL[i] + (width / N_COL); 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 // Info blocks, left to right
// Voltage // Voltage
float voltage = powerStatus->getBatteryVoltageMv() / 1000.0; float voltage = powerStatus->getBatteryVoltageMv() / 1000.0;
char voltageStr[6]; // "XX.XV" char voltageStr[8]; // e.g. "4.12"
sprintf(voltageStr, "%.2fV", voltage); snprintf(voltageStr, sizeof(voltageStr), "%.2f", voltage);
printAt(colC[0], labelT, "Bat", CENTER, TOP); printAt(colC[0], labelT, "Bat V", CENTER, TOP);
printAt(colC[0], valT, voltageStr, CENTER, TOP); printAt(colC[0], valT, voltageStr, CENTER, TOP);
// Divider // Divider
@@ -2351,8 +2543,8 @@ void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t
drawPixel(colR[0], y, BLACK); drawPixel(colR[0], y, BLACK);
// Channel Util // Channel Util
char chUtilStr[4]; // "XX%" char chUtilStr[8]; // e.g. "100%"
sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent()); snprintf(chUtilStr, sizeof(chUtilStr), "%2.f%%", airTime->channelUtilizationPercent());
printAt(colC[1], labelT, "Ch", CENTER, TOP); printAt(colC[1], labelT, "Ch", CENTER, TOP);
printAt(colC[1], valT, chUtilStr, 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); drawPixel(colR[1], y, BLACK);
// Duty Cycle (AirTimeTx) // Duty Cycle (AirTimeTx)
char dutyUtilStr[4]; // "XX%" char dutyUtilStr[8]; // e.g. "100%"
sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent()); snprintf(dutyUtilStr, sizeof(dutyUtilStr), "%2.f%%", airTime->utilizationTXPercent());
printAt(colC[2], labelT, "Duty", CENTER, TOP); printAt(colC[2], labelT, "Duty", CENTER, TOP);
printAt(colC[2], valT, dutyUtilStr, CENTER, TOP); printAt(colC[2], valT, dutyUtilStr, CENTER, TOP);
/* if (showGpsInfo) {
// Divider // Divider
for (int16_t y = valT; y <= divY; y += 3) for (int16_t y = valT; y <= divY; y += 3)
drawPixel(colR[2], y, BLACK); drawPixel(colR[2], y, BLACK);
// GPS satellites - todo const bool gpsDisabled = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED;
printAt(colC[3], labelT, "Sats", CENTER, TOP); const char *gpsLabel = "GPS";
printAt(colC[3], valT, "ToDo", CENTER, TOP); 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 // 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 for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item
@@ -35,6 +35,10 @@ enum MenuPage : uint8_t {
NODE_CONFIG_DISPLAY_TIMEOUT, NODE_CONFIG_DISPLAY_TIMEOUT,
NODE_CONFIG_BLUETOOTH, NODE_CONFIG_BLUETOOTH,
NODE_CONFIG_POSITION, 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_RESET,
NODE_CONFIG_ADMIN_MESSAGES, NODE_CONFIG_ADMIN_MESSAGES,
TIMEZONE, TIMEZONE,
+7
View File
@@ -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. 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 #### 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. 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.