Skip to main content
zerologs
Back to Learn Hub
Log Format Comparison

Gen2 vs Gen3: Log Differences

A technical comparison of Zero Motorcycles log formats across generations, based on reverse-engineering and real-world analysis.

Last updated: July 30, 2026

TL;DR

"Gen2" and "Gen3" on this page name a log format, not a motorcycle. Zero's own product generations do not line up with them model-for-model, and a third numbering, the one our parser records, differs again; the section below separates all three.

Gen2-format logs are 262,144-byte files whose entries are periodic binary snapshots carrying every channel at once, which the parser renders as labelled text. Every file we hold from an S, SR, DS or DSR (2014-2023) is one, and so is every file from an FX (2014-2025), FXS (2016-2021) or FXE (2022-2025). Gen3-format logs are ring buffers of length-prefixed binary records (131,200 bytes on the MBB, 131,328 on the BMS) and come from the SR/F and SR/S (2020 onward) and the DSR/X (2023 onward). They differ most in what a decoder gets out of each. A Gen2 riding entry hands you volts, amps, RPM and an odometer on one line. A native Gen3 MBB record does carry pack voltage and battery current, at record[34:38] and record[38:42], read by our decoder since 2026-09-07, but they are empty in the hibernation and power-up states that make up three quarters of a real log. Road speed and the odometer are not in that common record: they sit in the tail of the rarer 0x52 and 0x53 records, and the odometer only on the newer of two firmware layouts. So a Gen3 upload still needs its BMS companion before most electrical charts fill in: the MBB values exist, but they are sparse.

File Format

The most visible difference is the file itself: size, naming convention, and internal structure.

Gen2 format (binary snapshots)

Full Support
Models: S, SR, DS, DSR (2014-2023), FX (2014-2025), FXS (2016-2021), FXE (2022-2025), XU, MX
Platforms: SDS and XMX: two product generations write this one log format
Controller: Sevcon Gen4
File size: 262,144 bytes (256 KB) on 318 of the 335 files here; the other 17 (three VINs) are exactly half that, 131,072 B
MBB filename: VIN_MBB_YYYY-MM-DD.bin
BMS filename: VIN_BMS0_YYYY-MM-DD.bin
Entry structure: Binary records: 0xB2, a length byte, a type byte and a 4-byte Unix timestamp, then packed fields that the parser renders as labelled text
Entries per file: 6,713 - 9,229 (eight MBB dumps, seven VINs)

Gen3 (FST / Cypher III)

Full Support
Models: SR/F and SR/S (2020 onward), DSR/X (2023 onward)
Not the FXE: it is routed to the Gen3 decoder but writes the Gen2-format file above (see the note below)
Controller: Cypher III (proprietary)
MBB size: 131,200 bytes
BMS size: 131,328 bytes
MBB filename: YYYYMMDD_HH.MM_VIN_6.bin or _MBB.bin
BMS filename: YYYYMMDD_HH.MM_VIN_10.bin or _BMS1.bin
Entry structure: Length-prefixed 0xB2 records: 75-byte telemetry (type 0x51; a 71-byte form on the two oldest dumps here, 2021 and 2023), 29-byte motor record (0x54: shaft torque, motor current, bus voltage), the rarer 0x52 / 0x53 records that add road speed and range, variable-length ASCII (0xFD)
Records per MBB file: 2,238 - 2,535, 19,089 pooled (length-chain framing, eight full-size MBB dumps)

Note: newer Gen2 firmware uses VIN_MbbD_date.bin naming (lowercase "bb" + "D"). The format is the same; only the filename convention changed.

Why the record counts on this page say "length-chain framing"

There is more than one way to count records in a Gen3 file, and they do not agree, so every population on this page names the one it used. Length-chain framing is the one our MBB decoder uses: at each position the byte after the 0xB2 delimiter gives the record's on-disk extent, and the next record starts exactly that many bytes later. Simply counting 0xB2 bytes, or splitting on every one of them, gives different totals. It is also unsafe, because 0xB2 is a legal data byte that the writer escapes as FE 4D (with FE 01 for a literal 0xFE). Frame by the declared length first, then unescape the framed record. In the eight full-size MBB dumps measured here, 468 of the 19,089 records carry an escape, 477 escape bytes in all. That is small, but they land inside timestamps and state tokens, so ignoring them corrupts records rather than losing them. The byte-level rules are on the format specification.

Three things are called a "generation" here, and they disagree

1 · Product generation: what Zero built, as its own owner manuals state it. The manuals put the FX and FXS at generation 1 (XMX · modular) for their whole run; the S, SR, DS and DSR at generation 2 (SDS · Z-Force, Cypher II) from 2014 to 2023 and at generation 3 (FST · Cypher III) from 2024; the SR/F and SR/S at generation 3 from 2020 and the DSR/X from 2023; the FXE at generation 2 (XMX · modular, Cypher II); and the 2026 LS1, XE and XB at generation 3 on the LS and X-line platforms. Our bike catalogue renders those sheets directly.

2 · Log-format generation: which binary layout the file uses. That is what the two cards above describe, and it is the sense this page uses everywhere unless it says otherwise. It cuts across the product generations: an FX (product gen 1) and a DSR (product gen 2) write the same Gen2-format file.

