diff --git a/src/graphics/BRC.cpp b/src/graphics/BRC.cpp new file mode 100644 index 000000000..a65a8ee7c --- /dev/null +++ b/src/graphics/BRC.cpp @@ -0,0 +1,152 @@ +#include "BRC.h" +#include "GPSStatus.h" +#include "gps/GeoCoord.h" +#include "graphics/Screen.h" + +using namespace meshtastic; + +const int32_t BRC_LATI = (40.786958 * 1e7); +const int32_t BRC_LONI = (-119.202994 * 1e7); +const double BRC_LATF = 40.786958; +const double BRC_LONF = -119.202994; +const double BRC_NOON = 1.5; +const double RAD_TO_HOUR = (6.0 / 3.14159); +const double METER_TO_FEET = 3.28084; +const double FEET_TO_METER = 1.0 / METER_TO_FEET; + +// Pre-calculated street data for performance +struct StreetInfo { + float center; + float width; + const char *name; +}; + +/* +# python code to generate the StreetInfo + +esp_center = 2500 +street_info = [ + # name, width, preceeding block depth + ('Esp', 40, 60), # block size is fake + ('A', 30, 400), + ('B', 30, 250), + ('C', 30, 250), + ('D', 30, 250), + ('E', 40, 250), + ('F', 30, 450), # E-F block is exra deep + ('G', 30, 250), + ('H', 30, 250), + ('I', 30, 250), + ('J', 30, 150), + ('K', 50, 150), +] + +street_center = esp_center - street_info[0][1] //2 - street_info[0][2] +last_center = esp_center +for (name, street_width, block_width) in street_info: + offset = (street_width + block_width) // 2 + street_center += street_width //2 + block_width + + dia = street_center * 2 + dist = street_center - last_center + + print(f"{{{street_center}, {offset}, \"{name}\"}},\t// +{dist}ft\tdia: {dia:,}ft") + + last_center = street_center + street_center += street_width //2 + +street_center += 50 # extra buffer after the edge of k to include walk-in camping parking +print(f"{{{street_center}, 0, nullptr}},\t// +{street_center-last_center}ft") +*/ + +static const StreetInfo streets[] = { + {2500, 50, "Esp"}, // +0ft dia: 5,000ft + {2935, 215, "A"}, // +435ft dia: 5,870ft + {3215, 140, "B"}, // +280ft dia: 6,430ft + {3495, 140, "C"}, // +280ft dia: 6,990ft + {3775, 140, "D"}, // +280ft dia: 7,550ft + {4060, 145, "E"}, // +285ft dia: 8,120ft + {4545, 240, "F"}, // +485ft dia: 9,090ft + {4825, 140, "G"}, // +280ft dia: 9,650ft + {5105, 140, "H"}, // +280ft dia: 10,210ft + {5385, 140, "I"}, // +280ft dia: 10,770ft + {5565, 90, "J"}, // +180ft dia: 11,130ft + {5755, 100, "K"}, // +190ft dia: 11,510ft + {5830, 0, nullptr}, // +75ft +}; + +BRCAddress::BRCAddress(int32_t lat, int32_t lon) +{ + bearing = GeoCoord::bearing(BRC_LATF, BRC_LONF, DegD(lat), DegD(lon)) * RAD_TO_HOUR; + bearing += 12.0 - BRC_NOON; + while (bearing > 12.0) { + bearing -= 12.0; + } + + // In imperial units because that is how golden spike data is provided. + distance = GeoCoord::latLongToMeter(BRC_LATF, BRC_LONF, DegD(lat), DegD(lon)) * METER_TO_FEET; +}; + +int BRCAddress::radial(char *buf, size_t len) +{ + uint8_t hour = (uint8_t)(bearing); + uint8_t minute = (uint8_t)((bearing - hour) * 60.0); + hour %= 12; + if (hour == 0) { + hour = 12; + } + return snprintf(buf, len, "%d:%02d", hour, minute); +}; + +int BRCAddress::annular(char *buf, size_t len, bool noUnit) +{ + const char *unit = "m"; + float unitMultiplier = FEET_TO_METER; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + unitMultiplier = 1.0; + unit = "ft"; + } + if (noUnit) + unit = ""; + + if (bearing > 1.75 && bearing < 10.25) { + const char *street = nullptr; + float dist = 0; + // Find the appropriate street based on distance + for (const auto &s : streets) { + if (distance > s.center - s.width) { + street = s.name; + dist = distance - s.center; + } else { + break; + } + } + if (street) { + return snprintf(buf, len, "%s %d%s", street, int(dist * unitMultiplier), unit); + } + } + + return snprintf(buf, len, "%d%s", int(distance * unitMultiplier), unit); +}; + +int BRCAddress::full(char *buf, size_t len) +{ + auto l = radial(buf, len - 4); + buf += l; + *(buf++) = ' '; + *(buf++) = '&'; + *(buf++) = ' '; + buf += annular(buf, len - l - 4, false); + buf[l] = 0; // always null terminated + return l; +}; + +int BRCAddress::compact(char *buf, size_t len) +{ + auto l = radial(buf, len - 2); + buf += l; + *(buf++) = '&'; + buf += annular(buf, len - l - 2, true); + buf[l] = 0; // always null terminated + return l; +}; diff --git a/src/graphics/BRC.h b/src/graphics/BRC.h index 16daff260..9dc85e15c 100644 --- a/src/graphics/BRC.h +++ b/src/graphics/BRC.h @@ -1,147 +1,19 @@ #pragma once -#include "GPSStatus.h" -#include "gps/GeoCoord.h" -#include "graphics/Screen.h" - -using namespace meshtastic; - -const int32_t BRC_LATI = (40.786958 * 1e7); -const int32_t BRC_LONI = (-119.202994 * 1e7); -const double BRC_LATF = 40.786958; -const double BRC_LONF = -119.202994; -const double BRC_NOON = 1.5; -const double RAD_TO_HOUR = (6.0 / 3.14159); -const double METER_TO_FEET = 3.28084; -const double FEET_TO_METER = 1.0 / METER_TO_FEET; - -// Pre-calculated street data for performance -struct StreetInfo { - float center; - float width; - const char *name; -}; - -/* -# python code to generate the StreetInfo - -esp_center = 2500 -street_info = [ - # name, width, preceeding block depth - ('Esp', 40, 60), # block size is fake - ('A', 30, 400), - ('B', 30, 250), - ('C', 30, 250), - ('D', 30, 250), - ('E', 40, 250), - ('F', 30, 450), # E-F block is exra deep - ('G', 30, 250), - ('H', 30, 250), - ('I', 30, 250), - ('J', 30, 150), - ('K', 50, 150), -] - -street_center = esp_center - street_info[0][1] //2 - street_info[0][2] -last_center = esp_center -for (name, street_width, block_width) in street_info: - offset = (street_width + block_width) // 2 - street_center += street_width //2 + block_width - - dia = street_center * 2 - dist = street_center - last_center - - print(f"{{{street_center}, {offset}, \"{name}\"}},\t// +{dist}ft\tdia: {dia:,}ft") - - last_center = street_center - street_center += street_width //2 - -street_center += 50 # extra buffer after the edge of k to include walk-in camping parking -print(f"{{{street_center}, 0, nullptr}},\t// +{street_center-last_center}ft") -*/ - -static const StreetInfo streets[] = { - {2500, 50, "Esp"}, // +0ft dia: 5,000ft - {2935, 215, "A"}, // +435ft dia: 5,870ft - {3215, 140, "B"}, // +280ft dia: 6,430ft - {3495, 140, "C"}, // +280ft dia: 6,990ft - {3775, 140, "D"}, // +280ft dia: 7,550ft - {4060, 145, "E"}, // +285ft dia: 8,120ft - {4545, 240, "F"}, // +485ft dia: 9,090ft - {4825, 140, "G"}, // +280ft dia: 9,650ft - {5105, 140, "H"}, // +280ft dia: 10,210ft - {5385, 140, "I"}, // +280ft dia: 10,770ft - {5565, 90, "J"}, // +180ft dia: 11,130ft - {5755, 100, "K"}, // +190ft dia: 11,510ft - {5830, 0, nullptr}, // +75ft -}; +// For size_t/int32_t types on some platforms. +#include +// For size_t +#include class BRCAddress { public: - BRCAddress(int32_t lat, int32_t lon) - { - bearing = GeoCoord::bearing(BRC_LATF, BRC_LONF, DegD(lat), DegD(lon)) * RAD_TO_HOUR; - bearing += 12.0 - BRC_NOON; - while (bearing > 12.0) { - bearing -= 12.0; - } + BRCAddress(int32_t lat, int32_t lon); - // In imperial units because that is how golden spike data is provided. - distance = GeoCoord::latLongToMeter(BRC_LATF, BRC_LONF, DegD(lat), DegD(lon)) * METER_TO_FEET; - }; - - int radial(char *buf, size_t len) - { - uint8_t hour = (uint8_t)(bearing); - uint8_t minute = (uint8_t)((bearing - hour) * 60.0); - hour %= 12; - if (hour == 0) { - hour = 12; - } - return snprintf(buf, len, "%d:%02d", hour, minute); - }; - - int annular(char *buf, size_t len) - { - const char *unit = "m"; - float unitMultiplier = FEET_TO_METER; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - unitMultiplier = 1.0; - unit = "ft"; - } - - if (bearing > 1.75 && bearing < 10.25) { - const char *street = nullptr; - float dist = 0; - // Find the appropriate street based on distance - for (const auto &s : streets) { - if (distance > s.center - s.width) { - street = s.name; - dist = distance - s.center; - } else { - break; - } - } - if (street) { - return snprintf(buf, len, "%s %d%s", street, int(dist * unitMultiplier), unit); - } - } - - return snprintf(buf, len, "%d%s", int(distance * unitMultiplier), unit); - }; - - int full(char *buf, size_t len) - { - auto l = radial(buf, len - 4); - buf += l; - *(buf++) = ' '; - *(buf++) = '&'; - *(buf++) = ' '; - buf += annular(buf, len - l - 4); - buf[l] = 0; // always null terminated - return l; - }; + int radial(char *buf, size_t len); + int annular(char *buf, size_t len, bool noUnit); + int full(char *buf, size_t len); + int compact(char *buf, size_t len); private: float bearing; diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index b9c9e2fbf..b87cde62e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -907,7 +907,7 @@ void Screen::setFrames(FrameFocus focus) // Show detailed node views only on E-Ink builds #ifdef USE_EINK - fsi.positions.nodelist_lastheard = numframes; + fsi.positions.nodelist_bearings = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; indicatorIcons.push_back(icon_nodes); @@ -918,11 +918,15 @@ void Screen::setFrames(FrameFocus focus) fsi.positions.nodelist_distance = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; indicatorIcons.push_back(icon_distance); -#endif -#if HAS_GPS + fsi.positions.nodelist_bearings = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; indicatorIcons.push_back(icon_list); +#endif +#if HAS_GPS + fsi.positions.nodelist_brc = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawBRCList; + indicatorIcons.push_back(icon_bm); fsi.positions.gps = numframes; normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; @@ -1391,7 +1395,8 @@ int Screen::handleInputEvent(const InputEvent *event) this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_brc) { menuHandler::nodeListMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { menuHandler::wifiBaseMenu(); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 265900131..9e31cb992 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -662,6 +662,7 @@ class Screen : public concurrency::OSThread uint8_t nodelist_hopsignal = 255; uint8_t nodelist_distance = 255; uint8_t nodelist_bearings = 255; + uint8_t nodelist_brc = 255; uint8_t clock = 255; uint8_t firstFavorite = 255; uint8_t lastFavorite = 255; diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index d8746fb69..722fa143d 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -6,6 +6,7 @@ #include "UIRenderer.h" #include "gps/GeoCoord.h" #include "gps/RTC.h" // for getTime() function +#include "graphics/BRC.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" @@ -87,6 +88,8 @@ const char *getCurrentModeTitle(int screenWidth) #endif case MODE_DISTANCE: return "Distance"; + case MODE_BEARING: + return "Bearing"; default: return "Nodes"; } @@ -309,6 +312,9 @@ void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 case MODE_DISTANCE: drawNodeDistance(display, node, x, y, columnWidth); break; + case MODE_BEARING: + drawEntryCompass(display, node, x, y, columnWidth); + break; default: break; } @@ -335,6 +341,35 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } +void drawEntryBRC(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + + // Adjust max text width depending on column and screen width + int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + + const char *nodeName = getSafeNodeName(node); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + auto xText = x + ((isHighResolution) ? 6 : 3); + display->drawString(xText, y, nodeName); + + if (nodeDB->hasValidPosition(node)) { + char buf[14] = ""; + BRCAddress(node->position.latitude_i, node->position.longitude_i).compact(buf, 14); + auto nameWidth = display->getStringWidth("WWWW") - 2; // Fixed width so they are aligned. + display->drawString(xText + nameWidth, y, buf); + } + + if (node->is_favorite) { + if (isHighResolution) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } +} + void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, double userLon) { @@ -384,17 +419,46 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 */ } +void drawLastSeenExtra(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, + float /*myHeading*/, double /*userLat*/, double /*userLon*/) +{ + char timeStr[10]; + uint32_t seconds = sinceLastSeen(node); + if (seconds == 0 || seconds == UINT32_MAX) { + snprintf(timeStr, sizeof(timeStr), "?"); + } else { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + snprintf(timeStr, sizeof(timeStr), (days > 99 ? "?" : "%d%c"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); + int rightEdge = x + columnWidth - timeOffset; + if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time + rightEdge -= 1; + // display->setTextAlignment(TEXT_ALIGN_RIGHT); + int textWidth = display->getStringWidth(timeStr); + display->drawString(rightEdge - textWidth, y, timeStr); +} + // ============================= // Main Screen Functions // ============================= void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, - EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon) + EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon, + int totalColumns) { const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; const int rowYOffset = FONT_HEIGHT_SMALL - 3; - int columnWidth = display->getWidth() / 2; + int columnWidth = display->getWidth() / totalColumns; display->clear(); @@ -408,7 +472,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int visibleNodeRows = totalRowsAvailable; - int totalColumns = 2; int startIndex = scrollIndex * visibleNodeRows * totalColumns; if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) { @@ -446,7 +509,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t } // Draw column separator - if (shownCount > 0) { + if (shownCount > 0 && totalColumns > 1) { const int firstNodeY = y + 3; drawColumnSeparator(display, x, firstNodeY, lastNodeY); } @@ -482,7 +545,31 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, // Render screen based on currentMode const char *title = getCurrentModeTitle(display->getWidth()); - drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); + + if (currentMode == MODE_BEARING) { + float heading = 0; + bool validHeading = false; + auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + double lat = DegD(ourNode->position.latitude_i); + double lon = DegD(ourNode->position.longitude_i); + + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { + if (screen->hasHeading()) { + heading = screen->getHeading(); // degrees + validHeading = true; + } else { + heading = screen->estimatedHeading(lat, lon); + validHeading = !isnan(heading); + } + if (!validHeading) { + lastRenderedMode = MODE_COUNT; + return; + } + } + drawNodeListScreen(display, state, x, y, title, drawEntryCompass, drawCompassArrow, heading, lat, lon); + } else { + drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); + } // Track the last mode to avoid reinitializing modeStartTime lastRenderedMode = currentMode; @@ -539,6 +626,11 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); } +void drawBRCList(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawNodeListScreen(display, state, x, y, "BRC", drawEntryBRC, drawLastSeenExtra, 0, 0, 0, 1); +} + /// Draw a series of fields in a column, wrapping to multiple columns if needed void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) { @@ -564,4 +656,4 @@ void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields } // namespace NodeListRenderer } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index ea8df8bd9..bede8e73f 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -24,12 +24,12 @@ typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); // Node list mode enumeration -enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; +enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_BEARING = 3, MODE_COUNT = 4 }; // Main node list screen function void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, - double lon = 0); + double lon = 0, int totalColumns = 2); // Entry renderers void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); @@ -48,6 +48,7 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawBRCList(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // Utility functions const char *getCurrentModeTitle(int screenWidth); diff --git a/src/graphics/images.h b/src/graphics/images.h index beef3a1b2..1f053e56b 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -156,6 +156,18 @@ const uint8_t icon_list[] PROGMEM = { 0x82 // Row 7: #.....#. }; +// ➤ The Man Icon (8x8) +const uint8_t icon_bm[] PROGMEM = { + 0x42, // Row 0: .#....#. + 0x3C, // Row 1: ..####.. + 0x3C, // Row 2: ..####.. + 0x18, // Row 3: ...##... + 0x18, // Row 4: ...##... + 0x24, // Row 5: ..#..#.. + 0x24, // Row 6: ..#..#.. + 0x42 // Row 7: .#....#. +}; + // 📶 Signal Bars Icon (left to right, small to large with spacing) const uint8_t icon_signal[] PROGMEM = { 0b00000000, // ░░░░░░░