I was very excited to get our new heating system which uses the OpenTherm protocol to communicate between the boiler and the thermostat. I found the electronic schematics, including PCB design and PIC firmware to make an OpenTherm gateway. This would allow me to plot the different temperatures and modes and learn from the statistics to fine-tune the heating! Only to find out that my, newer, version of the boiler doesn’t use OpenTherm anymore, but uses another type of bus… Back to square one. Here is my journey to reverse-engineer the protocol.
I have an Elco Thision S Compact 25 M75H. The room sensor is a QAA75. The boiler has a built-in display which looks very similar to the QAA75’s screen, with the same button-labels (but different physical layout). Boiler and sensor are connected by 2 wires, with polarity-indications.
First step is always to look around on the internet. After some searching, I found out that the QAA75-sensor is actually made by Siemens. This broadened my search a bit. Turns out that Siemens refers to the communication as a “Boiler System Bus (BSB)”.
This is the first interesting piece: a bus. In contrast with the OpenTherm protocol, which is point-to-point, this name seems to indicate it’s a bus (which is point-to-multipoint). There are a lot of implications:
- data is most probably transmitted in the voltage, not in the current, and certainly not both
- it should be possible to passively listen in on the conversations
- it should be possible to act as an additional device on the bus and transmit messages, without changing the bus wiring
Unfortunately, that was all the info I could get from Google. Siemens also seems to offer a product called, which claims to translate the bus into a web-interface. But it’s priced a bit above my budget (and I prefer the fun of reverse engineering).
Layer 1 – Physical
I’m assuming the data is in the voltage, so I attached an oscilloscope to the two wires connecting boiler and room sensor. What I saw was not a big surprise: mostly 12Vdc to power the room sensor with approximately every 10 seconds a short burst of 0Vdc signals. I needed to capture these bursts in order to investigate them in detail, so I wired up my poor-mans logic-analyzer.
The result is a “sound”-file that can be viewed in e.g. Audacity:
The above screenshots show the burst every 10 seconds, at 3 zoom levels. You can see that these are actually two bursts, what seems to be a question, followed by an answer. You can also see the DC-decoupling getting slightly charged at the end of each burst and slowly discharging. The idle period is actually 12Vdc, filtered out by the DC-decoupling; the pulses go down to 0Vdc (shown as approximately negative full-scale). The time dimension has varying lengths of pulses; the shortest ones are ~210µs in length (~4800 bps).
So we have the amplitude conquered, next up in the time dimension: what form of line code is used? Figuring this out just boils down to trying to apply the different schemes and seeing if they add up. This particular case looks like very regular unipolar non-return-to-zero. At this point, it’s not clear whether the 12V corresponds to a binary 1 or 0.
One can also see (after staring at the bit sequence for long enough) that bit 0 mod 11 is low; and bit 10 mod 11 is high. This looks very familiar to the bitstream produced by UARTs: a start bit, 8 data bits, a parity bit (
odd even in this case) and a stop bit.
- bit order: in what order are the bits of each byte transmitted? Least significant bit first (like UARTs do)? Or Most significant bit first? I’m currently assuming LSb first.
- bit polarity: Does 12V correspond to a logical 1? Or does the physical low corresponds to a 1?
I’m currently assuming 12V=1, 0V=0. 12V=0, 0V=1
Converting to RS232
Since the protocol resembles RS-232 a bit, I figured it should be easy to convert from/to RS-232 and do the rest of the processing in software on bits instead of on voltages.
- The bus uses +12V idle; RS-232 expects -3~15V idle
- The bus starts with a startbit of 0V; RS-232 expects a start-bit of +3~15V (logical 0)
- The bus sends data
12V=1, 0V=012V=0, 0V=1; RS-232 expects data -3~15V=1, +3~15V=0
- The bus sends even parity; RS-232 can be configured for even, odd or no parity
- The bus sends a stopbit of 12V; RS-232 expects a stop-bit of -3~15V (logical 1)
So in order to have the bytes correctly recognized, we need to map +12V -> -3~15V and 0V -> +3~15V. This causes the idle, start,
data and stop-bits to match, but inverts the data-bits and hence the parity.
Putting all these components together on a self-made print looks like this:
Please note that this circuit converts a half-duplex bus into a full-duplex RS-232 port. The RS-232 device should be aware that the bus is half-duplex, and should try to avoid collisions! This is particularly hard with a general purpose computer with a general purpose operating system. My Atom-based motherboard with a linux 3.2.0 kernel only notifies me after receiving 8 bytes, which makes it almost impossible to avoid collisions. For now, I’m just ignoring this issue…
Layer 2 – Message format
With the above information, here is a sample of what I saw on the line:
23 75 ff f4 f9 c2 f2 fa e6 b0 73dc 8a 00 0b 06 3d 0d 05 19 4f 8c 4.526: 23 7f f5 f1 f8 f2 c2 fa e6 ff f1 1f 9e 55dc 80 0a 0e 07 0d 3d 05 19 00 0e e0 61 aa 6.638: 23 79 ff f4 f9 c2 f2 fa e6 a1 c4dc 86 00 0b 06 3d 0d 05 19 5e 3b 6.727: 23 7f f9 f1 f8 f2 c2 fa e6 ff f1 1f 79 2bdc 80 06 0e 07 0d 3d 05 19 00 0e e0 86 d4 Every almost every message seems to start with 0x23 0xdc. Note line 4.437 and 6.638: they only differ in the second byte, and the last two bytes. Which pops up the idea of a CRC-16 of some sort.
Hoping that Elco didn’t reinvent the wheel, I’ve tried some of the standard 16-bit CRCs. When applying the CCITT-one (poly 0x1021, init 0xffff, final xor 0x0000, no reflections) I saw something remarkable:
23 75 ff f4 f9 c2 f2 fa e6 b0 732adc 4.526: 23 7f f5 f1 f8 f2 c2 fa e6 ff f1 1f 9e 55c321 6.638: 23 79 ff f4 f9 c2 f2 fa e6 a1 c42adc 6.727: 23 7f f9 f1 f8 f2 c2 fa e6 ff f1 1f 79 2bc321 8.016: 23 79 ff f4 f9 c2 d2 fa 6c 17 c02adc 8.105: 23 7f f9 f1 f8 d2 c2 fa 6c ff fa 9f ef 3cc321 8.209: 23 79 ff f4 f9 c2 d2 fa e1 57 e52adc 8.297: 23 7f f9 f1 f8 d2 c2 fa e1 ff fa 82 c8 1ec321 9.017: 23 79 ff f4 f9 c2 f6 cf cb 72 ab2adc 9.103: 23 7f f9 f2 f8 f6 c2 cf cb ff f5 94 58fed3 9.936: 23 79 ff f4 f9 c2 fa fa de bf 3e2adc
Although the CRCs doesn’t match (they should return all 0), they do return the same value for messages of the same length! One possibility of this happening is that the initial register value is different from 0xffff. This change will ripple through and result in a length-dependent value.
So I have had the computer run through all possible init-values, looking for the value that results in a matching CRC. Unfortunately, the init-value was length-dependent as well. So we’re not on the right track.
Second try: maybe I had one of my previous assumptions wrong: what if I invert all bits: 12V becomes 0, 0V becomes 1. Flipping the bits resulted in a (different) length-dependend CRC. But re-running the init-value search turned up much more promising: initializing the register to 0x0000 resulted in a CRC match for all messages that I captured so far! This confirms that the bit-polarity is reverse of what I first assumed.
The message format
I try to interpret the meaning of each byte (or bit), and replace the hex-dump by the meaning. The following log-dumps will have more and more hex replaced by the meaning.
After some more staring at these hex-dumps, I started looking for a length-field. Byte 4 seems to match that description, indicating the total length of the message (including CRC).
The first byte seems to be 0xdc in almost all messages, although there are some 0xde’s.
The lower 7 bits of bytes 2 and 3 look like addresses. A pair of messages has these two fields reversed, adding to the belief that it’s indeed a question followed by an answer. This assumption is further confirmed by some messages that look like broadcasts, with multiple responses:
6.440: dc [1 src=0x06] [0 dst=0x7f] [len] 01 05 05 00 64 CRC OK 6.545: dc [1 src=0x00] [0 dst=0x06] [len] 02 05 05 00 64 00 61 00 88 00 04 48 05 0b 37 CRC OK 6.712: dc [1 src=0x0a] [0 dst=0x06] [len] 02 05 05 00 64 00 76 00 88 03 fc 00 09 de d7 CRC OK
Since address 0x06 is only visible if the room sensor is connected, it’s easy to derive its address; address 0x00 seem to be the boiler, since both 0x0a and 0x06 are talking to it; address 0x0a is probably the boiler-display.
From this point on, things get complicated. So I decide to only figure out the messages that I actually need, and simply ignore the rest. I’d like to capture:
- the current boiler temperature
- the desired boiler temperature
- the return temperature
- the outside temperature
- the temperature of the hot tap water
- the status of the system (whether it’s heating the tap water, or the radiators, or just “off”), preferably with indication of modulation percentage
Finding and decoding these messages wasn’t very difficult. I just requested that data on the room sensor’s screen, and noted what messages were exchanged to get that info. It doesn’t take too long until you figure out what message contains what data. Watching the hexdumps for a little longer also give an indication which bytes change when the value changes. Here’s what I found out so far:
- Bytes 8-9 0x05 0x19 indicate current boiler temperature, bytes 11-12 contain the temperature in 1/64th ºC
- 0x05 0x23 indicate the desired boiler temperature, but is otherwise identical to the current boiler temperature
- 0x05 0x1a: return temperature
- 0x05 0x21: outside temperature
- 0x05 0x2f: hot tap water temperature
- 0x30 0x34: status in byte 11
1.381: dc [1 src=0x06] [0 dst=0x00] [len] 06 3d 11 05 1a CRC OK 1.523: dc [1 src=0x00] [0 dst=0x06] [len] 07 11 3d 05 1a 00 08 a1 CRC OK Return=34.515625 2.317: dc [1 src=0x06] [0 dst=0x00] [len] 06 3d 0d 05 19 CRC OK 2.405: dc [1 src=0x00] [0 dst=0x06] [len] 07 0d 3d 05 19 00 0c 84 CRC OK Boilertemp=50.0625 2.509: dc [1 src=0x06] [0 dst=0x00] [len] 06 3d 0d 09 23 CRC OK 2.598: dc [1 src=0x00] [0 dst=0x06] [len] 07 0d 3d 09 23 00 0c 80 CRC OK Boilertemp_target=50 [...] 5.433: dc [1 src=0x06] [0 dst=0x00] [len] 06 3d 09 30 34 CRC OK 5.539: dc [1 src=0x00] [0 dst=0x06] [len] 07 09 3d 30 34 00 0a CRC OK Status=10 [...] 169.784: dc [1 src=0x06] [0 dst=0x00] [len] 06 3d 05 05 21 CRC OK 169.874: dc [1 src=0x00] [0 dst=0x06] [len] 07 05 3d 05 21 00 01 a9 CRC OK Outdoor=6.640625 170.735: dc [1 src=0x06] [0 dst=0x00] [len] 06 3d 31 05 2f CRC OK 170.823: dc [1 src=0x00] [0 dst=0x06] [len] 07 31 3d 05 2f 00 0b fb CRC OK Tap=47.921875