GitHub – kokx/duco-analysis
December 31, 2024

GitHub – kokx/duco-analysis

My newly built house has a promising feature: the DucoBox Energy Comfort D325 ventilation system with heat recovery. While the system uses heat from the exhaust air to effectively preheat the incoming air, its control options are limited to four basic modes via a simple push-button interface. I want more – specifically, integration with Home Assistant. Official solution? one Duco connection board. But when I noticed it was just an ESP32 in disguise, I knew there had to be a better way.

The system operates in four general modes: an automatic mode, which automatically selects the mode, and three manual modes, which set the airflow level. By default, these modes last 15 minutes, but holding the button longer will keep the mode active until it stops.

In order to use the Duco connection plate, we first need information about its connection to the ventilation system. Luckily, the manufacturer Duco has a great website Duco TVwhich contains many videos and detailed instructions on how to maintain your ventilation system, including how to install connection plates. The board connects via a 12-pin connector on the ventilation system PCB. Using a multimeter we can find out the basic pinout:

The connection board design is surprisingly simple: it simply connects the ventilation system’s communication pins directly to the ESP32’s built-in serial interface (pins GPIO16/17).

We connect a logic analyzer to these pins and use some probes to analyze the protocol:

Using Saleae Logic, we determine the baud rate (57600) and identify the protocol as “asynchronous serial” (usually UART):

Since the connection board exposes Modbus TCP, it makes sense to check if the wiring is actually MODBUS. If that were the case, our jobs would be a lot easier!

Although our initial Modbus hypothesis hit a dead end, a pattern emerged: each message begins with a unique byte 0xAA 0x55. In agreement analysis, this consistent pattern usually marks the beginning of the message:

Board:  aa 55 04 0c 30 0a 00 d0 3f
Box:    aa 55 02 0d 30 d4 84 aa 55 08 0e 30 01 00 00 00 00 00 dd 6e

Use split messages 0xAA 0x55 Bytes, providing some useful information when inserting rapidscada.net/modbus. These messages do appear to have valid CRC codes and appear to be valid Modbus messages:

But something strange still happens: the function codes on any messages don’t seem to be regular Modbus function codes. They do not match any Modbus function code. Usually the slave address seems to have something else: the length of the message minus the CRC.

Length includes edge cases: when 0xAA byte appears in the middle of the message, followed by 0x01ignores length and CRC, may prevent 0xAA 0x55 Occurs in the middle of the message.

It’s time to take a different approach. Let’s see what happens if we change the mode to MAN3 through the Connectivity Board’s web interface. Among the generated traces, the following stand out:

We can also trace and compare the resulting messages when setting up MAN1:

Board:   aa 55  05 0c 69 04 01 06  cd 80
Box:     aa 55  02 0d 69  14 be
Box:     aa 55  03 0e 69 01  8e 33

Board:   aa 55  05 0c cb 04 01 04  6f f9
Box:     aa 55  02 0d cb  95 07
Box:     aa 55  03 0e cb 01  f7 53

Pattern appears: third byte (excluding 0xAA 0x55 header) seems to identify the request/response, the second byte acts as a “function code” (does not correspond to Modbus function codes). The dialogue is the same except for the last data byte from the module.

Duco comes to our rescue again with their documentation! this Modbus TCP Information Sheet gives us a mapping between patterns and their respective code:

Duco Box uses comfort temperature to decide when to use the heat exchanger. We changed it from 24.0 °C to 24.5 °C via the web interface:

This time it’s a little more complicated! Let’s break it down:

Board:   aa 55  05 24 6a 00 12 0a  e1 36
Box:     aa 55  02 25 6a  4a bf
Box:     aa 55  09 26 6a 01 12 0a f0 00 00 00  2c ad

Board:   aa 55  05 24 6b 00 12 0a  e0 ca
Box:     aa 55  02 25 6b  8b 7f
Box:     aa 55  09 26 6b 01 12 0a f0 00 00 00  ed 61

Board:   aa 55  09 24 6c 01 12 0a f5 00 00 00  b5 2b
Box:     aa 55  02 25 6c  ca bd
Box:     aa 55  09 26 6c 01 12 0a f5 00 00 00  ac 4b

This looks like three conversations. The first two look identical except for the message identifier. The third module had a different message, and the box responded with a similar message. But the seventh byte is obviously different: it starts with 0xF0 arrive 0xF5. Conveniently, 0xF0 = 240 and 0xF5 = 245.

The first two conversations here seem to read the comfort temperature before changing it. So this gives us more information on how to read the comfort temperature!

Also, another pattern is starting to emerge here: it seems like every time a message is sent, the box confirms it with a text message before actually replying.

Reading the current schema is more complex. The connection board did not request this information when you requested it, but cached it. However, every few seconds, the motherboard sends out a burst of traffic. In automatic mode (= 0x00) mode the following happens:

Board:   aa 55  04 0c 1f 02 00  e6 36
Box:     aa 55  02 0d 1f  95 58
Box:     aa 55  16 0e 1f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ae 9f

Board:   aa 55  04 0c 20 02 01  17 fa
Box:     aa 55  02 0d 20  d5 48
Box:     aa 55  16 0e 20 00 01 2c 00 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  90 46