3 · Routing generation: the log_generation value our parser stores against your upload. It is a statement about which decoder ran, nothing more.

The FXE is where all three collide

The FXE is a Cypher II motorcycle (sense 1: generation 2), it is in the parser's GEN3_MODELS set so we store log_generation = 3 against it (sense 3), and its files are ordinary 262,144-byte MBB\0 / BMS\0 dumps that decode through the 538XX firmware path: a Gen2-lineage record format, not the native Gen3 one (sense 2). All three statements are true at once, and a sentence that picks one without saying which sense it means is wrong. No FXE file in this corpus is a native Gen3 ring buffer.

The mechanics: GEN3_MODELS is {SRF, SRS, SR/F, SR/S, DSRX, DSR/X, FXE} and the GEN2_ONLY_MODELS veto list is {S, SR, DS, DSR, FX, FXS, XU}. FXE appears in the first and not the second (backend/app/parser.py, lines 2443 and 2449). A hard veto fires first if the model is on the Gen2-only list or the tenth VIN character is a year code from A to J (2010-2018), and only then are the three positive signals consulted: model, Gen3 VIN pattern, Gen3-only text patterns with no Gen2 telemetry.

One consequence we have not been able to test. The veto list holds bare model names with no year term, and the manuals put the 2024-onward S, SR, DS and DSR on the FST / Cypher III platform. So a 2025 DSR would be vetoed onto the Gen2 path on its name alone, whatever its file contains. GEN3_MODELS also lists no LS1, XE or XB, so the 2026 light line has no route at all. Whether either is a real defect we cannot say: our 352-file corpus holds no 2024-or-later S, SR, DS or DSR and no light-line log, so there is nothing to measure. Until such a file exists it is an open risk, and the veto stays as it is: the comment above it in the parser records a regression it was written to prevent. If you have one, send it.

Data Logging Approach

Gen2: Periodic Snapshots

Gen2 logs a complete telemetry snapshot once a minute while riding, and periodic updates during charging and idle. Each entry contains all available data, decoded into readable key/value pairs. The snapshot record itself is binary, and the English you read is the parser’s table rather than words the motorcycle stored. Alongside those binary records the firmware also writes its own text, in 0xFD debug-string records: state changes, CAN errors, charger switching.

Measured on the published extracts: 56 of 169 Gen2 lines are text the bike itself wrote, all from those 0xFD records, against 43 of 293 on Gen3, where they come from the pack’s event records. The Gen3 main board writes text events too, but its decoder does not mark which records held one, so its lines are counted as our decode. Both generations mix binary records with firmware text.

// A real Gen2 MBB riding entry, as decoded (FXS 2016, MBBEntry)

MBB #929 15:22:15 Riding

pack_temp_high_celsius: 34, pack_temp_low_celsius: 32, state_of_charge_percent: 75, pack_voltage_volts: 108.28, motor_temp_celsius: 44, controller_temp_celsius: 32, motor_rpm: 1349, battery_current_amps: 4, motor_current_amps: 5, ambient_temp_celsius: 28, odometer_km: 30800, mods: 11, mods_raw: 3

MBB #930 15:23:15 Riding

pack_temp_high_celsius: 34, pack_temp_low_celsius: 32, state_of_charge_percent: 74, pack_voltage_volts: 106.924, motor_temp_celsius: 46, controller_temp_celsius: 32, motor_rpm: 1744, battery_current_amps: 23, motor_current_amps: 48, ambient_temp_celsius: 28, odometer_km: 30800, mods: 11, mods_raw: 3

// 21 lines in this window · timestamps real · every line is our decode of the bytes

Every field is available in every riding entry, and the 60-second cadence is exact: it is the dominant inter-entry gap in every Gen2 file measured here.

See this window line by line: the same extract with every field named, beside the battery lane that was writing at the same moment.

Gen3: Event-Based Ring Buffer

Gen3 uses a ring buffer that stores events as they occur. Telemetry is embedded in 75-byte binary records, and ASCII records log state changes, faults, and system messages. Entries are not chronological in the file, because the ring buffer wraps around.

// Real records, decoded field by field, from the moment a parked Gen3 goes to sleep (DSR/X 2023, Gen3MBBEntry)

MBB #248 08:24:18 State change

from: PWSU, to: HIB

MBB #249 08:24:18 MTC pending change by 3

from: 0, to: 0

MBB #250 08:24:18 Stopping 12v charge

DC-DC: 281776uV, Battery: 13153056uV, Combined: 12340647uV, Ambient: 33C

MBB #251 08:24:18 Unknown

// 26 lines in this window · timestamps real · mixed device text and our decode

See a parked Gen3 keep this up for an hour at a time, and the two clocks that make a Gen3 pair look as though the boards never write together.

The records mix binary and ASCII, must be sorted by timestamp after parsing, and give fewer entries per ride. The voltage and current words are real: they are populated on every RUN record and on no hibernation record, and our decoder has read them since 2026-09-07. Because they go to zero with the bus, volts and amps in practice still come from the BMS file. There is no km/h and no odometer in this common 75-byte record (both live only in the rarer 0x52 / 0x53 records); the third value above is the 12 V accessory rail, which stays alive while the pack is disconnected.

