theo740
New here
Hi all,
I wanted to share a working method to read a Bosch Classic Line (Gen 1, ~2011–2015) PowerPack frame battery over CAN on the bench — no motor, no display, and without touching the crypto handshake. The battery happily broadcasts its status in plaintext; you just have to find the right bus settings and decode the format. I verified everything across 6 batteries plus a discharge run.
Hardware
- ESP32 (built-in TWAI/CAN) + SN65HVD230 transceiver, a
- The battery's own 5 V keeps its processor awake; I fed 5 V from the ESP32 into the enable pin.
Connector pinout (frame battery, 5 equal-size contacts in a row)
- Pin 1: CAN-L
- Pin 2: +36 V (B+)
- Pin 3: +5 V (enable — without this the BMS sleeps and sends nothing)
- Pin 4: GND (B−)
- Pin 5: CAN-H
You only need 3 wires for reading: CAN-L, CAN-H, GND — plus 5 V on pin 3 to wake it. (CAN is differential, so if you get nothing, just swap H/L — I had to.)
The key finding: the bus runs at 125 kbit/s — NOT the 500k you read about for the newer generations. At 500k I saw a completely dead bus; after fixing wiring an autobaud sniffer locked onto 125 kbit/s and clean frames appeared.
What the battery sends on its own (≈ every 250 ms, alte:
- 0x30 — status (decoded below)
- 0x31 — mostly zeros; byte 1 is a 0/1 flag/variant
Frame 0x30 decode (8 bytes):
┌──────┬────────────────────────────────────────────────────────────────┐
│ Byte │ Meaning │
├──────┼────────────────────────────────────────────────────────────────┤
│ 0 │ 00 (reserved) │
├──────┼────────────────────────────────────────────────────────────────┤
│ 1 │ 0xC8 — constant on all batteries (model/type ID) │
├──────┼────────────────────────────────────────────────────────────────┤
│ 2 │ Pack voltage in whole volts (BMS-internal, ~±1 ) |
├──────┼────────────────────────────────────────────────────────────────┤
│ 3 │ Temperature in °C │
├──────┼───────────────────────────────────────────────
│ 4 │ State of charge in % │
├──────┼────────────────────────────────────────────────────────────────┤
│ 5 │ Number of charge LED bars │
├──────┼────────────────────────────────────────────────────────────────┤
│ 6 │ 00 (reserved) │
├──────┼────────────────────────────────────────────────────────────────┤
│ 7 │ fixed per-battery value (serial/ID — NOT a checksum) │
└──────┴────────────────────────────────────────────────────────────────┘
Example (battery at 40.6 V, 5 bars, 26 °C):
00 C8 28 1A 5A 05 00 50
→ B2 0x28 = 40 V · B3 0x1A = 26 °C · B4 0x5A = 90 % · B5 = 5 bars
How each byte was verified
- Voltage (B2): measured terminal voltage on 6 packs (37.7→37, 38.4→38, 40.3→40 …). It's whole-volt resolution only.
- SoC (B4) and bars (B5): matched the displayed LED bar0 %).
- Temperature (B3): I predicted one pack was "the coolest" — its B3 came out lowest (26 vs 30–35). Three packs known to be at the same temperature clustered at 32–34.
- Byte 7: I ran an exhaustive CRC-8 / checksum brute-focharge test settled it: over 25 frames B2, B3, B4 and B5 all changed while B7 stayed constant. So it's not a checksum of the payload — it's a fixed per-unit identifier. (Different packs have different B7: 140/80/61/110/65/80.)
What does NOT work (so you don't waste time):
- The battery ignores active requests — RTR scan 0x00–0x3F: no reply; the known challenge IDs 0x72/0x73: no 0x80/0x81. Standalone it only
ever emits 0x30/0x31.
- Full telemetry (per-cell voltages, current) and the power-output enable are gated behind the crypto handshake, which is still unbroken publicly. But remember: that handshake is authentication, not encryption — the status data above is plaintext and freely readable.
So for diagnostics (reading voltage / SoC / temperature off any Classic pack) you don't need to crack anything. Next step on my side is to sniff a complete system (motor + display + battery) to see the full operating-mode traffic.
Happy to answer questions, and I'd love confirmation from anyone with a Classic pack — especially whether your byte 7 is also constant per pack and what value you see.
Cheers.
I wanted to share a working method to read a Bosch Classic Line (Gen 1, ~2011–2015) PowerPack frame battery over CAN on the bench — no motor, no display, and without touching the crypto handshake. The battery happily broadcasts its status in plaintext; you just have to find the right bus settings and decode the format. I verified everything across 6 batteries plus a discharge run.
Hardware
- ESP32 (built-in TWAI/CAN) + SN65HVD230 transceiver, a
- The battery's own 5 V keeps its processor awake; I fed 5 V from the ESP32 into the enable pin.
Connector pinout (frame battery, 5 equal-size contacts in a row)
- Pin 1: CAN-L
- Pin 2: +36 V (B+)
- Pin 3: +5 V (enable — without this the BMS sleeps and sends nothing)
- Pin 4: GND (B−)
- Pin 5: CAN-H
You only need 3 wires for reading: CAN-L, CAN-H, GND — plus 5 V on pin 3 to wake it. (CAN is differential, so if you get nothing, just swap H/L — I had to.)
The key finding: the bus runs at 125 kbit/s — NOT the 500k you read about for the newer generations. At 500k I saw a completely dead bus; after fixing wiring an autobaud sniffer locked onto 125 kbit/s and clean frames appeared.
What the battery sends on its own (≈ every 250 ms, alte:
- 0x30 — status (decoded below)
- 0x31 — mostly zeros; byte 1 is a 0/1 flag/variant
Frame 0x30 decode (8 bytes):
┌──────┬────────────────────────────────────────────────────────────────┐
│ Byte │ Meaning │
├──────┼────────────────────────────────────────────────────────────────┤
│ 0 │ 00 (reserved) │
├──────┼────────────────────────────────────────────────────────────────┤
│ 1 │ 0xC8 — constant on all batteries (model/type ID) │
├──────┼────────────────────────────────────────────────────────────────┤
│ 2 │ Pack voltage in whole volts (BMS-internal, ~±1 ) |
├──────┼────────────────────────────────────────────────────────────────┤
│ 3 │ Temperature in °C │
├──────┼───────────────────────────────────────────────
│ 4 │ State of charge in % │
├──────┼────────────────────────────────────────────────────────────────┤
│ 5 │ Number of charge LED bars │
├──────┼────────────────────────────────────────────────────────────────┤
│ 6 │ 00 (reserved) │
├──────┼────────────────────────────────────────────────────────────────┤
│ 7 │ fixed per-battery value (serial/ID — NOT a checksum) │
└──────┴────────────────────────────────────────────────────────────────┘
Example (battery at 40.6 V, 5 bars, 26 °C):
00 C8 28 1A 5A 05 00 50
→ B2 0x28 = 40 V · B3 0x1A = 26 °C · B4 0x5A = 90 % · B5 = 5 bars
How each byte was verified
- Voltage (B2): measured terminal voltage on 6 packs (37.7→37, 38.4→38, 40.3→40 …). It's whole-volt resolution only.
- SoC (B4) and bars (B5): matched the displayed LED bar0 %).
- Temperature (B3): I predicted one pack was "the coolest" — its B3 came out lowest (26 vs 30–35). Three packs known to be at the same temperature clustered at 32–34.
- Byte 7: I ran an exhaustive CRC-8 / checksum brute-focharge test settled it: over 25 frames B2, B3, B4 and B5 all changed while B7 stayed constant. So it's not a checksum of the payload — it's a fixed per-unit identifier. (Different packs have different B7: 140/80/61/110/65/80.)
What does NOT work (so you don't waste time):
- The battery ignores active requests — RTR scan 0x00–0x3F: no reply; the known challenge IDs 0x72/0x73: no 0x80/0x81. Standalone it only
ever emits 0x30/0x31.
- Full telemetry (per-cell voltages, current) and the power-output enable are gated behind the crypto handshake, which is still unbroken publicly. But remember: that handshake is authentication, not encryption — the status data above is plaintext and freely readable.
So for diagnostics (reading voltage / SoC / temperature off any Classic pack) you don't need to crack anything. Next step on my side is to sniff a complete system (motor + display + battery) to see the full operating-mode traffic.
Happy to answer questions, and I'd love confirmation from anyone with a Classic pack — especially whether your byte 7 is also constant per pack and what value you see.
Cheers.