Merge branch 'develop' into work

This commit is contained in:
andrewdarkstar 2025-10-26 13:50:12 +03:00 committed by GitHub
commit c57b4abc5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 341 additions and 220 deletions

View File

@ -8,15 +8,15 @@ plugins:
uri: https://github.com/trunk-io/plugins uri: https://github.com/trunk-io/plugins
lint: lint:
enabled: enabled:
- checkov@3.2.483 - checkov@3.2.486
- renovate@41.148.2 - renovate@41.157.0
- prettier@3.6.2 - prettier@3.6.2
- trufflehog@3.90.8 - trufflehog@3.90.11
- yamllint@1.37.1 - yamllint@1.37.1
- bandit@1.8.6 - bandit@1.8.6
- trivy@0.67.2 - trivy@0.67.2
- taplo@0.10.0 - taplo@0.10.0
- ruff@0.14.0 - ruff@0.14.1
- isort@7.0.0 - isort@7.0.0
- markdownlint@0.45.0 - markdownlint@0.45.0
- oxipng@9.1.5 - oxipng@9.1.5

View File

@ -563,6 +563,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
int line = 1; int line = 1;
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// === Header === // === Header ===
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
@ -740,7 +741,6 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int yOffset = (isHighResolution) ? 0 : 5; int yOffset = (isHighResolution) ? 0 : 5;
std::string longNameStr; std::string longNameStr;
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
longNameStr = sanitizeString(ourNode->user.long_name); longNameStr = sanitizeString(ourNode->user.long_name);
} }
@ -1000,24 +1000,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
const char *displayLine = ""; // Initialize to empty string by default const char *displayLine = ""; // Initialize to empty string by default
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
bool usePhoneGPS = (ourNode && nodeDB->hasValidPosition(ourNode) && if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED);
if (usePhoneGPS) {
// Phone-provided GPS is active
displayLine = "Phone GPS";
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
// GPS disabled / not present
if (config.position.fixed_position) { if (config.position.fixed_position) {
displayLine = "Fixed GPS"; displayLine = "Fixed GPS";
} else { } else {
@ -1108,9 +1091,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// === Final Row: Altitude === // === Final Row: Altitude ===
char altitudeLine[32] = {0}; char altitudeLine[32] = {0};
int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode)) int32_t alt = geoCoord.getAltitude();
? ourNode->position.altitude
: geoCoord.getAltitude();
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET); snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET);
} else { } else {

View File

@ -13,45 +13,147 @@ void InkHUD::MapApplet::onRender()
return; return;
} }
// Helper: draw rounded rectangle centered at x,y
auto fillRoundedRect = [&](int16_t cx, int16_t cy, int16_t w, int16_t h, int16_t r, uint16_t color) {
int16_t x = cx - (w / 2);
int16_t y = cy - (h / 2);
// center rects
fillRect(x + r, y, w - 2 * r, h, color);
fillRect(x, y + r, r, h - 2 * r, color);
fillRect(x + w - r, y + r, r, h - 2 * r, color);
// corners
fillCircle(x + r, y + r, r, color);
fillCircle(x + w - r - 1, y + r, r, color);
fillCircle(x + r, y + h - r - 1, r, color);
fillCircle(x + w - r - 1, y + h - r - 1, r, color);
};
// Find center of map // Find center of map
// - latitude and longitude
// - will be placed at X(0.5), Y(0.5)
getMapCenter(&latCenter, &lngCenter); getMapCenter(&latCenter, &lngCenter);
// Calculate North+East distance of each node to map center
// - which nodes to use controlled by virtual shouldDrawNode method
calculateAllMarkers(); calculateAllMarkers();
// Set the region shown on the map
// - default: fit all nodes, plus padding
// - maybe overriden by derived applet
// - getMapSize *sets* passed parameters (C-style)
getMapSize(&widthMeters, &heightMeters); getMapSize(&widthMeters, &heightMeters);
// Set the metersToPx conversion value
calculateMapScale(); calculateMapScale();
// Special marker for own node // Draw all markers first
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && nodeDB->hasValidPosition(ourNode))
drawLabeledMarker(ourNode);
// Draw all markers
for (Marker m : markers) { for (Marker m : markers) {
int16_t x = X(0.5) + (m.eastMeters * metersToPx); int16_t x = X(0.5) + (m.eastMeters * metersToPx);
int16_t y = Y(0.5) - (m.northMeters * metersToPx); int16_t y = Y(0.5) - (m.northMeters * metersToPx);
// Cross Size // Add white halo outline first
constexpr uint16_t csMin = 5; constexpr int outlinePad = 1;
constexpr uint16_t csMax = 12; int boxSize = 11;
int radius = 2; // rounded corner radius
// Too many hops away // White halo background
if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) // Too many mops fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE);
printAt(x, y, "!", CENTER, MIDDLE);
else if (!m.hasHopsAway) // Unknown hops // Draw inner box
drawCross(x, y, csMin); fillRoundedRect(x, y, boxSize, boxSize, radius, BLACK);
else // The fewer hops, the larger the cross
drawCross(x, y, map(m.hopsAway, 0, config.lora.hop_limit, csMax, csMin)); // Text inside
setFont(fontSmall);
setTextColor(WHITE);
// Draw actual marker on top
if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) {
printAt(x + 1, y + 1, "X", CENTER, MIDDLE);
} else if (!m.hasHopsAway) {
printAt(x + 1, y + 1, "?", CENTER, MIDDLE);
} else {
char hopStr[4];
snprintf(hopStr, sizeof(hopStr), "%d", m.hopsAway);
printAt(x, y + 1, hopStr, CENTER, MIDDLE);
}
// Restore default font and color
setFont(fontSmall);
setTextColor(BLACK);
}
// Dual map scale bars
int16_t horizPx = width() * 0.25f;
int16_t vertPx = height() * 0.25f;
float horizMeters = horizPx / metersToPx;
float vertMeters = vertPx / metersToPx;
auto formatDistance = [&](float meters, char *out, size_t len) {
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
float feet = meters * 3.28084f;
if (feet < 528)
snprintf(out, len, "%.0f ft", feet);
else {
float miles = feet / 5280.0f;
snprintf(out, len, miles < 10 ? "%.1f mi" : "%.0f mi", miles);
}
} else {
if (meters >= 1000)
snprintf(out, len, "%.1f km", meters / 1000.0f);
else
snprintf(out, len, "%.0f m", meters);
}
};
// Horizontal scale bar
int16_t horizBarY = height() - 2;
int16_t horizBarX = 1;
drawLine(horizBarX, horizBarY, horizBarX + horizPx, horizBarY, BLACK);
drawLine(horizBarX, horizBarY - 3, horizBarX, horizBarY + 3, BLACK);
drawLine(horizBarX + horizPx, horizBarY - 3, horizBarX + horizPx, horizBarY + 3, BLACK);
char horizLabel[32];
formatDistance(horizMeters, horizLabel, sizeof(horizLabel));
int16_t horizLabelW = getTextWidth(horizLabel);
int16_t horizLabelH = getFont().lineHeight();
int16_t horizLabelX = horizBarX + horizPx + 4;
int16_t horizLabelY = horizBarY - horizLabelH + 1;
fillRect(horizLabelX - 2, horizLabelY - 1, horizLabelW + 4, horizLabelH + 2, WHITE);
printAt(horizLabelX, horizBarY, horizLabel, LEFT, BOTTOM);
// Vertical scale bar
int16_t vertBarX = 1;
int16_t vertBarBottom = horizBarY;
int16_t vertBarTop = vertBarBottom - vertPx;
drawLine(vertBarX, vertBarBottom, vertBarX, vertBarTop, BLACK);
drawLine(vertBarX - 3, vertBarBottom, vertBarX + 3, vertBarBottom, BLACK);
drawLine(vertBarX - 3, vertBarTop, vertBarX + 3, vertBarTop, BLACK);
char vertTopLabel[32];
formatDistance(vertMeters, vertTopLabel, sizeof(vertTopLabel));
int16_t topLabelY = vertBarTop - getFont().lineHeight() - 2;
int16_t topLabelW = getTextWidth(vertTopLabel);
int16_t topLabelH = getFont().lineHeight();
fillRect(vertBarX - 2, topLabelY - 1, topLabelW + 6, topLabelH + 2, WHITE);
printAt(vertBarX + (topLabelW / 2) + 1, topLabelY + (topLabelH / 2), vertTopLabel, CENTER, MIDDLE);
char vertBottomLabel[32];
formatDistance(vertMeters, vertBottomLabel, sizeof(vertBottomLabel));
int16_t bottomLabelY = vertBarBottom + 4;
int16_t bottomLabelW = getTextWidth(vertBottomLabel);
int16_t bottomLabelH = getFont().lineHeight();
fillRect(vertBarX - 2, bottomLabelY - 1, bottomLabelW + 6, bottomLabelH + 2, WHITE);
printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE);
// Draw our node LAST with full white fill + outline
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0);
int16_t centerX = X(0.5) + (self.eastMeters * metersToPx);
int16_t centerY = Y(0.5) - (self.northMeters * metersToPx);
// White fill background + halo
fillCircle(centerX, centerY, 8, WHITE); // big white base
drawCircle(centerX, centerY, 8, WHITE); // crisp edge
// Black bullseye on top
drawCircle(centerX, centerY, 6, BLACK);
fillCircle(centerX, centerY, 2, BLACK);
// Crosshairs
drawLine(centerX - 8, centerY, centerX + 8, centerY, BLACK);
drawLine(centerX, centerY - 8, centerX, centerY + 8, BLACK);
} }
} }
@ -63,110 +165,123 @@ void InkHUD::MapApplet::onRender()
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
{ {
// Find mean lat long coords // If we have a valid position for our own node, use that as the anchor
// ============================ meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet if (ourNode && nodeDB->hasValidPosition(ourNode)) {
// - averages the x, y and z coords *lat = ourNode->position.latitude_i * 1e-7;
// - uses tan to find angles for lat / long degrees *lng = ourNode->position.longitude_i * 1e-7;
// - longitude: triangle formed by x and y (on plane of the equator) } else {
// - latitude: triangle formed by z (north south), // Find mean lat long coords
// and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's surface // ============================
// - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet
// - averages the x, y and z coords
// - uses tan to find angles for lat / long degrees
// - longitude: triangle formed by x and y (on plane of the equator)
// - latitude: triangle formed by z (north south),
// and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's
// surface
// Working totals, averaged after nodeDB processed // Working totals, averaged after nodeDB processed
uint32_t positionCount = 0; uint32_t positionCount = 0;
float xAvg = 0; float xAvg = 0;
float yAvg = 0; float yAvg = 0;
float zAvg = 0; float zAvg = 0;
// For each node in db // For each node in db
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Skip if no position // Skip if no position
if (!nodeDB->hasValidPosition(node)) if (!nodeDB->hasValidPosition(node))
continue; continue;
// Skip if derived applet doesn't want to show this node on the map // Skip if derived applet doesn't want to show this node on the map
if (!shouldDrawNode(node)) if (!shouldDrawNode(node))
continue; continue;
// Latitude and Longitude of node, in radians // Latitude and Longitude of node, in radians
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD; float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD; float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
// Convert to cartesian points, with center of earth at 0, 0, 0 // Convert to cartesian points, with center of earth at 0, 0, 0
// Exact distance from center is irrelevant, as we're only interested in the vector // Exact distance from center is irrelevant, as we're only interested in the vector
float x = cos(latRad) * cos(lngRad); float x = cos(latRad) * cos(lngRad);
float y = cos(latRad) * sin(lngRad); float y = cos(latRad) * sin(lngRad);
float z = sin(latRad); float z = sin(latRad);
// To find mean values shortly // To find mean values shortly
xAvg += x; xAvg += x;
yAvg += y; yAvg += y;
zAvg += z; zAvg += z;
positionCount++; positionCount++;
}
// All NodeDB processed, find mean values
xAvg /= positionCount;
yAvg /= positionCount;
zAvg /= positionCount;
// Longitude from cartesian coords
// (Angle from 3D coords describing a point of globe's surface)
/*
UK
/-------\
(Top View) /- -\
/- (You) -\
/- . -\
/- . X -\
Asia - ... - USA
\- Y -/
\- -/
\- -/
\- -/
\- -----/
Pacific
*/
*lng = atan2(yAvg, xAvg) * RAD_TO_DEG;
// Latitude from cartesian coords
// (Angle from 3D coords describing a point on the globe's surface)
// As latitude increases, distance from the Earth's north-south axis out to our surface point decreases.
// Means we need to first find the hypotenuse which becomes base of our triangle in the second step
/*
UK North
/-------\ (Front View) /-------\
(Top View) /- -\ /- -\
/- (You) -\ /-(You) -\
/- /. -\ /- . -\
/- X²+Y²/ . X -\ /- Z . -\
Asia - /... - USA - ..... -
\- Y -/ \- X²+Y² -/
\- -/ \- -/
\- -/ \- -/
\- -/ \- -/
\- -----/ \- -----/
Pacific South
*/
float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect
*lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG;
} }
// All NodeDB processed, find mean values // Use either our node position, or the mean fallback as the center
xAvg /= positionCount; latCenter = *lat;
yAvg /= positionCount; lngCenter = *lng;
zAvg /= positionCount;
// Longitude from cartesian coords
// (Angle from 3D coords describing a point of globe's surface)
/*
UK
/-------\
(Top View) /- -\
/- (You) -\
/- . -\
/- . X -\
Asia - ... - USA
\- Y -/
\- -/
\- -/
\- -/
\- -----/
Pacific
*/
*lng = atan2(yAvg, xAvg) * RAD_TO_DEG;
// Latitude from cartesian coords
// (Angle from 3D coords describing a point on the globe's surface)
// As latitude increases, distance from the Earth's north-south axis out to our surface point decreases.
// Means we need to first find the hypotenuse which becomes base of our triangle in the second step
/*
UK North
/-------\ (Front View) /-------\
(Top View) /- -\ /- -\
/- (You) -\ /-(You) -\
/- /. -\ /- . -\
/- X²+Y²/ . X -\ /- Z . -\
Asia - /... - USA - ..... -
\- Y -/ \- X²+Y² -/
\- -/ \- -/
\- -/ \- -/
\- -/ \- -/
\- -----/ \- -----/
Pacific South
*/
float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect
*lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG;
// ---------------------------------------------- // ----------------------------------------------
// This has given us the "mean position" // This has given us either:
// This will be a position *somewhere* near the center of our nodes. // - our actual position (preferred), or
// What we actually want is to place our center so that our outermost nodes end up on the border of our map. // - a mean position (fallback if we had no fix)
// The only real use of our "mean position" is to give us a reference frame: //
// which direction is east, and which is west. // What we actually want is to place our center so that our outermost nodes
// end up on the border of our map. The only real use of our "center" is to give
// us a reference frame: which direction is east, and which is west.
//------------------------------------------------ //------------------------------------------------
// Find furthest nodes from "mean lat long" // Find furthest nodes from our center
// ======================================== // ========================================
float northernmost = latCenter; float northernmost = latCenter;
float southernmost = latCenter; float southernmost = latCenter;
float easternmost = lngCenter; float easternmost = lngCenter;
@ -184,14 +299,14 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
continue; continue;
// Check for a new top or bottom latitude // Check for a new top or bottom latitude
float lat = node->position.latitude_i * 1e-7; float latNode = node->position.latitude_i * 1e-7;
northernmost = max(northernmost, lat); northernmost = max(northernmost, latNode);
southernmost = min(southernmost, lat); southernmost = min(southernmost, latNode);
// Longitude is trickier // Longitude is trickier
float lng = node->position.longitude_i * 1e-7; float lngNode = node->position.longitude_i * 1e-7;
float degEastward = fmod(((lng - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
float degWestward = abs(fmod(((lng - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
if (degEastward < degWestward) if (degEastward < degWestward)
easternmost = max(easternmost, lngCenter + degEastward); easternmost = max(easternmost, lngCenter + degEastward);
else else
@ -250,7 +365,6 @@ InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float ln
m.hopsAway = hopsAway; m.hopsAway = hopsAway;
return m; return m;
} }
// Draw a marker on the map for a node, with a shortname label, and backing box // Draw a marker on the map for a node, with a shortname label, and backing box
void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
{ {
@ -324,6 +438,18 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
textX = labelX + paddingW; textX = labelX + paddingW;
} }
// Prevent overlap with scale bars and their labels
// Define a "safe zone" in the bottom-left where the scale bars and text are drawn
constexpr int16_t safeZoneHeight = 28; // adjust based on your label font height
constexpr int16_t safeZoneWidth = 60; // adjust based on horizontal label width zone
bool overlapsScale = (labelY + labelH > height() - safeZoneHeight) && (labelX < safeZoneWidth);
// If it overlaps, shift label upward slightly above the safe zone
if (overlapsScale) {
labelY = height() - safeZoneHeight - labelH - 2;
textY = labelY + (labelH / 2);
}
// Backing box // Backing box
fillRect(labelX, labelY, labelW, labelH, WHITE); fillRect(labelX, labelY, labelW, labelH, WHITE);
drawRect(labelX, labelY, labelW, labelH, BLACK); drawRect(labelX, labelY, labelW, labelH, BLACK);

View File

@ -127,6 +127,11 @@ void InkHUD::NodeListApplet::onRender()
// Y value (top) of the current card. Increases as we draw. // Y value (top) of the current card. Increases as we draw.
uint16_t cardTopY = headerDivY + padDivH; uint16_t cardTopY = headerDivY + padDivH;
// Clean up deleted nodes before drawing
cards.erase(
std::remove_if(cards.begin(), cards.end(), [](const CardInfo &c) { return nodeDB->getMeshNode(c.nodeNum) == nullptr; }),
cards.end());
// -- Each node in list -- // -- Each node in list --
for (auto card = cards.begin(); card != cards.end(); ++card) { for (auto card = cards.begin(); card != cards.end(); ++card) {
@ -141,6 +146,11 @@ void InkHUD::NodeListApplet::onRender()
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
// Skip deleted nodes
if (!node) {
continue;
}
// -- Shortname -- // -- Shortname --
// Parse special chars in the short name // Parse special chars in the short name
// Use "?" if unknown // Use "?" if unknown
@ -188,7 +198,7 @@ void InkHUD::NodeListApplet::onRender()
drawSignalIndicator(signalX, signalY, signalW, signalH, signal); drawSignalIndicator(signalX, signalY, signalW, signalH, signal);
} }
// Otherwise, print "hops away" info, if available // Otherwise, print "hops away" info, if available
else if (hopsAway != CardInfo::HOPS_UNKNOWN) { else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) {
std::string hopString = to_string(node->hops_away); std::string hopString = to_string(node->hops_away);
hopString += " Hop"; hopString += " Hop";
if (node->hops_away != 1) if (node->hops_away != 1)

View File

@ -436,6 +436,12 @@ void setup()
LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n");
#if defined(DEBUG_MUTE) && defined(DEBUG_PORT)
DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n");
DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO));
DEBUG_PORT.printf("Debug mute is enabled, there will be no serial output.\r\n");
#endif
initDeepSleep(); initDeepSleep();
#if defined(MODEM_POWER_EN) #if defined(MODEM_POWER_EN)

View File

@ -27,7 +27,7 @@
#ifdef USERPREFS_RINGTONE_NAG_SECS #ifdef USERPREFS_RINGTONE_NAG_SECS
#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS #define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS
#else #else
#define default_ringtone_nag_secs 60 #define default_ringtone_nag_secs 15
#endif #endif
#define default_mqtt_address "mqtt.meshtastic.org" #define default_mqtt_address "mqtt.meshtastic.org"

View File

@ -272,10 +272,10 @@ uint32_t RadioInterface::getTxDelayMsec()
uint8_t RadioInterface::getCWsize(float snr) uint8_t RadioInterface::getCWsize(float snr)
{ {
// The minimum value for a LoRa SNR // The minimum value for a LoRa SNR
const uint32_t SNR_MIN = -20; const int32_t SNR_MIN = -20;
// The maximum value for a LoRa SNR // The maximum value for a LoRa SNR
const uint32_t SNR_MAX = 10; const int32_t SNR_MAX = 10;
return map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax); return map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax);
} }

View File

@ -289,12 +289,7 @@ void RadioLibInterface::onNotify(uint32_t notification)
// actual transmission as short as possible // actual transmission as short as possible
txp = txQueue.dequeue(); txp = txQueue.dequeue();
assert(txp); assert(txp);
bool sent = startSend(txp); startSend(txp);
if (sent) {
// Packet has been sent, count it toward our TX airtime utilization.
uint32_t xmitMsec = getPacketTime(txp);
airTime->logAirtime(TX_LOG, xmitMsec);
}
LOG_DEBUG("%d packets remain in the TX queue", txQueue.getMaxLen() - txQueue.getFree()); LOG_DEBUG("%d packets remain in the TX queue", txQueue.getMaxLen() - txQueue.getFree());
} }
} }
@ -413,6 +408,10 @@ void RadioLibInterface::completeSending()
sendingPacket = NULL; sendingPacket = NULL;
if (p) { if (p) {
// Packet has been sent, count it toward our TX airtime utilization.
uint32_t xmitMsec = getPacketTime(p);
airTime->logAirtime(TX_LOG, xmitMsec);
txGood++; txGood++;
if (!isFromUs(p)) if (!isFromUs(p))
txRelay++; txRelay++;

View File

@ -35,6 +35,15 @@
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
2) // max number of packets which can be in flight (either queued from reception or queued for sending) 2) // max number of packets which can be in flight (either queued from reception or queued for sending)
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool;
Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
#elif defined(ARCH_STM32WL)
// On STM32 there isn't enough heap left over for the rest of the firmware if we allocate this statically.
// For now, make it dynamic again.
#define MAX_PACKETS \
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
2) // max number of packets which can be in flight (either queued from reception or queued for sending)
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool; static MemoryDynamic<meshtastic_MeshPacket> dynamicPool;
Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool; Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
#else #else