MBB Telemetry Comparison

What data is available in each generation's MBB (Main Bike Board) log. Gen3 offsets below are given relative to s, the position of the ASCII state string; on a type-0x51 telemetry record s = 42, and it is 42 on all 1,356 such records in the nine native Gen3 MBB files here (1,347 of them if you only count the eight state tokens the parser recognised before 2026-08, and all 1,356 with REV and PARK, which both decoders now read). Several Gen3 rows below were marked "present but unread" or "not recorded" until September 2026: the bytes were always there, and our decoder has read them since 2026-09-07. A report parsed before that date shows them only after a reparse.

Data FieldGen2Gen3
Pack VoltageEvery entry (Vpack, V)MBB [s-8] ([34:38], uint32 LE mV), decoded since 2026-09-07, only while the bus is live: non-zero on 144/144 RUN and 0/417 HIB records. The BMS reading (payload[38:41], uint24 LE mV, measured) is the one with continuous coverage
Battery CurrentEvery entry (BattAmps, A)MBB [s-4] ([38:42], int32 LE mA), decoded since 2026-09-07, populated with the voltage. The BMS reading (payload[20:24], int32 LE mA) is the one with continuous coverage
SOC (State of Charge)Every entry (PackSOC, %)Binary [s-9]: uint8 0-100% (dashboard value; BMS carries its own at payload[19])
Motor RPMEvery entry (MotRPM)[s-13] ([29:33], int32 LE): the controller's motor rpm, decoded since 2026-09-07, only while the bus is live
SpeedDerived from RPMRecorded, but only in the tail of the rarer 0x52 / 0x53 records (millimetres per hour); a 0x51 ends before it. Decoded since 2026-09-07
OdometerEvery entry (Odo, km)Only in the 0x52 / 0x53 tail, and only on the newer firmware layout (tenths of a km), decoded since 2026-09-07 and checked as a series per file. The older layout has none, and no 0x51 has one. [s+29] was published here as one, but it is the 12 V accessory rail (see the retraction below)
12 V accessory railNot recorded. None of the Gen2 binary record types the parser decodes has a 12 V field, and no 0xFD text record in the Gen2-format MBB files here prints the rail's voltage. Some firmware writes `Turning ON DCDC` and `Turning OFF DCDC` lines, with no voltagePost-state field 5 ([s+29], uint32 LE microvolts), decoded and emitted raw as aux_12v_uv. Alive even in hibernation, unlike everything else here
Motor TemperatureEvery entry (MotTemp, °C)post_1 ([s+13], uint32 LE °C), zero when the bike is parked. Until 2026-09-07 this word was labelled pack-high and stored as the battery temperature; a reparse corrects older reports
Controller TempEvery entry (CtrlTemp, °C)post_2 ([s+17], uint32 LE °C): the inverter, tracking the motor and zero when parked. Until 2026-09-07 it was labelled pack-low and stored as the motor temperature
Ambient TempEvery entry (AmbTemp, °C)post_0 ([s+9], uint32 LE): hundredths of a degree on most firmware, whole degrees on some; each file is internally consistent
Pack TemperatureEvery entry (hi/lo, °C)post_3 / post_4 ([s+21] / [s+25], uint32 LE °C): cell high and cell low, written in every state, charging included. Until 2026-09-07 these two words were labelled ambient and controller
TorqueNot recorded. While riding, the parser estimates it from battery power (pack voltage times battery current) times 9549 divided by motor RPM, so it includes the losses between pack and shaftThe 0x54 motor record, written beside every 0x51: shaft torque in micro-newton-metres, exactly 0.2 N·m per amp of motor current. Decoded since 2026-09-07; this page called it not recorded until then. On 538XX firmware it is derived (kW × 9549 / rpm)
Motor AmpsEvery entry (MotAmps)The 0x54 motor record, int32 LE whole amps, decoded since 2026-09-07, on firmware that writes 0x54 at all (one 2023 SR/F export here has none). The 538XX firmware variant carries it too, as int16 amps at record[26], right after the module bitmask
Power (kW)Derived (V × BattAmps)Derived from BMS voltage × BMS current; the MBB pair gives the same product, but only while the bus is live
Discharge LimitNot available0xFD ASCII records (watts, stored as kW)
Charge LimitNot available0xFD ASCII records (watts, stored as kW)
Controller ActiveNot availableRecord byte 17: 0x00=active (144/144 RUN records), 0x6A=standby (408/408 PWSU, 397/417 HIB), 0x60=transition. The decoder indexes it as binary_data[11] because its loader hands over the record from byte 6 onwards
Bike StateDerived from event typeASCII [s:s+9], null-padded. The parser knows ten tokens: RUN/STOP/HIB/STRT/PWSU/CHRG/WAIT/WAKE/REV/PARK. The last two were added in 2026-08; before that they were decoded as records with no state and their telemetry was dropped
Error CodesThe binary 0x2F record, shown as SEVCON CAN EMCY Frame, holds a 16-bit error code, the Sevcon controller's own 16-bit code and an error register. A few other binary records each stand for one fault, such as 0x31 MBB BMS Isolation Fault. Most warning and error lines in the MBB log are text the firmware stores in 0xFD records, such as `CAN1 ACK error. count=1`Fault pending/cleared ASCII records
Writes while switched offMain board writes nothing at all. The BMS wakes about once a day (86,401 s between blocks in the parked window measured here)Every hour, for ever. The main board declares 3,600 s of sleep and keeps to it to the second; with the 91 s it spends awake the true period is 3,691 s, so about 23 entries a day in a locked garage. This is what fills the ring buffer in roughly three days
Ride ModeNot recorded. No Gen2-format file in this corpus contains a ride-mode string of any spelling; `Mods` on a riding line is the battery-module presence bitmask, not a modeRecorded, but only as an ASCII event when it changes: `Changing riding mode to 3, SPORT`. Sparse: 8 events across 2 of the 9 native Gen3 MBB files here (tokens seen: 1 ECO, 3 SPORT, 4 ARM, 4 Full). No decoder extracts it, so the app's riding-mode field stays owner-declared

