diff --git a/src/configuration.h b/src/configuration.h
index 672d8b6f190..2cad8b013bf 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -313,6 +313,9 @@ along with this program. If not, see .
#define DA217_ADDR 0x26
#define BMI270_ADDR 0x68
#define BMI270_ADDR_ALT 0x69
+#define MPU9250_ADDR 0x68
+#define MPU9250_ADDR_ALT 0x69
+#define AK8963_ADDR 0x0C
#define ICM42607P_ADDR 0x68
#define ICM42607P_ADDR_ALT 0x69
diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp
index a919bd10586..08374901604 100644
--- a/src/detect/ScanI2C.cpp
+++ b/src/detect/ScanI2C.cpp
@@ -37,9 +37,9 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
{
- ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, SC7A20, BMA423, LSM6DS3, BMX160, STK8BAXX,
- ICM20948, BMM150, BMI270, ICM42607P, ISM330DHCX, QMA6100P};
- return firstOfOrNONE(13, types);
+ ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, SC7A20, BMA423, LSM6DS3, BMX160, STK8BAXX,
+ ICM20948, BMM150, BMI270, ICM42607P, ISM330DHCX, QMA6100P, MPU9250};
+ return firstOfOrNONE(14, types);
}
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h
index 87965245a0d..79ead44cd93 100644
--- a/src/detect/ScanI2C.h
+++ b/src/detect/ScanI2C.h
@@ -95,6 +95,7 @@ class ScanI2C
CST226SE,
CST3530,
BMI270,
+ MPU9250,
SEN5X,
SFA30,
CW2015,
diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp
index be326f4dbd8..eb5da2bebf8 100644
--- a/src/detect/ScanI2CTwoWire.cpp
+++ b/src/detect/ScanI2CTwoWire.cpp
@@ -880,6 +880,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = ICM42607P;
logFoundDevice("ICM-42607-P", (uint8_t)addr.address);
break;
+ } else if (registerValue == 0x71 || registerValue == 0x73) {
+ // MPU-9250 (0x71) / MPU-9255 (0x73) share 0x68/0x69 with MPU-6050 and BMX160;
+ // WHO_AM_I disambiguates them here.
+ type = MPU9250;
+ logFoundDevice(registerValue == 0x73 ? "MPU9255" : "MPU9250", (uint8_t)addr.address);
+ break;
} else if (registerValue == 0x68) { // WHO_AM_I from datasheet
type = MPU6050;
logFoundDevice("MPU6050", (uint8_t)addr.address);
diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h
index 571767715b9..4495ce666de 100755
--- a/src/motion/AccelerometerThread.h
+++ b/src/motion/AccelerometerThread.h
@@ -20,6 +20,7 @@
#include "LIS3DHSensor.h"
#include "LSM6DS3Sensor.h"
#include "MPU6050Sensor.h"
+#include "MPU9250Sensor.h"
#include "MotionSensor.h"
#ifdef HAS_QMA6100P
#include "QMA6100PSensor.h"
@@ -125,6 +126,9 @@ class AccelerometerThread : public concurrency::OSThread
sensor = new ICM20948Sensor(device);
break;
#endif
+ case ScanI2C::DeviceType::MPU9250:
+ sensor = new MPU9250Sensor(device);
+ break;
#if __has_include()
case ScanI2C::DeviceType::ICM42607P:
sensor = new ICM42607PSensor(device);
diff --git a/src/motion/MPU9250Sensor.cpp b/src/motion/MPU9250Sensor.cpp
new file mode 100644
index 00000000000..64bcbf0fb2a
--- /dev/null
+++ b/src/motion/MPU9250Sensor.cpp
@@ -0,0 +1,411 @@
+#include "MPU9250Sensor.h"
+
+#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
+
+#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
+#include "main.h" // global std::unique_ptr screen
+#endif
+
+namespace
+{
+// MPU-6500 die registers
+constexpr uint8_t MPU_SMPLRT_DIV = 0x19;
+constexpr uint8_t MPU_CONFIG = 0x1A;
+constexpr uint8_t MPU_GYRO_CONFIG = 0x1B;
+constexpr uint8_t MPU_ACCEL_CONFIG = 0x1C;
+constexpr uint8_t MPU_INT_PIN_CFG = 0x37;
+constexpr uint8_t MPU_ACCEL_XOUT_H = 0x3B;
+constexpr uint8_t MPU_USER_CTRL = 0x6A;
+constexpr uint8_t MPU_PWR_MGMT_1 = 0x6B;
+constexpr uint8_t MPU_WHO_AM_I = 0x75;
+
+// AK8963 magnetometer die registers
+constexpr uint8_t AK_WIA = 0x00;
+constexpr uint8_t AK_ST1 = 0x02;
+constexpr uint8_t AK_HXL = 0x03;
+constexpr uint8_t AK_ST2 = 0x09;
+constexpr uint8_t AK_CNTL1 = 0x0A;
+constexpr uint8_t AK_CNTL2 = 0x0B;
+constexpr uint8_t AK_ASAX = 0x10;
+
+// AK8963 control values
+constexpr uint8_t AK_MODE_POWERDOWN = 0x00;
+constexpr uint8_t AK_MODE_FUSE_ROM = 0x0F;
+constexpr uint8_t AK_MODE_CONT2_16BIT = 0x16; // continuous mode 2 (100 Hz), 16-bit output
+
+// Conversions
+// ±2g full scale, 16-bit signed -> g per LSB
+constexpr float ACCEL_LSB_PER_G = 16384.0f;
+// AK8963 16-bit output: 0.15 µT per LSB (datasheet 6.3)
+constexpr float MAG_UT_PER_LSB = 0.15f;
+} // namespace
+
+/**
+ * @brief Construct the driver for a detected MPU-9250/MPU-9255 device.
+ * @param foundDevice I2C scan result identifying the chip's address and bus port.
+ */
+MPU9250Sensor::MPU9250Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
+
+/**
+ * @brief Select the TwoWire bus instance the detected device lives on.
+ * @return Pointer to Wire1 when the device was scanned on the second port, else Wire.
+ */
+TwoWire *MPU9250Sensor::resolveBus() const
+{
+#if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1)
+ return device.address.port == ScanI2C::I2CPort::WIRE1 ? &Wire1 : &Wire;
+#else
+ return &Wire;
+#endif
+}
+
+/**
+ * @brief Write a single byte to a register on the given I2C address.
+ * @param i2cAddr Target die address (MPU-6500 or AK8963).
+ * @param reg Register address to write.
+ * @param value Byte value to store.
+ * @return true if the I2C transaction completed successfully.
+ */
+bool MPU9250Sensor::writeRegister(uint8_t i2cAddr, uint8_t reg, uint8_t value)
+{
+ bus->beginTransmission(i2cAddr);
+ bus->write(reg);
+ bus->write(value);
+ return bus->endTransmission() == 0;
+}
+
+/**
+ * @brief Read a block of consecutive registers from an I2C address.
+ * @param i2cAddr Target die address (MPU-6500 or AK8963).
+ * @param reg First register address to read.
+ * @param buf Destination buffer; must hold at least @p len bytes.
+ * @param len Number of bytes to read.
+ * @return true if exactly @p len bytes were received.
+ */
+bool MPU9250Sensor::readRegisters(uint8_t i2cAddr, uint8_t reg, uint8_t *buf, uint8_t len)
+{
+ bus->beginTransmission(i2cAddr);
+ bus->write(reg);
+ if (bus->endTransmission(false) != 0) {
+ return false;
+ }
+ const uint8_t received = bus->requestFrom((int)i2cAddr, (int)len);
+ if (received != len) {
+ return false;
+ }
+ for (uint8_t i = 0; i < len; ++i) {
+ buf[i] = bus->read();
+ }
+ return true;
+}
+
+/**
+ * @brief Reset and configure the MPU-6500 accelerometer/gyroscope die.
+ *
+ * Verifies WHO_AM_I, wakes the device onto the gyro PLL, sets the DLPF, sample
+ * rate and full-scale ranges, then enables I2C bypass so the on-package AK8963
+ * magnetometer becomes reachable on the main bus.
+ * @return true only if every required register write succeeded.
+ */
+bool MPU9250Sensor::initMPU6500()
+{
+ const uint8_t addr = deviceAddress();
+
+ // Sanity-check WHO_AM_I - scan should have already verified this but be defensive
+ uint8_t whoAmI = 0;
+ if (!readRegisters(addr, MPU_WHO_AM_I, &whoAmI, 1)) {
+ LOG_DEBUG("MPU9250 WHO_AM_I read failed at 0x%02X", addr);
+ return false;
+ }
+ if (whoAmI != 0x71 && whoAmI != 0x73) {
+ LOG_DEBUG("MPU9250 unexpected WHO_AM_I 0x%02X at 0x%02X", whoAmI, addr);
+ return false;
+ }
+
+ // Reset device
+ if (!writeRegister(addr, MPU_PWR_MGMT_1, 0x80)) {
+ return false;
+ }
+ delay(100);
+
+ // Wake up, select PLL with X gyro reference (better stability than internal osc)
+ if (!writeRegister(addr, MPU_PWR_MGMT_1, 0x01)) {
+ return false;
+ }
+ delay(50);
+
+ // DLPF 41 Hz (CONFIG = 3), sample rate 1 kHz / (1 + SMPLRT_DIV) = 200 Hz
+ if (!writeRegister(addr, MPU_CONFIG, 0x03) || !writeRegister(addr, MPU_SMPLRT_DIV, 0x04)) {
+ return false;
+ }
+
+ // ±2 g accel range
+ if (!writeRegister(addr, MPU_ACCEL_CONFIG, 0x00)) {
+ return false;
+ }
+ // ±250 dps gyro (gyro is unused for compass but configuring leaves the chip in a known state)
+ if (!writeRegister(addr, MPU_GYRO_CONFIG, 0x00)) {
+ return false;
+ }
+
+ // Expose AK8963 on the main I2C bus: disable internal master, enable bypass
+ if (!writeRegister(addr, MPU_USER_CTRL, 0x00) || !writeRegister(addr, MPU_INT_PIN_CFG, 0x02)) {
+ return false;
+ }
+ delay(10);
+
+ return true;
+}
+
+/**
+ * @brief Initialise the AK8963 magnetometer die.
+ *
+ * Confirms WHO_AM_I, reads the Fuse-ROM per-axis sensitivity adjustment (ASA)
+ * into asaScale, then switches the magnetometer into 16-bit continuous mode 2.
+ * @return true if the die responded and all setup writes succeeded.
+ */
+bool MPU9250Sensor::initAK8963()
+{
+ uint8_t wia = 0;
+ if (!readRegisters(AK8963_ADDR, AK_WIA, &wia, 1) || wia != 0x48) {
+ LOG_DEBUG("MPU9250 AK8963 WHO_AM_I unexpected (0x%02X)", wia);
+ return false;
+ }
+
+ // Soft reset
+ if (!writeRegister(AK8963_ADDR, AK_CNTL2, 0x01)) {
+ return false;
+ }
+ delay(100);
+
+ // Power-down then enter Fuse ROM access to read per-axis sensitivity (ASA)
+ if (!writeRegister(AK8963_ADDR, AK_CNTL1, AK_MODE_POWERDOWN)) {
+ return false;
+ }
+ delay(10);
+ if (!writeRegister(AK8963_ADDR, AK_CNTL1, AK_MODE_FUSE_ROM)) {
+ return false;
+ }
+ delay(10);
+
+ uint8_t asa[3] = {0};
+ if (!readRegisters(AK8963_ADDR, AK_ASAX, asa, 3)) {
+ LOG_DEBUG("MPU9250 AK8963 ASA read failed");
+ return false;
+ }
+ // Sensitivity adjustment: H_adj = H * ((ASA - 128) * 0.5 / 128 + 1) - datasheet 8.3.11
+ for (int i = 0; i < 3; ++i) {
+ asaScale[i] = (((float)asa[i] - 128.0f) * 0.5f / 128.0f) + 1.0f;
+ }
+
+ // Back to power-down, then enter continuous-mode-2 (100 Hz) at 16-bit resolution
+ if (!writeRegister(AK8963_ADDR, AK_CNTL1, AK_MODE_POWERDOWN)) {
+ return false;
+ }
+ delay(10);
+ if (!writeRegister(AK8963_ADDR, AK_CNTL1, AK_MODE_CONT2_16BIT)) {
+ return false;
+ }
+ delay(10);
+
+ return true;
+}
+
+/**
+ * @brief Bring the sensor fully online.
+ *
+ * Resolves the I2C bus, initialises both the MPU-6500 and AK8963 dies, and
+ * loads any persisted hard-iron calibration.
+ * @return true when the device is ready to produce headings.
+ */
+bool MPU9250Sensor::init()
+{
+ bus = resolveBus();
+ if (!bus) {
+ return false;
+ }
+
+ if (!initMPU6500()) {
+ LOG_DEBUG("MPU9250 MPU-6500 init failed");
+ return false;
+ }
+ if (!initAK8963()) {
+ LOG_DEBUG("MPU9250 AK8963 init failed");
+ return false;
+ }
+
+ loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
+ LOG_DEBUG("MPU9250 init ok (asa=%.2f,%.2f,%.2f)", asaScale[0], asaScale[1], asaScale[2]);
+ return true;
+}
+
+/**
+ * @brief Read one accelerometer and magnetometer sample.
+ * @param[out] accel Acceleration vector in g.
+ * @param[out] mag Magnetic field vector in µT, sensitivity- and axis-scaled.
+ * @return false if no fresh magnetometer sample was ready or the reading overflowed.
+ */
+bool MPU9250Sensor::readSensors(FusionVector &accel, FusionVector &mag)
+{
+ // Read 6 bytes of accel data
+ uint8_t raw[6] = {0};
+ if (!readRegisters(deviceAddress(), MPU_ACCEL_XOUT_H, raw, 6)) {
+ return false;
+ }
+ const int16_t ax = (int16_t)((raw[0] << 8) | raw[1]);
+ const int16_t ay = (int16_t)((raw[2] << 8) | raw[3]);
+ const int16_t az = (int16_t)((raw[4] << 8) | raw[5]);
+ accel.axis.x = (float)ax / ACCEL_LSB_PER_G;
+ accel.axis.y = (float)ay / ACCEL_LSB_PER_G;
+ accel.axis.z = (float)az / ACCEL_LSB_PER_G;
+
+ // Check magnetometer data-ready bit
+ uint8_t st1 = 0;
+ if (!readRegisters(AK8963_ADDR, AK_ST1, &st1, 1) || (st1 & 0x01) == 0) {
+ return false;
+ }
+
+ // Read 6 bytes of mag (little-endian) plus ST2 to release the data register.
+ // ST2 also exposes the magnetic overflow flag (bit 3) - discard the sample if set.
+ uint8_t magRaw[7] = {0};
+ if (!readRegisters(AK8963_ADDR, AK_HXL, magRaw, 7)) {
+ return false;
+ }
+ if (magRaw[6] & 0x08) {
+ return false;
+ }
+ const int16_t mx = (int16_t)((magRaw[1] << 8) | magRaw[0]);
+ const int16_t my = (int16_t)((magRaw[3] << 8) | magRaw[2]);
+ const int16_t mz = (int16_t)((magRaw[5] << 8) | magRaw[4]);
+
+ mag.axis.x = (float)mx * MAG_UT_PER_LSB * asaScale[0];
+ mag.axis.y = (float)my * MAG_UT_PER_LSB * asaScale[1];
+ mag.axis.z = (float)mz * MAG_UT_PER_LSB * asaScale[2];
+ return true;
+}
+
+/**
+ * @brief Periodic worker: sample, filter, compute tilt-compensated heading, publish.
+ *
+ * Applies hard-iron correction and a per-axis EMA, remaps the accel/mag frames
+ * to a common orientation, computes the compass heading via the Fusion library,
+ * and pushes it to the screen. Also drives the calibration state machine.
+ * @return Milliseconds until the next desired invocation.
+ */
+int32_t MPU9250Sensor::runOnce()
+{
+#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
+ FusionVector accel = {{0, 0, 0}};
+ FusionVector mag = {{0, 0, 0}};
+ if (!readSensors(accel, mag)) {
+ // Missed samples must not stall calibration: close the window on timeout
+ // even when no fresh magnetometer data is available.
+ if (doCalibration) {
+ finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ,
+ lowestZ);
+ }
+ return MOTION_SENSOR_CHECK_INTERVAL_MS;
+ }
+
+ if (doCalibration) {
+ beginCalibrationDisplay(showingScreen);
+ updateCalibrationExtrema(mag.axis.x, mag.axis.y, mag.axis.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
+ finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ,
+ lowestZ);
+ }
+
+ // Subtract hard-iron offsets
+ mag.axis.x -= (highestX + lowestX) / 2;
+ mag.axis.y -= (highestY + lowestY) / 2;
+ mag.axis.z -= (highestZ + lowestZ) / 2;
+
+ // Smooth raw inputs with a per-axis EMA to suppress dynamic acceleration
+ // noise during device rotation.
+ if (!filtersSeeded) {
+ accelFiltered = accel;
+ magFiltered = mag;
+ filtersSeeded = true;
+ } else {
+ for (int i = 0; i < 3; ++i) {
+ accelFiltered.array[i] = accelFilterAlpha * accel.array[i] + (1.0f - accelFilterAlpha) * accelFiltered.array[i];
+ magFiltered.array[i] = magFilterAlpha * mag.array[i] + (1.0f - magFilterAlpha) * magFiltered.array[i];
+ }
+ }
+ accel = accelFiltered;
+ mag = magFiltered;
+
+ // The MPU-6500 accelerometer and AK8963 magnetometer dies sit inside the
+ // same package but use different axis conventions (datasheet 7.4 vs 8.1).
+ // To express the mag reading in the accel frame we swap X<->Y and negate Z.
+ // Final tweaks for board orientation are handled by config.display.compass_orientation.
+ FusionVector ga, ma;
+ ga.axis.x = accel.axis.x;
+ ga.axis.y = -accel.axis.y;
+ ga.axis.z = -accel.axis.z;
+ ma.axis.x = mag.axis.y;
+ ma.axis.y = mag.axis.x;
+ ma.axis.z = -mag.axis.z;
+
+ // Compensate for non-flat case mounting. FusionCompass()
+ // assumes Z is the up axis - when the baseboard is mounted vertically
+ // (e.g. a case where the LCD sits perpendicular to the board), chip Z is
+ // horizontal and the tilt-comp math becomes unstable. Override which chip
+ // axis is treated as world-up via the MPU9250_UP_AXIS_* defines.
+ // _PZ (default) - chip +Z up (baseboard flat)
+ // _PX - chip +X up (vertical mount, silkscreen "north" up)
+ // _NX - chip -X up
+ // _PY - chip +Y up
+ // _NY - chip -Y up
+#ifndef MPU9250_UP_AXIS
+#define MPU9250_UP_AXIS MPU9250_UP_AXIS_PZ
+#endif
+#if MPU9250_UP_AXIS == MPU9250_UP_AXIS_PX
+ ga = FusionRemap(ga, FusionRemapAlignmentNZPYPX);
+ ma = FusionRemap(ma, FusionRemapAlignmentNZPYPX);
+#elif MPU9250_UP_AXIS == MPU9250_UP_AXIS_PZ
+ // Default orientation (chip +Z up), no remap needed.
+#elif MPU9250_UP_AXIS == MPU9250_UP_AXIS_NX
+ ga = FusionRemap(ga, FusionRemapAlignmentPZPYNX);
+ ma = FusionRemap(ma, FusionRemapAlignmentPZPYNX);
+#elif MPU9250_UP_AXIS == MPU9250_UP_AXIS_PY
+ ga = FusionRemap(ga, FusionRemapAlignmentPXNZPY);
+ ma = FusionRemap(ma, FusionRemapAlignmentPXNZPY);
+#elif MPU9250_UP_AXIS == MPU9250_UP_AXIS_NY
+ ga = FusionRemap(ga, FusionRemapAlignmentPXPZNY);
+ ma = FusionRemap(ma, FusionRemapAlignmentPXPZNY);
+#else
+#error "MPU9250_UP_AXIS must be one of MPU9250_UP_AXIS_PZ/PX/NX/PY/NY"
+#endif
+
+ if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) {
+ ma = FusionRemap(ma, FusionRemapAlignmentNXNYPZ);
+ ga = FusionRemap(ga, FusionRemapAlignmentNXNYPZ);
+ }
+
+ float heading = FusionCompass(ga, ma, FusionConventionNed);
+ heading = applyCompassOrientation(heading);
+ if (screen)
+ screen->setHeading(heading);
+#endif
+ return MOTION_SENSOR_CHECK_INTERVAL_MS;
+}
+
+/**
+ * @brief Begin hard-iron calibration, seeding extrema from the current sample.
+ * @param forSeconds Duration of the calibration window in seconds.
+ */
+void MPU9250Sensor::calibrate(uint16_t forSeconds)
+{
+#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
+ LOG_DEBUG("MPU9250 calibration started for %is", forSeconds);
+ FusionVector accel, mag;
+ if (readSensors(accel, mag)) {
+ seedCalibrationExtrema(mag.axis.x, mag.axis.y, mag.axis.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
+ } else {
+ seedCalibrationExtrema(0.0f, 0.0f, 0.0f, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
+ }
+ startCalibrationWindow(forSeconds);
+#endif
+}
+
+#endif
diff --git a/src/motion/MPU9250Sensor.h b/src/motion/MPU9250Sensor.h
new file mode 100644
index 00000000000..8b62cf9317d
--- /dev/null
+++ b/src/motion/MPU9250Sensor.h
@@ -0,0 +1,69 @@
+#pragma once
+#ifndef _MPU9250_SENSOR_H_
+#define _MPU9250_SENSOR_H_
+
+#include "MotionSensor.h"
+
+#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
+
+#include "Fusion/Fusion.h"
+#include
+
+// Numeric IDs for selecting which chip axis maps to world-up.
+// Set MPU9250_UP_AXIS to one of these in variant.h or via build flags.
+#define MPU9250_UP_AXIS_PZ 0
+#define MPU9250_UP_AXIS_PX 1
+#define MPU9250_UP_AXIS_NX 2
+#define MPU9250_UP_AXIS_PY 3
+#define MPU9250_UP_AXIS_NY 4
+
+// InvenSense MPU-9250/MPU-9255 9-axis IMU driver (e.g. RAK1905 WisBlock).
+// Drives the MPU-6500 accel die at deviceAddress() and the AK8963 magnetometer
+// at AK8963_ADDR over I2C bypass mode.
+class MPU9250Sensor : public MotionSensor
+{
+ private:
+ TwoWire *bus = nullptr;
+ bool showingScreen = false;
+
+ // Per-axis AK8963 factory sensitivity adjustment scale factors derived from
+ // the chip's Fuse ROM (ASA registers). Applied to every raw mag sample.
+ float asaScale[3] = {1.0f, 1.0f, 1.0f};
+
+ // Hard-iron calibration extrema persisted to flash.
+ static constexpr const char *compassCalibrationFileName = "/prefs/compass_mpu9250.dat";
+ float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
+
+ // Per-axis EMA on raw accel + mag: the stateless compass fusion turns dynamic
+ // acceleration into heading noise, so filtering the inputs steadies rotation.
+ static constexpr float accelFilterAlpha = 0.15f;
+ static constexpr float magFilterAlpha = 0.20f;
+ FusionVector accelFiltered = {{0, 0, 0}};
+ FusionVector magFiltered = {{0, 0, 0}};
+ bool filtersSeeded = false;
+
+ // Pick the correct TwoWire instance for the detected device.
+ TwoWire *resolveBus() const;
+
+ // Low-level I2C helpers - one address per call so we can address both dies.
+ bool writeRegister(uint8_t i2cAddr, uint8_t reg, uint8_t value);
+ bool readRegisters(uint8_t i2cAddr, uint8_t reg, uint8_t *buf, uint8_t len);
+
+ // Init helpers.
+ bool initMPU6500();
+ bool initAK8963();
+
+ // Read accel (g) and mag (µT) into Fusion vectors; returns false if mag
+ // had no fresh sample on this tick.
+ bool readSensors(FusionVector &accel, FusionVector &mag);
+
+ public:
+ explicit MPU9250Sensor(ScanI2C::FoundDevice foundDevice);
+ virtual bool init() override;
+ virtual int32_t runOnce() override;
+ virtual void calibrate(uint16_t forSeconds) override;
+};
+
+#endif
+
+#endif