View File

@ -94,8 +94,8 @@ int32_t ExternalNotificationModule::runOnce()
// audioThread->isPlaying() also handles actually playing the RTTTL, needs to be called in loop // audioThread->isPlaying() also handles actually playing the RTTTL, needs to be called in loop
isRtttlPlaying = isRtttlPlaying || audioThread->isPlaying(); isRtttlPlaying = isRtttlPlaying || audioThread->isPlaying();
#endif #endif
if ((nagCycleCutoff < millis()) && !isRtttlPlaying) { if ((nagCycleCutoff <= millis())) {
// let the song finish if we reach timeout // Turn off external notification immediately when timeout is reached, regardless of song state
nagCycleCutoff = UINT32_MAX; nagCycleCutoff = UINT32_MAX;
LOG_INFO("Turning off external notification: "); LOG_INFO("Turning off external notification: ");
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
@ -103,7 +103,6 @@ int32_t ExternalNotificationModule::runOnce()
externalTurnedOn[i] = 0; externalTurnedOn[i] = 0;
LOG_INFO("%d ", i); LOG_INFO("%d ", i);
} }
LOG_INFO("");
#ifdef HAS_I2S #ifdef HAS_I2S
// GPIO0 is used as mclk for I2S audio and set to OUTPUT by the sound library // GPIO0 is used as mclk for I2S audio and set to OUTPUT by the sound library
// T-Deck uses GPIO0 as trackball button, so restore the mode // T-Deck uses GPIO0 as trackball button, so restore the mode