Retracted: [s+29] is not an odometer, and this page said it was

Post-state field 5 was published here as a lifetime odometer in metres, and its behaviour was explained away as jitter. It is not an odometer. Every one of the 1,121 complete 75-byte type-0x51 records in this corpus satisfies v = 5128k + 4864 (a 12-bit ADC with a 5.128 mV least-significant bit over about 21 V of full scale), and read as microvolts the field spans 0.143 V to 13.440 V, which is the band and the unit the firmware's own text events use for the accessory battery ("Stopping 12v charge. DC-DC 302288uV, Battery 13142800uV"). An exhaustive scan of every uint32 window in that record finds no monotone field anywhere except the timestamp itself. There is no odometer in a 0x51 record at all. That sentence used to end "in a native Gen3 MBB record", which was a claim about one record stated as a claim about the motorcycle. The real odometer is in the tail of the longer 0x52 / 0x53 records, on the newer firmware layout only, and has been decoded since 2026-09-07.

The decoder has been changed to match: it no longer writes odometer_km from this field, and emits it as aux_12v_uv instead. A native Gen3 MBB whose log holds no newer-layout 0x52 / 0x53 record therefore reports no odometer and no per-ride distance rather than a fabricated one. A real odometer does exist on Gen2 and on the 538XX firmware variant, at a different offset in a different record.

Also corrected: the MBB does carry pack voltage and current, which we said it did not

An earlier revision of this page said the three uint32 slots before the state string were mislabelled as voltage, current and speed, and concluded that a Gen3 MBB record contains no electrical data. The first half was right; the conclusion was too strong. Pack voltage is a uint32 little-endian in millivolts at record[34:38] and battery current a signed int32 in milliamps at record[38:42]. That is thirteen bytes further into the record than the slot that was mislabelled, so these are different bytes, not a second reading of the same ones.

The evidence is a population gate and a physical curve. Over the 1,121 complete 75-byte records, [34:38] is non-zero on 144 of 144 RUN records, 5 of 5 REV and 4 of 4 PARK, and on zero of 417 HIB, zero of 408 PWSU and zero of 51 WAIT. It is populated exactly when the high-voltage system is live, with no exceptions in either direction. The curve is stronger evidence. In a 2021 file (whose telemetry records use the older 71-byte form, so they are outside the 1,121 just counted), 32 consecutive CHRG records show the field climbing monotonically from 102.081 V to 114.075 V over 5.5 hours while the current word holds between −11.6 A and −15.3 A and SOC goes 41 to 100. On the last six records SOC is pinned at 100 while the voltage still rises, 111.756 V to 114.075 V. That is a constant-voltage absorption phase; no padding field and no SOC lookup table produces one. A second file, a different SR/F, repeats it over 15 records (106.605 V to 114.894 V, SOC 60 to 100). Integrating V·I over the SOC change gives about 13.6 kWh and 12.4 kWh of full-pack equivalent, against a pack that is 14.4 kWh maximum and 12.6 kWh nominal.

The scope caveat is severe: every dynamic datapoint above comes from a 538ZF-prefix VIN, the 2020-2023 SR/F platform. The one true PAG-WMI motorcycle here, a DSR/X, contributes 278 type-0x51 records with 18 of them populated, one RUN record and no CHRG records at all, so not a single conservation test can be run on that hardware. The alternative is not excluded: that the layout is shared but DSR/X-era firmware populates it only in states this corpus barely contains.

The Pearson correlation this page used to cite is retired. It argued for a different word, record[21:25], from r = 0.913 against BMS pack voltage, on one motorcycle and one file pair. Re-run on that same pair (129 RUN records matched to a BMS row within 60 seconds), seven uint32 windows in the record score |r| ≥ 0.90 against BMS pack voltage, and the winner is not record[21:25]: the word at offset 30 scores 0.925 against its 0.907, and that is four different bytes again. A correlation does not identify an offset, and treating it as though it does is exactly the mistake that put a 12 V rail on this page as an odometer.

What settles record[21:25] instead is arithmetic: it is a multiple of 100 mV on 281 of its 281 non-zero readings, where record[34:38] is on 2 of 302: the same quantity at a hundredth of the resolution. And it is zero on all 54 CHRG records here while record[34:38] is populated on 47 of them. Prefer record[34:38].

