Skip to content
PNKP237 edited this page Apr 22, 2024 · 7 revisions

Programming for Opel displays

I would like to use this place to document how to send stuff to the displays in more depth, hopefully saving others from spending countless hours on end with a logic analyzer, watching whatever is happening on the bus - not much entertainment to be had there. While reverse engineering efforts by JJToB saved me a lot of time, there is still more to be said on the quirks of various Opel displays. I will repeat some stuff from his research for clarity.

Printing data

Conversion to UTF-16

Text written to the display should be encoded in UTF-16BE. Simple way to achieve that is to keep the upper surrogate byte as 0x0 and the lower surrogate can be whatever ASCII you desire. Proper conversion is not as simple, but you can check "TextHandler.ino" for a memory-safe implementation. Also see "Unrecognized chars" in the "Pitfalls to avoid" section.

Waiting for the display to acknowledge your transmission

After sending the first message (data byte 0 0x10) you need to wait for the display to acknowledge your transmission.

6C1 # 10 6F 40 00 6C 03 10 0F

This usually happens after few milliseconds in the form of a message with identifier 0x2C1:

2C1 # 30 00 00 00 00 00 00 00

Afterwards, data frames can be sent in rapid succession.

Text formatting

These displays have some basic ability to format how the text is displayed on screen. Formatting strings have to be included in each line preceding the text you're printing to the display and as such they count towards the total character count. Each of these "formatting commands" is prefixed with an escape char which is not visible in the browser (0x1B).

