> Plain-text rendering of a visual post, for machine consumers.
> Charts, diagrams and images are omitted; their captions are kept inline.
> Titles on collapsible asides are dropped, their contents kept.
> The complete version is at https://abhay.fyi/teethmaxxing/

# Teethmaxxing with Claude Code and Oral-B iO

> Reverse engineering a smart toothbrush, and watching a brushing session in 3D.

- Author: Abhay Kashyap (https://abhay.fyi/#person)
- Published: 2026-10-30
- Canonical: https://abhay.fyi/teethmaxxing/

---
// imported as URLs so the build fingerprints them and they cache for good

The world has been *maxxing* lately. From looks to tokens to even Richard Nixon. If it yields aura, you max it. Simple.

Naturally, I've been on a *maxxing* journey myself. **Sensormaxxing**.

Industrial digital twins that connect people, processes, and machines into world models? Sign me the f up.

But that journey has a mandatory detour. **Teethmaxxing**.

  
  

toothbrush twinning - sensor stream captured & mapped onto a 3d model.

Claude and I pull raw sensor data off my Oral-B iO5, make sense of it, and turn it into a live model of the brush.

I visually walk through the message structure, what each part represents, and how it feeds into Oral-B's machine learning model. I also show which signals matter with a few ablation tests, then do some good old model distillation.

But before all that, let's meet the twin. Replay real brushing sessions and see where Oral-B's model thinks the brush is. And if you have a compatible brush and browser, connect it live, record your own session, and play it back.

built entirely by claude. if you find a bug, tell me and i'll let claude know.

---

## 1. hello, ~world~ teethmaxxer.

The world of teethmaxxing isn't something I chose. I humpty-dumptied my way into it when I dropped and cracked my Oral-B iO3.

Until then, I was a humble tokenmaxxer, a wannabe-Bryan-Johnson pillmaxxer, and a doomscrolling Nixonmaxxer.

So when my toothbrush died, I turned to Claude. It tokenmaxxed its way through the market and went, "Get the iO5, bro. It's got a 6-axis IMU."

I did. A few days later, it was chirping in my house.

Oral-B iO5 greeting teethmaxxers with mysterious bytes.

So what's the chirp?

Inside the iO5 is a motion sensor, a pressure sensor, and Bluetooth. The motion sensor is a 6-axis IMU, basically an accelerometer plus a gyroscope. Oral-B's app listens to all of it to show you how you brushed.

But it's all just Bluetooth. If the app can hear it, so can Claude.

The moment you pick up the iO5, it wakes up and starts broadcasting standard Bluetooth messages. That's the chirp above. Each message starts with a tag for P&G, Oral-B's parent company, followed by 11 mysterious bytes.

To solve this mystery, we needed clues. So I pressed buttons and told Claude what I pressed, while it watched which bytes changed. Four of them made sense. One was the state, one was a timer, one was the mode, and one was some kind of flag.

The rest stayed a mystery, so we connected to the brush next.

A Bluetooth broadcast is a message a device sends repeatedly to anyone in range. No connection is needed, and any Bluetooth scanner can pick it up.

Each message carries the sender's address and up to 31 bytes of data. The iO5 uses 22 of them. That includes `0x00DC`, Procter & Gamble's company ID from the Bluetooth [public registry](https://www.bluetooth.com/specifications/assigned-numbers/). The 11 bytes after it are Oral-B's own format.

The toothbrush sends this about 25 times a second for 30 seconds. Moving it around doesn't change the message but pressing a button does. 

First time Claude read the message, the 11 bytes looked like this.

Then I pressed one button at a time while Claude watched the bytes change.

- **Power on, then off.** The press shows up in a single message, as a blip in byte 4. In that same message, byte 3 goes from `08` (awake) to `03` (running), and bytes 8 and 10 change too. From then on, byte 6 counts up once a second.

  Pressing power again blips byte 4 once more and switches byte 3 to `09` (stopped). Everything else holds, including byte 6's count, so it's probably seconds brushed. Once the brush sleeps and wakes up again, the whole message is back to where it started. Bytes 8 and 10 are still a mystery.

- **Mode button.** Each press blips byte 4 for one message, same as power, and moves byte 7 to a new value: `01`, `05`, `03`, `00`, `04`. That's five values, and the iO5 has five modes. Byte 4 blips on every press, so it's probably a flag of some sort.

Four bytes down, seven to go. For those, we had to connect to the brush.

---

## 2. toothbrush pair bonding

The teethmaxxing literature is clear. True ascension means pair-bonding with your toothbrush, through oxytocin or the ritual burning of a manual one.

Unfortunately, I'm no hardmaxxer. I come from AI, where maxxing is soft... and differentiable. So I settled for Bluetooth.

And Claude connected on the first try. Turns out the iO5 has no standards and is down for something casual. It started oversharing right away.

the iO5's motion register (ff0d), pushing a new notification every 40 ms.

So what's in there?

Once connected, the iO5 works like a tiny file system, with 25 registers across three folders. Some you can read whenever you want. Others push updates once you subscribe.

Same as before, we needed clues. Claude read every register, then I pressed buttons again while it watched what changed. Most of what made sense were old friends from the broadcast, like the state, the timer, the mode and the button presses. The new ones were a clock, a 30-second pacer, and a countdown for when to replace the brush head.

Then there were the two that wouldn't shut up, one for pressure, one for motion, each pushing 25 readings a second. And *that's* what we came for. But what o' what can we do with them?

Time for toothbrush ~AI~ SI.

A Bluetooth connection is one-on-one, unlike a broadcast. Once connected, the device works like a small file system. Its data sits in registers, grouped into folders. Bluetooth calls them characteristics and services.

Each register holds a few raw bytes, up to 30 on this brush. Some you can read, some you can write, and some you can subscribe to, so the brush sends every new value as it changes.

Registers go by long IDs, but only a short code in the middle, like `ff01`, changes from one to the next. The rest is the same everywhere, and part of it spells "PGMS" and "Oral-B." PG is probably Procter & Gamble. MS, no idea.

The iO5 has 25 registers in three folders, on top of the 3 standard ones every Bluetooth device has. Three matched values from the broadcast. The rest showed themselves as I pressed buttons and Claude subscribed to everything, and 11 of the 25 fell into place.

Of these, `ff0d` mattered most, since it's the motion sensor. Every 40 ms, it pushes 20 bytes - two readings and a marker.

Each reading has a timestamp, three gyroscope values and three accelerometer values. Each push carries the newest reading and the one before it, so every reading shows up twice in the stream. Lay the brush flat and one accelerometer value settles at 32, which is gravity, 1 g.

That leaves 14 registers a mystery. Some only open up after the right write, which is a rabbit hole for another day. But we got what we came for.

---

## 3. recurrent dental network

You can't max what you can't measure. Looksmaxxers have the mirror. Tokenmaxxers have the leaderboard. Teethmaxxers have a neural network.

Oral-B ships one with its app. It watches the sensor stream and tells you where you're teethmaxxing, where you're plaquemaxxing, and where you're straight up gum-recession-maxxing.

But to get to it, I turned on developer mode on my phone and plugged it into my computer. Rascally Claude did the rest.

The first thing it found is that the model splits the mouth into 20 zones and picks one. Roughly, upper/lower jaw, left/front/right side, and outer/inner/chewing surface, plus a few catch-alls, like out of your mouth.

all 20 places oral-b's model thinks your brush can be.

But why?

What we really want to know is which tooth the brush is on right now. But figuring that out from only a 6-axis IMU is *hard*.

For one, sensor readings are approximations. Each one comes with a little random noise, and since a sensor only samples at fixed intervals, a smooth motion becomes a series of snapshots. On the iO5, both are rough. Its sensors are consumer-grade, and it sends just 25 readings a second, each squeezed into a single byte that rounds off small movements.

Even perfect readings wouldn't be enough. The accelerometer feels gravity, so it always knows which way is down. But it has a blind spot. Spin the brush around the up-down axis and it can't tell the brush moved.

the accelerometer has a blind spot. spin the brush around the up-down axis and it sees nothing.

The IMU's other sensor, the gyroscope, can feel that spin. It measures how fast the brush is turning, so its readings tell us which way the brush is facing. But it has its own small error, and that error builds up. So the blind spot shrinks, but the estimate still drifts over time.

A 9-axis IMU counters this with a magnetometer, a compass that knows where north is, but the iO5 doesn't have one.

All of this makes it hard to track exactly where the brush is. But the real goal is to know where you're brushing. The exact tooth would be ideal, but the region is a far easier target. It still shows the habits worth fixing, like favoring one side or skimping on the inner surfaces.

So you train a model on lots of recorded brushing, with the zone marked at every moment. Oral-B uses a small two-layer bidirectional recurrent net that reads 26 readings at a time, about a second of brushing.

oral-b's position model: a two-layer bidirectional recurrent net.

So does it work?

To check, Claude ran the model on a [public dataset](https://github.com/icl-mq/toothbrushing-dataset) of people brushing. It's small, just 14 electric-brush sessions from 3 people, so **the numbers are rough**. It also labels only 16 zones, with no "out of mouth". So the model's four extra zones were collapsed into one "other" zone, along with the dataset's unlabelled seconds.

Claude then ran an ablation study to see what the model relies on.

- The model picks the right zone 63% of the time (chance is 1 in 17). Narrowed to left/front/right side, it's right 82% of the time (chance is 1 in 3), and to upper/lower jaw, 81% (chance is 1 in 2).
- Most of its predictive power comes from the accelerometer, which alone gets 34% by sensing how the brush is tilted. The gyroscope does little by itself, but on top of the accelerometer it tracks how the brush turns, twisting around the handle and sweeping around the mouth.
- Finally, the recurrent net's memory matters a lot. With only the current second to go on, the model is right just 25% of the time. With memory, accuracy climbs and starts to saturate at about 10 seconds back.

That makes the recurrent net an interesting choice. If mostly recent history matters, a simpler network with a fixed window might suffice. So we decided to distill Oral-B's 1.2M-parameter model into something smaller.

Claude first built a synthetic dataset drawn from real manual brushing. It then trained a few small 'student' models to match Oral-B's predictions. They ranged from a plain softmax to fully connected nets and CNNs. The softmax got 51% right. A one-layer CNN, a hundred times smaller than Oral-B's model, got 61%, just behind the original's 63%.

With Oral-B's training data, the gap might have closed further. Then again, maybe Oral-B already modelmaxxed and picked the recurrent net anyway.

### the model

A recurrent net reads a sequence one step at a time and carries a memory from each step to the next. A bidirectional one also reads each chunk backwards, so every step sees both what came before and what comes after. Oral-B's is a small one, two layers of 256 units, with 20 zones out.

The app runs the model once a second on the latest 26 readings. Inside, the net is unrolled over those readings, one step per reading, both forward and backward. Each step ends in its own prediction across the 20 zones. The zone predicted most often becomes the zone for that second.

This means the memory works at two levels. Within a second, it passes from one reading to the next. Between seconds, only the forward memory carries into the next run, while the backward pass starts fresh every time.

one second of the model, unrolled.

The model is tagged for 26 readings a second, but the iO5 sends 25. Nobody seems to mind.

On its high setting, the app runs four versions of this model, all from February 2020. It adds up their predictions at each step before taking the vote. A 2021 version also ships in the app but never gets used. Thomas Gregg's [oralb-ha](https://github.com/thomasgregg/oralb-ha) documents the same model, so this is a confirmation, not a discovery.

### the dataset

Both datasets come from one study at Macquarie University ([Hussain et al., 2020](https://doi.org/10.1145/3419016.3431489)).

- [On GitHub](https://github.com/icl-mq/toothbrushing-dataset), as icl-mq, it has 44 brushing sessions from 15 people, with every moment hand-labelled as one of 16 zones. Only 14 use an electric brush, all from 3 people. It was recorded between July and September 2020 with a motion sensor stuck to the handle.
- [On Mendeley Data](https://data.mendeley.com/datasets/hx5kkkbr3j/1), it has the same recordings plus a second round of 57 sessions, 14 of them electric. That round has no zone labels.

The tests use the 14 electric sessions. The four 2020 versions were built before the study was recorded, so they never saw it. The 2021 version might have, but it does no better (63% vs 64% for the 2020 four), so it's the one used throughout. The study's sensor isn't mounted the way the iO's is, so Claude tried all 48 ways of lining up the axes and kept the best, which won by a wide margin. The worst session scored 50%.

### the ablation

Each test hid some sensor channels for a whole session, holding them at their average value, then scored the model again. Another wiped its memory every second.

| What the model sees | Right zone |
|---|---|
| everything | 63% |
| accelerometer only | 34% |
| gyroscope only | 10% |
| accelerometer + twist around the handle | 47% |
| accelerometer + twist + sweep around the mouth | 62% |
| everything, memory wiped every second | 25% |

Adding back the previous second pushes accuracy to 42%, and history starts to saturate at about ten seconds, at 63%.

### the distillation

The synthetic dataset has 187k brushing clips, each around a minute long, about 3k hours in total. Each clip is drawn from a real manual session, starting from one of its strokes and chaining pauses, glides and scrubbing, with the sensor readings worked out from physics.

Oral-B's model scored every second of every clip, and the students, each seeing the last 15 seconds, learned to match it by minimizing KL divergence. (The 14 test sessions were never used for training or for generating the synthetic data.)

| Model | Parameters | Right zone |
|---|---|---|
| Oral-B's model (teacher, 2 × 256 GRU) | 1.2M | 63% |
| one-layer CNN (1 × 32 filters) | 12k | 61% |
| fully connected net (1 × 64 units) | 6.2k | 58% |
| smaller CNN (1 × 8 filters) | 2.5k | 57% |
| softmax (linear) | 1.5k | 51% |

Nearly all of what the students miss is telling left from right.

The CNN gets the side right 78% of the time against the teacher's 82%, while upper versus lower jaw is a match at 81%.

I've been coding with AI since GitHub Copilot's beta in 2021. Since then, I've gone from hitting tab, to reviewing small functions, to reading whole files. These days, I barely look at the code and only look at the end artifacts.

This post took me two days. Last year, it would have taken me weeks, if not months. I mostly talked to my computer, saying ideas out loud and steering the design and the story, while Claude did the grunt work of writing code and running experiments. Claude continues to amaze me.

With this post, Claude put on its machine learning hat and ran comprehensive ablation studies, mostly on its own. Sure, it made a mistake or two, but it worked with the rigor of a good PhD student. For instance, to line up the study's sensor with the iO's, it tried all 48 ways the axes could match and kept the winner. I only found that out later.

That's a 90th-percentile machine learning engineer, working overnight while I sleepmaxxed.

---

## 4. digital twinning

The teethmaxxing detour is quite the adventure, but it's a gateway to something bigger. **Sensormaxxing.**

Our digital twin uses two types of sensors, each covering the other's weakness. Even then, it takes outside knowledge to get closer to reality. But add a [camera like Dyson's](https://dyson.com/discover/news/latest/introducing-camerajet), and you close the gap even further. Still, a toothbrush is a narrow view into your life. Add your smartwatch, your thermostat and your fancy air purifier, and you don't just see how you brush, you start to see how you live.

The same goes for offices, factories and even cities. Each sensor sees a tiny slice of people and things. The more you add, the clearer the picture, and you don't just see what each part is doing, you see how the whole thing behaves.

This was the promise of IoT from a decade ago. Sensors went in, but the models were narrow, so it never took off. Now, models are far more general. But they learned the world through our experience, from what we wrote, said and filmed. They've rarely sensed it on their own, except in places like playing games and writing code.

If we gave Claude the brush's motion data, it couldn't tell where in your mouth the brush is. Sure it could write the code to train one but it can't just see it on its own, out of the box. And the industrial world runs on sensors like these, on every pump, motor and conveyor belt.

But unlike our words, the chirps from these sensors never make it online. They stay inside each plant, fragmented and isolated, each machine rarely seeing the world beyond its tiny slice. Connect them to each other, and to the people and processes around them, and you get a twin that adapts instead of decaying, in an ever-changing world.

But that's the big picture. Back down here, sensormaxxing still starts with [a toothbrush](#live-simulator).