Documentation
External devices
The tyre pressure sensor, a home-made display, the Third Eye mirror and the Dragy GPS receiver: what the phone can find next to itself.
Besides the wheel, the phone talks to four more things nearby: the tyre pressure sensor, a home-made display, the Third Eye mirror and the Dragy GPS receiver. They have little in common — each lives at its own end of Bluetooth, and all 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.
Bluetooth chargers connect as the main device from the scanner, rather than as an accessory to the wheel. See Chargers: SKAT and HW for connection, password and charge settings.
After a manual Disconnect, LoEUC stops connecting to that wheel automatically, including across app restarts. Connecting to the same wheel manually allows automatic connections again. Other saved wheels remain available. Automatic connection attempts do not interrupt ride playback.
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 accepted for execution, 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.
Firmware setup:
- In service
1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c01, create the mandatory telemetry characteristic1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c02withWRITE_NO_RESPONSE, and control characteristic1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c08withNOTIFY,WRITE_NO_RESPONSEand a CCCD descriptor (0x2902). - Advertise the service UUID. Enable External display in LoEUC and select the display.
- Wait for the phone to enable notifications through the control characteristic's CCCD.
- Send a seven-byte notification for each press. Updating the characteristic value alone
is insufficient. For example,
10 00 00 00 00 00 01toggles the light with press number 1;13 00 00 00 00 00 02sounds the horn with number 2. - Receive the phone's 15-byte frames in the same characteristic's write handler. Match the opcode at byte 7 and the press number at bytes 9–14 to your request.
The seven-byte command has no LE header, CRC or trailing zero. Send binary bytes, not hex
text. The press number is unsigned 48-bit big-endian: use a new number for a new press and
keep the same number when retrying that press. Use milliseconds or a persistent counter so
rebooting does not reuse recent numbers. The phone remembers the last eight executed
command/number pairs.
Result 1 means the phone accepted the request, not that the wheel confirmed execution.
Unknown opcodes and truncated commands are ignored. Shutdown is also refused with code 5
when speed is unknown: it requires finite abs(speed) < 3 km/h. A refusal is not remembered
as an executed request.
A display that connects on its own#
Android only. LoEUC for iPhone does not currently implement a GATT server or phone advertising; there the phone connects to an advertising 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.
Commands: the display writes to the phone#
The display is the BLE Central and the phone is the Peripheral. The characteristics are on the phone. Enable External display in LoEUC and connect the phone to the wheel.
- Scan for service
1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c01and connect to your phone. - Read
1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c06: it contains UTF-8 JSON.phone_holds_wheelindicates the wheel connection;available_commandsis a comma-separated string of supported commands:light,auto_light,shutdown,horn. - Send seven binary bytes using Write with response to
1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c05. - After the write response, read
1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c06again and match the result to the press number.
Example packets, each with a new press number:
10 00 00 00 00 00 01 headlight / next mode
11 00 00 00 00 00 02 wheel auto light
12 00 00 00 00 00 03 turn the wheel off
13 00 00 00 00 00 04 horn
The same seven-byte format and press-number rules described above apply. This channel does
not accept 0x14 or 0x15. Sending commands does not require a telemetry subscription;
to receive telemetry, subscribe to 1d0f2f9c-6d1a-4d96-ae61-2a4fd97c2c02 through its CCCD.
Example response after headlight command number 1:
{
"phone_holds_wheel": true,
"available_commands": "light,shutdown,horn",
"command_id": "light",
"command_outcome": "accepted",
"command_sent_at_millis": 1,
"answered_at_millis": 1790000000100
}
command_sent_at_millis is the press number you sent, even when it is a counter rather than
a timestamp. Match it together with command_id. Only the latest result is retained and it
is shared, so obtain a result before sending the next command.
command_outcome |
Meaning |
|---|---|
accepted |
Accepted by the phone for execution; not a confirmation from the wheel |
unsupported |
Command is not supported |
not_connected |
Phone is not connected to the wheel |
duplicate |
This command/number pair has already been executed |
unknown |
Unknown result |
command_detail may contain a reason for refusal. Result fields are absent before the first
command. A successful ATT write response only means the packet was parsed: read the JSON
for the command result. Unknown opcodes and short packets produce a write error.
The JSON may exceed a single ATT packet. Support long reads (Read Blob with offsets),
or negotiate a sufficiently large MTU and verify that the entire JSON was received.
Shutdown: the current …2c05 path does not check speed on the phone, unlike …2c08.
Block shutdown on the display while moving or when speed is unknown.
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.
Dragy Pro#
Connection & sensors. Android and iPhone.
Dragy Pro is a separate GPS receiver riders take along for acceleration runs. It computes position and speed from satellites up to 25 times a second — far more often and more precisely than the phone's GPS — and LoEUC can read it during the ride, alongside the wheel.
Connecting. There is nothing to search for by hand. When the wheel is connected and a Dragy is switched on nearby, the dashboard offers to connect it to the ride. Connect links the receiver: from then on it joins every ride by itself as soon as it is nearby. Not this ride stops the question until the end of the ride, and the next ride asks again. To stop the offer altogether, turn off Offer to connect in the receiver's settings.
The tile. Once connected, a Dragy tile appears at the top of the dashboard: satellites in the solution, whether there is a 3D fix, how many points arrive per second, when the receiver connected, accuracy and battery. Move and resize it like any other tile: a narrow one keeps the satellites and the fix, a wide one shows everything. Tapping the tile opens the receiver's settings; the Dragy item in Connection & sensors leads there too.
What goes into the ride. The phone's GPS keeps running while the Dragy is connected: its speed is recorded as a separate metric. When Dragy is selected, its valid fixes take priority for GPS speed, the track and speedometer calibration. If the receiver loses the sky, the phone's GPS takes over after three seconds. Acceleration runs calculate separate Dragy times for 0–30, 0–60 and 0–100 km/h.
Next to each wheel record the app saves the Dragy's speed, satellite count, accuracy, heading, altitude, fix type and battery charge. These metrics are available in a separate Dragy section when adding a dashboard tile, and in alert rules. Coordinates go into the track only when track recording is on — the same as with the phone's GPS.
Settings.
- Update rate — 10, 20 or 25 times a second. Only the Dragy Pro (model DRG71) goes above 10. The Dragy resets the rate to 10 on every connection, so the phone sends it again each time and shows how many points actually arrive. Higher rates drain the receiver's battery faster.
- Track and GPS speed from Dragy — turn it off if you want the ride to follow the phone, with the Dragy only showing its state.
- Start-up assistance — on connection the phone gives the receiver the exact time and its own approximate position, so it finds satellites faster.
- Interference — signs of jamming and spoofing that the receiver reports itself.
- Disconnect until the next ride and Forget the receiver — the latter removes the link, and the Dragy will be offered again on the next ride.
This support is new: it is built from the description of the Dragy protocol and has seen little testing on a real receiver so far. If the stream does not start or the tile shows something wrong, send the diagnostic log — it records the receiver's connection.