mirror of
https://github.com/meshtastic/firmware.git
synced 2025-10-27 15:02:41 +00:00
Merge branch 'develop' into work
This commit is contained in:
commit
c57b4abc5d
@ -8,15 +8,15 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.483
|
||||
- renovate@41.148.2
|
||||
- checkov@3.2.486
|
||||
- renovate@41.157.0
|
||||
- prettier@3.6.2
|
||||
- trufflehog@3.90.8
|
||||
- trufflehog@3.90.11
|
||||
- yamllint@1.37.1
|
||||
- bandit@1.8.6
|
||||
- trivy@0.67.2
|
||||
- taplo@0.10.0
|
||||
- ruff@0.14.0
|
||||
- ruff@0.14.1
|
||||
- isort@7.0.0
|
||||
- markdownlint@0.45.0
|
||||
- oxipng@9.1.5
|
||||
|
||||
@ -563,6 +563,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
|
||||
// === Header ===
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
@ -740,7 +741,6 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
int yOffset = (isHighResolution) ? 0 : 5;
|
||||
std::string longNameStr;
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
|
||||
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
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
|
||||
bool usePhoneGPS = (ourNode && nodeDB->hasValidPosition(ourNode) &&
|
||||
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.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
if (config.position.fixed_position) {
|
||||
displayLine = "Fixed GPS";
|
||||
} else {
|
||||
@ -1108,9 +1091,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
|
||||
// === Final Row: Altitude ===
|
||||
char altitudeLine[32] = {0};
|
||||
int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode))
|
||||
? ourNode->position.altitude
|
||||
: geoCoord.getAltitude();
|
||||
int32_t alt = geoCoord.getAltitude();
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET);
|
||||
} else {
|
||||
|
||||
@ -13,45 +13,147 @@ void InkHUD::MapApplet::onRender()
|
||||
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
|
||||
// - latitude and longitude
|
||||
// - will be placed at X(0.5), Y(0.5)
|
||||
getMapCenter(&latCenter, &lngCenter);
|
||||
|
||||
// Calculate North+East distance of each node to map center
|
||||
// - which nodes to use controlled by virtual shouldDrawNode method
|
||||
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);
|
||||
|
||||
// Set the metersToPx conversion value
|
||||
calculateMapScale();
|
||||
|
||||
// Special marker for own node
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode))
|
||||
drawLabeledMarker(ourNode);
|
||||
|
||||
// Draw all markers
|
||||
// Draw all markers first
|
||||
for (Marker m : markers) {
|
||||
int16_t x = X(0.5) + (m.eastMeters * metersToPx);
|
||||
int16_t y = Y(0.5) - (m.northMeters * metersToPx);
|
||||
|
||||
// Cross Size
|
||||
constexpr uint16_t csMin = 5;
|
||||
constexpr uint16_t csMax = 12;
|
||||
// Add white halo outline first
|
||||
constexpr int outlinePad = 1;
|
||||
int boxSize = 11;
|
||||
int radius = 2; // rounded corner radius
|
||||
|
||||
// Too many hops away
|
||||
if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) // Too many mops
|
||||
printAt(x, y, "!", CENTER, MIDDLE);
|
||||
else if (!m.hasHopsAway) // Unknown hops
|
||||
drawCross(x, y, csMin);
|
||||
else // The fewer hops, the larger the cross
|
||||
drawCross(x, y, map(m.hopsAway, 0, config.lora.hop_limit, csMax, csMin));
|
||||
// White halo background
|
||||
fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE);
|
||||
|
||||
// Draw inner box
|
||||
fillRoundedRect(x, y, boxSize, boxSize, radius, BLACK);
|
||||
|
||||
// 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)
|
||||
{
|
||||
// Find mean lat long coords
|
||||
// ============================
|
||||
// - 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
|
||||
// If we have a valid position for our own node, use that as the anchor
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
|
||||
*lat = ourNode->position.latitude_i * 1e-7;
|
||||
*lng = ourNode->position.longitude_i * 1e-7;
|
||||
} else {
|
||||
// Find mean lat long coords
|
||||
// ============================
|
||||
// - 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
|
||||
uint32_t positionCount = 0;
|
||||
float xAvg = 0;
|
||||
float yAvg = 0;
|
||||
float zAvg = 0;
|
||||
// Working totals, averaged after nodeDB processed
|
||||
uint32_t positionCount = 0;
|
||||
float xAvg = 0;
|
||||
float yAvg = 0;
|
||||
float zAvg = 0;
|
||||
|
||||
// For each node in db
|
||||
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
// For each node in db
|
||||
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
|
||||
// Latitude and Longitude of node, in radians
|
||||
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
|
||||
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
|
||||
// Latitude and Longitude of node, in radians
|
||||
float latRad = node->position.latitude_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
|
||||
// Exact distance from center is irrelevant, as we're only interested in the vector
|
||||
float x = cos(latRad) * cos(lngRad);
|
||||
float y = cos(latRad) * sin(lngRad);
|
||||
float z = sin(latRad);
|
||||
// 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
|
||||
float x = cos(latRad) * cos(lngRad);
|
||||
float y = cos(latRad) * sin(lngRad);
|
||||
float z = sin(latRad);
|
||||
|
||||
// To find mean values shortly
|
||||
xAvg += x;
|
||||
yAvg += y;
|
||||
zAvg += z;
|
||||
positionCount++;
|
||||
// To find mean values shortly
|
||||
xAvg += x;
|
||||
yAvg += y;
|
||||
zAvg += z;
|
||||
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
|
||||
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;
|
||||
// Use either our node position, or the mean fallback as the center
|
||||
latCenter = *lat;
|
||||
lngCenter = *lng;
|
||||
|
||||
// ----------------------------------------------
|
||||
// This has given us the "mean position"
|
||||
// This will be a position *somewhere* near the center of our nodes.
|
||||
// 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 "mean position" is to give us a reference frame:
|
||||
// which direction is east, and which is west.
|
||||
// This has given us either:
|
||||
// - our actual position (preferred), or
|
||||
// - a mean position (fallback if we had no fix)
|
||||
//
|
||||
// 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 southernmost = latCenter;
|
||||
float easternmost = lngCenter;
|
||||
@ -184,14 +299,14 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
||||
continue;
|
||||
|
||||
// Check for a new top or bottom latitude
|
||||
float lat = node->position.latitude_i * 1e-7;
|
||||
northernmost = max(northernmost, lat);
|
||||
southernmost = min(southernmost, lat);
|
||||
float latNode = node->position.latitude_i * 1e-7;
|
||||
northernmost = max(northernmost, latNode);
|
||||
southernmost = min(southernmost, latNode);
|
||||
|
||||
// Longitude is trickier
|
||||
float lng = node->position.longitude_i * 1e-7;
|
||||
float degEastward = fmod(((lng - 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 lngNode = node->position.longitude_i * 1e-7;
|
||||
float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
|
||||
float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
|
||||
if (degEastward < degWestward)
|
||||
easternmost = max(easternmost, lngCenter + degEastward);
|
||||
else
|
||||
@ -250,7 +365,6 @@ InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float ln
|
||||
m.hopsAway = hopsAway;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Draw a marker on the map for a node, with a shortname label, and backing box
|
||||
void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
@ -324,6 +438,18 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
|
||||
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
|
||||
fillRect(labelX, labelY, labelW, labelH, WHITE);
|
||||
drawRect(labelX, labelY, labelW, labelH, BLACK);
|
||||
|
||||
@ -127,6 +127,11 @@ void InkHUD::NodeListApplet::onRender()
|
||||
// Y value (top) of the current card. Increases as we draw.
|
||||
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 --
|
||||
for (auto card = cards.begin(); card != cards.end(); ++card) {
|
||||
|
||||
@ -141,6 +146,11 @@ void InkHUD::NodeListApplet::onRender()
|
||||
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
|
||||
|
||||
// Skip deleted nodes
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// -- Shortname --
|
||||
// Parse special chars in the short name
|
||||
// Use "?" if unknown
|
||||
@ -188,7 +198,7 @@ void InkHUD::NodeListApplet::onRender()
|
||||
drawSignalIndicator(signalX, signalY, signalW, signalH, signal);
|
||||
}
|
||||
// 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);
|
||||
hopString += " Hop";
|
||||
if (node->hops_away != 1)
|
||||
|
||||
@ -436,6 +436,12 @@ void setup()
|
||||
|
||||
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();
|
||||
|
||||
#if defined(MODEM_POWER_EN)
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
#ifdef USERPREFS_RINGTONE_NAG_SECS
|
||||
#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS
|
||||
#else
|
||||
#define default_ringtone_nag_secs 60
|
||||
#define default_ringtone_nag_secs 15
|
||||
#endif
|
||||
|
||||
#define default_mqtt_address "mqtt.meshtastic.org"
|
||||
|
||||
@ -272,10 +272,10 @@ uint32_t RadioInterface::getTxDelayMsec()
|
||||
uint8_t RadioInterface::getCWsize(float 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
|
||||
const uint32_t SNR_MAX = 10;
|
||||
const int32_t SNR_MAX = 10;
|
||||
|
||||
return map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax);
|
||||
}
|
||||
|
||||
@ -289,12 +289,7 @@ void RadioLibInterface::onNotify(uint32_t notification)
|
||||
// actual transmission as short as possible
|
||||
txp = txQueue.dequeue();
|
||||
assert(txp);
|
||||
bool sent = 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);
|
||||
}
|
||||
startSend(txp);
|
||||
LOG_DEBUG("%d packets remain in the TX queue", txQueue.getMaxLen() - txQueue.getFree());
|
||||
}
|
||||
}
|
||||
@ -413,6 +408,10 @@ void RadioLibInterface::completeSending()
|
||||
sendingPacket = NULL;
|
||||
|
||||
if (p) {
|
||||
// Packet has been sent, count it toward our TX airtime utilization.
|
||||
uint32_t xmitMsec = getPacketTime(p);
|
||||
airTime->logAirtime(TX_LOG, xmitMsec);
|
||||
|
||||
txGood++;
|
||||
if (!isFromUs(p))
|
||||
txRelay++;
|
||||
|
||||
@ -35,6 +35,15 @@
|
||||
(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;
|
||||
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;
|
||||
Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
|
||||
#else
|
||||
|
||||
@ -94,8 +94,8 @@ int32_t ExternalNotificationModule::runOnce()
|
||||
// audioThread->isPlaying() also handles actually playing the RTTTL, needs to be called in loop
|
||||
isRtttlPlaying = isRtttlPlaying || audioThread->isPlaying();
|
||||
#endif
|
||||
if ((nagCycleCutoff < millis()) && !isRtttlPlaying) {
|
||||
// let the song finish if we reach timeout
|
||||
if ((nagCycleCutoff <= millis())) {
|
||||
// Turn off external notification immediately when timeout is reached, regardless of song state
|
||||
nagCycleCutoff = UINT32_MAX;
|
||||
LOG_INFO("Turning off external notification: ");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
@ -103,7 +103,6 @@ int32_t ExternalNotificationModule::runOnce()
|
||||
externalTurnedOn[i] = 0;
|
||||
LOG_INFO("%d ", i);
|
||||
}
|
||||
LOG_INFO("");
|
||||
#ifdef HAS_I2S
|
||||
// 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
|
||||
|
||||
@ -159,6 +159,7 @@ ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket
|
||||
LOG_DEBUG("---- Received Packet:");
|
||||
LOG_DEBUG("mp.from %d", mp.from);
|
||||
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("---- Node Information of Received Packet (mp.from):");
|
||||
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
|
||||
if (fileToWrite.println(
|
||||
"time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload")) {
|
||||
if (fileToWrite.println("time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx "
|
||||
"snr,distance,hop limit,payload,rx rssi")) {
|
||||
LOG_INFO("File was written");
|
||||
} else {
|
||||
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.
|
||||
fileToAppend.printf("\"%s\"\n", p.payload.bytes);
|
||||
fileToAppend.printf("%i,", mp.rx_rssi); // RX RSSI
|
||||
|
||||
fileToAppend.flush();
|
||||
fileToAppend.close();
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ NimBLECharacteristic *logRadioCharacteristic;
|
||||
NimBLEServer *bleServer;
|
||||
|
||||
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
|
||||
{
|
||||
@ -144,21 +144,28 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
protected:
|
||||
virtual int32_t runOnce() override
|
||||
{
|
||||
bool shouldBreakAndRetryLater = false;
|
||||
|
||||
while (runOnceHasWorkToDo()) {
|
||||
// Important that we service onRead first, because the onRead callback blocks NimBLE until we clear
|
||||
// onReadCallbackIsWaitingForData.
|
||||
shouldBreakAndRetryLater = runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead
|
||||
runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio
|
||||
/*
|
||||
PROCESS fromPhoneQueue BEFORE toPhoneQueue:
|
||||
|
||||
if (shouldBreakAndRetryLater) {
|
||||
// onRead still wants data, but it's not available yet. Return so we can try again when a packet may be ready.
|
||||
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
|
||||
LOG_INFO("BLE runOnce breaking to retry later (leaving onRead waiting)");
|
||||
#endif
|
||||
return 100; // try again in 100ms
|
||||
}
|
||||
In normal STATE_SEND_PACKETS operation, it's unlikely that we'll have both writes and reads to process at the same
|
||||
time, because either onWrite or onRead will trigger this runOnce. And in STATE_SEND_PACKETS, it's generally ok to
|
||||
service either the reads or writes first.
|
||||
|
||||
However, during the initial setup wantConfig packet, the clients send a write and immediately send a read, and they
|
||||
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
|
||||
@ -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)
|
||||
if (bleServer && isConnected()) {
|
||||
int32_t conn_handle = nimbleBluetoothConnHandle.load();
|
||||
if (conn_handle != -1) {
|
||||
requestHighThroughputConnection(static_cast<uint16_t>(conn_handle));
|
||||
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
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
|
||||
if (bleServer && isConnected()) {
|
||||
int32_t conn_handle = nimbleBluetoothConnHandle.load();
|
||||
if (conn_handle != -1) {
|
||||
requestLowerPowerConnection(static_cast<uint16_t>(conn_handle));
|
||||
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
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
|
||||
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
|
||||
size_t numBytes = 0;
|
||||
@ -234,28 +237,15 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
numBytes = getFromRadio(fromRadioBytes);
|
||||
|
||||
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
|
||||
// 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
|
||||
/*
|
||||
Client expected a read, but we have nothing to send.
|
||||
|
||||
// Return true to tell runOnce to shouldBreakAndRetryLater, so we don't busy-loop in runOnce even though
|
||||
// onRead is still waiting!
|
||||
return true;
|
||||
}
|
||||
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.
|
||||
|
||||
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 {
|
||||
// Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible.
|
||||
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.
|
||||
onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; }
|
||||
@ -466,10 +454,6 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
|
||||
virtual void onRead(NimBLECharacteristic *pCharacteristic)
|
||||
#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.
|
||||
|
||||
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
|
||||
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
|
||||
// Restart Advertising
|
||||
|
||||
@ -244,6 +244,10 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
|
||||
// pinMode(PIN_POWER_EN1, INPUT_PULLDOWN);
|
||||
#endif
|
||||
|
||||
#ifdef RAK_WISMESH_TAP_V2
|
||||
digitalWrite(SDCARD_CS, LOW);
|
||||
#endif
|
||||
|
||||
#ifdef TRACKER_T1000_E
|
||||
#ifdef GNSS_AIROHA
|
||||
digitalWrite(GPS_VRTC_EN, LOW);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user