Add Superrollo (GW60/HCS361) TX and RX (#3290)

keeloqtx: Superrollo 67-bit transmit mode with rolling counter.
subghzd: GW60 receive decoder (reports as KeeLoq; manufacturer key read from KEELOQKEYS/MFCODES). The manufacturer key is not included; add a Superrollo entry to MFCODES to enable TX/RX.
This commit is contained in:
rollorentner
2026-08-16 20:10:04 +02:00
committed by GitHub
parent 47c94dbf26
commit 86584feb16
9 changed files with 173 additions and 4 deletions
+4 -2
View File
@@ -74,7 +74,7 @@ void SubGhzDRecentEntryDetailView::update_data() {
console.writeln("Action: 0x" + to_string_hex(func_code));
}
if (entry_.sensorType == FPS_KEELOQ) {
if (entry_.sensorType == FPS_KEELOQ || entry_.sensorType == FPS_SUPERROLLO) {
console.writeln("Fix: " + to_string_hex(fix));
console.writeln("Encrypted: " + to_string_hex(encrypted));
console.writeln("Manufacturer: " + mf_name);
@@ -243,6 +243,8 @@ const char* SubGhzDView::getSensorTypeName(FPROTO_SUBGHZD_SENSOR type) {
return "Ido 11x";
case FPS_INTERTECHNOV3:
return "InterTehcno v3";
case FPS_SUPERROLLO:
return "Superrollo";
case FPS_KEELOQ:
return "KeeLoq";
case FPS_KINGGATESSTYLO4K:
@@ -671,7 +673,7 @@ void SubGhzDRecentEntryDetailView::parseProtocol() {
return;
}
if (entry_.sensorType == FPS_KEELOQ) {
if (entry_.sensorType == FPS_KEELOQ || entry_.sensorType == FPS_SUPERROLLO) {
uint64_t data_rev = FProtoGeneral::subghz_protocol_blocks_reverse_key(entry_.data, 64);
btn = data_rev >> 60;
+38 -2
View File
@@ -34,7 +34,7 @@ void KeeloqTXView::update_hop() {
hop = data.btn << 28 | (apri_serial & 0xFFF) << 16 | data.counter;
} else if (
data.mf_name == "DTM_Neo" || data.mf_name == "FAAC_RC,XT" || data.mf_name == "Mutanco_Mutancode" || data.mf_name == "Came_Space" || data.mf_name == "Genius_Bravo" || data.mf_name == "GSN" || data.mf_name == "Rosh" || data.mf_name == "Rossi" || data.mf_name == "Peccinin" || data.mf_name == "Steelmate" || data.mf_name == "Cardin_S449") {
data.mf_name == "DTM_Neo" || data.mf_name == "FAAC_RC,XT" || data.mf_name == "Mutanco_Mutancode" || data.mf_name == "Came_Space" || data.mf_name == "Genius_Bravo" || data.mf_name == "GSN" || data.mf_name == "Rosh" || data.mf_name == "Rossi" || data.mf_name == "Peccinin" || data.mf_name == "Steelmate" || data.mf_name == "Cardin_S449" || data.mf_name == "Superrollo") {
hop = data.btn << 28 | (data.serial & 0xFFF) << 16 | data.counter;
} else if (
data.mf_name == "NICE_Smilo" || data.mf_name == "NICE_MHOUSE" || data.mf_name == "JCM_Tech") {
@@ -75,6 +75,12 @@ void KeeloqTXView::update_payload() {
}
}
if (data.mf_name == "Superrollo") {
text_payload.set(to_string_hex((uint64_t)(uint32_t)encrypt));
encode_data_gw60((uint32_t)encrypt);
return;
}
payload = (uint64_t)fix << 32 | encrypt;
uint64_t preview_payload = FProtoGeneral::subghz_protocol_blocks_reverse_key(payload, 64);
@@ -83,6 +89,34 @@ void KeeloqTXView::update_payload() {
encode_data();
}
// Superrollo GW60 (HCS361) 67-bit OOK frame at Te=450us.
void KeeloqTXView::encode_data_gw60(uint32_t hop_enc) {
int bits[67];
for (uint32_t i = 0; i < 32; i++) bits[i] = (hop_enc >> i) & 1;
for (uint32_t i = 0; i < 28; i++) bits[32 + i] = (data.serial >> i) & 1;
for (uint32_t i = 0; i < 4; i++) bits[60 + i] = (data.btn >> i) & 1;
bits[64] = 1;
int crc0 = 0, crc1 = 0;
for (uint32_t i = 0; i < 65; i++) {
int new_crc1 = crc0 ^ bits[i];
int new_crc0 = new_crc1 ^ crc1;
crc0 = new_crc0 & 1;
crc1 = new_crc1 & 1;
}
bits[65] = crc0;
bits[66] = crc1;
std::string s{};
for (int i = 0; i < 9; i++) s += "100";
s += "11111111110000000000";
for (int i = 0; i < 67; i++) s += keeloq_fragments[bits[i]];
for (int i = 0; i < 18; i++) s += "0";
encoded_data = s;
pause_duration = 0;
}
void KeeloqTXView::encode_data() {
std::string fragments{};
@@ -206,9 +240,11 @@ void KeeloqTXView::start_tx() {
transmitter_model.enable();
const double te_us = (data.mf_name == "Superrollo") ? 450.0 : 400.0;
baseband::set_ook_data(
bitstream_length,
OOK_SAMPLERATE * (400.0 / 1000000.0),
OOK_SAMPLERATE * (te_us / 1000000.0),
repeat,
pause_duration);
}
@@ -106,6 +106,7 @@ class KeeloqTXView : public View {
std::string encoded_data{};
void encode_data();
void encode_data_gw60(uint32_t hop_enc);
uint32_t repeat = 4;
uint32_t pause_duration = 0;
+124
View File
@@ -0,0 +1,124 @@
#ifndef __FPROTO_SUPERROLLO_H__
#define __FPROTO_SUPERROLLO_H__
#include "subghzdbase.hpp"
// Superrollo GW60 (HCS361) 67-bit OOK decoder. Reports FPS_SUPERROLLO; SubGhzD
// treats FPS_SUPERROLLO like KeeLoq for decrypt/display (see ui_subghzd.cpp), so
// the "Superrollo" keystore key decrypts it while it stays identifiable in logs.
typedef enum : uint8_t {
SuperrolloStepReset = 0,
SuperrolloStepPreambleLow,
SuperrolloStepSyncLow,
SuperrolloStepSaveDuration,
SuperrolloStepCheckDuration,
} SuperrolloDecoderStep;
class FProtoSubGhzDSuperrollo : public FProtoSubGhzDBase {
public:
FProtoSubGhzDSuperrollo() {
sensorType = FPS_SUPERROLLO;
te_short = 450;
te_long = 900;
te_delta = 200;
min_count_bit_for_found = 64;
}
void feed(bool level, uint32_t duration) {
switch (parser_step) {
case SuperrolloStepReset:
if (level) {
if (DURATION_DIFF(duration, te_short) < te_delta) {
header_count++;
parser_step = SuperrolloStepPreambleLow;
} else if (
(header_count >= 4) &&
(DURATION_DIFF(duration, te_short * 10) < te_delta * 10)) {
parser_step = SuperrolloStepSyncLow;
} else {
header_count = 0;
}
} else {
header_count = 0;
}
break;
case SuperrolloStepPreambleLow:
if ((!level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
parser_step = SuperrolloStepReset;
} else {
parser_step = SuperrolloStepReset;
header_count = 0;
}
break;
case SuperrolloStepSyncLow:
if ((!level) && (DURATION_DIFF(duration, te_short * 10) < te_delta * 10)) {
parser_step = SuperrolloStepSaveDuration;
decode_data = 0;
decode_count_bit = 0;
} else {
parser_step = SuperrolloStepReset;
header_count = 0;
}
break;
case SuperrolloStepSaveDuration:
if (level) {
te_last = duration;
parser_step = SuperrolloStepCheckDuration;
} else if (duration >= te_short * 12) {
endFrame();
}
break;
case SuperrolloStepCheckDuration:
if (!level) {
if ((DURATION_DIFF(te_last, te_short) < te_delta) &&
(DURATION_DIFF(duration, te_long) < te_delta)) {
if (decode_count_bit < min_count_bit_for_found)
subghz_protocol_blocks_add_bit(1);
else
decode_count_bit++;
parser_step = SuperrolloStepSaveDuration;
} else if (
(DURATION_DIFF(te_last, te_long) < te_delta) &&
(DURATION_DIFF(duration, te_short) < te_delta)) {
if (decode_count_bit < min_count_bit_for_found)
subghz_protocol_blocks_add_bit(0);
else
decode_count_bit++;
parser_step = SuperrolloStepSaveDuration;
} else if (duration >= te_short * 12) {
endFrame();
} else {
parser_step = SuperrolloStepReset;
header_count = 0;
}
} else {
parser_step = SuperrolloStepReset;
header_count = 0;
}
break;
}
}
uint16_t header_count = 0;
private:
void endFrame() {
if ((decode_count_bit >= min_count_bit_for_found) &&
(decode_count_bit <= min_count_bit_for_found + 3)) {
data_count_bit = min_count_bit_for_found;
if (callback) callback(this);
}
parser_step = SuperrolloStepReset;
decode_data = 0;
decode_count_bit = 0;
header_count = 0;
}
};
#endif
@@ -29,6 +29,7 @@ So include here the .hpp, and add a new element to the protos vector in the cons
#include "s-ido.hpp"
#include "s-intertechnov3.hpp"
#include "s-keeloq.hpp"
#include "s-superrollo.hpp"
#include "s-kinggates_stylo_4k.hpp"
#include "s-linear.hpp"
#include "s-linear_delta3.hpp"
@@ -85,6 +86,7 @@ class SubGhzDProtos : public FProtoListGeneral {
protos[FPS_IDO] = new FProtoSubGhzDIdo();
protos[FPS_INTERTECHNOV3] = new FProtoSubGhzDIntertechnoV3();
protos[FPS_KEELOQ] = new FProtoSubGhzDKeeLoq();
protos[FPS_SUPERROLLO] = new FProtoSubGhzDSuperrollo();
protos[FPS_KINGGATESSTYLO4K] = new FProtoSubGhzDKinggatesStylo4K();
protos[FPS_LINEAR] = new FProtoSubGhzDLinear();
protos[FPS_LINEARDELTA3] = new FProtoSubGhzDLinearDelta3();
@@ -60,6 +60,7 @@ enum FPROTO_SUBGHZD_SENSOR : uint8_t {
FPS_MARANTEC24,
FPS_HOLTEKHT6P20B,
FPS_RESTAURANT_PAGER,
FPS_SUPERROLLO,
FPS_COUNT
};
+1
View File
@@ -0,0 +1 @@
Superrollo;BADBEEF;1;5
+1
View File
@@ -0,0 +1 @@
Superrollo;BADBEEF;1;7
+1
View File
@@ -0,0 +1 @@
Superrollo;BADBEEF;1;3