Since 2026-09-07 our decoder reads both, gated on a live bus, together with the controller's DC-bus pair, the motor rpm, and the road speed and odometer in the 0x52 / 0x53 tail. Because all of them go to zero with the bus, the BMS file remains the better electrical source. The full measurement is on the format specification.

Gen2 Advantage

Complete telemetry in every entry. SOC, motor RPM, motor amps and all temperatures are always available, from one file, and so is a real odometer. (Not the ride mode: no Gen2-format file in this corpus records one; see the field table above.) High data density: 6,713 to 9,229 entries in the eight MBB dumps measured for this page. The upstream zero-log-parser library decodes each binary record into typed fields.

Gen3 Advantage

Event-driven, so idle time costs few entries. Exact state transitions are recorded rather than inferred. The BMS side is higher-resolution than Gen2: millivolt cell minimum and maximum, an IR-compensated open-circuit voltage, milliamp current, and a measured pack voltage. Gen3-only: power limits in watts, cell OCV, the controller-active flag.

Record type bytes, the full offset map, the 538XX firmware variant and the calibration evidence behind each field live on the Gen3 log format specification.

State Machine & Session Detection

Gen2: Mode-Based

Every entry has a clear mode label. Session detection is straightforward:

RidingActive riding with full telemetry
ChargingCharger connected, current flowing
IdleKey on, not riding
ParkedKey off, system monitoring

Gen3: State Machine

Uses a state machine with transitions. Session detection requires pattern matching:

RUNMotor active, riding
CHRGCharger connected
STRTSystem starting up
PWSUPower-up self test
STOPKill switch / key off
HIBHibernation (deep sleep)
WAITWaiting for input
WAKEWaking from hibernation

These eight are the tokens the parser recognised before 2026-08. What the files contain is lopsided: of the 1,121 complete 75-byte telemetry records here, 417 are HIB and 408 PWSU, against 144 RUN, 51 WAIT, 44 WAKE, 41 STOP and 7 CHRG. STRT never appears on one. The other nine carry REV (five) or PARK (four), all on the one DSR/X in this corpus. Until 2026-08 the parser dropped their telemetry; it has read both tokens since then, and all nine carry a populated pack voltage (102.561 V to 102.630 V).

Gen3 Session Detection Signals

Since Gen3 doesn't have clear "Riding" labels, we reconstruct sessions from multiple signals:

RUN state in telemetry (Ride)
Key Sw: ON / Kill Sw: STOP (Ride boundary)
Kickstand Sw: UP/DOWN (Ride start/end)
Time gap > 2 hours (Session split)
CHARGER LSS events (Charge start)
Contactor close/open (Charge boundary)
Voltage increase pattern (Charge detection)
"Saving Stats, Hibernating" (Session end)

BMS Log Differences

Gen2 BMS

Binary records in the same format as the MBB's, which the parser renders with a label on each field:

// A real Gen2 BMS entry, as decoded

Discharge level 2017-06-14 09:06:22

SOC 92%, current 8A, amp_hours 2,

cell low 3.994V unloaded 4.013V high 4.0V,

balance 6mV, pack_temp 26C, bms_temp 35C,

pack_voltage 111.923V, mode Bike On

  • Cell voltages (high, low) in every entry
  • SOC (State of Charge) always present
  • Pack and board temperatures
  • Cell balance delta (mV)
  • Charging/Discharging/Balancing modes

Gen3 BMS decoded

Binary records delimited by 0xB2. The 52-byte type-0x4B record is the primary telemetry source for the whole bike:

Two sibling records carry the same fields at a shifted offset: the 70-byte 0x4C at +4 and the 78-byte 0x4D at +8. Reading them at the 0x4B offsets lands in a pad block and the cell-voltage sanity check drops the row, which is what used to happen: dispatching on the type byte instead took the decoder from 1,625 telemetry rows to 2,319 across these seven files, +42.7%.

0xB2 + 1B length + 1B type (0x4B) + 4B Unix timestamp (LE) + 1B + 44B payload

Payload[13-14]: CvLow uint16 LE (mV)

Payload[15-16]: Cell OCV low uint16 LE (mV)

Payload[17-18]: CvHigh uint16 LE (mV)

Payload[19]: SOC uint8 (0-100%)

Payload[20-21]: Current int16 LE (mA, neg=charge)

Payload[38-40]: Pack voltage uint24 LE (mV, measured)

Payload[43]: No temperature (read as one until 2026-07-31 and retracted)

  • Cell voltages min/max (mV), CC/CV profile visible
  • SOC 0-100% (0 = BMS sleeping, actual value when active)
  • Current in mA: charging ~-15A CC, taper in CV phase
  • Pack voltage is measured, not inferred from the cells
  • Charging session detection with power calculation
