Gen2 vs Gen3: Log Differences
A technical comparison of Zero Motorcycles log formats across generations, based on reverse-engineering and real-world analysis.
TL;DR
Gen2 logs (S, SR, FX, DS — 2013-2021) use 256 KB files with periodic snapshots containing all telemetry in text format. Gen3 logs (SR/F, SR/S — 2019+) use 128 KB files with an event-based ring buffer containing binary telemetry payloads. Gen3 requires binary-level decoding and a completely different approach to session detection.
File Format
The most visible difference is the file itself: size, naming convention, and internal structure.
Gen2 (SDS Platform)
Full SupportGen3 (FST / Cypher III)
Full SupportNote: Gen2 newer firmware uses VIN_MbbD_date.bin naming (lowercase "bb" + "D"). The format remains the same, only the filename convention changed.
Data Logging Approach
Gen2: Periodic Snapshots
Gen2 logs a complete telemetry snapshot every 15-30 seconds during riding, and periodic updates during charging and idle. Each entry contains all available data in human-readable text format.
// Example Gen2 MBB entry
Riding
PackTemp: h 32C, l 28C, PackSOC: 87%,
Vpack:117.3V, MotAmps:23, BattAmps:18,
Mods: 1, MotTemp:45C, CtrlTemp:33C,
AmbTemp:22C, MotRPM:3200, Odo:15234km
Every field is available in every riding entry. Dense, complete data.
Gen3: Event-Based Ring Buffer
Gen3 uses a ring buffer that stores events as they occur. Telemetry is embedded in 75-byte binary payloads, and text events log state changes, faults, and system messages. Entries are not chronologicalin the file — the ring buffer wraps around.
// Example Gen3 MBB entries
75 bytes | state=RUN | SOC=95% V=112.7V I=37A 43km/h T=21C
text | State change: from: STOP, to: RUN
text | Disch limits curr 400 cap 400 pow 111
75 bytes | state=RUN | V=103.2V I=62A RPM=4500
Mixed binary + text. Must sort by timestamp after parsing. Fewer entries per ride.
MBB Telemetry Comparison
What data is available in each generation's MBB (Main Bike Board) log:
| Data Field | Gen2 | Gen3 |
|---|---|---|
| Pack Voltage | Every entry (Vpack, V) | Binary [s-21]: uint32 LE (mV) + secondary [s-8] |
| Battery Current | Every entry (BattAmps, A) | Binary [s-17]: int32 LE (mA, signed) + secondary [s-4] |
| SOC (State of Charge) | Every entry (PackSOC, %) | Binary [s-9]: uint8 (0-100%, dashboard value) |
| Motor RPM | Every entry (MotRPM) | Binary post-state field 3 (uint32 LE) |
| Speed | Derived from RPM | Binary [s-13]: uint32 LE (~cm/s, x0.036=km/h) |
| Odometer | Every entry (Odo, km) | Binary post-state field 5 (uint32 LE, meters) |
| Motor Temperature | Every entry (MotTemp, °C) | Binary [s+17]: uint32 LE (°C) |
| Controller Temp | Every entry (CtrlTemp, °C) | Binary [s+13]: uint32 LE (°C) |
| Ambient Temp | Every entry (AmbTemp, °C) | Binary [s+25]: uint32 LE (°C) |
| Pack Temperature | Every entry (hi/lo, °C) | Binary [s+21]: uint32 LE (°C) |
| Torque | Not available | Binary post-state field 4 (uint32 LE, Nm) |
| Motor Amps | Every entry (MotAmps) | Not available (battery current only) |
| Power (kW) | Derived (V × BattAmps) | Derived (V × I / 1000) |
| Discharge Limit | Not available | Text events (watts, stored as kW) |
| Charge Limit | Not available | Text events (watts, stored as kW) |
| Controller Active | Not available | Byte 11: 0x00=active, 0x6A=standby |
| Bike State | Derived from event type | ASCII [s:s+9]: RUN/STOP/HIB/CHRG/WAIT/PWSU |
| Error Codes | Text events | Fault pending/cleared text events |
| Ride Mode | Custom/Eco/Sport/etc. | Not in Gen3 logs |
Gen2 Advantage
Complete telemetry in every entry. SOC, mode, motor amps, and all temperatures always available. High data density: 6-8K entries per file. Simple text parsing.
Gen3 Advantage
Event-driven means no wasted entries during idle. Captures exact state transitions. Higher precision (millivolts, milliamps, meters). Exclusive: torque (Nm), power limits (kW), cell OCV, dual V/I sensor pairs, controller active flag.
State Machine & Session Detection
Gen2: Mode-Based
Every entry has a clear mode label. Session detection is straightforward:
Gen3: State Machine
Uses a state machine with transitions. Session detection requires pattern matching:
Gen3 Session Detection Signals
Since Gen3 doesn't have clear "Riding" labels, we reconstruct sessions from multiple signals:
BMS Log Differences
Gen2 BMS
Text-based entries with clear labels:
Discharging
PackTemp:h 31C l 27C PackSOC:89%
Vpack:117.2V Cell:H 4105 L 4092 mV
Delta:13mV, BrdTemp:28C
- 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 entries delimited by 0xB2, with 52-byte telemetry:
0xB2 + 2B header + 4B Unix timestamp (LE) + 1B + 44B payload
Payload[13-14]: CvLow 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[43]: Temperature uint8 (C)
- 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
- Temperature and pack voltage (derived from cells x28)
- Charging session detection with power calculation
The Ring Buffer Problem
Gen3 uses a ring buffer (circular buffer) for log storage. This has significant implications for data analysis:
Data Retention: Only 5-7 Days
Gen3 bikes (SR/F, SR/S, DSR/X) can only store approximately 5-7 days of data in their ring buffer. Here's why:
- 1.The ring buffer holds ~2,300 entries (~131 KB of flash memory)
- 2.Even when parked, the bike wakes up every ~1 hour for a STRT→PWSU→HIB cycle
- 3.Each wake-up generates ~15-20 log entries (state changes, BMS checks, stats save)
- 4.At ~350 entries/day, the buffer fills in ~6.5 days, overwriting all older data
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.
How it works
The log has a fixed capacity (~2,300 entries). When full, new entries overwrite the oldest. This means the file contains entries 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 fills the buffer in 5-7 days, losing all ride history
- Odometer values from different ring buffer positions may differ significantly
- Some timestamps may be corrupted (1970 or 2047) — RTC clock issues
MBB vs BMS Data Sources
Gen3 MBB binary telemetry does not contain voltage, current, or speed data. These fields are stored only in the BMS (Battery Management System) log. The MBB records state snapshots (RUN/STOP/HIB), SOC%, temperatures, and an approximate odometer.
MBB provides:
- State (RUN, STOP, HIB, CHRG...)
- SOC% (dashboard value)
- Temperatures (active states only)
- Odometer (approximate)
- Text events & fault codes
BMS provides:
- Cell voltages (min/max)
- Battery current (mA)
- Pack SOC%
- Load state & report mode
- Temperature
Real-World Example: "Stored" SR/S
An SR/S stored for winter (Oct 2025 – May 2026) showed 0 rides despite having 13,153 km on the odometer. The ring buffer was entirely filled with periodic wake-up cycles (STRT→PWSU→HIB every ~1 hour), which had overwritten all previous ride data. The 9 different odometer readings (12,635 – 13,153 km) are historical snapshots from different ring buffer positions, not actual distance traveled during the logging period.
Charging Detection
Gen2
Simple and reliable:
- Entry has mode = "Charging"
- SOC increases over time
- Charge current visible in BMS
- Session ends when SOC stops increasing
Gen3
Requires multi-signal detection:
- State = CHRG (explicit)
- CHARGER LSS events (charger node registration)
- Contactor events (Precharging from module)
- Voltage increase during non-RUN periods (fallback)
Known Unknowns
Gen3 log format is not officially documented. Here's what we've decoded so far and what remains unknown:
| Area | Status | Notes |
|---|---|---|
| MBB Voltage | decoded | [s-21]: uint32 LE millivolts. Secondary at [s-8]. Dual sensor pairs confirmed |
| MBB Current | decoded | [s-17]: signed int32 LE milliamps (+discharge, -regen). Secondary at [s-4] |
| MBB Speed | decoded | [s-13]: uint32 LE (~cm/s, x0.036=km/h). Validated 2-113 km/h range |
| MBB SOC | decoded | [s-9]: uint8 0-100% (dashboard SOC, ~8-9% higher than BMS true SOC) |
| MBB Motor RPM | decoded | Post-state field 3: uint32 LE. Correlated with speed field |
| MBB Odometer | decoded | Post-state field 5: uint32 LE, value in meters. Converted to km in DB |
| MBB Torque | decoded | Post-state field 4: uint32 LE, Nm. Gen3-exclusive metric |
| MBB State machine | decoded | 4-char ASCII: RUN/STOP/HIB/WAIT/PWSU/WAKE/CHRG/STRT (9-byte null-padded field) |
| MBB Temperatures | decoded | 4 sensors: [s+13]=controller, [s+17]=motor, [s+21]=battery, [s+25]=ambient (uint32 LE, °C) |
| MBB Controller flag | decoded | Byte 11: 0x00=active/running, 0x6A=standby, 0x60=transitional |
| MBB Power limits | decoded | Text events: "Disch limits pow X" / "Ch limits pow X" in watts, stored as kW |
| MBB Pre-state header | decoded | Byte 0: type (0x69). Bytes 1-4: counter. Bytes 5-6: entry num. Bytes 7-14: padding |
| MBB Post-field 0 | partial | Post-state field 0: internal metric (~400-2000), loosely thermal-correlated |
| MBB Entry variants | decoded | 75B (standard s=36), 76B (+1 null s=37, absolute offsets), 92B (+13 extra s=40) |
| MBB Ride sessions | working | RUN state + time-gap splitting + event boundaries. Stored as UnifiedSession |
| MBB Charge sessions | working | CHRG + CHARGER LSS + contactor + voltage pattern. Stored as UnifiedSession |
| MBB Text events | decoded | Kill Sw, Key Sw, Kickstand, Immobilizer, State change, Faults, Flags, 12V charge, Limits |
| MBB Status entries | partial | 29-byte periodic power/current snapshots (correlated fields, factor 200000) |
| MBB Ride mode | missing | Eco/Sport/etc. not found in Gen3 logs — possibly stored in separate dealer-only logs |
| BMS Cell voltages | decoded | uint16 LE at payload[13] (min) and payload[17] (max), millivolts |
| BMS Cell OCV | decoded | uint16 LE payload[15]: IR-compensated Open Circuit Voltage, used for SOH estimation |
| BMS SOC | decoded | uint8 payload[19], 0-100%. SOC=0 means sleeping/not computed |
| BMS Current | decoded | int16 LE payload[20], milliamps. Negative = charging. CC/CV taper confirmed |
| BMS Charging flag | decoded | Payload[22-23]: 0xFF,0xFF when charging, 0x00,0x00 otherwise |
| BMS Load state | decoded | Payload[27]: 0x78=idle, 0xFF=active. Payload[28]: bus engaged (0x01=load) |
| BMS Report mode | decoded | Payload[37]: 1=standard, 3=minimal/sleep, 7=active high-rate |
| BMS State code | decoded | Payload[24]: progressive state machine (0x04-0x12), cycles idle, increments active |
| BMS Temperature | decoded | uint8 at payload[43], degrees Celsius |
| BMS Pack voltage | decoded | Derived: (CvMin + CvMax) / 2 × 28 cells. Also available from MBB binary |
| BMS Charge sessions | working | Detected from sustained negative current + SOC increase + charging flag |
| BMS Timestamp | decoded | Unix uint32 LE at entry offset 3 (same epoch as MBB) |
| BMS Text events | decoded | State transitions, hibernate, faults, cell type, cellbox comms, 12V latch |
| BMS Remaining | partial | ~16 bytes padding/reserved + counters at 0-1, 38-39, sub-state at 42 |
| Gen3 Error codes | partial | Fault pending/cleared events parsed, meanings differ from Gen2 codes |
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 optimization). The storage strategy differs by generation:
Gen2 Parsing Flow
- Parse text-based entries (structured key:value pairs)
- Map event type (Riding/Charging/etc.) to bike_state
- Extract all metrics directly from text fields
- Calculate speed from RPM, power from V×I
- Store with log_generation = 2
- 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
- Decode binary payloads (state-anchored offsets for MBB, 0xB2-delimited for BMS)
- Extract state machine value as bike_state
- Convert units (mV→V, mA→A, meters→km, W→kW)
- Parse text events for power limits and state transitions
- Store with log_generation = 3
- Gen3-only fields promoted to dedicated columns
Gen3-exclusive columns:
bike_state, controller_active,
discharge_limit_kw, charge_limit_kw,
cell_voltage_ocv_min, is_under_load,
bms_state_code, torque_nm
Shared columns (same as Gen2):
pack_voltage_v, battery_amps, speed_kmh,
odometer_km, motor_temp_c, soc, power_kw
Gen3 Entry Types in Database
Gen3 MBB entries are heterogeneous: not every entry has every field. Binary telemetry entries have voltage, current, speed, temps — but NOT power limits. Text entries have power limits — but NOT telemetry. This is by design:
Binary Telemetry (75-92B)
V, I, SOC, speed, RPM, torque, temps, odometer, bike_state, controller_active
Text Events (Power Limits)
discharge_limit_kw, charge_limit_kw — BMS capability reports
Text Events (State/System)
State changes, faults, key/kill switch, kickstand, contactor events
Gen3 log format details are based on community reverse-engineering of production log files from SR/F, SR/S, and DSR/X motorcycles. All major telemetry fields have been decoded. A few secondary bytes (~16 in BMS, status entries in MBB) remain as padding/reserved. Last verified: February 2026. Contributions welcome!