← Work

IMU 3D Viewer

Personal project

What it is

A Raspberry Pi Pico with an ICM20948 reads nine axes of accelerometer, gyroscope and magnetometer data, fuses them into an orientation on the board itself, and publishes over MQTT at 100 Hz. A Python viewer draws a 3D box that turns with the board in real time, alongside its other readings: temperature, pressure, humidity, luminosity, UV and a VOC index.

What I built

Firmware. MicroPython on the Pico. A complementary AHRS filter fuses all nine axes into a quaternion on the microcontroller, so the laptop receives an answer rather than raw axes.

Transport. MQTT, so the board and the viewer know nothing about each other and the Pico is not tethered to the machine.

Two viewers. One takes roll, pitch and yaw and composes three rotation matrices. The other takes a quaternion and builds a single matrix from it directly.

Magnetometer calibration. Nearby metal pulls the readings off-centre and magnetic fields stretch them out of round, corrected with an offset and a matrix, with 2D and 3D plots to check it. It matters because yaw rests entirely on the magnetometer: the other two sensors cannot tell you which way is north.

What I learned

My first version fused nine axes into a quaternion on the Pico, turned that into roll, pitch and yaw, sent the three angles over MQTT, and rebuilt a rotation from them in the viewer. The filter had already produced the quaternion I needed, so I was converting it into something lossier and then trying to put it back together at the other end. Switching the viewer to quaternions did not make it cleverer, it removed a step I had added myself.

The three-angle viewer has to build its rotation from three separate matrices, one per axis. That is where gimbal lock comes from. Tip the board up near vertical and roll and yaw end up turning it around the same axis. Two of my three angles are doing the same job at that point, and the box on screen stops matching the board. Multiplying the matrices in a different order does not help. The quaternion viewer makes one matrix and never has the problem.

I send the whole orientation in every message rather than a change to it, so when a packet goes missing over wifi the viewer skips a frame and carries on. If I had sent gyroscope readings for the laptop to add up instead, one lost packet would have fully messed up the orientation, which would have only worsened as time went on.

I plotted the magnetometer readings as a point cloud to check the calibration, which made the problem obvious: they sat off-centre and squashed, then snapped back into a sphere once I corrected them. Reading the raw numbers never showed me that.

What it does not do yet