Board:   aa 55  04 0c 21 02 02  06 3b
Box:     aa 55  02 0d 21  14 88
Box:     aa 55  16 0e 21 00 00 00 00 ff 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00  8c 35

In MAN3 (= 0x06) model:

Board:  aa 55  04 0c c3 02 00  27 cc
Box:    aa 55  02 0d c3  94 c1
Box:    aa 55  16 0e c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  a1 ce

Board:  aa 55  04 0c c4 02 01  57 cd
Box:    aa 55  02 0d c4  d5 03
Box:    aa 55  16 0e c4 06 02 64 00 ff 00 00 00 00 00 00 00 7f 03 00 00 58 21 29 67  11 4c

Board:  aa 55  04 0c c5 02 02  46 0c
Box:    aa 55  02 0d c5  14 c3
Box:    aa 55  16 0e c5 06 00 00 00 ff 00 00 83 02 ff 00 00 7f 03 00 00 58 21 29 67  17 d8

The fourth response byte shows the current mode, but is not present in every message.

The web interface of the connection board also shows the various nodes being used by the ventilation system:

Let’s see if this corresponds to the message:

[...]
Board:  aa 55  04 0c c6 02 03  77 cc
Box:    aa 55  02 0d c6 54 c2
Box:    aa 55  16 0e c6 06 00 00 00 ff 00 00 8f 03 ff 00 00 7f 03 00 00 58 21 29 67  c8 e4

Board:  aa 55  04 0c c7 02 04  67 ce
Box:    aa 55  02 0d c7 95 02
Box:    aa 55  16 0e c7 06 00 00 00 ff 00 00 84 04 ff 00 00 7f 03 00 00 58 21 29 67  37 75

Board:  aa 55  04 0c c8 02 05  96 0d
Box:    aa 55  02 0d c8 d5 06
Box:    aa 55  16 0e c8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  1b b5

[...]

Board:  aa 55  04 0c f8 02 34  57 d6
Box:    aa 55  02 0d f8 d5 12
Box:    aa 55  16 0e f8 06 00 00 00 ff 00 00 00 00 00 00 00 7a 03 00 00 54 21 29 67  0d 8b

[...]

Board:  aa 55  04 0c 07 02 43  27 c0
Box:    aa 55  02 0d 07 95 52
Box:    aa 55  16 0e 07 06 00 00 00 ff 00 00 00 00 00 00 00 7a 03 00 00 55 21 29 67  17 77

Bingo! Only nodes respond with actual data. Looking further, it seems there are other messages to get information about the node:

Board:  aa 55  04 0c 3a 01 01  36 cd
Box:    aa 55  02 0d 3a 54 83
Box:    aa 55  09 0e 3a 11 03 01 01 00 00 1b  12 26

[...]

Board:  aa 55  04 0c 7f 0a 01  20 28
Box:    aa 55  02 0d 7f 95 70
Box:    aa 55  08 0e 7f 01 01 00 00 00 00  5e 6a

This provides more information: the fourth byte contains 0x11 = 17according to the information sheet, this is the Duco Box. Likewise, other components can be identified this way. Given the following list of nodes:

  • Node 1, type 17 (0x11), BOX
  • Node 2, type 8 (0x08), UCBAT
  • Node 3, type 12 (0x0c), UCCO2
  • Node 4, type 12 (0x0c), UCCO2
  • Node 52, type 18 (0x12), switch
  • Node 67, type 9 (0x09), UC

After understanding the basic protocol, we turn our attention to the more complex challenge: decoding CO2 sensor readings. In the web interface, they display values ​​between 0 (low air quality) and 100 (high air quality). However, what is actually conveyed does not appear to be the case. Let’s do a long trace and see some messages from request nodes 3 and 4:

[...]
Board:  aa 55  07 10 85 01 03 00 49 04  04 6e
Box:    aa 55  02 11 85  1d f3
Box:    aa 55  0c 12 85 01 04 d4 00 d9 02 00 00 5a 76  2c 34
[...]

Responses include 0x02d9 = 729. This is probably a reasonable value for a CO2 ppm reading. Time to give it a quick test! Breathing directly into one of the sensors may increase the value a bit:

Board:  aa 55  07 10 87 01 04 00 49 04  04 f8
Box:    aa 55  02 11 87  9c 32
Box:    aa 55  0c 12 87 01 04 e0 00 10 27 00 00 8d 76  26 c5

This changes the value to 0x2710 = 10000. This is probably the effective maximum value for the CO2 sensor. Return to a lower value after some time:

Board:  aa 55  07 10 9c 01 04 00 49 04  07 13
Box:    aa 55  02 11 9c  dc 39
Box:    aa 55  0c 12 9c 01 04 e2 00 86 15 00 00 90 76  fb 5a

The sensor returns to 0x1586 = 5510.

Using similar techniques, it is possible to find out more information from the module, such as the serial number, when the filter was replaced, and the current flow level of the system.

All this makes it possible to create ESPhome components. This PR is now available on GitHub: esphome/esphome#7993

2024-12-27 12:29:14

Leave a Reply

Your email address will not be published. Required fields are marked *