Pack voltage, corrected. payload[38:41] was long documented as an "internal counter (~43,000-49,000) that drops under load". It drops under load because it is the pack voltage: a uint24 LE millivolt reading. Across the seven Gen3 BMS files sampled here, 2,189 of the 2,319 telemetry rows the decoder emits (94.4%, per-file range 92.1-96.5%) satisfy 28 × CvMin ≤ value ≤ 28 × CvMax, which is the acceptance envelope the decoder uses. The cell-average estimate (CvMin + CvMax) / 2 × 28 is only the fallback for rows outside that envelope, typically sleep frames. (An earlier revision quoted 1,473 of 1,557 = 94.6%: the same test, but on the smaller population the decoder emitted before it learned the 0x4C and 0x4D shifts.)

The Ring Buffer Problem

Gen3 uses a ring buffer (circular buffer) for log storage, with these consequences for data analysis:

Data Retention: A Parked Bike Fills It in About 3 Days

A ring-buffer bike (SR/F, SR/S, DSR/X; not the FXE, which writes the Gen2 format) that sits parked overwrites its whole MBB ring buffer in roughly 3 days.

  • 1.The ring buffer holds 2,238-2,535 records in 131,200 bytes of flash (length-chain framing, eight full-size MBB dumps, 19,089 pooled)
  • 2.Even when parked, the bike wakes up every ~1 hour for a STRT→PWSU→HIB cycle: 444 of the 462 hibernation markers in those files declare a 3,600-second sleep
  • 3.Each wake-up cycle writes a median of 31 records (1,680 on-disk bytes, 3,691-second median interval), over the 416 inter-marker cycles here whose gap really is about an hour (a pair of consecutive hibernation markers 3,000 to 4,200 seconds apart). Counting only the markers that declare 3,600 seconds gives 411 cycles and the same three medians. (An earlier revision of this page said 426, which reproduces under neither rule.)
  • 4.That is ~744 records/day: 131,200 ÷ 1,680 = 78 hourly cycles, so the buffer wraps in ~3.25 days by byte count and 3.0-3.4 days by record count, overwriting all older data
  • 5.Do not assume the BMS file reaches back as far. Across the six same-bike, same-export MBB/BMS pairs here the BMS window runs anywhere from 0.19× to 20.9× the MBB window (median 1.02×), so which buffer runs out first is not fixed. Download both together. (There are seven same-day pairs in the corpus; the six that give these ratios are the ones downloaded in the same session, which is the comparison that means anything.)

To capture ride history, download your logs within 1-2 days of riding. If you wait longer, the ride data will likely be overwritten by hibernation cycles.

Three days is how fast the buffer turns over while that hourly cadence is running, not the span you will see in every file. The cadence does not run for ever: eighteen of those 462 markers declare a 31,536,000-second (one-year) sleep instead, and the eight MBB dumps measured here span anywhere from 3.1 to 336 days of wall-clock time. So a Gen3 log can reach back nearly a year and still contain almost nothing but wake-up cycles.

How it works

The log has a fixed capacity (2,238-2,535 records in the dumps here). When full, new records overwrite the oldest. This means the file contains records from different time periods, and they are not in chronological order.

// Ring buffer visualization (simplified)

File position: [oldest ... wrap point ... newest]

Timestamps: [Jan 15 ... Dec 3 ... Jan 14]

^ write pointer wraps here

Consequences

  • Entries must be sorted by timestamp before analysis
  • Old riding data gets overwritten by newer hibernation cycles
  • A "stored" bike that keeps waking hourly fills the buffer in about 3 days, losing all ride history
  • There is no odometer in the common 0x51 telemetry record to interpolate across the wrap point. A native Gen3 MBB carries one only in its rarer 0x52 / 0x53 records, and only on the newer firmware layout; otherwise the bike's real odometer reaches us from the summary block, from a Gen2 riding entry or from the 538XX firmware variant
  • Some timestamps are corrupt. The Gen3 framers reject any record whose uint32 at offset 3 falls outside 2019-01-01 (Unix 1546300800, the SR/F launch) to today + 365 days

MBB vs BMS Data Sources

The words at record[34:38] and record[38:42] hold pack voltage and battery current while the pack is live, and our decoder has read them since 2026-09-07, but they are zero in the states that fill most of a parked bike's buffer, and road speed and the odometer arrive only on the rarer 0x52 / 0x53 records. In practice the BMS (Battery Management System) log is the electrical source, and that is why the app asks for both files: upload only the MBB from a Gen3 bike and most electrical charts stay sparse or empty, however complete the ride list looks.

MBB provides:

  • State (RUN, STOP, HIB, CHRG...)
  • SOC% (dashboard value)
  • Temperatures: ambient, motor, inverter, cell high and low
  • 12 V accessory rail (microvolts)
  • Text events & fault codes
  • Pack V & current, bus pair, motor rpm (only while the bus is live)
  • Road speed, range, odometer (0x52 / 0x53 only)

BMS provides:

  • Cell voltages (min/max)
  • Battery current (mA)
  • Pack SOC%
  • Load state & report mode
  • No temperature (payload[43] retracted)

Real-World Example: a "stored" SR/S

One SR/S dump in this corpus, downloaded on 31 December 2025, frames 2,416 records whose timestamps reach back 336 days. It contains not one RUN record. Of its 154 complete telemetry records, 76 are HIB and 76 PWSU, with one WAIT and one STOP: nearly a year of file, all of it the bike waking up hourly to go back to sleep. Any ride taken in those 336 days was overwritten long before the log was pulled.

