What the bus was actually saying - LS-CAN catalog, take two

New findings and corrections to CAN map

What the bus was actually saying - LS-CAN catalog, take two

In the first decoding post I ended with a YAML file and many unknown IDs. Then the dashboard went into the car and started using that file for real. Wrong numbers on the screen are a fast way to learn which decodes were wrong.

So I went back to the car. Same Vectra C. Same method: listen only, then poke and watch - do one action, undo it, see which bytes change. This post covers three things:

  1. Fixes that mattered for the dashboard (clock, speed, wipers, brightness).
  2. A simple map of which modules sit on which CAN speed.
  3. A short list of IDs I still want to decode next.

Everything here comes from sniffing and poke-and-watch. A later post will talk about dealer tools. Not this one.

Files attached to this post:

  • LS_CAN_IDS.yaml - updated signal list (18 frames, 40 signals)
  • CAN_LS_20261014.LOG - about 37 seconds of listen-only capture, same CSV format as before

If you still have the June CAN_LS.LOG and the old YAML, keep them open. Comparing the two logs helps a lot.

Where we left off

In Making sense of the noise I built a catalog by poke-and-watch: press one stalk, watch which bytes flip, write it down. That gave RPM, speed, doors, blinkers, outdoor temperature, and many "I saw this ID, but I do not know it yet" entries.

The dashboard post then used those names in LVGL with can_ls_read_phys() - so the screen reads signals by name instead of raw hex.

Half the bus was still unknown. The 0x621-0x629 group especially. The screen ignores it. That part is still a mystery.

One rule changed: if I cannot take one line from a real log and get a value that matches the car, I do not give that signal a name. Guessing was blocking the IDs I can still learn.

Poke-and-watch still works - and still tricks you

Same method as last time:

  1. Start the sniffer and wait for idle traffic.
  2. Do one action (for example left blinker).
  3. Note which IDs and bytes change.
  4. Undo the action.
  5. Check which bytes go back.
  6. Write it down.

Without that, you only have noise.

Three mistakes I kept making:

  1. Two things change together. Open a door and the interior light also changes. Now you have two changing columns and one story.
  2. Forum maps are not your car. Notes from Astra / GMLAN forums are useful hints. On this Vectra some of those IDs never appear. Some "wrong" IDs do appear.
  3. A number is not always what it looks like. A byte that goes up every second is not always "seconds." A value near the speed field is not always speed. A counter near the odometer is not always trip distance.

So I made the YAML smaller on purpose. The unknown IDs got a clearer plan.

Same car, new quiet session

October evening. Engine idle. Same LS-CAN tap on the OBD port (pin 1, single-wire, 33.3 kbps). Listen only. Same line format as June:

timestamp_us,bus,direction,id,dlc,byte0,byte1,...

About 37 seconds, 1572 frames, 35 IDs. Thirty-four of those IDs were already in the June log. The car did not invent a new language - only newer values.

Things that should move over months did move:

Signal June log October log
Odometer 0x190 00 01 40 FE 80 → 327,674 km 00 01 4A 92 80 → 338,506 km
Outdoor temp 0x445 00 7B → 21.5 °C 00 67 → 11.5 °C
Blinkers 0x260 left / off / right same codes: 25, 00, 3A

The four decode steps from the first post still work for these. The problems were elsewhere.

Fixes that mattered

Clock 0x440 - hour is a shift, not minus 0x80

From the October log, early in the session (wall clock about 20:40):

792471,LS-CAN,RX,0x440,8,A0,A8,64,02,00,68,A0,1A

Old idea: hour ≈ byte0 - 0x80. That gives 0xA0 - 0x80 = 32. There is no 32 o'clock.

What matches the capture:

  • hour = byte0 >> 3 → 0xA0 >> 3 = 20
  • minutes = if byte1 >= 0x80 then byte1 - 0x80, else byte1 / 4 → 0xA8 - 0x80 = 40
  • seconds = byte2 / 4 → 0x64 / 4 = 25

20:40:25. The next 0x440 lines increase byte2 by four each second (64, 68, 6C, 70...). That matches 0.25 s packing. The old -0x80 hour idea is wrong.

Speed 0x108 - packed, not two full speed bytes

Idle line:

96528,LS-CAN,RX,0x108,8,11,0A,E4,00,00,00,EA,BB
  • Engine state 0x11 → running
  • RPM = 0x0AE4 big-endian × 0.25 → 697 rpm
  • Speed on this Vectra is (byte4 × 2) + (byte5 >> 7), not "byte4 and byte5 are both speed"

Here the speed bits are zero → 0 km/h. That is correct for a parked car. If you treat byte5 as a second speed byte, trip/status flags (often with 0x80) look like fake speed.

Wheel pulses are not the trip meter