[fS_gm - 7 chars, results in regular size text, aligned to the left side of the screen

[fS_dm - 7 chars, results in reduced size text, aligned to the left side of the screen

[cm - 4 chars, results in centered text

Few more which I have not tested - I believe these only apply to GID and CID, possibly formatting in the "Audio" source selection menu:

[lm
[tr31m
[tl34m
[fS_bm

Sending an empty field or clearing a field

In order to clear a field on the screen you can either send a line containing nothing (with specified length of 0) or containing a whitespace, omiting the formatting data (then the length should be 1). Factory radios do it using the latter method. If you don't clear a text field it will clear itself after some time.

Single line displays (TID, BID, 1-line GID)

When sending text strings to a single-line display you can skip sending strings to IDs other than 10 (equivalent to the middle line of a 3-line displays like GID and CID). It is not necessary but it saves some processing time.

Preventing radios from updating the display

It is possible to abuse ISO 15765-2 flow control in order to have the radio stop its transmission before the whole message is sent, which will result in the display not updating with "Aux". Just transmit a message with identifier 0x2C1, with databyte 0 value of 0x32 - this tells the transmitting node to abort its transmission.

2C1 # 32 00 00 00 00 00 00 00

Note that factory radios don't like their messages blocked repeatedly and will call it quits at some point, resulting either in radio rebooting or abrupt shutdown of all devices connected on the bus.

Another approach is to abuse variable frame separation time, which is set by the node meant to receive the message, but if you're faster, you can basically tell the radio to wait up to 127 ms between each frame. The value is set by databyte 2.

2C1 # 30 00 7F 00 00 00 00 00

That buys you enough time to transmit any text to the screen before the radio even sends one of its consecutive frames, meaning its transmission is not recognized by the display as valid.

Pitfalls to avoid

Unrecognized chars

When sending any text to the display you have to account for the fact that in case the letter is not recognized it will not get printed to the display (sic) but the text coming after the unrecognized character will not be displayed as well, even if it is valid. If you were to send a sequence containing three strings meant for the album, title and artist fields on the main audio screen, such as this:

dolor sit amet
Lorem ∆ ipsum
consectetur adipiscing

∆ is an unrecognized char - here's what will show up on the screen:

dolor sit amet
Lorem 
consectetur adipiscing

As you can see, this does not apply to data after the offending string. Other strings are unaffected, even if they were transmitted after the one containing unrecognized chars. In my case I filter out entire ranges of unsupported chars as part of the conversion to UTF-16, but even that has some issues. Character support may vary between the same model of a display with different firmware versions.

Messages containing no unused data are ignored

Let's say that the last part of the message containing text meant to be displayed will contain only useful data:

6C1 # 22 61 00 73 00 68 00 69

In this case we have UTF-16 text (please note that the upper surrogate of "a" (00) was transmitted as part of the previous frame):

0x(00)61 0x0073 0x0068 0x0069

Even if the amount of data and character count have been specified properly, EVERYTHING we have just transmitted would be ignored. Why - no idea. I found that a good workaround for this problem is to add a space in the last text field you're writing to, so there is always some unused data in the last message. Example:

6C1 # 22 61 00 73 00 68 00 69
6C1 # 23 00 20 AA BB CC DD EE
	       ^------------^ unused data

Now that the last message has been padded with an additional whitespace character (0x0020), remember to reflect that change in char count that comes after specifying the field you're writing to (which is why I prepare the whole message before sending anything) and the total payload (first message, databytes 1 and 4). Whatever is in those 5 bytes at the end does not matter but they have to be transmitted. Simple way to check for this is to count how many bytes are to be transmitted (that is required anyway since the first message has to specify total payload in bytes) and check the remainder after dividing it by 7. If it's 0, then add a space at the end.

CAN arbitration and errors

When writing to displays, some collision is to be expected, due to the radio sending its text payload approximately every 5 seconds when printing "Aux" (and more often if you're interacting with the radio or there's some RDS data, of course). Good approach is to watch for the size of the total message payload (message id 0x6C1, databyte 1) and wait for the radio to finish its business before sending your data.


GM's diagnostic protocol (measurement blocks)

Diagnostic tools, such as Tech2 and OP-COM make use of GM's protocol to request data from various modules present in the vehicle. It is possible to request that data in several combinations - this is extremely useful in terms of this project, as ESP32 only has a single CAN peripheral. Medium-speed CAN is especially limited in terms of how much data is available to be sniffed passively.

Requesting measurement blocks from displays over MS-CAN

This allows to request data such as outside temperature, coolant temperature, ignition status, speed and few more. Tested with GID, although CID should work as well. Measurement blocks can be requested in one or more messages. Example request:

246 # 07 AA 03 01 0B 0E 11 5E
               ^------------^ Databytes 3 to 7 - measurement blocks, each databyte specifies a desired measurement block
         ^---^ Databytes 1,2 - command requesting measurement blocks (there's some function to the databyte 2 but I'm not sure)
      ^^ Databyte 0 - amount of bytes in message(s) 

The display will respond with identifier 0x546:

546 # 01 00 00 00 00 00 00 00
546 # 0B 00 00 00 00 71 FF 7F
546 # 0E 00 00 00 00 00 00 00
546 # 11 FF 00 00 00 00 00 00
546 # 5E A6 08 00 B4 D4 02 3B
      ^^

Notice that databyte 0 of each message specifies which measurement block the message is referencing. In my extremely limited testing I have found that block 0B contains data such as engine coolant temperature - databyte 5, the value in celsius is offset by -40.

It is possible to request less data - let's say we only want block 0B. In such case we fill the message with identical bytes specifing the measurement block, like so:

246 # 07 AA 03 0B 0B 0B 0B 0B

Requesting two measurement blocks - in this case we receive 0E and 0B:

246 # 07 AA 03 0E 0B 0B 0B 0B

Requesting more measurements

It is also possible to request more measurement blocks than 5 by encoding the request in multiple packets. Example:

246 # 10 08 AA 03 01 0B 0E 11

in this case you need to wait for a flow control message from the display before you continue transmitting:

646 # 30 00 00 00 00 00 00 00

Afterwards, continue sending messages specifying more measurement blocks:

246 # 21 5E 19 00 00 00 00 00

The display will now send all the measurement blocks:

546 # 01 00 00 00 00 00 00 00
546 # 0B 00 00 00 00 71 FF 7F
546 # 0E 00 00 00 00 00 00 00
546 # 11 FF 00 00 00 00 00 00
546 # 5E A6 08 00 B4 D4 02 3B
546 # 19 00 7F 00 00 FF 06 07

Repeated measurements

If you have already requested a set of measurement blocks at least once and received them, you can send a simple message as follows:

246 # 01 3E 00 00 00 00 00 00

The display will respond with all previously requested measurement blocks, without the need to specify them all over again.

Tested hardware

This "research" has been conducted using following display modules:

  • 2005 Astra H BID
  • 2006 Vectra C GID
  • 2008 Astra H CID
  • 2009 Astra H GID

Most of my testing is done on a '06 VC GID + CD30MP3 (Delphi-Grundig), as that is what I have on hand. Some additional sanity checks before major releases are done with BID+CD30MP3 and, of course, in my vehicle with GID+CD70Navi.

Clone this wiki locally