Retracted. This box used to say the same bike showed "9 different odometer readings, 12,635 – 13,153 km", offered as snapshots from different ring-buffer positions. Those were not kilometres. That file's field 5 takes 11 distinct values from 12.635 V to 13.168 V: the 12 V accessory rail, in microvolts, read as though it were metres. A parked bike's 12 V battery drifting by half a volt is exactly what you would expect to see; a parked bike's odometer moving 518 km is not, and that should have been the tell.

Charging Detection

Gen2

Simple and reliable:

  1. Entry has mode = "Charging"
  2. SOC increases over time
  3. Charge current visible in BMS
  4. Session ends when SOC stops increasing

Gen3

Requires multi-signal detection:

  1. State = CHRG (explicit)
  2. CHARGER LSS events (charger node registration)
  3. Contactor events (Precharging from module)
  4. Voltage increase during non-RUN periods (fallback)
Gotcha: "Ch limits" entries are BMS capability reports, not charging indicators. They appear regardless of charging state.

Signal 1 is rare in practice: only 7 of the 1,121 complete telemetry records here are in state CHRG. That is why the other three signals exist.

Decode status: the seven rows that matter

The Gen3 format is not officially documented, and nothing on this page comes from Zero Motorcycles. Seven entries carry most of the practical consequences. Counts below are measured over eight full-size native-Gen3 MBB dumps from five motorcycles (three SR/F, one SR/S, one DSR/X) plus a ninth truncated dump from a sixth, and seven Gen3 BMS files from those same five, all decoded through the shipping parser for this page.

AreaStatusNotes
MBB voltage / currentpartialDecoded since 2026-09-07, gated on a live bus; this row read "present in the record, read by nothing" until then. uint32 LE mV at record[34:38] and int32 LE mA at record[38:42], non-zero on 144 of 144 RUN records and 0 of 417 HIB, 0 of 408 PWSU, 0 of 51 WAIT. A charge series read from them shows a textbook CV absorption phase. Every dynamic sample comes from SR/F-platform bikes, so it is unconfirmed on DSR/X-era firmware. The BMS file has the continuous coverage
MBB speed / RPM / odometerpartialDecoded since 2026-09-07; this row read "missing" until then, on a measurement of the 0x51 record alone. Motor rpm is in every 0x51 at record[29:33], only while the bus is live. Road speed and the dash range are in the tail of the rarer 0x52 / 0x53 records, and so is the odometer, on the newer firmware layout only, checked as a series per file. No 0x51 carries an odometer, and the field this page published as one is the 12 V rail
MBB state machinedecoded9-byte null-padded ASCII at record[42:51]: RUN/STOP/HIB/STRT/PWSU/CHRG/WAIT/WAKE, plus REV and PARK, accepted since 2026-08. Anchors every other offset: byte 42 on all 1,356 type-0x51 records here
MBB temperaturespartialFive uint32 post-state words at [s+9]/[s+13]/[s+17]/[s+21]/[s+25]: ambient, motor, inverter, cell high, cell low. Until 2026-09-07 they were read one label out (as an internal metric, pack-high, pack-low, ambient and controller), and the motor reading was stored as the battery temperature; a reparse corrects older reports. RUN populates the four temperature words after ambient in 144 of 144 records, but "zero unless awake" is a tendency, not a rule: HIB fills the motor/inverter pair on 20 of 417 records (4.8%) and PWSU the cell pair on 34 of 408 (8.3%)
MBB 12 V raildecodedPost-state field 5 at [s+29], uint32 LE microvolts, emitted as aux_12v_uv. All 1,121 complete telemetry records land on a 5,128 uV lattice (a 12-bit ADC over ~21 V), spanning 0.143 V to 13.440 V. This is the field this page published as an odometer in metres
BMS pack voltagedecodedMeasured uint24 LE mV at payload[38:41]. 2,189 of the 2,319 emitted rows (94.4%, per file 92.1-96.5%) fall inside 28 x CvMin .. 28 x CvMax; the cell-average x28 estimate is only the fallback
BMS state codepartialpayload[24], and still opaque. 37 distinct values across those 2,319 rows; 0x00-0x23 covers 2,276 of them, plus 19 rows of 0xF0 and 24 of 0xFF, the flash sentinels. 0x0B skews unloaded (516 of 603 rows) and 0x0C loaded (256 of 566), but no enum survives testing: the four codes once published as idle/active/unlock/precharge cover only 58.6% of rows, and an unlabelled 0x10 (69 of 111 rows under load, 62.2%) ranks above 0x0C on the same load ordering

The remaining rows are on the Gen3 log format specification, which carries the calibration evidence for each one: every MBB and BMS record type byte, the pre- and post-state header layout, the 29-byte 0x54 motor record, the ASCII event taxonomy, the 78-byte 0x4D health record with its full-charge-capacity register, the 50-byte FST Mono variant and the 538XX firmware variant.

Database Storage & Parsing Pipeline

When a log file is uploaded, a background worker parses it and stores entries in PostgreSQL (with TimescaleDB for time-series optimisation). The storage strategy differs by generation. Detected sessions are written to two VIN-keyed tables, unified_ride_sessions and unified_charging_sessions, each linked to its source files through a many-to-many table rather than a foreign key, because one session can be reconstructed from several overlapping log files, which is exactly what a wrapping ring buffer produces.