Bytes 6-7 of 0x108 are a 16-bit counter. Factor: 1.5748 cm per tick. A few thousand raw counts are only tens of metres of wheel travel - not hundreds of kilometres of "trip."

Resetting the trip on the instrument cluster does not clear this counter. That is why the dashboard trip from the previous post adds speed/pulses in firmware. The bus does not send a clear "trip A" value on LS-CAN in an obvious place.

Wipers 0x175 - mode is one byte

Frame 0x175 comes from the CIM (column module): stalks, wash, wheel buttons, wiper speed. Older notes treated wiper mode as a messy 16-bit value. On this car the mode is simply byte 1:

byte1 Meaning
16 single wipe
160 intermittent
176 constant
208 constant fast

Byte 4 still has speed levels (51 / 102 / 153 / 204 for levels 1-4). Same idea as before - just cleaner.

While I used the stalk, short bits also appeared in byte2 (0x10 / 0x20 for left/right). Frame 0x260 keeps the steady blinker state. Two frames, one action.

Interior brightness - 4% minimum when lights are on

959970,LS-CAN,RX,0x235,2,00,47

The percent math from the previous post is still correct. What I learned better is how the car behaves.

  • Light switch off, door closed → backlight off.
  • Lights on → you can dim down, but not to zero. The bus stops around 0x47 ≈ 4%. The cluster stays a little lit.
  • To turn the backlight fully off, flip the light switch to off. The dimmer knob alone will not do it.

So in this log, with lights on and a dark cabin, 0x47 is the lowest dim level, not "off."

What I removed from the YAML

The June YAML grew to about 38 frames / 50+ signals, including many "sample only" and Astra guesses. The new LS_CAN_IDS.yaml has 18 frames / 40 signals.

Still named and used:

0x100 wake, 0x108 engine/rpm/speed, 0x130 injection counter, 0x145 coolant (+ winter bit), 0x155 A/C, 0x170 key, 0x175 CIM, 0x190 odometer, 0x230 doors, 0x235 interior light, 0x23A light switch, 0x260 blinkers, 0x350 exterior lights, 0x360 clutch, 0x370 handbrake, 0x415 passenger window, 0x440 clock, 0x445 outdoor temp.

Still on the bus in October, still unnamed (17 IDs) - next targets:

0x090, 0x115, 0x11A, 0x140, 0x220, 0x305, 0x315, 0x330, 0x340, 0x405, 0x420, 0x525, 0x621, 0x622, 0x625, 0x626, 0x629.

A few notes:

  • 0x500 "battery" appears in some community maps. It is missing from both June and October Vectra LS-CAN logs. Removed.
  • 0x145 - I dropped the cruise/phase guesses. Left: winter mode + coolant. Coolant is still raw - 40 °C.
  • 0x350 stayed. It does appear on this bus (exterior lights / ignition flags). Older notes saying "not on Vectra LS" were wrong for my car. For ignition on the dashboard I still prefer 0x108 / 0x170 if they disagree with one bit.
  • 0x405 / 0x420 are not the door-open icons. The dashboard already uses 0x230 for that - bits that stay set while a door is open. These higher IDs look more like short events from the door modules (for example lock button press). They stay in the log, but not in the named YAML, until I do a careful lock/unlock/window session - same style as blinkers.

A smaller YAML did not make the dashboard worse. It made the icons stop lying.

Which modules live on which CAN speed

As in the first CAN post, the Vectra C does not have one single CAN bus. It has three speeds. Different modules live on different buses.

Network Speed Wire type Typical modules
HS-CAN 500 kbps Dual wire Engine, gearbox, ABS, power steering, CIM diagnostics, headlamp leveling
MS-CAN ~95.6 kbps (older) / ~92.5 kbps (newer) Dual wire Radio (EHU), info display (DIS), phone module (UHP), DAB, climate panel (ECC)
LS-CAN (also called SW-CAN) 33.3 kbps Single wire (OBD pin 1) Body, doors, cluster, underhood/rear modules, airbag, parking aid, …

More detail for this Vectra C / Signum family:

HS-CAN - 500 kbps

Engine (Z16 / Z18 / Z19 / …), gearbox (AF / MTA / CVTronic), ABS/TC/ESP, EHPS (power steering), CIM (steering column - diagnostics on HS; stalk data also on LS), immobiliser, AHL/AFL (headlamp leveling).

MS-CAN - about 95.6 / 92.5 kbps

EHU (head unit), DIS (display on MS path), UHP (phone), DAB (~95.6). ECC climate around ~92.5 in my notes.

LS-CAN / SW-CAN - 33.3 kbps

BCM (body), UEC/REC (underhood / rear electrical centres), instrument cluster, DDM/PDM (driver / passenger door), SDM (airbag), PAS (parking aid), SLM (shift lever), TPMS, EHS (heater), SAC (climate on SW path), seat memory/heating, SRM (roof, if fitted), and DIS/TID/GID-CID entries that talk on SW-CAN.

