Documentation
External devices
The tyre pressure sensor, a home-made display and the Third Eye mirror: what the phone can find next to itself.
Besides the wheel, the phone talks to three more things nearby: the tyre pressure sensor, a home-made display and the Third Eye mirror. They have little in common — each lives at its own end of Bluetooth, and all three are set up in Connection & sensors — but none of them is the wheel, so they are collected here.
A watch is a device at that same end of the link, but it has a section of its own: Watch.
Tyre pressure sensor (TPMS)#
Connection & sensors.
The sensor screws onto the valve and broadcasts pressure, temperature and its own battery every few seconds. The app listens to that air alongside the wheel: there is nothing to connect to — the sensor answers nobody and accepts nothing.
iBar and compatibles, Pecham/SYTPMS and Sysgration are supported. Experimental decoders also cover TPMSII, Michelin TMS, WODHMIEY/ITPMS K and Salutica FOBO, based on the open-source tpms_ble integration; these additional formats have not yet been verified on real sensors. The app recognises the family from the advertisement, including manufacturer-only packets.
Binding. A sensor that is found is offered to the current wheel: Connect binds it, Not now postpones, Ignore hides that sensor for good — so somebody else's wheel in the parking lot stops asking every time. Changed your mind? Search ignored sensors brings them back. Replaced the sensor? Replace TPMS.
If the sensor is not found. It broadcasts rarely and not always: spin the wheel or let a little air out to wake it, and keep the phone close. The search is time-limited and says so when the time runs out — a silent screen would be the worst of it here.
Tyre pressure and temperature land on the dashboard as ordinary metrics, and an alert is built on them like on any other. The sensor's own battery is shown in volts for the families that report volts — a percentage there would be an invention.
The last pressure stays on the tile when the sensor goes quiet, without a 30-second display timeout. It is not a fresh measurement: stale readings are excluded from alerts. The last reading is not retained across app restarts. On iPhone, manufacturer-only sensors without FBB0/27A5 are discovered only while the app is in the foreground; background scanning keeps the existing service filter.
External display#
Connection & sensors. Android and iPhone.
Telemetry can go to a home-made display — an ESP32 one, for instance. Turn on External display and the phone finds it on the air by itself, connects and starts sending frames. The display neither scans nor needs buttons to pick a phone: it advertises, the phone connects.
The line under the switch says what is going on: the search is running, or a display was found and is being written to. Below it is the list of every display audible on the air — its name, if the hardware advertises one, and the signal level. The phone writes to the first one it finds, because most people have exactly one; if you have several, tap the row you want and the phone writes only there from then on. The choice is remembered and never silently replaced — when the chosen display is off the air, the phone waits for it instead of writing to a stranger.
The list lives while the settings section is open: keeping the scan running for a whole ride costs radio and battery, and nobody is looking at the list meanwhile.
If the display stayed at home, the phone keeps looking for it and does nothing else: Android retries every five seconds, the iPhone leaves the scan running. If the display drops off the air mid-ride, the phone reconnects by itself as soon as it is back.
What the display must do#
- advertise service
1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c01; - expose characteristic
1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c02as writable, preferably without response; - accept a twenty-byte frame into it, five times a second.
Twenty bytes is the default MTU minus the ATT header: the frame arrives with no MTU negotiation at all. The phone writes without response — at five frames a second, acknowledging each one costs more than it saves.
The frame#
The same compact frame that goes to the watch over its fallback channel. Big-endian, signed integers.
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | 0x4C, magic L |
| 1 | 1 | 0x45, magic E |
| 2 | 1 | version, currently 0x01 |
| 3 | 1 | frame counter, wraps at 256 |
| 4..5 | 2 | field presence mask |
| 6..7 | 2 | speed, km/h ×10 |
| 8..9 | 2 | PWM, % ×10 |
| 10..11 | 2 | voltage, V ×10 |
| 12..13 | 2 | charge, % ×10 |
| 14..15 | 2 | temperature, °C ×10 |
| 16..17 | 2 | trip distance, km ×10 |
| 18..19 | 2 | PWM alarm limit, % ×10 |
Mask bits, from bit zero: speed, PWM, voltage, charge, temperature, trip, PWM limit. A cleared
bit means the field is absent from this frame and its value must not be read. Those two bytes
hold 0x8000 (Int16.MIN) anyway, so a display that reads fields directly still tells "no
value" from zero.
The receiver must check the magic and the version and silently drop anything that is not
4C 45 01: the frame version is the only way this format can ever be extended.
The frame counter exists so the display can tell a fresh frame from a repeat: a link without acknowledgement loses packets silently, and a display whose counter has not moved for a few seconds is entitled to show the data as stale.
If the display can do more#
Seven values is everything that fits into twenty bytes. A display that wants more keeps a second
characteristic, 1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c07, also writable, and receives everything
into it once a second: up to thirty-three wheel metrics and the summary of the current ride.
The characteristic is optional. Without it the phone sends only the twenty-byte frame, exactly as before, and a display built from this page a year ago notices nothing.
How the snapshot arrives#
The snapshot weighs a couple of hundred bytes and does not fit one write at the usual MTU, so it travels in chunks. Every chunk carries a header, and the first one also carries the length and checksum of the whole transfer:
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | 0x4C |
| 1 | 1 | 0x45 |
| 2 | 1 | chunking version, 0x01 |
| 3 | 1 | flags: bit 0 — last chunk |
| 4..5 | 2 | chunk number, from zero |
| 6..9 | 4 | chunk 0 only: length of the whole transfer |
| 10..13 | 4 | chunk 0 only: CRC32 of the whole transfer |
| then | data |
Assemble by chunk number, then check the length and the CRC32 (plain IEEE, polynomial
0xEDB88320). Without that check a lost chunk glues itself into garbage that looks like data. The
phone writes with response: losing any chunk kills the whole snapshot, and once a second the
acknowledgement pays for itself.
The chunk size is the negotiated MTU minus three bytes. Android asks for 517 and takes what it gets; the iPhone does not ask — the system negotiates for it. A display that agrees to a large MTU receives the snapshot in one or two writes instead of ten.
What is inside the snapshot#
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | 0x4C |
| 1 | 1 | 0x45 |
| 2 | 1 | version, 0x03 |
| 3 | 1 | flags: bit 0 — a ride block follows |
| 4..11 | 8 | timestamp, ms |
| 12 | 1 | N — how many metrics |
| 13.. | 5×N | entries: metric code (1 byte) and value (4 bytes, signed) |
Then, if bit 0 is set, a 22-byte ride block: duration in ms (4), distance (4), peak speed (4), peak PWM (4), lowest charge (4), how many times the alerts fired (2).
Everything is big-endian. Metric and ride values are multiplied by 1000, not by 10 as in the
compact frame: cell spread is measured in millivolts, and one scale has to hold both that and a
six-figure odometer. 0x80000000 (Int32.MIN) means "the phone does not know this value".
Metric codes are handed out once and never reused, so an unfamiliar code must be skipped rather than shown under somebody else's name: a snapshot from a phone newer than your display is an ordinary thing.
| Code | Metric | Code | Metric |
|---|---|---|---|
| 1 | speed, km/h | 18 | battery temperature max, °C |
| 2 | PWM, % | 19 | pitch, ° |
| 3 | pack voltage, V | 20 | roll, ° |
| 4 | average cell, V | 21 | fall protection angle, ° |
| 5 | cell spread, V | 22 | danger speed, km/h |
| 6 | battery current, A | 23 | tyre pressure, bar |
| 7 | output current, A | 24 | trip distance, km |
| 8 | phase current, A | 25 | odometer, km |
| 9 | power, W | 26 | charge voltage, V |
| 10 | motor power, W | 27 | charge current, A |
| 11 | apparent power, VA | 28 | charge power, W |
| 12 | charge, % | 29 | charger temperature, °C |
| 13 | controller temperature, °C | 30 | target voltage, V |
| 14 | MOSFET temperature, °C | 31 | charged, Ah |
| 15 | motor temperature, °C | 32 | charged, Wh |
| 16 | board temperature, °C | 33 | charging (0 or 1) |
| 17 | battery temperature min, °C |
Metrics your wheel does not report are simply absent from the snapshot — which is not the same as zero.
Buttons on the display#
A display with buttons can control the wheel: switch the headlight, sound the horn, turn the wheel off, toggle the phone's auto light, the wheel's own auto light (Inmotion V12 and newer) and the parking alarm (the alarm plugin). The wheel's auto light can be toggled from the display only with Android for now; everything else works with Android and iPhone. The phone builds the bytes for the wheel, with the same code as the dashboard buttons, so the commands are the same for every wheel: the display does not need to know which wheel is connected. The phone never offers a button the wheel does not have.
The phone turns the wheel off only when it is standing, below 3 km/h, whatever the display asks for: switching a wheel off under a rider means dropping them.
For this the display keeps one more characteristic, 1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c08,
with notify and write-without-response. Without it everything works as before, just without
buttons.
A press is a seven-byte notification from the display: the opcode and a press number (six bytes, big-endian). The number stops a repeated notification from switching off the light it has just switched on; a new press takes a new number.
| Opcode | Command |
|---|---|
0x10 |
headlight, toggle |
0x11 |
wheel auto light, toggle |
0x12 |
turn the wheel off |
0x13 |
horn |
0x14 |
phone auto light, toggle |
0x15 |
parking alarm, disarm or arm |
The state is a fifteen-byte frame the phone writes into the same characteristic: on every change, right after a press, and once a second:
| Offset | Size | Field |
|---|---|---|
| 0..2 | 3 | 4C 45 20, magic and version |
| 3 | 1 | frame counter |
| 4 | 1 | switches, two bits each: 0-1 headlight, 2-3 phone auto light, 4-5 alarm, 6-7 wheel auto light |
| 5 | 1 | exactness, one bit per switch in the same order |
| 6 | 1 | bit 0: horn available, bit 1: turn-off available |
| 7 | 1 | opcode of the last press, 0 if there was none |
| 8 | 1 | how it ended: 1 done, 2 the wheel cannot, 3 no wheel, 4 repeat, 5 turn-off refused while moving |
| 9..14 | 6 | that press's number |
Two switch bits: 00, the wheel does not have it, hide the button; 01, it has it but the
state is unknown; 10, off; 11, on. The headlight and the wheel's auto light have no
state and are always 01: the wheel does not report it, and the phone will not guess from its
own commands, because the light may have been switched with the wheel's own button. The phone's
auto light and the alarm do have a state, and carry the exactness bit.
A display that connects on its own#
Android only. An iPhone cannot advertise itself, so only the first order works there — the phone finds the display.
The opposite order works too: the display finds the phone itself and subscribes to notifications on the same characteristic. That path came first and still works — same service, same characteristic, same frame, only read by subscription instead of waiting for a write.
The same switch keeps it alive. The phone used to stop advertising as soon as it saw a LoEUC watch, which left watch owners without a display; now External display, once on, keeps the channel up regardless of the watch.
Third Eye#
Connection & sensors. Android and iPhone.
Third Eye is a small mirror display for the handlebar, sold ready-made. It has an app of its own, but the number on it lags noticeably behind what is happening; LoEUC writes to the same mirror in the same format, only more often and without the extra delay.
Turn on Third Eye and the phone finds the mirror on the air by itself (it advertises a name
starting with ThirdEye_), connects and starts writing. The line under the switch says whether
the search is running or the mirror is already taking frames, and below it is the list of every
mirror audible on the air with its signal level. The first time, the phone writes to the first
mirror it finds and remembers it, and a mirror you pick by hand is remembered and never replaced.
The switch stays on by itself, not just until the end of the ride: turn it on once, and on the next ride the phone connects to the mirror as soon as it appears on the air. It connects only to its own, the one from last time, so it will not take over a friend's mirror in a group ride. To move to another mirror, pick it in the list.
While the wheel is connected, the phone keeps looking for its mirror for the whole ride: every 10 seconds for the first 10 minutes, every 30 after that. A mirror switched on mid-ride, or one that rebooted, connects by itself within half a minute.
The mirror has two short lines, and what goes in them is up to you: any metric the wheel reports fits either one, from speed and charge to PWM and cell voltage. The default is speed on top and charge below, as in the stock app. Either line can be switched off, and then the mirror shows one.
The mirror reports its own battery once a minute. It is available as an ordinary dashboard metric — put it on a tile if you would rather not find the mirror flat in the middle of a ride.
Nothing but the frame text goes to the mirror; the phone never changes its settings.