View File

@ -159,6 +159,7 @@ ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket
LOG_DEBUG("---- Received Packet:"); LOG_DEBUG("---- Received Packet:");
LOG_DEBUG("mp.from %d", mp.from); LOG_DEBUG("mp.from %d", mp.from);
LOG_DEBUG("mp.rx_snr %f", mp.rx_snr); LOG_DEBUG("mp.rx_snr %f", mp.rx_snr);
LOG_DEBUG("mp.rx_rssi %f", mp.rx_rssi);
LOG_DEBUG("mp.hop_limit %d", mp.hop_limit); LOG_DEBUG("mp.hop_limit %d", mp.hop_limit);
LOG_DEBUG("---- Node Information of Received Packet (mp.from):"); LOG_DEBUG("---- Node Information of Received Packet (mp.from):");
LOG_DEBUG("n->user.long_name %s", n->user.long_name); LOG_DEBUG("n->user.long_name %s", n->user.long_name);
@ -234,8 +235,8 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
} }
// Print the CSV header // Print the CSV header
if (fileToWrite.println( if (fileToWrite.println("time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx "
"time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload")) { "snr,distance,hop limit,payload,rx rssi")) {
LOG_INFO("File was written"); LOG_INFO("File was written");
} else { } else {
LOG_ERROR("File write failed"); LOG_ERROR("File write failed");
@ -297,6 +298,8 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
// TODO: If quotes are found in the payload, it has to be escaped. // TODO: If quotes are found in the payload, it has to be escaped.
fileToAppend.printf("\"%s\"\n", p.payload.bytes); fileToAppend.printf("\"%s\"\n", p.payload.bytes);
fileToAppend.printf("%i,", mp.rx_rssi); // RX RSSI
fileToAppend.flush(); fileToAppend.flush();
fileToAppend.close(); fileToAppend.close();

View File

@ -49,7 +49,7 @@ NimBLECharacteristic *logRadioCharacteristic;
NimBLEServer *bleServer; NimBLEServer *bleServer;
static bool passkeyShowing; static bool passkeyShowing;
static std::atomic<int32_t> nimbleBluetoothConnHandle{-1}; // actual handles are uint16_t, so -1 means "no connection" static std::atomic<uint16_t> nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection"
class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
{ {
@ -144,21 +144,28 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
protected: protected:
virtual int32_t runOnce() override virtual int32_t runOnce() override
{ {
bool shouldBreakAndRetryLater = false;
while (runOnceHasWorkToDo()) { while (runOnceHasWorkToDo()) {
// Important that we service onRead first, because the onRead callback blocks NimBLE until we clear /*
// onReadCallbackIsWaitingForData. PROCESS fromPhoneQueue BEFORE toPhoneQueue:
shouldBreakAndRetryLater = runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead
runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio
if (shouldBreakAndRetryLater) { In normal STATE_SEND_PACKETS operation, it's unlikely that we'll have both writes and reads to process at the same
// onRead still wants data, but it's not available yet. Return so we can try again when a packet may be ready. time, because either onWrite or onRead will trigger this runOnce. And in STATE_SEND_PACKETS, it's generally ok to
#ifdef DEBUG_NIMBLE_ON_READ_TIMING service either the reads or writes first.
LOG_INFO("BLE runOnce breaking to retry later (leaving onRead waiting)");
#endif However, during the initial setup wantConfig packet, the clients send a write and immediately send a read, and they
return 100; // try again in 100ms expect the read will respond to the write. (This also happens when a client goes from STATE_SEND_PACKETS back to
} another wantConfig, like the iOS client does when requesting the nodedb after requesting the main config only.)
So it's safest to always service writes (fromPhoneQueue) before reads (toPhoneQueue), so that any "synchronous"
write-then-read sequences from the client work as expected, even if this means we block onRead for a while: this is
what the client wants!
*/
// PHONE -> RADIO:
runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio
// RADIO -> PHONE:
runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead
} }
// the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback // the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback
@ -171,9 +178,9 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
// Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds) // Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds)
if (bleServer && isConnected()) { if (bleServer && isConnected()) {
int32_t conn_handle = nimbleBluetoothConnHandle.load(); uint16_t conn_handle = nimbleBluetoothConnHandle.load();
if (conn_handle != -1) { if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
requestHighThroughputConnection(static_cast<uint16_t>(conn_handle)); requestHighThroughputConnection(conn_handle);
} }
} }
} }
@ -184,9 +191,9 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
// Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete // Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete
if (bleServer && isConnected()) { if (bleServer && isConnected()) {
int32_t conn_handle = nimbleBluetoothConnHandle.load(); uint16_t conn_handle = nimbleBluetoothConnHandle.load();
if (conn_handle != -1) { if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
requestLowerPowerConnection(static_cast<uint16_t>(conn_handle)); requestLowerPowerConnection(conn_handle);
} }
} }
} }
@ -220,12 +227,8 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
} }
} }
bool runOnceHandleToPhoneQueue() void runOnceHandleToPhoneQueue()
{ {
// Returns false normally.
// Returns true if we should break out of runOnce and retry later, such as setup states where getFromRadio returns 0
// bytes.
// Stack buffer for getFromRadio packet // Stack buffer for getFromRadio packet
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
size_t numBytes = 0; size_t numBytes = 0;
@ -234,28 +237,15 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
numBytes = getFromRadio(fromRadioBytes); numBytes = getFromRadio(fromRadioBytes);
if (numBytes == 0) { if (numBytes == 0) {
// Client expected a read, but we have nothing to send. /*
// Returning a 0-byte packet breaks clients during the config phase, so we have to block onRead until there's a Client expected a read, but we have nothing to send.
// packet ready.
if (isSendingPackets()) {
// In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond
// notifies regularly, to make sure they have nothing else to read.
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
LOG_DEBUG("BLE getFromRadio returned numBytes=0, but in STATE_SEND_PACKETS, so clearing "
"onReadCallbackIsWaitingForData flag");
#endif
} else {
// In other states, this breaks clients.
// Return early, leaving onReadCallbackIsWaitingForData==true so onRead knows to try again.
// This gives runOnce a chance to handleToRadio and produce a response.
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
LOG_DEBUG("BLE getFromRadio returned numBytes=0. Blocking onRead until we have data");
#endif
// Return true to tell runOnce to shouldBreakAndRetryLater, so we don't busy-loop in runOnce even though In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond
// onRead is still waiting! notifies regularly, to make sure they have nothing else to read.
return true;
} In other states, this is fine **so long as we've already processed pending onWrites first**, because the client
may requesting wantConfig and immediately doing a read.
*/
} else { } else {
// Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. // Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible.
if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) { if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) {
@ -282,8 +272,6 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
// Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed. // Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed.
onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push
} }
return false;
} }
bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; } bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; }
@ -466,10 +454,6 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
virtual void onRead(NimBLECharacteristic *pCharacteristic) virtual void onRead(NimBLECharacteristic *pCharacteristic)
#endif #endif
{ {
// In some cases, it seems a new connection starts with a read.
// The API has no bytes to send, leading to a timeout. This short-circuits this problem.
if (!bluetoothPhoneAPI->isConnected())
return;
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
int currentReadCount = bluetoothPhoneAPI->readCount.fetch_add(1); int currentReadCount = bluetoothPhoneAPI->readCount.fetch_add(1);
@ -726,7 +710,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
// Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection
memset(lastToRadio, 0, sizeof(lastToRadio)); memset(lastToRadio, 0, sizeof(lastToRadio));
nimbleBluetoothConnHandle = -1; // -1 means "no connection" nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection"
#ifdef NIMBLE_TWO #ifdef NIMBLE_TWO
// Restart Advertising // Restart Advertising

View File

@ -244,6 +244,10 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
// pinMode(PIN_POWER_EN1, INPUT_PULLDOWN); // pinMode(PIN_POWER_EN1, INPUT_PULLDOWN);
#endif #endif
#ifdef RAK_WISMESH_TAP_V2
digitalWrite(SDCARD_CS, LOW);
#endif
#ifdef TRACKER_T1000_E #ifdef TRACKER_T1000_E
#ifdef GNSS_AIROHA #ifdef GNSS_AIROHA
digitalWrite(GPS_VRTC_EN, LOW); digitalWrite(GPS_VRTC_EN, LOW);