The same box can appear on more than one network depending on year. DIS is a good example (MS vs "ON SWCAN"). My OBD socket still has no MS-CAN, so radio/climate wiring is a problem for later. This post and the dashboard only listen to LS-CAN on pin 1.

CAN network layout in my car

Which decoded LS frames belong where (best guess - who needs the data / who likely sends it):

Area IDs What they carry
Column (CIM) 0x175 Stalks, wash, wheel buttons, wiper mode/speed
Body / doors 0x230, 0x415 Door open, locks, window motion
Lights 0x23A, 0x260, 0x350, 0x235 Light switch, blinkers, exterior lights, cabin brightness
Cluster / time / climate 0x190, 0x440, 0x445, 0x155 Odometer, clock, outdoor temp, A/C bit
Powertrain copy on LS 0x108, 0x145, 0x130, 0x360, 0x370 RPM/speed/pulses, coolant, injection counter, clutch, handbrake
Key / wake 0x170, 0x100 Key position, bus wake

Two useful points for the dashboard:

  1. Most of what the screen needs is already on LS-CAN. That is why firmware only starts the single-wire bus. HS-CAN (500 kbps) is better for deep engine/ABS data. For doors, stalks, and a simple RPM/speed display, LS is enough.
  2. The radio listens to wheel buttons - it does not create them. CIM keeps sending 0x175 even if the head unit is unplugged. That matters if you replace the factory radio and keep the column.

One more idea that helps when you get stuck: asking a module with a diagnostic tool uses different IDs than idle broadcast. Knowing that the cluster answers request 0x255 does not tell you what 0x621 means when nobody asked. Sometimes the same real-world value appears in both places later (for example outdoor temp). Then you can compare. Until then: poke, sniff, match.

Soft guesses for unnamed IDs - next evening in the car, not YAML names yet:

ID Soft guess (not proven on my car) Next test
0x405 / 0x420 Door module button / lock events One lock, unlock, trunk, or window at a time
0x115 Brake or light switch Brake alone, then light switch alone
0x11A Gear position (Astra notes) Move gear lever; ignore Astra notes if DLC differs
0x305 Cluster buttons / lights (Astra notes) Cluster buttons / light switch
0x621-0x629 Fast mystery group Quiet idle, then one stalk / door / key change; watch which byte moves

Same YAML path in firmware

The dashboard still does not hard-code mystery math in UI widgets. Signals live in YAML. A small Python script builds can_ls_signals_gen.h. At runtime the code looks up names like "EngineRPMRaw" / "BlinkerModeCode" / "OutdoorTempRaw" and does extract → mask → scale (plus a few helpers for clock, packed speed, and brightness).

Same loop as in June. Only the YAML content got cleaner.

const can_ls_sig_t *sig = can_ls_sig_by_name(&CAN_LS_FRAMES[slot], "OutdoorTempRaw");
float c = can_ls_read_phys(cache[slot].data, cache[slot].dlc, sig);
/* 0x67 → 11.5 °C on the October log */

Try one line yourself

Open CAN_LS_20261014.LOG, find the ID in the new YAML, and use the four steps. Example - outdoor temp:

792471,LS-CAN,RX,0x445,2,00,67
  • extract data[1] = 0x67 = 103
  • mask full byte
  • scale 103 × 0.5 + (-40) = 11.5 °C

Or blinkers during the stalk test:

10693002,LS-CAN,RX,0x260,3,25,40,80   → left (37)
12332090,LS-CAN,RX,0x260,3,00,40,80   → off
13373257,LS-CAN,RX,0x260,3,3A,40,80   → right (58)

If an ID is missing from the YAML, that is on purpose. The bus is still talking. I just do not want to pretend I know what it says - yet.

What is next

This "take two" did not decode many new IDs. It stopped naming ones I cannot prove, fixed wrong values on the dashboard, and gave me a clearer list for the next evening in the car.

Next steps - still poke-and-watch:

  • One-action tests for gear, lock, fog, door buttons, brake - first targets: 0x115, 0x11A, 0x305, 0x405, 0x420.
  • Longer quiet idle, then one clear change, against the 0x621 group (622, 625, 626, 629). Fast traffic alone is not a decode. A byte that flips with a stalk is.
  • Optional: compare a real value (door open, outdoor temp, …) with a diagnostic tool on the matching module later. Idle sniffing alone will not be enough.
  • Hardware from the dashboard post is still true: make a proper PCB for the P4 stack, think before removing EHU/DIS, keep the instrument cluster until HS warning lights and the 0x621 group are less of a black hole.

Open the new YAML next to CAN_LS_20261014.LOG, decode a few lines by hand. If something does not match your car - good. That is how the June file got smaller, and how the next IDs will get names.