Files
Christopher Van HooseandClaude Fable 5 fcc146f57a M9 IMU (QMI8658) + tilt-compensated heading
A two-axis magnetic heading assumes the device is level. At this latitude the
field dips ~60 degrees, so the vertical component is 1.6x the horizontal one
and tipping the device leaks it into the pair the heading is made from: about
1.5 degrees of heading per degree of tilt. That is what "it drifts" was once
the calibration was sound -- it was the hand holding it.

Driver: variants/thinknode_m9/M9Imu.{h,cpp}, QMI8658 at 0x6B on the peripheral
bus, accelerometer only (the gyro is most of the power budget and nothing here
needs it): +/-2 g at 62.5 Hz with the low-pass on, soft reset with a 160 ms
wait that covers both die variants, and the same idle-suspend as the compass so
it costs nothing when unused. CTRL1's ADDR_AI bit is set and read back -- with
it clear the burst read silently returns six copies of one byte, which looks
like a working sensor reporting nonsense. HAS_M9_IMU -> CAP_IMU ->
wada.sys.accel(), caps().accel.

Axes MEASURED, not guessed, by holding three attitudes and logging:
  flat, screen up        z = -1.02  -> +Z into the screen (down)
  on bottom edge, top up x = +0.97  -> +X at the top edge (forward)
  on left edge, right up y = +1.08  -> +Y at the right edge
So the IMU is already in the aerospace body frame, and it agrees with the
magnetometer's independently measured +Z-into-screen. (Meshtastic's M9 driver
passes both sensors through untransformed and mirrors the heading on the sign
of accel Z, so its compass flips when the device is turned over. Not copied.)

Heading now rotates the field back into the horizontal plane using gravity
(NXP AN4248 / ST AN3192) before taking the angle, and reports the tilt angle;
past 55 degrees it says "too steep to read" rather than lying. Calibration
returns to a 3D fit because tilt compensation needs the vertical offset too --
but the instruction is now "turn it every way ON ONE SPOT", and the
accelerometer VERIFIES it: coverage is measured by how far gravity swung, so a
flat spin is refused by name ("turn it nose over tail") instead of silently
fitting a degenerate sphere. Simulated in the harness against a modelled M9 in
a known attitude: offsets recovered exactly, and the heading holds within 3
degrees through 20 degrees of roll and pitch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 10:00:44 -04:00

51 lines
1.7 KiB
C

#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include "../../src/helpers/ClockFloorRTC.h" // monotonic send-timestamp floor (issue #89)
#include "M9Board.h"
#include <RadioLib.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/radiolib/CustomLR1110Wrapper.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/MomentaryButton.h>
#include <helpers/ui/ST7789LCDDisplay.h>
#endif
#include "helpers/sensors/EnvironmentSensorManager.h"
// Wadamesh-owned NMEA provider (the core one plus speed/course for wada.sys.gps)
#include "../../src/helpers/WadaNmeaLocationProvider.h"
#if defined(HAS_M9_KEYBOARD)
#include "M9Keyboard.h"
#endif
#if defined(HAS_M9_COMPASS)
#include "M9Compass.h"
#endif
#if defined(HAS_M9_IMU)
#include "M9Imu.h"
#endif
extern ThinkNodeM9Board board;
extern WRAPPER_CLASS radio_driver;
extern RADIO_CLASS radio;
extern ClockFloorRTC rtc_clock;
extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
// (no user_btn: the M9 has no user/BOOT button — see target.cpp)
#endif
bool radio_init();
mesh::LocalIdentity radio_new_identity();
// Shared SPI bus accessor for the microSD mount code in UITask.cpp (mirrors
// tdeckSharedSPI()). M9's radio/display use the global `SPI` instance
// directly (no dedicated local SPIClass — see target.cpp), so this just
// hands that same instance to SD.begin().
SPIClass *m9SharedSPI();
// Speed/course over ground from the GPS for the Lua host (HAS_GPS_MOTION).
// Plain function so UITask.cpp needs no provider type: false = no fix / no
// RMC yet; course_deg is NAN while not moving (see WadaNmeaLocationProvider).
bool wadaGpsMotion(float *speed_kmh, float *course_deg);