diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index fa9d98a0e65..0fc34ddb3b5 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -60,6 +60,7 @@ along with this program. If not, see . #include "main.h" #include "mesh-pb-constants.h" #include "mesh/Channels.h" +#include "mesh/Default.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" @@ -98,6 +99,7 @@ namespace graphics // This means the *visible* area (sh1106 can address 132, but shows 128 for example) #define IDLE_FRAMERATE 1 // in fps +#define COMPASS_ACTIVE_FRAMERATE 20 // DEBUG #define NUM_EXTRA_FRAMES 3 // text message and debug frame @@ -135,6 +137,60 @@ static bool heartbeat = false; extern bool hasUnreadMessage; +static inline float wrapHeading360(float heading) +{ + if (heading < 0.0f) { + heading += 360.0f; + } else if (heading >= 360.0f) { + heading -= 360.0f; + } + return heading; +} + +void Screen::setHeading(float heading) +{ + const float wrappedHeading = wrapHeading360(heading); + + if (!hasCompass) { + hasCompass = true; + compassHeading = wrappedHeading; + return; + } + + // Interpolate using shortest-path angular delta to avoid jumps around 0/360. + float delta = wrappedHeading - compassHeading; + if (delta > 180.0f) { + delta -= 360.0f; + } else if (delta < -180.0f) { + delta += 360.0f; + } + + // Adaptive filtering: + // - Strong damping for tiny deltas (jitter) + // - Faster response for larger turns + const float absDelta = (delta >= 0.0f) ? delta : -delta; + if (absDelta < 1.0f) { + return; + } + + float alpha = 0.35f; + if (absDelta > 25.0f) { + alpha = 0.85f; + } else if (absDelta > 10.0f) { + alpha = 0.65f; + } + + float step = delta * alpha; + const float maxStep = 12.0f; + if (step > maxStep) { + step = maxStep; + } else if (step < -maxStep) { + step = -maxStep; + } + + compassHeading = wrapHeading360(compassHeading + step); +} + // ============================== // Overlay Alert Banner Renderer // ============================== @@ -272,10 +328,25 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int float Screen::estimatedHeading(double lat, double lon) { static double oldLat, oldLon; - static float b; + static float b = -1.0f; + static uint32_t lastHeadingAtMs = 0; + const uint32_t now = millis(); + const uint32_t gpsUpdateIntervalSecs = + Default::getConfiguredOrDefault(config.position.gps_update_interval, default_gps_update_interval); + uint32_t effectiveUpdateIntervalSecs = gpsUpdateIntervalSecs; + if (config.position.position_broadcast_smart_enabled) { + const uint32_t smartMinIntervalSecs = Default::getConfiguredOrDefault( + config.position.broadcast_smart_minimum_interval_secs, default_broadcast_smart_minimum_interval_secs); + if (smartMinIntervalSecs > effectiveUpdateIntervalSecs) { + effectiveUpdateIntervalSecs = smartMinIntervalSecs; + } + } + // Two expected update windows; keep arithmetic 32-bit to avoid pulling in larger 64-bit helpers. + const uint32_t headingStaleMs = + (effectiveUpdateIntervalSecs > (UINT32_MAX / 2000U)) ? UINT32_MAX : (effectiveUpdateIntervalSecs * 2000U); if (oldLat == 0) { - // just prepare for next time + // Need at least two position points before we can infer heading. oldLat = lat; oldLon = lon; @@ -283,12 +354,20 @@ float Screen::estimatedHeading(double lat, double lon) } float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon); - if (d < 10) // haven't moved enough, just keep current bearing + if (d < 10) { // haven't moved enough, keep previous heading (invalid until first real movement) + if (lastHeadingAtMs != 0 && (now - lastHeadingAtMs) >= headingStaleMs) { + // Heading is stale after prolonged no-movement; force reacquire. + b = -1.0f; + oldLat = lat; + oldLon = lon; + } return b; + } b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG; oldLat = lat; oldLon = lon; + lastHeadingAtMs = now; return b; } @@ -923,9 +1002,22 @@ int32_t Screen::runOnce() // but we should only call setTargetFPS when framestate changes, because // otherwise that breaks animations. - if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { + uint32_t desiredFramerate = IDLE_FRAMERATE; +#if HAS_GPS && !defined(USE_EINK) + if (showingNormalScreen && hasCompass) { + const uint8_t currentFrame = ui->getUiState()->currentFrame; + if ((framesetInfo.positions.gps != 255 && currentFrame == framesetInfo.positions.gps) || + (framesetInfo.positions.waypoint != 255 && currentFrame == framesetInfo.positions.waypoint) || + (framesetInfo.positions.firstFavorite != 255 && currentFrame >= framesetInfo.positions.firstFavorite && + currentFrame <= framesetInfo.positions.lastFavorite)) { + desiredFramerate = COMPASS_ACTIVE_FRAMERATE; + } + } +#endif + + if (targetFramerate != desiredFramerate && ui->getUiState()->frameState == FIXED) { // oldFrameState = ui->getUiState()->frameState; - targetFramerate = IDLE_FRAMERATE; + targetFramerate = desiredFramerate; ui->setTargetFPS(targetFramerate); forceDisplay(); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index e259f7691e0..5a1a2d6da2c 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -330,15 +330,11 @@ class Screen : public concurrency::OSThread // Function to allow the AccelerometerThread to set the heading if a sensor provides it // Mutex needed? - void setHeading(long _heading) - { - hasCompass = true; - compassHeading = fmod(_heading, 360); - } + void setHeading(float heading); bool hasHeading() { return hasCompass; } - long getHeading() { return compassHeading; } + float getHeading() { return compassHeading; } void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; } uint32_t getEndCalibration() { return endCalibrationAt; } @@ -782,4 +778,4 @@ extern std::vector functionSymbol; extern std::string functionSymbolString; extern graphics::Screen *screen; -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index 42600ce96e1..fe54d68e714 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -1,10 +1,6 @@ #include "configuration.h" #if HAS_SCREEN #include "CompassRenderer.h" -#include "NodeDB.h" -#include "UIRenderer.h" -#include "configuration.h" -#include "gps/GeoCoord.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include @@ -21,8 +17,8 @@ struct Point { void rotate(float angle) { - float cos_a = cos(angle); - float sin_a = sin(angle); + float cos_a = cosf(angle); + float sin_a = sinf(angle); float new_x = x * cos_a - y * sin_a; float new_y = x * sin_a + y * cos_a; x = new_x; @@ -51,21 +47,30 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, if (currentResolution == ScreenResolution::High) { radius += 4; } - Point north(0, -radius); - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - north.rotate(-myHeading); - north.translate(compassX, compassY); + float northX = 0.0f; + float northY = -radius; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) { + const float c = cosf(-myHeading); + const float s = sinf(-myHeading); + const float rx = northX * c - northY * s; + const float ry = northX * s + northY * c; + northX = rx; + northY = ry; + } + northX += compassX; + northY += compassY; display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setColor(BLACK); + const int16_t nLabelWidth = display->getStringWidth("N"); if (currentResolution == ScreenResolution::High) { - display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); + display->fillRect(northX - 8, northY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6); } else { - display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); + display->fillRect(northX - 4, northY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6); } display->setColor(WHITE); - display->drawString(north.x, north.y - 3, "N"); + display->drawString(northX, northY - 3, "N"); } void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) @@ -113,11 +118,46 @@ void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, f display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y); } -float estimatedHeading(double lat, double lon) +bool getHeadingRadians(double lat, double lon, float &headingRadian) +{ + headingRadian = 0.0f; + + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) + return true; + + if (!screen) + return false; + + if (screen->hasHeading()) { + headingRadian = screen->getHeading() * DEG_TO_RAD; + return true; + } + + const float estimatedHeadingDeg = screen->estimatedHeading(lat, lon); + if (!(estimatedHeadingDeg >= 0.0f)) + return false; + + headingRadian = estimatedHeadingDeg * DEG_TO_RAD; + return true; +} + +float adjustBearingForCompassMode(float bearingRadian, float headingRadian) { - // Simple magnetic declination estimation - // This is a very basic implementation - the original might be more sophisticated - return 0.0f; // Return 0 for now, indicating no heading available + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + return bearingRadian - headingRadian; + + return bearingRadian; +} + +float radiansToDegrees360(float angleRadian) +{ + constexpr float fullTurnDeg = 360.0f; + float degrees = angleRadian * RAD_TO_DEG; + if (degrees < 0.0f) + degrees += fullTurnDeg; + else if (degrees >= fullTurnDeg) + degrees -= fullTurnDeg; + return degrees; } uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) @@ -137,4 +177,4 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) } // namespace CompassRenderer } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/CompassRenderer.h b/src/graphics/draw/CompassRenderer.h index ca7532b6671..d7762384769 100644 --- a/src/graphics/draw/CompassRenderer.h +++ b/src/graphics/draw/CompassRenderer.h @@ -1,7 +1,6 @@ #pragma once #include "graphics/Screen.h" -#include "mesh/generated/meshtastic/mesh.pb.h" #include #include @@ -25,7 +24,9 @@ void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, u void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing); // Navigation and location functions -float estimatedHeading(double lat, double lon); +bool getHeadingRadians(double lat, double lon, float &headingRadian); +float adjustBearingForCompassMode(float bearingRadian, float headingRadian); +float radiansToDegrees360(float angleRadian); uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); } // namespace CompassRenderer diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 654c272229d..e0c5df1249f 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -409,14 +409,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } - if (strlen(distStr) > 0) { - int offset = (currentResolution == ScreenResolution::High) - ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) - int rightEdge = x + columnWidth - offset; - int textWidth = display->getStringWidth(distStr); - display->drawString(rightEdge - textWidth, y, distStr); - } + const char *distanceLabel = (strlen(distStr) > 0) ? distStr : "?"; + int offset = (currentResolution == ScreenResolution::High) + ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) + int rightEdge = x + columnWidth - offset; + int textWidth = display->getStringWidth(distanceLabel); + display->drawString(rightEdge - textWidth, y, distanceLabel); } void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) @@ -467,8 +466,8 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } -void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, - double userLat, double userLon) +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, + float myHeadingRadian, double userLat, double userLon) { if (!nodeDB->hasValidPosition(node)) return; @@ -482,11 +481,11 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 double nodeLat = node->position.latitude_i * 1e-7; double nodeLon = node->position.longitude_i * 1e-7; float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon); - float bearingToNode = RAD_TO_DEG * bearing; - float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); + float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian); + float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing); // Shrink size by 2px int size = FONT_HEIGHT_SMALL - 5; - CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing); + CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearingDeg); /* float angle = relativeBearing * DEG_TO_RAD; float halfSize = size / 2.0; @@ -516,12 +515,27 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 */ } +void drawCompassUnknown(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float, double, + double) +{ + if (!nodeDB->hasValidPosition(node)) + return; + + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + int centerX = x + columnWidth - arrowXOffset; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(centerX, y, "?"); +} + // ============================= // 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 headingRadian, double lat, double lon) { const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; const int rowYOffset = FONT_HEIGHT_SMALL - 3; @@ -606,7 +620,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t renderer(display, node, xPos, yPos, columnWidth); if (extras) - extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); + extras(display, node, xPos, yPos, columnWidth, headingRadian, lat, lon); lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL); yOffset += rowYOffset; @@ -801,9 +815,13 @@ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t #endif void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - float heading = 0; - bool validHeading = false; + float headingRadian = 0.0f; auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (!ourNode || !nodeDB->hasValidPosition(ourNode)) { + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0); + return; + } + double lat = DegD(ourNode->position.latitude_i); double lon = DegD(ourNode->position.longitude_i); @@ -815,21 +833,12 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, lastSwitchTime = now; } #endif - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { -#if HAS_GPS - if (screen->hasHeading()) { - heading = screen->getHeading(); // degrees - validHeading = true; - } else { - heading = screen->estimatedHeading(lat, lon); - validHeading = !isnan(heading); - } -#endif - - if (!validHeading) - return; + if (!CompassRenderer::getHeadingRadians(lat, lon, headingRadian)) { + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, lat, lon); + return; } - drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); + + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, headingRadian, lat, lon); } /// Draw a series of fields in a column, wrapping to multiple columns if needed diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index be80a7d80bc..4aa21714111 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -32,7 +32,7 @@ enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATIO // 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, + EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float headingRadian = 0, double lat = 0, double lon = 0); // Entry renderers @@ -43,8 +43,8 @@ void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); // Extras renderers -void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, - double userLat, double userLon); +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, + float myHeadingRadian, double userLat, double userLon); // Screen frame functions void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 78d10988157..b94c25a277e 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -41,6 +41,15 @@ static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y) } } +static void drawCompassStatusText(OLEDDisplay *display, int16_t compassX, int16_t compassY, const char *statusLine1, + const char *statusLine2) +{ + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(compassX, compassY - FONT_HEIGHT_SMALL, statusLine1); + display->drawString(compassX, compassY, statusLine2); + display->setTextAlignment(TEXT_ALIGN_LEFT); +} + void graphics::UIRenderer::rebuildFavoritedNodes() { favoritedNodes.clear(); @@ -692,51 +701,54 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat display->drawString(x, getTextPositions(display)[line++], batLine); } + bool showCompass = false; + float myHeading = 0.0f; + float bearing = 0.0f; + const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode)); + const bool hasNodePositionFix = nodeDB->hasValidPosition(node); + const char *statusLine1 = nullptr; + const char *statusLine2 = nullptr; + if (hasOwnPositionFix && hasNodePositionFix) { + const auto &op = ourNode->position; + showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading); + if (showCompass) { + const auto &p = node->position; + bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading); + } else { + statusLine1 = "No"; + statusLine2 = "Heading"; + } + } else if (!hasOwnPositionFix || !hasNodePositionFix) { + statusLine1 = "No"; + statusLine2 = "Fix"; + } + // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- if (SCREEN_WIDTH > SCREEN_HEIGHT) { - bool showCompass = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { - showCompass = true; - } - if (showCompass) { + if (showCompass || statusLine1) { const int16_t topY = getTextPositions(display)[1]; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); const int16_t usableHeight = bottomY - topY - 5; int16_t compassRadius = usableHeight / 2; if (compassRadius < 8) compassRadius = 8; - const int16_t compassDiam = compassRadius * 2; const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + const int16_t compassDiam = compassRadius * 2; - const auto &op = ourNode->position; - float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - - const auto &p = node->position; - /* unused - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - */ - float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = 0; + display->drawCircle(compassX, compassY, compassRadius); + if (showCompass) { + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); } else { - bearing -= myHeading; + drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); } - - display->drawCircle(compassX, compassY, compassRadius); - CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); } // else show nothing } else { // Portrait or square: put compass at the bottom and centered, scaled to fit available space - bool showCompass = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { - showCompass = true; - } - if (showCompass) { + if (showCompass || statusLine1) { int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2) : getTextPositions(display)[1]; const int margin = 4; @@ -747,8 +759,8 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat #else const int navBarHeight = 0; #endif - int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; // --------- END PATCH FOR EINK NAV BAR ----------- + int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; if (availableHeight < FONT_HEIGHT_SMALL * 2) return; @@ -762,25 +774,13 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat int compassX = x + SCREEN_WIDTH / 2; int compassY = yBelowContent + availableHeight / 2; - const auto &op = ourNode->position; - float myHeading = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - } - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); - - const auto &p = node->position; - /* unused - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - */ - float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) - bearing -= myHeading; - graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); - display->drawCircle(compassX, compassY, compassRadius); + if (showCompass) { + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); + } else { + drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); + } } // else show nothing } @@ -1216,6 +1216,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // === Header === graphics::drawCommonHeader(display, x, y, titleStr); + const int *textPos = getTextPositions(display); // === First Row: My Location === #if HAS_GPS @@ -1230,12 +1231,12 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - drawSatelliteIcon(display, x, getTextPositions(display)[line]); + drawSatelliteIcon(display, x, textPos[line]); int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; - display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); + display->drawString(x + 11 + xOffset, textPos[line++], displayLine); } else { // Onboard GPS - UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); + UIRenderer::drawGps(display, 0, textPos[line++], gpsStatus); } config.display.heading_bold = origBold; @@ -1244,18 +1245,36 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), int32_t(gpsStatus->getAltitude())); - // === Determine Compass Heading === - float heading = 0; + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode)); + const bool hasLiveGpsFix = + (gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0)); + const bool hasSensorHeading = screen->hasHeading(); + float heading = 0.0f; bool validHeading = false; - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - validHeading = true; - } else { - if (screen->hasHeading()) { - heading = radians(screen->getHeading()); - validHeading = true; + const char *statusLine1 = nullptr; + const char *statusLine2 = nullptr; + if (hasSensorHeading || hasLiveGpsFix || hasOwnPositionFix) { + double headingLat = 0.0; + double headingLon = 0.0; + if (hasLiveGpsFix) { + headingLat = DegD(gpsStatus->getLatitude()); + headingLon = DegD(gpsStatus->getLongitude()); + } else if (hasOwnPositionFix) { + const auto &op = ourNode->position; + headingLat = DegD(op.latitude_i); + headingLon = DegD(op.longitude_i); + } + validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading); + } + + if (!validHeading) { + if (hasSensorHeading || hasLiveGpsFix || hasOwnPositionFix) { + statusLine1 = "No"; + statusLine2 = "Heading"; } else { - heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); - validHeading = !isnan(heading); + statusLine1 = "No"; + statusLine2 = "Fix"; } } @@ -1273,18 +1292,18 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true); #endif - display->drawString(0, getTextPositions(display)[line++], uptimeStr); + display->drawString(0, textPos[line++], uptimeStr); } else { - display->drawString(0, getTextPositions(display)[line++], "Last: ?"); + display->drawString(0, textPos[line++], "Last: ?"); } // === Third Row: Line 1 GPS Info === - UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1"); + UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line1"); if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC && uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // === Fourth Row: Line 2 GPS Info === - UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2"); + UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line2"); } // === Final Row: Altitude === @@ -1295,14 +1314,14 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } else { snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt); } - display->drawString(x, getTextPositions(display)[line++], altitudeLine); + display->drawString(x, textPos[line++], altitudeLine); } #if !defined(M5STACK_UNITC6L) - // === Draw Compass if heading is valid === - if (validHeading) { + // === Draw Compass === + if (validHeading || statusLine1) { // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- if (SCREEN_WIDTH > SCREEN_HEIGHT) { - const int16_t topY = getTextPositions(display)[1]; + const int16_t topY = textPos[1]; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height const int16_t usableHeight = bottomY - topY - 5; @@ -1315,29 +1334,33 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // Center vertically and nudge down slightly to keep "N" clear of header const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); display->drawCircle(compassX, compassY, compassRadius); - - // "N" label - float northAngle = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + if (validHeading) { + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); + + // "N" label + float northAngle = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } else { + drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); + } } else { // Portrait or square: put compass at the bottom and centered, scaled to fit available space // For E-Ink screens, account for navigation bar at the bottom! - int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2; + int yBelowContent = textPos[5] + FONT_HEIGHT_SMALL + 2; const int margin = 4; int availableHeight = #if defined(USE_EINK) @@ -1358,25 +1381,29 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU int compassX = x + SCREEN_WIDTH / 2; int compassY = yBelowContent + availableHeight / 2; - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); display->drawCircle(compassX, compassY, compassRadius); - - // "N" label - float northAngle = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + if (validHeading) { + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); + + // "N" label + float northAngle = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } else { + drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); + } } } #endif diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 4db80ba183c..632727b9240 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -15,15 +15,6 @@ WaypointModule *waypointModule; -static inline float degToRad(float deg) -{ - return deg * PI / 180.0f; -} -static inline float radToDeg(float rad) -{ - return rad * 180.0f / PI; -} - ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) @@ -91,9 +82,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // === Header === graphics::drawCommonHeader(display, x, y, titleStr); - - const int w = display->getWidth(); - const int h = display->getHeight(); + const int *textPos = graphics::getTextPositions(display); // Decode the waypoint const meshtastic_MeshPacket &mp = devicestate.rx_waypoint; @@ -108,71 +97,118 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); // Will contain distance information, passed as a field to drawColumns - char distStr[20]; + char distStr[20] = ""; // Get our node, to use our own position meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - // Dimensions / co-ordinates for the compass/circle - const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h); - const int16_t compassX = x + w - (compassDiam / 2) - 5; - const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) - ? y + h / 2 - : y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2; + // Match compass sizing/placement to favorite node screen logic. + const int w = display->getWidth(); + int16_t compassRadius = 8; + int16_t compassX = x + w - compassRadius - 8; + int16_t compassY = y + display->getHeight() / 2; + + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + const int16_t topY = textPos[1]; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); + const int16_t usableHeight = bottomY - topY - 5; + compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + compassX = x + SCREEN_WIDTH - compassRadius - 8; + compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + } else { + // Waypoint content uses rows 1..4, so place the compass below that block. + const int yBelowContent = textPos[4] + FONT_HEIGHT_SMALL + 2; + const int margin = 4; +#if defined(USE_EINK) + const int iconSize = (graphics::currentResolution == graphics::ScreenResolution::High) ? 16 : 8; + const int navBarHeight = iconSize + 6; +#else + const int navBarHeight = 0; +#endif + const int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; + if (availableHeight > 0) { + compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; + if (compassRadius < 8) + compassRadius = 8; + compassX = x + SCREEN_WIDTH / 2; + compassY = yBelowContent + availableHeight / 2; + } + } + const uint16_t compassDiam = compassRadius * 2; + + const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode)); + const char *statusLine1 = nullptr; + const char *statusLine2 = nullptr; - // If our node has a position: - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { + // Distance only needs our own position fix; compass/bearing additionally needs heading. + if (hasOwnPositionFix) { const meshtastic_PositionLite &op = ourNode->position; - float myHeading; - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = 0; - } else { - if (screen->hasHeading()) - myHeading = degToRad(screen->getHeading()); - else - myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - } - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2)); - - // Compass bearing to waypoint - float bearingToOther = - GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); - // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly - // If the top of the compass is not a static north we need adjust bearingToOther based on heading - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) - bearingToOther -= myHeading; - graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); - - float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; - bearingToOtherDegrees = radToDeg(bearingToOtherDegrees); - - // Distance to Waypoint - float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + const float d = + GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + + // Always show distance once we have an own-position fix, even without heading. if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { float feet = d * METERS_TO_FEET; - snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", - feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees); + snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft" : "%.1fmi", + feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET); } else { - snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, - bearingToOtherDegrees); + snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm" : "%.1fkm", d < 2000 ? d : d / 1000); } - } - else { - display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); + float myHeading = 0.0f; + const bool hasHeading = + graphics::CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading); + if (hasHeading) { + // Draw compass circle + display->drawCircle(compassX, compassY, compassRadius); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + + // Compass bearing to waypoint + float bearingToOther = + GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); + bearingToOther = graphics::CompassRenderer::adjustBearingForCompassMode(bearingToOther, myHeading); + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + + const float bearingToOtherDegrees = graphics::CompassRenderer::radiansToDegrees360(bearingToOther); + + // Distance to waypoint with relative bearing when heading is available. + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + float feet = d * METERS_TO_FEET; + snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", + feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees); + } else { + snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, + bearingToOtherDegrees); + } - // ? in the distance field - snprintf(distStr, sizeof(distStr), "? %s ?°", - (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km"); + } else { + statusLine1 = "No"; + statusLine2 = "Heading"; + } + } else { + // No own fix yet, so compass/bearing data would be misleading. + statusLine1 = "No"; + statusLine2 = "Fix"; } - // Draw compass circle - display->drawCircle(compassX, compassY, compassDiam / 2); + if (statusLine1) { + display->drawCircle(compassX, compassY, compassRadius); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(compassX, compassY - FONT_HEIGHT_SMALL, statusLine1); + display->drawString(compassX, compassY, statusLine2); + } display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here! - display->drawString(0, graphics::getTextPositions(display)[line++], lastStr); - display->drawString(0, graphics::getTextPositions(display)[line++], wp.name); - display->drawString(0, graphics::getTextPositions(display)[line++], wp.description); - display->drawString(0, graphics::getTextPositions(display)[line++], distStr); + display->drawString(0, textPos[line++], lastStr); + display->drawString(0, textPos[line++], wp.name); + display->drawString(0, textPos[line++], wp.description); + if (distStr[0]) + display->drawString(0, textPos[line++], distStr); } #endif diff --git a/src/motion/BMM150Sensor.cpp b/src/motion/BMM150Sensor.cpp index 4b3a1215c13..f48d20288b1 100644 --- a/src/motion/BMM150Sensor.cpp +++ b/src/motion/BMM150Sensor.cpp @@ -7,9 +7,6 @@ extern graphics::Screen *screen; #endif -// Flag when an interrupt has been detected -volatile static bool BMM150_IRQ = false; - BMM150Sensor::BMM150Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} bool BMM150Sensor::init() @@ -23,24 +20,7 @@ int32_t BMM150Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN float heading = sensor->getCompassDegree(); - - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } + heading = applyCompassOrientation(heading); if (screen) screen->setHeading(heading); #endif @@ -90,4 +70,4 @@ bool BMM150Singleton::init(ScanI2C::FoundDevice device) return true; } -#endif \ No newline at end of file +#endif diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp index 5888c20bec1..02303faa4ff 100644 --- a/src/motion/BMX160Sensor.cpp +++ b/src/motion/BMX160Sensor.cpp @@ -16,6 +16,7 @@ bool BMX160Sensor::init() if (sensor.begin()) { // set output data rate sensor.ODR_Config(BMX160_ACCEL_ODR_100HZ, BMX160_GYRO_ODR_100HZ); + loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); LOG_DEBUG("BMX160 init ok"); return true; } @@ -33,42 +34,12 @@ int32_t BMX160Sensor::runOnce() sensor.getAllData(&magAccel, NULL, &gAccel); if (doCalibration) { - - if (!showingScreen) { - powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration - showingScreen = true; - if (screen) - screen->startAlert((FrameCallback)drawFrameCalibration); - } - - if (magAccel.x > highestX) - highestX = magAccel.x; - if (magAccel.x < lowestX) - lowestX = magAccel.x; - if (magAccel.y > highestY) - highestY = magAccel.y; - if (magAccel.y < lowestY) - lowestY = magAccel.y; - if (magAccel.z > highestZ) - highestZ = magAccel.z; - if (magAccel.z < lowestZ) - lowestZ = magAccel.z; - - uint32_t now = millis(); - if (now > endCalibrationAt) { - doCalibration = false; - endCalibrationAt = 0; - showingScreen = false; - if (screen) - screen->endAlert(); - } - - // LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, - // lowestY, highestY, lowestZ, highestZ); + beginCalibrationDisplay(showingScreen); + updateCalibrationExtrema(magAccel.x, magAccel.y, magAccel.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, + lowestZ); } - int highestRealX = highestX - (highestX + lowestX) / 2; - magAccel.x -= (highestX + lowestX) / 2; magAccel.y -= (highestY + lowestY) / 2; magAccel.z -= (highestZ + lowestZ) / 2; @@ -88,23 +59,7 @@ int32_t BMX160Sensor::runOnce() float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } + heading = applyCompassOrientation(heading); if (screen) screen->setHeading(heading); #endif @@ -119,15 +74,8 @@ void BMX160Sensor::calibrate(uint16_t forSeconds) sBmx160SensorData_t gAccel; LOG_DEBUG("BMX160 calibration started for %is", forSeconds); sensor.getAllData(&magAccel, NULL, &gAccel); - highestX = magAccel.x, lowestX = magAccel.x; - highestY = magAccel.y, lowestY = magAccel.y; - highestZ = magAccel.z, lowestZ = magAccel.z; - - doCalibration = true; - uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided - endCalibrationAt = millis() + calibrateFor; - if (screen) - screen->setEndCalibration(endCalibrationAt); + seedCalibrationExtrema(magAccel.x, magAccel.y, magAccel.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + startCalibrationWindow(forSeconds); #endif } diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h index ddca5767c74..d60477521c9 100644 --- a/src/motion/BMX160Sensor.h +++ b/src/motion/BMX160Sensor.h @@ -17,6 +17,7 @@ class BMX160Sensor : public MotionSensor private: RAK_BMX160 sensor; bool showingScreen = false; + static constexpr const char *compassCalibrationFileName = "/prefs/compass_bmx160.dat"; float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; public: @@ -39,4 +40,4 @@ class BMX160Sensor : public MotionSensor #endif -#endif \ No newline at end of file +#endif diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index ecada208575..e44994a6006 100644 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -26,7 +26,11 @@ bool ICM20948Sensor::init() return false; // Enable simple Wake on Motion - return sensor->setWakeOnMotion(); + bool wakeOnMotionOk = sensor->setWakeOnMotion(); + if (wakeOnMotionOk) { + loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + } + return wakeOnMotionOk; } #ifdef ICM_20948_INT_PIN @@ -47,7 +51,8 @@ int32_t ICM20948Sensor::runOnce() int32_t ICM20948Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN - if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { + if (screen && !doCalibration && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && + !config.device.double_tap_as_button_press) { if (!isAsleep) { LOG_DEBUG("sleeping IMU"); sensor->sleep(true); @@ -69,38 +74,10 @@ int32_t ICM20948Sensor::runOnce() } if (doCalibration) { - - if (!showingScreen) { - powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration - showingScreen = true; - if (screen) - screen->startAlert((FrameCallback)drawFrameCalibration); - } - - if (magX > highestX) - highestX = magX; - if (magX < lowestX) - lowestX = magX; - if (magY > highestY) - highestY = magY; - if (magY < lowestY) - lowestY = magY; - if (magZ > highestZ) - highestZ = magZ; - if (magZ < lowestZ) - lowestZ = magZ; - - uint32_t now = millis(); - if (now > endCalibrationAt) { - doCalibration = false; - endCalibrationAt = 0; - showingScreen = false; - if (screen) - screen->endAlert(); - } - - // LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, - // lowestY, highestY, lowestZ, highestZ); + beginCalibrationDisplay(showingScreen); + updateCalibrationExtrema(magX, magY, magZ, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, + lowestZ); } magX -= (highestX + lowestX) / 2; @@ -122,23 +99,7 @@ int32_t ICM20948Sensor::runOnce() float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } + heading = applyCompassOrientation(heading); if (screen) screen->setHeading(heading); #endif @@ -169,26 +130,16 @@ int32_t ICM20948Sensor::runOnce() void ICM20948Sensor::calibrate(uint16_t forSeconds) { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN - LOG_DEBUG("Old calibration data: highestX = %f, lowestX = %f, highestY = %f, lowestY = %f, highestZ = %f, lowestZ = %f", - highestX, lowestX, highestY, lowestY, highestZ, lowestZ); - LOG_DEBUG("BMX160 calibration started for %is", forSeconds); + LOG_DEBUG("ICM20948 cal start %is", forSeconds); if (sensor->dataReady()) { sensor->getAGMT(); - highestX = sensor->agmt.mag.axes.x; - lowestX = sensor->agmt.mag.axes.x; - highestY = sensor->agmt.mag.axes.y; - lowestY = sensor->agmt.mag.axes.y; - highestZ = sensor->agmt.mag.axes.z; - lowestZ = sensor->agmt.mag.axes.z; + seedCalibrationExtrema(sensor->agmt.mag.axes.x, sensor->agmt.mag.axes.y, sensor->agmt.mag.axes.z, highestX, lowestX, + highestY, lowestY, highestZ, lowestZ); } else { - highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; + seedCalibrationExtrema(0.0f, 0.0f, 0.0f, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); } - doCalibration = true; - uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided - endCalibrationAt = millis() + calibrateFor; - if (screen) - screen->setEndCalibration(endCalibrationAt); + startCalibrationWindow(forSeconds); #endif } // ---------------------------------------------------------------------- @@ -314,11 +265,6 @@ bool ICM20948Singleton::setWakeOnMotion() status = intEnableWOM(true); LOG_DEBUG("ICM20948 init set intEnableWOM - %s", statusString()); return status == ICM_20948_Stat_Ok; - - // Clear any current interrupts - ICM20948_IRQ = false; - clearInterrupts(); - return true; } #endif diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h index 091cb9a1e95..d8369b3ca16 100644 --- a/src/motion/ICM20948Sensor.h +++ b/src/motion/ICM20948Sensor.h @@ -83,6 +83,7 @@ class ICM20948Sensor : public MotionSensor ICM20948Singleton *sensor = nullptr; bool showingScreen = false; bool isAsleep = false; + static constexpr const char *compassCalibrationFileName = "/prefs/compass_icm20948.dat"; #ifdef MUZI_BASE float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, lowestZ = 98.000000; @@ -103,4 +104,4 @@ class ICM20948Sensor : public MotionSensor #endif -#endif \ No newline at end of file +#endif diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index d0bfe4e2ce3..83231aea90c 100644 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -1,10 +1,37 @@ #include "MotionSensor.h" +#include "FSCommon.h" +#include "SPILock.h" +#include "SafeFile.h" #include "graphics/draw/CompassRenderer.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C char timeRemainingBuffer[12]; +namespace +{ +constexpr uint32_t COMPASS_CALIBRATION_MAGIC = 0x4D43414CL; // "MCAL" +constexpr uint16_t COMPASS_CALIBRATION_VERSION = 1; + +struct CompassCalibrationRecord { + uint32_t magic; + uint16_t version; + uint16_t reserved; + float highestX; + float lowestX; + float highestY; + float lowestY; + float highestZ; + float lowestZ; +}; + +bool isRangeValid(float highest, float lowest) +{ + // NaN/Inf guard without pulling in extra math helpers. + return (highest == highest) && (lowest == lowest) && (highest > lowest); +} +} // namespace + // screen is defined in main.cpp extern graphics::Screen *screen; @@ -32,33 +59,237 @@ ScanI2C::I2CPort MotionSensor::devicePort() return device.address.port; } +bool MotionSensor::saveMagnetometerCalibration(const char *filePath, float highestX, float lowestX, float highestY, float lowestY, + float highestZ, float lowestZ) +{ +#ifdef FSCom + if (!isRangeValid(highestX, lowestX) || !isRangeValid(highestY, lowestY) || !isRangeValid(highestZ, lowestZ)) { + return false; + } + + FSCom.mkdir("/prefs"); + CompassCalibrationRecord record = { + COMPASS_CALIBRATION_MAGIC, COMPASS_CALIBRATION_VERSION, 0, highestX, lowestX, highestY, lowestY, highestZ, lowestZ}; + + auto file = SafeFile(filePath, true); + const size_t written = file.write(reinterpret_cast(&record), sizeof(record)); + return (written == sizeof(record)) && file.close(); +#else + return false; +#endif +} + +bool MotionSensor::loadMagnetometerCalibration(const char *filePath, float &highestX, float &lowestX, float &highestY, + float &lowestY, float &highestZ, float &lowestZ) +{ +#ifdef FSCom + CompassCalibrationRecord record = {}; + size_t bytesRead = 0; + + spiLock->lock(); + auto file = FSCom.open(filePath, FILE_O_READ); + if (!file) { + spiLock->unlock(); + return false; + } + bytesRead = file.read(reinterpret_cast(&record), sizeof(record)); + file.close(); + spiLock->unlock(); + + const bool headerValid = (bytesRead == sizeof(record)) && (record.magic == COMPASS_CALIBRATION_MAGIC) && + (record.version == COMPASS_CALIBRATION_VERSION) && (record.reserved == 0U); + const bool rangeValid = isRangeValid(record.highestX, record.lowestX) && isRangeValid(record.highestY, record.lowestY) && + isRangeValid(record.highestZ, record.lowestZ); + if (!headerValid || !rangeValid) { + return false; + } + + highestX = record.highestX; + lowestX = record.lowestX; + highestY = record.highestY; + lowestY = record.lowestY; + highestZ = record.highestZ; + lowestZ = record.lowestZ; + + return true; +#else + return false; +#endif +} + +void MotionSensor::beginCalibrationDisplay(bool &showingScreen) +{ +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN + if (!showingScreen) { + powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration + showingScreen = true; + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); + } +#else + (void)showingScreen; +#endif +} + +void MotionSensor::finishCalibrationIfExpired(bool &showingScreen, const char *filePath, float highestX, float lowestX, + float highestY, float lowestY, float highestZ, float lowestZ) +{ + const uint32_t now = millis(); + if ((int32_t)(now - endCalibrationAt) < 0) + return; + + doCalibration = false; + endCalibrationAt = 0; + showingScreen = false; + saveMagnetometerCalibration(filePath, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN + if (screen) { + screen->setEndCalibration(0); + screen->endAlert(); + } +#endif +} + +void MotionSensor::startCalibrationWindow(uint16_t forSeconds) +{ + doCalibration = true; + const uint32_t calibrateFor = static_cast(forSeconds) * 1000U; + endCalibrationAt = millis() + calibrateFor; +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN + if (screen) + screen->setEndCalibration(endCalibrationAt); +#endif +} + +void MotionSensor::seedCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY, + float &lowestY, float &highestZ, float &lowestZ) +{ + highestX = lowestX = x; + highestY = lowestY = y; + highestZ = lowestZ = z; +} + +void MotionSensor::updateCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY, + float &lowestY, float &highestZ, float &lowestZ) +{ + if (x > highestX) + highestX = x; + if (x < lowestX) + lowestX = x; + if (y > highestY) + highestY = y; + if (y < lowestY) + lowestY = y; + if (z > highestZ) + highestZ = z; + if (z < lowestZ) + lowestZ = z; +} + +float MotionSensor::applyCompassOrientation(float heading) +{ + switch (config.display.compass_orientation) { + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: + return heading + 90; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: + return heading + 180; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: + return heading + 270; + default: + return heading; + } +} + #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { if (screen == nullptr) return; - // int x_offset = display->width() / 2; - // int y_offset = display->height() <= 80 ? 0 : 32; - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_MEDIUM); - display->drawString(x, y, "Calibrating\nCompass"); - uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000; - sprintf(timeRemainingBuffer, "( %02d )", timeRemaining); - display->setFont(FONT_SMALL); - display->drawString(x, y + 40, timeRemainingBuffer); + const int16_t width = display->getWidth(); + const int16_t height = display->getHeight(); + const bool compactLayout = (height <= 80); + const int16_t margin = 4; + + const uint32_t now = millis(); + const uint32_t endCalibrationAt = screen->getEndCalibration(); + uint32_t timeRemaining = 0; + if (endCalibrationAt > now) { + timeRemaining = (endCalibrationAt - now + 999) / 1000; + } int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); + uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(width, height); + const int16_t compassRadius = compassDiam / 2; // coordinates for the center of the compass/circle if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - compassX = x + display->getWidth() - compassDiam / 2 - 5; - compassY = y + display->getHeight() / 2; + compassX = x + width - compassRadius - margin; + compassY = y + height / 2; + } else { + compassX = x + width - compassRadius - margin; + compassY = y + FONT_HEIGHT_SMALL + (height - FONT_HEIGHT_SMALL) / 2; + } + + const int16_t textLeft = x + 1; + const int16_t textRight = compassX - compassRadius - margin; + const int16_t textWidth = textRight - textLeft; + int16_t lineY = y; + + display->setTextAlignment(TEXT_ALIGN_LEFT); + if (textWidth > 12) { + const char *title = "Cal"; + const char *line1 = "Figure-8"; + const char *line2 = "Rotate axes"; + const char *line3 = "Away from metal"; + + display->setFont(FONT_SMALL); + if (!compactLayout && display->getStringWidth("Compass Calibration") <= textWidth) { + display->setFont(FONT_MEDIUM); + title = "Compass Calibration"; + line1 = "Move in figure-8"; + line2 = "Rotate all axes"; + line3 = "Keep from metal"; + display->drawString(textLeft, lineY, title); + lineY += FONT_HEIGHT_MEDIUM; + display->setFont(FONT_SMALL); + } else if (display->getStringWidth("Compass Cal") <= textWidth) { + title = "Compass Cal"; + if (textWidth >= display->getStringWidth("Move in figure-8")) { + line1 = "Move in figure-8"; + line2 = "Rotate all axes"; + line3 = "Keep from metal"; + } + display->drawString(textLeft, lineY, title); + lineY += FONT_HEIGHT_SMALL; + } else { + display->drawString(textLeft, lineY, title); + lineY += FONT_HEIGHT_SMALL; + } + + display->drawString(textLeft, lineY, line1); + lineY += FONT_HEIGHT_SMALL; + display->drawString(textLeft, lineY, line2); + lineY += FONT_HEIGHT_SMALL; + if (!compactLayout || textWidth >= display->getStringWidth(line3)) { + display->drawString(textLeft, lineY, line3); + } + } + + if (textWidth >= display->getStringWidth("000s left")) { + snprintf(timeRemainingBuffer, sizeof(timeRemainingBuffer), "%lus left", (unsigned long)timeRemaining); } else { - compassX = x + display->getWidth() - compassDiam / 2 - 5; - compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; + snprintf(timeRemainingBuffer, sizeof(timeRemainingBuffer), "%lus", (unsigned long)timeRemaining); + } + display->setFont(FONT_SMALL); + if (textWidth > 12) { + display->drawString(textLeft, y + height - FONT_HEIGHT_SMALL - 1, timeRemainingBuffer); } + display->drawCircle(compassX, compassY, compassDiam / 2); graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2)); } diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h index 8eb3bf95b88..71b71f73ac0 100644 --- a/src/motion/MotionSensor.h +++ b/src/motion/MotionSensor.h @@ -2,7 +2,7 @@ #ifndef _MOTION_SENSOR_H_ #define _MOTION_SENSOR_H_ -#define MOTION_SENSOR_CHECK_INTERVAL_MS 100 +#define MOTION_SENSOR_CHECK_INTERVAL_MS 50 #define MOTION_SENSOR_CLICK_THRESHOLD 40 #include "../configuration.h" @@ -54,6 +54,20 @@ class MotionSensor static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #endif + bool saveMagnetometerCalibration(const char *filePath, float highestX, float lowestX, float highestY, float lowestY, + float highestZ, float lowestZ); + bool loadMagnetometerCalibration(const char *filePath, float &highestX, float &lowestX, float &highestY, float &lowestY, + float &highestZ, float &lowestZ); + void beginCalibrationDisplay(bool &showingScreen); + void finishCalibrationIfExpired(bool &showingScreen, const char *filePath, float highestX, float lowestX, float highestY, + float lowestY, float highestZ, float lowestZ); + void startCalibrationWindow(uint16_t forSeconds); + static void seedCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY, + float &lowestY, float &highestZ, float &lowestZ); + static void updateCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY, + float &lowestY, float &highestZ, float &lowestZ); + static float applyCompassOrientation(float heading); + ScanI2C::FoundDevice device; // Do calibration if true @@ -63,4 +77,4 @@ class MotionSensor #endif -#endif \ No newline at end of file +#endif