Files
wadamesh/variants/thinknode_m9/M9Compass.h
T
Christopher Van HooseandClaude Fable 5 b273f20e26 M9 compass (QMC6309) + GPS motion for Lua apps; GPS Compass Store app
Firmware
- variants/thinknode_m9/M9Compass.{h,cpp}: QMC6309 at 0x7C on the peripheral
  bus. Probe by chip id, soft reset with the explicit clear, CTRL2 0x20
  (50 Hz, +/-32 G, set/reset on), CTRL1 0x61 (normal, OSR 8/8), read-back
  verified. Synchronous read from the Lua host bridge, 1 s cache, lazy
  re-probe while the rail-powered part is still in POR, OVFL kept + flagged +
  logged. HAS_M9_COMPASS=1 in the env -> new hardware gate CAP_COMPASS.
- src/helpers/WadaNmeaLocationProvider.h: Wadamesh-owned copy of the core
  MicroNMEALocationProvider that also exposes RMC speed/course (the core
  keeps its parser private; no libdeps patch). M9 target.cpp builds on it,
  HAS_GPS_MOTION=1, wadaGpsMotion() for UITask.
- wada.sys.gps(): + alt (m), + speed_kmh/course where the board provides
  them (course only while moving -- an empty RMC course parses as 0), and
  nil while the user has GPS switched off. wada.sys.compass(): {x,y,z,ovfl}
  Gauss, sensor frame, uncalibrated, registered only where CAP_COMPASS;
  caps().compass. Calibration, axis mapping and the heading maths live in
  the app so they can be adjusted per user without a firmware cut.
- Host: luaHostContactAt reads the RTC once per contacts() walk instead of
  once per contact (an I2C transaction each on the M9); pressCb reports
  press coordinates in body content space (scroll offset folded in).

App
- deploy/apps/gpscompass/1.0 + apps.json: rotating rose with a fixed index,
  live fix readout, bearing/range to a selected contact, magnetometer
  heading with hard-iron calibration (C), frame rotate/mirror (O/F, the
  M9's sensor orientation is undocumented), GPS-course fallback on every
  other board, saturation warning. Not baked into lua_builtin.h on purpose
  (CAP_BUILTIN_LUA_APPS also removes the Store > Apps tab).

Verified: M9, V4, V4-R8, T-Deck compile (M9 flash +2 KB); two adversarial
review passes, all confirmed findings fixed; host Lua harness (vendored
Lua, LUA_32BITS) -- calibration recovers a simulated bias exactly, heading
error 0 deg, worst tick ~10k of the 100k budget. Hardware validation list
(axis orientation, bias magnitude, 0x7C ACK) in M9_PORT.md.

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

48 lines
2.4 KiB
C++

#pragma once
// ThinkNode M9 magnetometer — QST QMC6309 on the peripheral I2C bus (Wire,
// SDA=7 / SCL=6) at 7-bit address 0x7C. The chip sits on the GPIO18 peripheral
// rail with the LCD/GPS, which ThinkNodeM9Board::begin() claims for the whole
// runtime, so it is powered whenever the firmware runs.
//
// This is deliberately a dumb sensor reader: it hands out the raw field vector
// in Gauss, sensor frame, uncalibrated. Hard-iron calibration, the sensor-to-
// screen axis mapping (not documented anywhere — Meshtastic's own M9 driver
// marks its heading offset "must be verified on real hardware" and never uses
// it) and the heading maths live in the consumer (the GPS Compass Lua app),
// where they can be adjusted and persisted per user without a firmware cut.
//
// Register map (QMC6309 datasheet Rev A, cross-checked against SlimeVR,
// madflight and the emfcamp Tildagon drivers — NOT against SensorLib, whose
// setOutputDataRate() writes the ODR into the wrong register):
// 0x00 chip id (0x90) 0x01..0x06 X/Y/Z int16 little-endian
// 0x09 status: bit0 DRDY (cleared by reading 0x09), bit1 OVFL
// 0x0A CTRL1: OSR2[7:5] OSR1[4:3] MODE[1:0] (00 suspend, 01 normal, 11 cont.)
// 0x0B CTRL2: SOFT_RST[7] ODR[6:4] RNG[3:2] SET/RESET[1:0]
// Sensitivity at the ±32 G range used here: 1000 LSB/G (1 mG per count).
#if defined(HAS_M9_COMPASS) && defined(ESP32)
#include <stdint.h>
class TwoWire;
/** Probe + configure the chip on `w`. Safe to call when the chip is absent or
* not yet out of power-on reset: the read path re-probes on its own. Logs one
* line to Serial either way (matches the keyboard bring-up style). */
void m9CompassBegin(TwoWire& w);
/** True once the chip has answered with its id and taken the configuration. */
bool m9CompassPresent();
/** Latest field vector in Gauss, sensor frame, uncalibrated. Reads the chip
* synchronously when a new sample is ready (three short I2C transactions,
* well under 1 ms at 100 kHz on a healthy bus), otherwise returns the cached
* sample while it is younger than a second. False = no chip, bus error, or
* nothing fresh. `overflow` (optional) is set when the chip flagged the
* sample as saturated (an axis beyond ±32000 counts): the values are still
* returned so a consumer can show "away from magnets" rather than "no
* compass", but they are not a usable heading. */
bool m9CompassRead(float* x_gauss, float* y_gauss, float* z_gauss, bool* overflow = nullptr);
#endif