Towards a universal DRU fireplace remote using the Flipper Zero

In the manual of my gas fireplace’s remote I came across this bit of text:

Voordat het toestel in gebruik wordt genomen, moet een communicatiecode ingesteld worden tussen de afstandsbediening en de ontvanger. De code wordt willekeurig gekozen uit de 65000 codes die beschikbaar zijn. Hierdoor is de kans klein dat andere afstandsbedieningen in uw omgeving dezelfde code gebruiken en de werking van uw toestel beïnvloeden.

Translated into English, it says something like:

Before using the device, a communication code needs to be set between the remote and the receiver. This code is chosen randomly from the 65000 codes that are available. Because of this, the chances are slim that a different remote in your environment uses the same code, which would interfere with the working of your device.

The number 65000 is suspiciously close to 2^16 (65536). This means that the Mertik GV60 (the remote type) might send a “unique-enough” 2-byte identifier over the air, along with the command for the heater.

Since this remote transmits at 433.92 MHz, it can be interesting to see what the Flipper Zero makes of this signal. To do this, I used the “Read Raw” functionality in the Sub-GHz app on the Flipper.

Flipper detecting the frequency of the DRU fireplace remote Flipper after reading the signal of the DRU fireplace remote

Dumping files for two different remotes, and for four different operations (higher, lower, ignite, turn off), we end up with eight files:

Since only one of these remotes works with my fireplace, it’s safe to assume they have different identifiers. This will be nice later, if we are going to compare the signals.

Reading a bit more in the manual, it also seemed unlikely to me that there was an actual bi-directional handshake when connecting a remote to the fireplace. To pair it, you need to put the receiver in pairing mode, and press the flame higher or lower button within 20 seconds. This makes me suspect that the 2-byte identifier is hardcoded in the remote, since the remote itself does not have to be put in some kind of pairing mode.

Now we need to make sense of the Flipper Zero’s .sub-files. The documentation mentions that a raw .sub file contains timings, but does not have a lot of information beyond that:

RAW_Data, contains an array of timings, specified in micro seconds. Values must be non-zero, start with a positive number, and interleaved (change sign with each value).

Of course I am not the first person to look at those files, so I found the fzsubtk script on Github. In absence of a software license, I just read this as inspiration to make my own visualisation.

While parsing the .sub-file, I discovered something that probably shouldn’t happen when dumping these files. I had a Raw_Data line that started with a negative value, which should not be possible. Of course I have submitted this as a Github issue: flipperzero-firmware#2260. I quickly received a reply, and it should be fixed for newer versions of the Flipper Zero firmware.

import numpy

def read_sub_ghz_file(filename):
    Read a .sub file as produced by Flipper Zero, and prepare it for plotting.

    This method contains some fixes that might truncate some of the data.
    These should be fixed with a newer release of the Flipper Zero firmware.
    with open(filename, 'r') as f:
        values, durations = [], []
        for line in f.readlines():
            if line.startswith("RAW_Data:"):
                data = [int(x) for x in line[10:].split(' ')]
                # The two fixes below are for Github issue flipperzero-firmware#2260
                if data[0] > 0 and data[1] > 0:
                    data = data[2:]
                if data[0] < 0:
                    data = data[1:]
                for i, point in enumerate(data):
                    if i % 2 == 0:
    durations, values = numpy.abs(numpy.array(durations)), numpy.cumsum(numpy.abs(numpy.array(values)))
    max_len = min([len(durations), len(values)])
    return values[:max_len], durations[:max_len]
from matplotlib import pyplot

remote1_lower = read_sub_ghz_file('Remote1_lower.sub')
remote0_lower = read_sub_ghz_file('Remote0_lower.sub')

# all the numbers below don't mean anything, and are just to align the plot a bit
pyplot.figure(figsize=(16, 8))
pyplot.ylim(-500, 2000)
pyplot.xlim(-2500, 15000)
pyplot.step(remote0_lower[0] - 941300,
            remote0_lower[1], where='pre')
pyplot.step(remote1_lower[0] - 761825,
            remote1_lower[1] - 400, where='pre')

Now that we have plotted the signals produced by two different remotes nicely, it is time to start speculating on the encoding. My best guess currently is that we’re looking for a 3-byte sequence: two bytes to identify the remote, and one byte that specifies the command to execute. These are the raw bits I think I can read from the plot:

signal_blue =   '1100001001000000110011000'
signal_orange = '1000000100001000010011111'

len(signal_blue) // 8

There are many different ways to encode a digital signal over analog radio. This video by Jacob Schrum explains some common ones quite well, and has helpful examples.

I might return to this project later, in an attempt to find the encoding. I’ll be familiarizing myself with some signal processing tools, or perhaps try to bruteforce all possible encodings with some custom scripting.

Replaying the signal is nice, but the end goal of course is to create a Flipper application that can ignite any DRU fireplace. Sources used: