From c9b4ef460d0f39a9af3a2cfd2bc1073766f2d54a Mon Sep 17 00:00:00 2001 From: Todd Herbert Date: Sun, 16 Jun 2024 16:33:43 +1200 Subject: [PATCH 1/5] Clear expired or deleted waypoint frame --- src/graphics/Screen.cpp | 46 +++++++++++++++++++++++++++++++++++++++-- src/graphics/Screen.h | 3 +++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 60168cffcf..38a22d3f9e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -43,6 +43,7 @@ along with this program. If not, see . #include "meshUtils.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" +#include "modules/WaypointModule.h" #include "sleep.h" #include "target_specific.h" @@ -446,6 +447,36 @@ static bool shouldDrawMessage(const meshtastic_MeshPacket *packet) return packet->from != 0 && !moduleConfig.store_forward.enabled; } +// +static bool shouldDrawWaypoint(const meshtastic_MeshPacket *packet) +{ +#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 scratch; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(packet->decoded.payload.bytes, packet->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { + // Valid waypoint + if (scratch.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"); + return devicestate.has_rx_waypoint = false; +#else + return false; +#endif +} + // Draw power bars or a charging indicator on an image of a battery, determined by battery charge voltage or percentage. static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const PowerStatus *powerStatus) { @@ -1806,6 +1837,8 @@ void Screen::setup() textMessageObserver.observe(textMessageModule); if (inputBroker) inputObserver.observe(inputBroker); + if (waypointModule) + waypointObserver.observe(waypointModule); // Modules can notify screen about refresh MeshModule::observeUIEvents(&uiFrameEventObserver); @@ -2133,9 +2166,12 @@ 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 we have a waypoint (not expired, not deleted) if (devicestate.has_rx_waypoint && shouldDrawMessage(&devicestate.rx_waypoint)) { - normalFrames[numframes++] = drawWaypointFrame; + // If waypoint valid + if (shouldDrawWaypoint(&devicestate.rx_waypoint)) + normalFrames[numframes++] = drawWaypointFrame; } // then all the nodes @@ -2736,6 +2772,12 @@ int Screen::handleInputEvent(const InputEvent *event) return 0; } +int Screen::handleWaypoint(const meshtastic_MeshPacket *arg) +{ + // TODO: move to appropriate frame when redrawing + setFrames(); +} + } // namespace graphics #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index f4d7197152..b1bbffc3b2 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -126,6 +126,8 @@ class Screen : public concurrency::OSThread CallbackObserver(this, &Screen::handleStatusUpdate); CallbackObserver textMessageObserver = CallbackObserver(this, &Screen::handleTextMessage); + CallbackObserver waypointObserver = + CallbackObserver(this, &Screen::handleWaypoint); CallbackObserver uiFrameEventObserver = CallbackObserver(this, &Screen::handleUIFrameEvent); CallbackObserver inputObserver = @@ -336,6 +338,7 @@ class Screen : public concurrency::OSThread int handleTextMessage(const meshtastic_MeshPacket *arg); int handleUIFrameEvent(const UIFrameEvent *arg); int handleInputEvent(const InputEvent *arg); + int handleWaypoint(const meshtastic_MeshPacket *arg); /// Used to force (super slow) eink displays to draw critical frames void forceDisplay(bool forceUiUpdate = false); From f155ff59d3207459a152236433044f3e23cc2e62 Mon Sep 17 00:00:00 2001 From: Todd Herbert Date: Sun, 16 Jun 2024 17:16:51 +1200 Subject: [PATCH 2/5] Return 0 to CallbackObserver --- src/graphics/Screen.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 38a22d3f9e..cfeb9449f0 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2776,6 +2776,7 @@ int Screen::handleWaypoint(const meshtastic_MeshPacket *arg) { // TODO: move to appropriate frame when redrawing setFrames(); + return 0; } } // namespace graphics From 33342e614fdb31b6229c32b47fa0c8a4aaa3c3f2 Mon Sep 17 00:00:00 2001 From: Todd Herbert Date: Sun, 16 Jun 2024 22:14:00 +1200 Subject: [PATCH 3/5] Add a missing comment --- src/graphics/Screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index cfeb9449f0..23bcea58a6 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -447,7 +447,7 @@ static bool shouldDrawMessage(const meshtastic_MeshPacket *packet) return packet->from != 0 && !moduleConfig.store_forward.enabled; } -// +// Determine whether the waypoint frame should be drawn (waypoint deleted? expired?) static bool shouldDrawWaypoint(const meshtastic_MeshPacket *packet) { #if !MESHTASTIC_EXCLUDE_WAYPOINT From 79ea04fd380116f4051240c5764ba70bb21d2edc Mon Sep 17 00:00:00 2001 From: Todd Herbert Date: Sun, 23 Jun 2024 00:58:42 +1200 Subject: [PATCH 4/5] Draw compass for waypoint frame --- src/graphics/Screen.cpp | 216 +++++++++++++++++++++++++++------------- 1 file changed, 145 insertions(+), 71 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 23bcea58a6..0ec6286c9b 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -60,6 +60,9 @@ along with this program. If not, see . #include "platform/portduino/PortduinoGlue.h" #endif +/// Convert an integer GPS coords to a floating point +#define DegD(i) (i * 1e-7) + using namespace meshtastic; /** @todo remove */ namespace graphics @@ -457,11 +460,11 @@ static bool shouldDrawWaypoint(const meshtastic_MeshPacket *packet) // Decode the message, to find the expiration time (is waypoint still valid) // This handles "deletion" as well as expiration - meshtastic_Waypoint scratch; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(packet->decoded.payload.bytes, packet->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { + meshtastic_Waypoint wp; + memset(&wp, 0, sizeof(wp)); + if (pb_decode_from_bytes(packet->decoded.payload.bytes, packet->decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { // Valid waypoint - if (scratch.expire > getTime()) + if (wp.expire > getTime()) return devicestate.has_rx_waypoint = true; // Expired, or deleted @@ -471,7 +474,8 @@ static bool shouldDrawWaypoint(const meshtastic_MeshPacket *packet) // If decoding failed LOG_ERROR("Failed to decode waypoint\n"); - return devicestate.has_rx_waypoint = false; + devicestate.has_rx_waypoint = false; + return false; #else return false; #endif @@ -1122,43 +1126,6 @@ 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) { @@ -1484,8 +1451,35 @@ static void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t com drawLine(display, N1, N4); } -/// Convert an integer GPS coords to a floating point -#define DegD(i) (i * 1e-7) +// Get a string representation of the time passed since something happened +static void 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"); +} static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -1525,34 +1519,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"); + getTimeAgoStr(sinceLastSeen(node), lastStr, sizeof(lastStr)); static char distStr[20]; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { @@ -1627,6 +1595,112 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ drawColumns(display, x, y, fields); } +/// Draw the last waypoint we received +static void drawWaypointFrame(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]; + 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}; + + // Co-ordinates for the center of the compass/circle + int16_t compassX = 0, compassY = 0; + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + compassX = x + SCREEN_WIDTH - getCompassDiam(display) / 2 - 5; + compassY = y + SCREEN_HEIGHT / 2; + } else { + compassX = x + SCREEN_WIDTH - getCompassDiam(display) / 2 - 5; + compassY = y + FONT_HEIGHT_SMALL + (SCREEN_HEIGHT - 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 = estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + 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; + drawNodeHeading(display, compassX, compassY, 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, getCompassDiam(display) / 2); + + // Must be after distStr is populated + drawColumns(display, x, y, fields); +} + Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_OledType screenType, OLEDDISPLAY_GEOMETRY geometry) : concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32) { From b8f17085fd962c71a3542fe1cd93f126e844149d Mon Sep 17 00:00:00 2001 From: Todd Herbert Date: Mon, 24 Jun 2024 17:38:18 +1200 Subject: [PATCH 5/5] Display our own waypoints --- src/graphics/Screen.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0ec6286c9b..eb92d824e7 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2242,10 +2242,8 @@ void Screen::setFrames() } // If we have a waypoint (not expired, not deleted) - if (devicestate.has_rx_waypoint && shouldDrawMessage(&devicestate.rx_waypoint)) { - // If waypoint valid - if (shouldDrawWaypoint(&devicestate.rx_waypoint)) - normalFrames[numframes++] = drawWaypointFrame; + if (devicestate.has_rx_waypoint && shouldDrawWaypoint(&devicestate.rx_waypoint)) { + normalFrames[numframes++] = drawWaypointFrame; } // then all the nodes