How a Zero talks over Bluetooth
A Zero answers a small set of four-letter commands over Bluetooth: who it is, every cell voltage, what the motor and the dash are doing, and its whole log, block by block. This page lists every command we know, what each reply holds and in which unit, and draws real replies from 4 recorded sessions with a 2016 FXS. Every row says how we know it: measured on those captures, taken from the open-source ZeroBLE library, or known but not yet captured.
TL;DR
- Gen2 motorcycles speak Bluetooth Classic, Serial Port Profile on RFCOMM channel 1. Gen3 motorcycles speak Bluetooth LE through a serial-over-BLE service, 0xFEFB. The 2026 XB, XE and LS1 have no known interface.
- There is no login. Once the phone is bonded the link is encrypted and the first frame can already ask for data — and any bonded device could also send the commands that change the motorcycle. ZeroLogs sends reads only, from a fixed allowlist.
- A Gen2 frame is F1 F2 F4 F8, a count of 32-bit payload words, a four-letter ASCII tag, the payload, F8 F4 F2 F1 and a CRC-32. A request is 20 bytes; the reply repeats its tag. In our captures all 3,995 requests and 3,992 replies frame and check correctly.
- Live data comes from five reads in a round robin — MbbR, PwPk, DSt1, DSt2, DSt3 — one request in flight at a time, about 85 ms per round trip, so each value refreshes every ~0.5 s.
- Speed arrives in whole mph and the odometer in whole miles, while the trip meters and range arrive in hundredths of a kilometre. Both units are checked: the speed integrated as mph reproduces the trip meter, and the odometer matches the bike’s own log to the kilometre.
- A log download is one CmCh (read address 0) and then MbbD, or BmsD for the battery, repeated: a 4-byte flag and 256 bytes per answer, ~11 s stalls, and an end flag. On the FXS, 1,048 answers in 334 s gave the whole 262,144-byte log.
- Some fields are not settled — DSt3’s three consumption words, two BtSt words, the sign of the currents — and the tables say so rather than guess.
On this page
Which motorcycle speaks which radio
Zero has shipped two Bluetooth protocols, one per electronics generation, and they have less in common than their shared frame magic suggests. A Gen2 motorcycle — the FX and FXS, and the S, SR, DS and DSR up to 2023 — is a Bluetooth Classic serial device. A Gen3 motorcycle (the Cypher III platform: the SR/F and SR/S from 2020, the DSR/X from 2023, and the S, SR, DS and DSR from 2024) is a Bluetooth Low Energy peripheral that tunnels a serial stream through GATT. "Gen2" and "Gen3" are the electronics and log generations used across these Learn pages; Zero’s own manuals file the FX and FXS as product generation 1.
The practical difference for a rider is the phone. Android opens a Classic serial link to any bonded device; iOS does not let an app do that unless the accessory is in Apple’s MFi programme, so a third-party iPhone app can reach a Gen3 bike over BLE but not a Gen2 one.
Everything measured on this page comes from one Gen2 motorcycle, a 2016 FXS whose main board reports part number 40-08290C. The Gen3 side rests on ZeroBLE, an MIT-licensed library; its author’s ZeroNavBridge app, built on it, is reported working on the DSR/X (2023), SR/F (2023), SR/S (2021–2023) and the Gen3 S. We have not captured a Gen3 bike ourselves yet, and every Gen3 row says so.
| Gen2 | Gen3 | |
|---|---|---|
| Radio | Bluetooth Classic (BR/EDR) | Bluetooth Low Energy |
| Profile | Serial Port Profile over RFCOMM | GATT service 0xFEFB, a serial-over-BLE profile |
| Where | RFCOMM server channel 1, SPP UUID 00001101-0000-1000-8000-00805f9b34fb | Service 0000FEFB-0000-1000-8000-00805F9B34FB |
| Security | An OS-level bond; the link is encrypted with its key | Pairing as the bike’s manual describes; one phone per bike |
| Login in the protocol | None | None known |
| Message names | Four ASCII letters (PrVr, MbbR …) | Numbers (1, 3, 22 …) plus a counter |
| Phones | Android (iOS needs MFi) | Android and iOS |
| Status | Measured | ZeroBLE (open source) |
Connecting: the bond, the channel, and no login
On Gen2 the motorcycle is paired once in the phone’s own Bluetooth settings. That bond stores a link key; every later connection authenticates with it and turns on encryption before any data moves. The serial link itself is RFCOMM server channel 1. In our captures the phone opens channel 1 directly, with no service-discovery lookup first, and then the first frame on the link is already a request for data. Nothing in the protocol asks who is calling.
Treat the link as single-user: close one app before connecting with another. We have not tested what a Gen2 bike does with a second connection.
After connecting, the session in our captures asks for the protocol version, the power pack, the battery status, one dash read and the bike’s identity, then settles into the live round robin described further down. In both rides and the download, every reply arrived before the next request went out.
No login means no permission check either
The protocol has commands that change ride-mode limits, the clock, charging and firmware (see "Commands that change the motorcycle" below), and the bike does not ask who sends them: any device holding a bond can. That is why the ZeroLogs app sends nothing that is not on a short allowlist of reads, and refuses everything else in code. On the security side, CISA advisory ICSA-26-111-06 (CVE-2026-1354) describes forced Bluetooth pairing on Zero firmware 44 and earlier; its June 2026 update says a fixed firmware is available through Zero’s over-the-air (FOTA) platform.
The Gen3 link: a serial port inside GATT
A Gen3 bike advertises service 0xFEFB, the 16-bit UUID the Bluetooth SIG assigned to Telit (formerly Stollmann), whose "Terminal I/O" profile carries a serial stream over BLE. The client connects over LE without autoConnect, asks for an MTU of 512, subscribes to the characteristics below through their 0x2902 descriptors, and writes without response. Flow control is by credits: each side grants the other a number of packets it may send.
Every Gen3 owner’s manual from 2020 to 2026 gives the pairing ID as Zero_XXXXX, the last five digits of the VIN, and notes that "only one cell phone can be paired to your motorcycle at a time". No Gen2 manual carries that note.
0000000x-0000-1000-8000-008025000000)| x | Name | Role |
|---|---|---|
| 01 | UART TX | serial data |
| 02 | UART RX | serial data |
| 03 | local credits | flow control |
| 04 | remote credits | flow control |
| 09 | MTU RX | negotiated packet size |
| 0A | MTU TX | negotiated packet size |
Status: ZeroBLE (open source). We have not captured a Gen3 bike yet.
The frame
Every Gen2 message, in both directions, has the same shape: a 4-byte start marker F1 F2 F4 F8; N, the payload length counted in 32-bit words, as a little-endian u32; a four-character ASCII tag; 4 × N bytes of payload; the end marker F8 F4 F2 F1; and a CRC-32 of everything from the start marker to the end marker, stored little-endian. The CRC is the ordinary zlib / IEEE 802.3 one, so any standard library computes it.
Requests carry no payload (N = 0), which makes every request exactly 20 bytes; the one exception is CmCh, below. The reply repeats the request’s tag. Multi-byte numbers inside a payload are little-endian too.
In our captures every one of the 3,995 requests and 3,992 replies frames at 12 + 4N and passes its CRC. One thing to get right first: a reply can arrive split across several Bluetooth packets, so reassemble the stream (on Android the socket does this for you) before looking for the markers.
Gen3 keeps the start marker and the CRC-32 but changes the rest: a u32 block count, a u16 message type and a u16 counter replace N and the tag, the payload is padded to whole 4-byte blocks, and the CRC comes before the end marker instead of after it.
PrVr request and reply from the captures; the page’s own frame builder reproduces every request in them byte for byte, CRC included. The CRC is the ordinary zlib / IEEE CRC-32, stored little-endian. The Gen3 row is the layout the open-source ZeroBLE library sends; we have no Gen3 capture of our own yet.| Tag | Meaning | What to do | Status |
|---|---|---|---|
| ReSd | The bike did not take the last request | Send the same request again | Known, not yet captured |
request f1 f2 f4 f8 00 00 00 00 50 72 56 72 f8 f4 f2 f1 9d 88 03 7b
start N = 0 "PrVr" end CRC-32, LE
reply f1 f2 f4 f8 01 00 00 00 50 72 56 72 00 17 00 00 f8 f4 f2 f1 c0 10 3d ed
start N = 1 "PrVr" payload end CRC-32, LEThe reply’s payload, 00 17 00 00, is the protocol version the 2016 FXS reports. Both frames are exactly the bytes in the capture, and this page’s own frame builder reproduces every request in the captures, CRC included.
Every Gen2 command, and what comes back
These are the reads. Each is a 20-byte request; the reply size is the one the 2016 FXS sends. Offsets in the sections below count from the first payload byte, after the 12-byte header (start marker, N, tag).
| Tag | Asks for | Reply payload | Status |
|---|---|---|---|
| PrVr | The protocol version | 1 word | Measured |
| Gbki | Who the bike is: VIN, make, model, main-board part number | 26 words (104 B) | Measured |
| PwPk | Power pack: pack voltage, every cell, SoC, Ah, temperature probes, current | 25 words (100 B) on a 28-cell pack | Measured |
| BtSt | Battery status: voltage, Ah, SoC, pack temperatures, status bits, odometer | 11 words (44 B) | Measured |
| MbbR | Main board: torque, rpm, motor and controller temperature, currents, speed, switches | 7 words (28 B) | Measured |
| DSt1 | Dash: trip A, motor rpm, the dash’s code | 2 words | Measured |
| DSt2 | Dash: trip B, range estimate, motor temperature | 2 words | Measured |
| DSt3 | Dash: minutes until charged, three consumption words | 3 words | Measured; scales not settled |
| Ecus | Every control unit: index, model year, hardware and firmware part numbers and revisions | a count, then 30 B per unit | Known, not yet captured |
| Errs | The active fault codes | a count, then a u16 per code | Known, not yet captured |
| MbbD | The main board’s log, one block per request | 65 words: 4-byte flag + 256 B | Measured |
| BmsD | The battery’s log, the same way | 65 words | Answered on the 2016 FXS |
| CmCh | The command channel; a download sets the read address to 0 with it | 11 words, echoed | Measured (read address only) |
A bike that does not support a read may simply not answer it; treat silence after a few seconds as "not supported" rather than retrying forever.
PrVr and Gbki: who is on the line
PrVr answers one word, 00 17 00 00 on this bike. Gbki answers 104 bytes of mostly NUL-terminated ASCII. The offsets below are measured on the one bike captured; read the strings up to their first NUL rather than trusting fixed lengths.
| Starts at byte | Field | Example |
|---|---|---|
| 0 | VIN, 17 ASCII characters + NUL | [VIN] (redacted here) |
| 18 | Make | Zero Motorcycles |
| 38 | Model | FXS |
| 70 | Main-board part number | 40-08290C |
| 96 | u32, meaning not known | 41 |
Status: Measured, one bike. Where each string starts is measured; how long each field is allowed to be is not known.
PwPk: every cell, twice a second
PwPk is the richest live read. Its cell list has no length field: cell voltages, as u16 millivolts, run until the first value under 128, and that value is the state of charge. This 28-cell pack read 3.673 V to 3.764 V at connect, a spread of 91 mV.
After the state of charge come the pack’s capacity and remaining charge in amp-hours, then thirteen temperature slots in °C, where −100 means "no sensor fitted". On this bike slots 0–3 and 8–9 track the pack temperature the main board logs at the same moments, slots 4–7 are absent, and slots 10–12 sit with the electronics: slot 10 follows MbbR’s motor temperature (equal in 541 of 566, and never more than 1 °C apart) and slot 12 its controller temperature (545 of 566, and never more than 1 °C apart), pairing each reply with the MbbR read less than a second before it. The pack current sits 8 bytes before the end; its sign convention is not settled, and the last 6 bytes are not decoded.
PwPk reply, decoded: the pack’s 28 series cells as the bike reports them, in millivolts. The list has no length field — it runs until the first value under 128, which is the state of charge (60 %). The y-axis starts at 3.65 V so a 91 mV spread is visible; each dot is a reading, not a bar of magnitude.| Bytes | Field | Unit | Example at connect |
|---|---|---|---|
| 0–3 | Pack voltage | u32, mV | 105.386 V |
| 4–59 | 28 cell voltages | u16 each, mV, all ≥ 128 | 3,673–3,764 mV |
| 60–61 | State of charge | u16, % | 60 % |
| 62–63 | Capacity | u16, Ah | 52 Ah |
| 64–65 | Remaining | u16, Ah | 32 Ah |
| 66–91 | 13 temperature slots | s16 each, °C; −100 = none | 34 33 34 34 — — — — 34 33 42 46 35 |
| 92–93 | Pack current | s16, A; sign not settled | -3 A |
| 94–99 | Not decoded | ed ff ae 00 00 00 |
Status: Measured. The state of charge here (60 %) and BtSt’s (62 %) are two different readings and need not agree.
BtSt: the battery, and the odometer in miles
BtSt repeats the pack voltage and amp-hours and adds two things PwPk lacks: a word of status bits and the odometer, in whole miles: 20,776 mi (33,436 km) at the first ride and 20,818 mi (33,503 km) at the second. The log the same bike wrote, read by this site’s parser, shows the same two odometer values in kilometres at those two rides.
| Bytes | Field | Unit | Example at connect |
|---|---|---|---|
| 0–3 | Pack voltage | u32, mV | 105.341 V |
| 4–5 | Capacity | u16, Ah | 52 Ah |
| 6–7 | Remaining | u16, Ah | 32 Ah |
| 8 | State of charge | u8, % | 62 % |
| 10–25 | Pack temperatures | 8 × s16, °C; −100 = none | 34 33 34 34 — — — — |
| 28–31 | Status bits | u32, see below | bike on |
| 32–35 | Odometer | u32, whole miles | 20,776 mi |
| 36–39 | Not settled | u32 | 28,472 riding, 0 parked |
| 40–43 | A slow counter, unit not settled | u32 | 135 → 138 over 42 mi |
Status: Measured.
| Bit | Meaning | Seen |
|---|---|---|
| 0x10000000 | Charger 1 attached | yes, in 1 reply |
| 0x08000000 | Charger 2 attached | |
| 0x04000000 | Bike on | yes, in all 10 replies |
| 0x02000000 | Pack hot | |
| 0x01000000 | Pack critically hot | |
| 0x00800000 | Pack cold | |
| 0x00400000 | Charge low | |
| 0x00200000 | Charge critically low | |
| 0x00100000 | Cells out of balance (the battery’s own judgement) |
"Bike on" and "Charger 1 attached" are measured; the other names are known but not yet seen set.
MbbR: motor, speed and switches
MbbR is the main board’s live read, and its speed is a whole number of miles per hour. The check: integrate it over each ride and read it as mph, and it gives 4.71 km and 1.90 km, against 4.80 km and 2.00 km on the dash’s trip meter; read as km/h it would give only 2.93 and 1.18 km. The motor turns a steady 68.3 rpm per mph (median, both rides). Over the ride drawn below the reading peaked at 38 mph and 2,601 rpm.
Bytes 20–27 are four percentages that stayed the same in every reply, riding and parked. They are known as the limits of the Eco mode (top speed, torque, coast regen, brake regen) — the values are measured, the naming is not — and not those of whatever mode is selected: the active mode is not in any Gen2 read we know of.
| Bytes | Field | Unit | Riding | Parked |
|---|---|---|---|---|
| 0–1 | Motor torque | s16, N·m | -3 | 0 |
| 2–3 | Motor speed | s16, rpm | 838 | 0 |
| 4–5 | Motor temperature | s16, °C | 43 | 44 |
| 6–7 | Controller temperature | s16, °C | 35 | 34 |
| 8–9 | Battery current | s16, A; sign not settled | 0 | 0 |
| 10–11 | Motor current | s16, A; sign not settled | -15 | 0 |
| 12–15 | Speed | u32, whole mph | 12 | 0 |
| 16–19 | Status bits | u32, see below | armed | kickstand down, brake |
| 20–27 | Eco-mode limits | 4 × u16, % | 85 / 20 / 0 / 100 | 85 / 20 / 0 / 100 |
Status: Measured.
| Bit | Meaning | Seen |
|---|---|---|
| 0x10000000 | Kickstand down | yes, parked |
| 0x08000000 | Kill switch off | |
| 0x04000000 | Armed (ready to ride) | yes, riding |
| 0x02000000 | Temperature warning | |
| 0x01000000 | Brake | yes |
| 0x00200000 | Charging |
DSt1, DSt2, DSt3: what the dash shows
The three DSt reads are short and come back with the numbers on the dash. Trip meters and the range estimate are in hundredths of a kilometre, so the trip advances in steps of 10 — the dash’s 0.1 km. The dash’s code is a plain number: it read 0 riding and 45 parked, and what each value means is not published, so show it as a number, never as an alarm.
DSt3’s first word is a minutes-until-full figure: 185 in a short session with BtSt’s "charger 1 attached" bit set. It is 0 most of the time otherwise, but in 38 of the 564 replies during the two rides it read 75 to 148, for a few seconds at a time, around 20 mph with the brake bit set — perhaps while regenerating; that is not settled. The other three words look like consumption figures, but under the "Wh/km × 100" scale that suggests itself they read 19.4 and 2.31 Wh/km, which no Zero does. Keep them raw until a ride settles them.
| Read | Bytes | Field | Unit | At connect |
|---|---|---|---|---|
| DSt1 | 0–3 | Trip A | u32, km × 100 | 38.60 km |
| DSt1 | 4–5 | Motor speed | s16, rpm | 0 |
| DSt1 | 6–7 | Dash code | u16 | 0 (45 parked) |
| DSt2 | 0–3 | Trip B | u32, km × 100 | 1,374.10 km |
| DSt2 | 4–5 | Range estimate | u16, km × 100 | 119.06 km |
| DSt2 | 6–7 | Motor temperature | s16, °C | 44 °C |
| DSt3 | 0–1 | Minutes until charged | u16 | 0 (185 on the charger) |
| DSt3 | 4–7 | Consumption, instant? | u32, scale not settled | 0 |
| DSt3 | 8–9 | Consumption, average? | u16, scale not settled | 1,940 |
| DSt3 | 10–11 | Consumption, lifetime? | u16, scale not settled | 231 |
Status: Measured. DSt2’s motor temperature equals MbbR’s in 519 of 558, and never more than 1 °C apart, pairing each reply with the MbbR read less than a second before it.
Ecus and Errs: firmware and faults
Two more reads are known but not yet in our captures. Ecus lists the control units: a u32 count, then one 30-byte record per unit. Errs lists the active fault codes: a u32 count, then one u16 per code, with zero codes as padding. Not every Gen2 bike may answer Errs.
| Bytes | Field |
|---|---|
| 0 | Unit index: 6 main board, 8 battery, 9 a second battery |
| 2–3 | Model year, u16 (the main board sends it × 10) |
| 4–15 | Hardware part number, ASCII |
| 16 | Hardware revision |
| 17–28 | Firmware part number, ASCII (empty = skip the record) |
| 29 | Firmware revision |
Status: Known, not yet captured.
A live session, drawn
Once connected, the session polls five reads in a fixed order — MbbR, PwPk, DSt1, DSt2, DSt3 — with one request in flight at a time. The median round trip is 85 ms, so each reading comes back about every 0.5 s. PrVr, Gbki and BtSt are asked only in the first second or so after connecting.
The second figure is 8 minutes of one ride, every MbbR and DSt2 reply decoded at the offsets above. Nothing in it is smoothed.
MbbR and DSt2 reply of one 8:13 session on the 2016 FXS, decoded at the offsets in the tables below and drawn as sent: the speed really is in whole mph on the wire, whatever the dash shows. Each dot is one reply; the polling ran in bursts, and the faint line only joins readings across the pauses between them. The range estimate drops 34.1 km while trip B advances 4.8 km — the dash’s own arithmetic, not ours.How a log download works
The same link carries the motorcycle’s log — the file a computer would otherwise read over the diagnostic port. On Gen2 it comes in 256-byte blocks, and the request has no block number: the bike keeps its own read pointer and serves the next block each time it is asked.
- 1Stop the live polling, so only download frames are in flight.
- 2Send CmCh with the read address set to 0. Its payload is 11 words:
04 00 00 00and then zeros. The bike echoes it. - 3Send MbbD (N = 0). The answer is a 4-byte flag and 256 bytes of the log.
- 4Flag
00 00 00 c0is a block of data. If its 256 bytes are identical to the block you just kept, it is a resend: drop it. Otherwise append it. - 5Repeat MbbD. When the bike pauses, wait for the answer. In the capture every one of the 1,048 requests got exactly one answer, with never more than one request in flight; the 17 slow ones came after about 11 s, and each was simply the next block.
- 6Flag
00 00 00 e0is the end of the file: stop there. Asking again only brings the end flag again; in the capture the read started over from the first block only after the client sent CmCh a second time. - 7Send CmCh again and resume polling. If the bike ever sends ReSd, repeat your last request.
MbbD answers and the end flag are all in the capture, and the request carries no block number — the bike keeps its own read pointer. In the capture the client simply waited out each stall, and the next answer was the next block. BmsD runs the same loop for the battery’s log. Gen3 (right) addresses every block by number; that procedure is known but not yet captured by us.| Value | |
|---|---|
| Answers received | 1,048 |
| Blocks kept | 1,024 × 256 B = 262,144 B |
| Duration | 334.3 s |
| Median gap between answers | 0.144 s |
| Stalls | 17, each 10.97–11.16 s |
| End flag | first at 308.2 s, 6 in a row |
Kept in arrival order, an exact resend dropped and stopped at the first end flag, the blocks make the whole log: the project’s parser reads it with no duplicated block and no step backwards in time.
What the capture did differently
In the recorded download the first MbbD went out a moment before the CmCh. Its block (the start of the log) came again right after the CmCh, and that is the one exact repeat the rule above drops. Polling also did not fully stop: one MbbR went out between the two.
The battery’s log: BmsD
BmsD runs the same loop for the battery management system: CmCh with read address 0, then BmsD repeated. On the 2016 FXS the battery kept answering past 1,024 blocks without ever sending the end flag (seen with the ZeroLogs app in October 2026), so a client needs a stop of its own. ZeroLogs stops at the main board’s block count, 1,024 on this bike; whether that is exactly the size of the battery’s log is not confirmed yet.
A bike with a second battery (a Power Tank) selects the second battery management system through CmCh before BmsD. That step is known but we have not tried it on a bike.
Gen3: numbered blocks
Gen3 addresses every block. LOG_INIT (type 24, empty body) answers with the number of files and, per file, its size in bytes (u32 LE) and its block count (u16 LE); from protocol major version 27 on, each file entry is also preceded by its unit id. LOG_BLOCK (type 25) asks for one block: a file number (u8) and a block number (u16 LE). The answer’s body is 128 bytes of raw flash. The main board’s file comes first, then the battery’s. LOG_DONE (type 26) ends the session; the bike answers LOG_DONE or ACK.
Ask for exactly the block count, 0 to count − 1. A request one past the end still returns 128 bytes, and they are not log data: in the Gen3 files we hold that carry such a block, it is almost always a blank page — the marker 00 F0 FF 00 and zeros. A full Gen3 board log is 131,200 bytes, 1,025 blocks of 128.
The Gen3 message types
Gen3 messages are numbered. The table lists the reads; the writes are in the next section. Types 16 to 21 carry two different names in the sources we have. The second set (dash gauges, scheduled charge, custom ride modes, ACK, timestamp, charge target) is used here: it agrees with ZeroBLE wherever ZeroBLE names the type (16, 18, 19, 20).
| Type | Name | What the reply carries | Status |
|---|---|---|---|
| 1 | Protocol version | major, minor, patch. Major ≥ 27 changes the LOG_INIT layout | ZeroBLE (open source) |
| 2 | Resend | the other side asks for the last message again | ZeroBLE (open source) |
| 3 | Status (bike info) | VIN, make, model, charger connected, charging, SoC, ready (armed), charge power (× 100 W), minutes to full. Also a passcode field: discard it at decode, never store or log it | ZeroBLE (open source) |
| 4 | Power pack | layout not published | Known, not yet captured |
| 5 | Battery status | layout not published | Known, not yet captured |
| 6 | Main board read | motor torque, rpm, motor and controller temperature, battery and motor current, speed | Known, not yet captured |
| 10 | Dash statistics | layout not published | Known, not yet captured |
| 11 | Control units | per unit: node id and type, firmware revision, active bank, firmware and hardware part numbers | Known, not yet captured |
| 12 | Error codes | a u32 count, then the active codes as u16 | Known, not yet captured |
| 19 | ACK | acknowledges a message | ZeroBLE (open source) |
| 22 | Live data (ride share) | odometer (× 100 m), energy used (kWh), consumption, speed (km/h × 100), lean angle (° × 10, left negative), power, torque, regen and brake regen, latitude and longitude (radians × 10⁸), SoC | ZeroBLE (open source) |
| 23 | Range estimation | range per route type (city, highway, mixed, freeway) and the model’s inputs: pack capacity new / mid-life / old, ride style, temperature, terrain | Known, not yet captured |
| 24–26 | Log download | LOG_INIT, LOG_BLOCK, LOG_DONE — see the download section | Known, not yet captured |
Ride-mode settings (types 13 and 18) are both read and written with the same type, so a reader that allows the type cannot tell a read from a change. ZeroLogs leaves them out.
Commands that change the motorcycle
The protocol also carries commands that change how the motorcycle behaves, set its clock or load firmware. They are listed so that a capture can be recognised — never with a payload. The ZeroLogs app sends none of them: its Bluetooth code accepts only the reads in the tables above and the single CmCh that sets the read address to 0, and refuses anything else before it reaches the radio.
| Generation | Message | What it changes |
|---|---|---|
| Gen2 | CmCh, drive parameters | the custom ride mode’s limits: top speed, torque, coast regen, brake regen |
| Gen2 | CmCh, test mode | a test mode; what it does is not documented |
| Gen2 | CmCh, unit index, boot bank, update status and image info | the firmware-update machinery |
| Gen2 | Upld, Hash | a firmware image and its hash |
| Gen3 | 13 / 18 ride modes (written back) | ride-mode settings |
| Gen3 | 16 dash gauges | what the dash displays |
| Gen3 | 17 scheduled charge, 21 charge target | when charging starts and where it stops |
| Gen3 | 20 timestamp | sets the motorcycle’s clock |
| Gen3 | 27 | charging or connectivity configuration (the sources disagree on its name) |
| Gen3 | 31 navigation | turn-by-turn directions on the dash |
| Gen3 | 32 option status, 33 option set | report and turn on the optional features (33 with a key). 32 only reports, but its type number sits with the feature writes, so ZeroLogs sends neither |
| Gen3 | 9 upload, 14 hash, 28 firmware init, 29 firmware install | firmware |
Do not experiment with writes on a motorcycle you ride
A wrong drive parameter or a half-written firmware image can leave a bike that does not behave as its rider expects, or does not start. Reading is harmless; a command you cannot name is not.
Capture your own session
Everything measured here started as an Android Bluetooth log, and the Gen3 rows will only become measured the same way. A capture holds the bike’s VIN, so it is sent to us privately, never published.
- 1Turn on Developer options: Settings → About phone → tap Build number seven times.
- 2In Developer options set "Enable Bluetooth HCI snoop log" to Enabled (not Filtered), then turn Bluetooth off and on.
- 3Connect to the motorcycle with any app that talks to it, and do one thing at a time. Write down what you did, in order.
- 4Developer options → Take bug report → Full report, and keep the .zip (the log is
FS/data/misc/bluetooth/logs/btsnoop_hci.loginside it). - 5Set the snoop log back to Disabled: it records every Bluetooth exchange while it is on.
- 6Upload the .zip or the .log on the Help decode page of your ZeroLogs dashboard.
Frequently asked questions
Do I need a password or PIN to read data from a Zero over Bluetooth?
No. The phone pairs with the motorcycle once in its Bluetooth settings; after that the link is encrypted, and the protocol itself has no login. The first frame after connecting is already a request for data.
Can an iPhone talk to a Gen2 Zero?
Not from a third-party app. Gen2 bikes use Bluetooth Classic serial (SPP), which iOS opens only for accessories in Apple’s MFi programme. Gen3 bikes use Bluetooth LE, which iOS apps can use.
Why does the bike send miles when its log is in kilometres?
That is how the protocol is laid out: MbbR’s speed is in whole mph and BtSt’s odometer in whole miles, while DSt1 and DSt2 give the trip meters and range in hundredths of a kilometre. On the FXS, BtSt’s 20,776 mi is the 33,436 km its own log recorded, and MbbR’s speed integrated as mph gives 4.71 km over a ride the trip meter puts at 4.80 km. We have only measured this one bike.
Why is a log download over Bluetooth so slow?
It moves 256 bytes per request, one request at a time, about 0.144 s each, and the bike pauses for about 11 s every so often (17 times in one download). A 256 KiB main-board log took 5.6 minutes on the FXS.
Can two phones or two apps connect at the same time?
On Gen3, every owner’s manual from 2020 to 2026 says only one phone can be paired at a time. On Gen2 we have not tested a second connection; close one app before opening another.
Does reading data over Bluetooth change anything on the bike?
The reads do not. The only command a download needs besides them sets the read pointer to the start of the log. The commands that do change the bike are listed on this page so they can be recognised and avoided.
How sure is the Gen3 part of this page?
Less than the Gen2 part. Gen2 is measured on our own captures. Gen3 comes from the open-source ZeroBLE library — its author’s ZeroNavBridge app, built on it, is reported working on the DSR/X, SR/F, SR/S and Gen3 S — and from what is known but not yet captured; each row says which. A Gen3 capture would settle it.
The Gen2 measurements come from 4 sessions recorded in August 2026 between an Android phone and a 2016 FXS (two rides, a log download and a short reconnect), read with this project’s own btsnoop tools: Bluetooth packets reassembled, every frame checked against its CRC, every reply decoded at the offsets on this page. Times are given from the start of each session; the VIN is redacted.
Sources:
This is an independent interoperability reference, not affiliated with or endorsed by Zero Motorcycles, Inc. Offsets were measured on one 2016 FXS and may differ on other firmware. Read freely; never send a command you cannot name, and never one from the "change the motorcycle" table to a bike you ride.