Hi there @Str0nArm, I too have a Hörmann HSM4 868mhz transmitter (not a bi-secure one, like referenced later in this thread), which appears to replay just fine.
I’ve looked at your HORMANN.sub and I get some results when trying to decode it manually:
$ ./hoermann-hsm4-868mhz-test.py HORMANN.sub 2>/dev/null | sort -ru | head -n5
0000.0000.0110.0111.0010.0111.0100.0101.0000.1011.1100.0
0000.0000.0110.0111.0010.0111.0100.0101.0000.1011.110
0000.0000.0110.0111.0010.0111.0100.0101.0000.0
0000.0000.0110.0111.0010.0111.0100.010
0000.0000.0110.0111.0010.0111.0100
The results from my files are even prettier:
$ ./hoermann-hsm4-868mhz-test.py 2.sub 2>/dev/null | uniq
0000.0000.1101.0101.xxxx.xxxx.xxxx.xxxx.1000.1011.1100.0
0000.0000.1101.0
Looks like around 24ms of On signal, then 8x Off-On-On, where every pulse is is 500us (or 1000us in the case of two consequtive On or Offs).
I’ve seen two types of pulse groupings: Off-On-On (LHH) and Off-On-Off (LHL), which I’m guessing mean 0 and 1 respectively.
I’m assuming those first 8 bits are always 0, so they’re probably not part of the payload. That leaves 45 - 8 = 37 bits of data, which is an odd number. I did check whether it was maybe 5x9 bits of which the last bit was (odd) parity, but that did not compute.
45 bits is also what I’ve seen on this page: Garagedoor Remote Duplicator Compatibility List
I’m now looking for more valid samples of this HSM4 to see if I can find some logic (parity or otherwise) in the excess bits, so there are maybe only 32 bits left for the actual secret. Do you have additional codes/sub-files?
Here’s the script I’m using to decode the sub file:
#!/usr/bin/env python3
import sys
HIGH = 'H'
LOW = 'L'
def subghz_file_to_usecs(filename):
"""
Read Flipper SubGhz file and yield data points
Filetype: Flipper SubGhz RAW File
Version: 1
Frequency: 868350000
Preset: FuriHalSubGhzPresetOok650Async
Protocol: RAW
RAW_Data: 1602 -638120 74971 ...
RAW_Data: 489 -1042 1005 ...
"""
with open(filename) as fp:
for line in fp:
if line.startswith('RAW_Data: '):
for usec in line.strip().split()[1:]:
yield int(usec)
def hoermann_usec_to_pulses(usecs):
us_per_bit = 500 # expect a signal change every 500us
us_safe_diff = 75 # allow signals to be 75us early or late
seen_preamble = False
for usec in usecs:
# If the time is negative, it was LOW for that duration.
# If it is positive, it was HIGH.
# Expect a timing of 500us, but accept 80us delta.
pos = -usec if usec < 0 else usec
count = pos // us_per_bit
left = pos % us_per_bit
if left < us_safe_diff:
left = 0
elif left > (us_per_bit - us_safe_diff):
count += 1
left = 0
if count >= 16 and usec > 0:
# We've been high for over 8ms, assume preamble.
seen_preamble = True
elif seen_preamble and left:
# If there is data that does not fit the timing. Report it.
# But only if we've found the preamble.
sys.stderr.write(f'Odd timing of {usec}, back to null\n')
seen_preamble = False
if seen_preamble:
for i in range(0, count):
yield LOW if usec < 0 else HIGH
def hoermann_find_preamble_and_data(on_off_signals):
# Start first after the preamble, which is around 12ms of high, so
# around 24 highs.
try:
while True:
signals = []
count = 0
for signal in on_off_signals:
if signal == HIGH:
count += 1
elif count >= 20:
break
else:
count = 0
else:
break
while True:
signals.append(signal)
signal = next(on_off_signals)
if signal == HIGH and signals[-2:] == [HIGH, HIGH]:
if signals[0:3] == [LOW, HIGH, HIGH]:
yield ''.join(signals)
break
except StopIteration:
if signals:
yield ''.join(signals)
def hoermann_pulses_to_bits(pulses):
for triplets in pulses:
if len(triplets) % 3 != 0:
sys.stderr.write(f'Bad triplet in {triplets}, short message?\n')
triplets = triplets[0:(len(triplets) // 3 * 3)]
for i in range(0, len(triplets), 3):
triplet = triplets[i:i+3]
if triplet == 'LHH':
yield 0
elif triplet == 'LHL':
yield 1
else:
sys.stderr.write(f'Unknown triplet {triplet}, stopping\n')
break
yield None
def only_one(it):
for i in it:
yield i
if i is None:
break
def to_bin(num, length):
s = bin(num)[2:].zfill(length)
return '.'.join(s[i:i+4] for i in range(0, len(s), 4))
def hoermann_bits_to_num(bits):
# Group bits in groups of 9 and assume odd parity.
num = 0
parity = 1
for n, bit in enumerate(bits):
if bit is None:
yield bit
elif n % 9 == 8:
assert bit == parity, (bin(num), bit, 'expected', parity)
yield (num, bin(num), bit, 'expected', parity)
num = 0
parity = 1
else:
num <<= 1
num |= bit
parity ^= bit
if n % 9 != 0:
sys.stderr.write(f'got leftover {num} at {(n//9*9)}+{(n%9)}\n')
def play_func(bits):
num = length = 0
for bit in bits:
if bit is None:
yield to_bin(num, length)
num = length = 0
else:
num <<= 1
num |= bit
length += 1
if length:
yield to_bin(num)
it = subghz_file_to_usecs(sys.argv[1])
it = hoermann_usec_to_pulses(it)
it = hoermann_find_preamble_and_data(it)
if 1: # toggle to 0 to see pulses
it = hoermann_pulses_to_bits(it)
#it = only_one(it)
#it = hoermann_bits_to_num(it)
it = play_func(it)
for n, signal in enumerate(it):
if signal in (0, 1, LOW, HIGH):
sys.stdout.write(str(signal))
if (n % 48) == 47:
sys.stdout.write('\n')
elif (n % 8) == 7:
sys.stdout.write(' ')
elif signal is None:
sys.stdout.write('\n')
else:
sys.stdout.write(str(signal) + '\n')
sys.stdout.write('\n')
At the bottom, you can toggle the 1
to 0
to view raw(er) pulses.
I’d love to hear other thoughts what the excess bits might mean. Or if others can chime in with more sub file examples.