Gen2 Parsing Flow

  1. Decode the binary records with the upstream zero-log-parser, which returns typed fields for each entry
  2. Map event type (Riding/Charging/etc.) to bike_state
  3. Read every metric from those typed fields
  4. Calculate speed from RPM, power from V×I
  5. Store with log_generation = 2
  6. Detect ride/charge sessions from event patterns

Dedicated columns:

pack_voltage_v, battery_amps, motor_rpm,

speed_kmh, odometer_km, motor_temp_c,

controller_temp_c, ambient_temp_c, soc,

power_kw, bike_state

Gen3 Parsing Flow

  1. Frame 0xB2 records and validate the timestamp at offset 3 before trusting the record
  2. Anchor MBB offsets on the ASCII state string; read BMS payloads at fixed offsets
  3. Extract the state machine value as bike_state
  4. Convert units (mV→V, mA→A, mm/h→km/h, tenths of a km→km, W→kW)
  5. Parse ASCII records for power limits, faults and state transitions
  6. Store with log_generation = 3

Gen3-exclusive columns:

bike_state, controller_active,

discharge_limit_kw, charge_limit_kw,

cell_voltage_ocv_min, is_under_load,

bms_state_code

Shared columns (same as Gen2):

pack_voltage_v, battery_amps, soc,

power_kw (from the BMS file)

motor_temp_c, motor_rpm, torque_nm

(from the MBB, while the bus is live)

speed_kmh, odometer_km (from the

MBB's 0x52 / 0x53 records only)

Gen3 Entry Types in Database

Gen3 MBB entries are heterogeneous: not every entry carries every field, and the MBB's electrical channels are populated only while the high-voltage bus is live. Charts that look sparse on a Gen3 report are usually showing this, not a parsing failure:

MBB 0x51 telemetry (75 B)

bike_state, soc, five temperatures, controller_active, aux_12v_uv in the raw-fields blob, and, while the bus is live, pack voltage and current, the controller's bus pair and motor rpm (decoded since 2026-09-07). No speed and no odometer: those are in the longer 0x52 / 0x53 records

MBB 0xFD ASCII (variable)

discharge_limit_kw, charge_limit_kw (BMS capability reports), state changes, faults, key/kill switch, kickstand, contactor events

BMS 0x4B telemetry (52 B)

pack_voltage_v, battery_amps, power_kw, soc, cell min/max/OCV, is_under_load, bms_state_code. No temperature: payload[43] was read as one until 2026-07-31 and is retracted

Where the byte-level detail went

This page used to end with three collapsed technical sections. They were removed rather than patched, because a second, contradictory byte map on the same domain is worse than none. Of the 22 rows in their MBB offset table, more than half named the wrong field, two pointed past the end of a 75-byte record, and one mistook a byte of the Unix timestamp (0x69, the high byte of every timestamp between late October 2025 and May 2026, which covers 14,207 of the 19,089 records framed here) for a record-type marker. The full record-by-record Gen3 specification now lives at /learn/gen3-log-format.

  • Gen3 MBB binary payload. Framing rules, the 0x51 / 0x54 / 0xFD record types, the pre- and post-state layout and what each slot holds: read the MBB record spec.
  • Gen3 BMS binary. The 52-byte 0x4B telemetry record, the 78-byte 0x4D health record and its full-charge-capacity register, the 70-byte 0x4C and the 50-byte FST Mono variant: read the BMS record spec.
  • Gen3 session detection. How rides and charges are reconstructed from state transitions, ASCII boundary events and time gaps once the records are sorted: read the session-detection section.

For how long a Gen3 ring buffer keeps your rides before overwriting them, see Why your Gen3 log shows no rides.

How much of this is known. Nothing here comes from Zero Motorcycles. The Gen3 format is reverse-engineered from production log files, and the summary is uneven. The BMS record is largely decoded: cell minimum and maximum, open-circuit voltage, SOC, current, measured pack voltage, load and report mode all read out and cross-check against each other (a temperature byte was read too, and retracted on 31 July 2026). The MBB record took longer. Its 75-byte 0x51 yields a state, an SOC byte, five temperatures, a 12 V rail and, only while the bus is live, the pack-voltage/current pair, the controller's bus pair and the motor rpm; its rarer 0x52 / 0x53 records add road speed and, on newer firmware, the odometer. Until 7 September 2026 this paragraph said the MBB had no odometer, no speed and no RPM, a claim about one record stated as a claim about the motorcycle. Between February and July 2026 four fields on this page were found to be mislabelled, two were fictional, and one, the "odometer", was both wrong and being written into the database; that claim has been retracted and the decoder changed. The figures that replaced them were measured on 1,121 MBB and 2,319 BMS telemetry records from five motorcycles (a sixth contributes a truncated dump), through the shipping parser rather than a re-implementation. If you can falsify any of it with your own logs, please say so: that is how every error above was found. Last verified: 30 July 2026 against backend/app/parser.py; the MBB channel rows were revised on 3 October 2026 to match the decoder corrections of 7 September 2026.