Skip to main content

read the undocumented mems accelerometer + gyroscope on apple silicon macs via iokit hid

Project description

apple-silicon-accelerometer

more information: read the article on Medium

it turns out modern macbook pros have an undocumented mems accelerometer + gyroscope managed by the sensor processing unit (spu). this project reads both via iokit hid, along with lid angle and ambient light sensors from the same interface

demo

try it

git clone https://github.com/olvvier/apple-silicon-accelerometer
cd apple-silicon-accelerometer
python3 -m venv .venv && source .venv/bin/activate
pip install -e .[demo]
sudo .venv/bin/python3 motion_live.py

what is this

apple silicon chips (M2/M3/M4/M5) have a hard to find mems IMU (accelerometer + gyroscope) managed by the sensor processing unit (SPU). it's not exposed through any public api or framework. this project reads raw 3-axis acceleration and angular velocity data at ~800hz via iokit hid callbacks.

only tested on macbook pro m3 pro so far - might work on other apple silicon macs but no guarantees

how it works

the sensor lives under AppleSPUHIDDevice in the iokit registry, on vendor usage page 0xFF00. usage 3 is the accelerometer, usage 9 is the gyroscope (same physical IMU, believed to be Bosch BMI286 based on teardowns). the driver is AppleSPUHIDDriver which is part of the sensor processing unit. we open it with IOHIDDeviceCreate and register an asynchronous callback via IOHIDDeviceRegisterInputReportCallback. data comes as 22-byte hid reports with x/y/z as int32 little-endian at byte offsets 6, 10, 14. divide by 65536 to get the value in g (accel) or deg/s (gyro). callback rate is ~100hz (decimated from ~800hz native)

orientation is computed by fusing accel + gyro with a Mahony AHRS quaternion filter and displayed as roll/pitch/yaw gauges

you can verify the device exists on your machine with:

ioreg -l -w0 | grep -A5 AppleSPUHIDDevice

install (beta API)

pip install macimu

if you get externally-managed-environment (homebrew python), use a venv:

python3 -m venv .venv && source .venv/bin/activate && pip install macimu
from macimu import IMU

if __name__ == '__main__':
    with IMU() as imu:
        accel = imu.latest_accel()       # Sample(x, y, z) in g
        gyro = imu.latest_gyro()         # Sample(x, y, z) in deg/s

        for s in imu.read_accel():       # all new samples since last call
            print(s.x, s.y, s.z)

requires root (sudo) because iokit hid device access needs elevated privileges. note: accelerometer reads ~1g at rest (gravity). use macimu.filters.remove_gravity() to isolate dynamic acceleration.

check if sensor exists (no root needed)

from macimu import IMU
print(IMU.available())   # True on macbook pro m2+

real-time orientation (roll / pitch / yaw)

fuses accel + gyro with a mahony quaternion filter, no math needed on your side

from macimu import IMU

if __name__ == '__main__':
    with IMU(orientation=True) as imu:
        o = imu.orientation()
        print(f"{o.roll:.1f}° {o.pitch:.1f}° {o.yaw:.1f}°")
        print(o.qw, o.qx, o.qy, o.qz)  # raw quaternion

timestamped samples (hardware timestamps from iokit)

each sample includes a precise timestamp from the hid report (mach_absolute_time), not a python-side clock. every report gets its own unique timestamp.

from macimu import IMU

if __name__ == '__main__':
    with IMU() as imu:
        for s in imu.read_accel_timed():
            print(f"t={s.t:.6f}  x={s.x:.3f}  y={s.y:.3f}  z={s.z:.3f}")

streaming with callback

import time
from macimu import IMU

def on_sample(s):
    print(s.x, s.y, s.z)

if __name__ == '__main__':
    with IMU() as imu:
        stop = imu.on_accel(on_sample)  # background thread
        time.sleep(10)
        stop()                          # unregister

sample rate control

IMU(sample_rate=200)  # ~200 hz (preferred way)
IMU(sample_rate=50)   # ~50 hz
IMU(decimation=1)     # ~800 hz (full native rate)
IMU(decimation=8)     # ~100 hz (default)

signal processing (zero-dependency biquad butterworth filters)

from macimu import IMU
from macimu.filters import magnitude, remove_gravity, high_pass, low_pass, peak_detect

if __name__ == '__main__':
    with IMU() as imu:
        samples = imu.read_accel()
        m = magnitude(samples[0].x, samples[0].y, samples[0].z)
        dynamic = remove_gravity(samples)               # kalman filter gravity removal
        smooth = low_pass(samples, 5.0, 100.0)          # 2nd-order butterworth
        taps = high_pass(samples, 10.0, 100.0, order=4) # 4th-order, -24 dB/oct
        mags = [magnitude(s.x, s.y, s.z) for s in samples]
        hits = peak_detect(mags, threshold=1.2)          # detect impacts

mock mode (no root needed, for development / testing)

from macimu import IMU

imu = IMU.mock(duration=10.0, rate=100)  # synthetic sinusoidal data
for s in imu.stream_accel():
    print(s)

record and replay

from macimu import IMU

if __name__ == '__main__':
    # record
    with IMU() as imu:
        imu.record_to("session.csv")
        time.sleep(10)

    # replay (no root needed)
    imu = IMU.from_recording("session.csv")
    for s in imu.stream_accel_timed():
        print(s)

api reference

constructor

IMU(accel=True, gyro=True, als=False, lid=False, orientation=False, decimation=8, sample_rate=None)

class methods (no root needed)

method returns description
IMU.available() bool check if sensor exists
IMU.device_info() dict sensors list, serial, product name
IMU.mock(duration, rate, noise) IMU synthetic data for testing
IMU.from_recording(path) IMU replay from csv

