#include "MotionSensor.h" #include "FSCommon.h" #include "SPILock.h" #include "SafeFile.h" #include "concurrency/LockGuard.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); } struct CompassAccelSample { float x = 0.0f; float y = 0.0f; float z = 0.0f; uint32_t sampledAtMs = 0; bool valid = false; }; concurrency::Lock latestCompassAccelLock; CompassAccelSample latestCompassAccelSample; } // namespace // screen is defined in main.cpp extern std::unique_ptr screen; MotionSensor::MotionSensor(ScanI2C::FoundDevice foundDevice) { device.address.address = foundDevice.address.address; device.address.port = foundDevice.address.port; device.type = foundDevice.type; LOG_DEBUG("Motion MotionSensor port: %s address: 0x%x type: %d", devicePort() == ScanI2C::I2CPort::WIRE1 ? "Wire1" : "Wire", (uint8_t)deviceAddress(), deviceType()); } ScanI2C::DeviceType MotionSensor::deviceType() { return device.type; } uint8_t MotionSensor::deviceAddress() { return device.address.address; } 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; } } void MotionSensor::publishCompassAccelSample(float x, float y, float z) { concurrency::LockGuard guard(&latestCompassAccelLock); latestCompassAccelSample.x = x; latestCompassAccelSample.y = y; latestCompassAccelSample.z = z; latestCompassAccelSample.sampledAtMs = millis(); latestCompassAccelSample.valid = true; } bool MotionSensor::getLatestCompassAccelSample(float &x, float &y, float &z, uint32_t &ageMs) { uint32_t sampledAtMs = 0; { concurrency::LockGuard guard(&latestCompassAccelLock); if (!latestCompassAccelSample.valid) { return false; } x = latestCompassAccelSample.x; y = latestCompassAccelSample.y; z = latestCompassAccelSample.z; sampledAtMs = latestCompassAccelSample.sampledAtMs; } ageMs = millis() - sampledAtMs; return true; } #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { if (screen == nullptr) return; 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(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 + 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 { 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)); } #endif #if !MESHTASTIC_EXCLUDE_POWER_FSM void MotionSensor::wakeScreen() { if (powerFSM.getState() == &stateDARK) { LOG_DEBUG("Motion wakeScreen detected"); if (config.display.wake_on_tap_or_motion) powerFSM.trigger(EVENT_INPUT); } } void MotionSensor::buttonPress() { LOG_DEBUG("Motion buttonPress detected"); powerFSM.trigger(EVENT_PRESS); } #else void MotionSensor::wakeScreen() {} void MotionSensor::buttonPress() {} #endif #endif