reading data

method returns description
imu.read_accel() list[Sample] new samples since last call (x, y, z in g)
imu.read_gyro() list[Sample] new samples since last call (x, y, z in deg/s)
imu.read_accel_timed() list[TimedSample] with hardware timestamp (t, x, y, z)
imu.read_gyro_timed() list[TimedSample] same for gyro
imu.latest_accel() Sample | None most recent sample
imu.latest_gyro() Sample | None most recent sample
imu.read_all() dict latest from all enabled sensors

orientation & sensors

method returns description
imu.orientation() Orientation | None roll, pitch, yaw (deg) + quaternion
imu.read_lid() float | None lid angle in degrees
imu.read_als() ALSReading | None lux + 4 spectral channels

streaming

method returns description
imu.stream_accel() generator blocking, yields Sample
imu.stream_gyro() generator blocking, yields Sample
imu.stream_accel_timed() generator blocking, yields TimedSample
imu.stream_gyro_timed() generator blocking, yields TimedSample
imu.on_accel(callback) stop_fn background thread, call stop() to end
imu.on_gyro(callback) stop_fn background thread, call stop() to end

lifecycle

method / property description
imu.start() / imu.stop() manual lifecycle (or use with IMU() as imu:)
imu.is_running True if worker is active
imu.effective_sample_rate measured hz (or None if not enough data)
imu.record_to(path) start writing samples to csv

filters (from macimu.filters import ...) -- biquad butterworth, zero external deps

function description
magnitude(x, y, z) euclidean magnitude
remove_gravity(samples, Q, R) kalman filter gravity subtraction
GravityKalman(Q, R) real-time gravity estimator (stateful)
low_pass(samples, cutoff_hz, rate, order=2) butterworth low-pass (-12 dB/oct per order of 2)
high_pass(samples, cutoff_hz, rate, order=2) butterworth high-pass
bandpass(samples, low, high, rate, order=2) cascaded hp + lp
filtfilt_low_pass(samples, cutoff_hz, rate) zero-phase lp (no lag, offline only)
filtfilt_high_pass(samples, cutoff_hz, rate) zero-phase hp (no lag, offline only)
median_filter(samples, window=5) spike / outlier removal
peak_detect(values, threshold, min_spacing) find peaks in 1d signal
rolling_rms(samples, window) rolling root-mean-square of magnitude

exceptions: macimu.SensorNotFound if no SPU device, PermissionError if not root

demo dashboard

git clone https://github.com/olvvier/apple-silicon-accelerometer
cd apple-silicon-accelerometer
python3 -m venv .venv && source .venv/bin/activate
pip install -e .[demo]
sudo .venv/bin/python3 motion_live.py

the demo includes vibration detection, orientation gauges, experimental heartbeat (bcg), lid angle, ambient light, and optional keyboard flash

keyboard flash mode (bundled KBPulse)

motion_live.py can flash the keyboard backlight from vibration intensity in near realtime. the repo now vendors KBPulse, including a prebuilt apple silicon binary at KBPulse/bin/KBPulse.

run as usual:

sudo python3 motion_live.py

optional overrides:

sudo python3 motion_live.py --no-kbpulse
sudo python3 motion_live.py --kbpulse-bin /path/to/KBPulse

with uv

If you have uv/uvx installed, you can also just

sudo uvx git+https://github.com/olvvier/apple-silicon-accelerometer.git

code structure

  • macimu/ - python package (pip install macimu): high-level IMU class + low-level iokit bindings, shared memory ring buffers
  • motion_live.py - demo app: vibration detection, heartbeat bcg, terminal ui
  • KBPulse/ - vendored keyboard backlight driver code + binary (KBPulse/bin/KBPulse)

heartbeat demo

place your wrists on the laptop near the trackpad and wait 10-20 seconds for the signal to stabilize. this uses ballistocardiography - the mechanical vibrations from your heartbeat transmitted through your arms into the chassis. experimental, not reliable, just a fun use-case to show what the sensor can pick up. the bcg bandpass is 0.8-3hz and bpm is estimated via autocorrelation on the filtered signal

notes

  • experimental / undocumented AppleSPU hid path
  • requires sudo
  • may break on future macos updates
  • use at your own risk
  • not for medical use

tested on

  • macbook pro m3 pro, macos 15.6.1
  • python 3.14

known incompatible

  • intel macs (no spu)
  • m1 macbook pro (2020)
  • mac studio m4 max

license

MIT


not affiliated with Apple or any employer

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

macimu-0.2.0.tar.gz (22.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

macimu-0.2.0-py3-none-any.whl (19.9 kB view details)

Uploaded Python 3

File details

Details for the file macimu-0.2.0.tar.gz.

File metadata

  • Download URL: macimu-0.2.0.tar.gz
  • Upload date:
  • Size: 22.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for macimu-0.2.0.tar.gz
Algorithm Hash digest
SHA256 6fc7737ef75357de18d95e2d0ffe240b96763a704345a7e799d396ed3967189c
MD5 73d7153e81d2ec6a209b10c7705c9380
BLAKE2b-256 41b6172b8e4435e5451c1157974ffe7f1f9267c4e1f38406d2466f1e5e88981a

See more details on using hashes here.

File details

Details for the file macimu-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: macimu-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 19.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for macimu-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 70dfd9f0c8897bb816ebe85a89f5b781945b6929b79649e1e8e5c68151467c3c
MD5 c58d48259ead8707e2acaa5868664107
BLAKE2b-256 41975b9329654a04732df3b1fafb34a666b3de65ed31198b5eb49ac6a1b2f394

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page