mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-20 14:39:01 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70ac1a03a1 | |||
| 2261e53981 | |||
| f86d3e51f1 |
@@ -1 +1 @@
|
||||
v2.2.0
|
||||
v2.3.1
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.3.1
|
||||
v2.3.2
|
||||
|
||||
@@ -272,6 +272,10 @@ set(EXTCPPSRC
|
||||
#flex_rx
|
||||
external/flex_rx/main.cpp
|
||||
external/flex_rx/ui_flex_rx.cpp
|
||||
|
||||
#subcarrx
|
||||
external/subcarrx/main.cpp
|
||||
external/subcarrx/ui_subcar.cpp
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -340,4 +344,5 @@ set(EXTAPPLIST
|
||||
morse_practice
|
||||
adult_toys_controller
|
||||
flex_rx
|
||||
subcarrx
|
||||
)
|
||||
|
||||
+9
@@ -88,6 +88,7 @@ MEMORY
|
||||
ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k
|
||||
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
|
||||
ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k
|
||||
ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k
|
||||
|
||||
}
|
||||
|
||||
@@ -484,5 +485,13 @@ SECTIONS
|
||||
KEEP(*(.external_app.app_sstvrx.application_information));
|
||||
*(*ui*external_app*sstvrx*);
|
||||
} > ram_external_app_sstvrx
|
||||
|
||||
.external_app_subcarrx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_subcarrx.application_information));
|
||||
*(*ui*external_app*subcarrx*);
|
||||
} > ram_external_app_subcarrx
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_subcar.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::subcarrx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<SubCarView>();
|
||||
}
|
||||
} // namespace ui::external_app::subcarrx
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_subcarrx.application_information"), used)) application_information_t _application_information_subcarrx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::subcarrx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "SubCar",
|
||||
/*.bitmap_data = */ {
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0x30,
|
||||
0x0C,
|
||||
0x30,
|
||||
0x0C,
|
||||
0x30,
|
||||
0x0C,
|
||||
0x30,
|
||||
0x0C,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xC0,
|
||||
0x03,
|
||||
0x80,
|
||||
0x01,
|
||||
0x80,
|
||||
0x01,
|
||||
0x80,
|
||||
0x01,
|
||||
0x80,
|
||||
0x01,
|
||||
0x80,
|
||||
0x07,
|
||||
0x80,
|
||||
0x03,
|
||||
0x80,
|
||||
0x07,
|
||||
0x80,
|
||||
0x01,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_acars */ {'P', 'S', 'C', 'D'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_subcar.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::subcarrx {
|
||||
|
||||
std::string SubCarRecentEntry::to_csv() {
|
||||
std::string csv = ";";
|
||||
csv += SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)sensorType);
|
||||
csv += ";" + to_string_dec_uint(bits) + ";";
|
||||
csv += to_string_hex(data, 64 / 4) + ";" + to_string_hex(data2, 64 / 4);
|
||||
return csv;
|
||||
}
|
||||
|
||||
void SubCarLogger::log_data(SubCarRecentEntry& data) {
|
||||
log_file.write_entry(data.to_csv());
|
||||
}
|
||||
|
||||
void SubCarRecentEntryDetailView::update_data() {
|
||||
// process protocol data
|
||||
parseProtocol();
|
||||
// set text elements
|
||||
text_type.set(SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)entry_.sensorType));
|
||||
|
||||
text_id.set("0x" + to_string_hex(serial));
|
||||
if (entry_.bits > 0) console.writeln("Bits: " + to_string_dec_uint(entry_.bits));
|
||||
if (!btn.empty()) console.writeln("Btn: " + btn);
|
||||
if (cnt != SD_NO_CNT) console.writeln("Cnt: " + to_string_dec_uint(cnt));
|
||||
if (entry_.data != 0) console.writeln("Data : " + to_string_hex(entry_.data));
|
||||
if (entry_.data2 != 0) console.writeln("Data2: " + to_string_hex(entry_.data2));
|
||||
}
|
||||
|
||||
SubCarRecentEntryDetailView::SubCarRecentEntryDetailView(NavigationView& nav, const SubCarRecentEntry& entry)
|
||||
: nav_{nav},
|
||||
entry_{entry} {
|
||||
add_children({&button_done,
|
||||
&text_type,
|
||||
&text_id,
|
||||
&console,
|
||||
&labels});
|
||||
|
||||
button_done.on_select = [&nav](const ui::Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
update_data();
|
||||
}
|
||||
|
||||
void SubCarRecentEntryDetailView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
void SubCarView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
SubCarView::SubCarView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&button_clear_list,
|
||||
&check_log,
|
||||
&labels,
|
||||
&recent_entries_view});
|
||||
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
logger = std::make_unique<SubCarLogger>();
|
||||
|
||||
button_clear_list.on_select = [this](Button&) {
|
||||
recent.clear();
|
||||
recent_entries_view.set_dirty();
|
||||
};
|
||||
field_frequency.set_step(10000);
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
logging = v;
|
||||
if (logger && logging) {
|
||||
logger->append(logs_dir.string() + "/SubCarLOG_" + to_string_timestamp(rtc_time::now()) + ".CSV");
|
||||
logger->write_header();
|
||||
}
|
||||
};
|
||||
check_log.set_value(logging);
|
||||
const Rect content_rect{0, header_height, screen_width, screen_height - header_height};
|
||||
recent_entries_view.set_parent_rect(content_rect);
|
||||
recent_entries_view.on_select = [this](const SubCarRecentEntry& entry) {
|
||||
nav_.push<SubCarRecentEntryDetailView>(entry);
|
||||
};
|
||||
baseband::set_subghzd_config(0, receiver_model.sampling_rate()); // 0=am
|
||||
receiver_model.enable();
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
on_tick_second();
|
||||
};
|
||||
}
|
||||
|
||||
void SubCarView::on_tick_second() {
|
||||
for (auto& entry : recent) {
|
||||
entry.inc_age(1);
|
||||
}
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
void SubCarView::on_data(const SubCarDataMessage* data) {
|
||||
SubCarRecentEntry key{data->sensorType, data->data, data->data2, data->bits};
|
||||
if (logger && logging) {
|
||||
logger->log_data(key);
|
||||
}
|
||||
auto matching_recent = find(recent, key.key());
|
||||
if (matching_recent != std::end(recent)) {
|
||||
// Found within. Move to front of list, increment counter.
|
||||
(*matching_recent).reset_age();
|
||||
recent.push_front(*matching_recent);
|
||||
recent.erase(matching_recent);
|
||||
} else {
|
||||
recent.emplace_front(key);
|
||||
truncate_entries(recent, 64);
|
||||
}
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
SubCarView::~SubCarView() {
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
const char* SubCarView::getSensorTypeName(FPROTO_SUBCAR_SENSOR type) {
|
||||
switch (type) {
|
||||
case FPC_SUZUKI:
|
||||
return "Suzuki";
|
||||
case FPC_VW:
|
||||
return "VW";
|
||||
case FPC_SUBARU:
|
||||
return "Subaru";
|
||||
case FPC_KIAV5:
|
||||
return "Kia V5";
|
||||
case FPC_KIAV3V4:
|
||||
return "Kia V3/V4";
|
||||
case FPC_KIAV2:
|
||||
return "Kia V2";
|
||||
case FPC_KIAV1:
|
||||
return "Kia V1";
|
||||
case FPC_KIAV0:
|
||||
return "Kia V0";
|
||||
case FPC_FORDV0:
|
||||
return "Ford V0";
|
||||
case FPC_FIATV0:
|
||||
return "Fiat V0";
|
||||
case FPC_BMWV0:
|
||||
return "BMW V0";
|
||||
|
||||
case FPC_Invalid:
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string SubCarView::pad_string_with_spaces(int snakes) {
|
||||
std::string paddedStr(snakes, ' ');
|
||||
return paddedStr;
|
||||
}
|
||||
|
||||
void SubCarView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
}
|
||||
|
||||
void subaru_decode_count(const uint8_t* KB, uint16_t* count) {
|
||||
uint8_t lo = 0;
|
||||
if ((KB[4] & 0x40) == 0)
|
||||
lo |= 0x01;
|
||||
if ((KB[4] & 0x80) == 0)
|
||||
lo |= 0x02;
|
||||
if ((KB[5] & 0x01) == 0)
|
||||
lo |= 0x04;
|
||||
if ((KB[5] & 0x02) == 0)
|
||||
lo |= 0x08;
|
||||
if ((KB[6] & 0x01) == 0)
|
||||
lo |= 0x10;
|
||||
if ((KB[6] & 0x02) == 0)
|
||||
lo |= 0x20;
|
||||
if ((KB[5] & 0x40) == 0)
|
||||
lo |= 0x40;
|
||||
if ((KB[5] & 0x80) == 0)
|
||||
lo |= 0x80;
|
||||
|
||||
uint8_t REG_SH1 = (KB[7] << 4) & 0xF0;
|
||||
if (KB[5] & 0x04)
|
||||
REG_SH1 |= 0x04;
|
||||
if (KB[5] & 0x08)
|
||||
REG_SH1 |= 0x08;
|
||||
if (KB[6] & 0x80)
|
||||
REG_SH1 |= 0x02;
|
||||
if (KB[6] & 0x40)
|
||||
REG_SH1 |= 0x01;
|
||||
|
||||
uint8_t REG_SH2 = ((KB[6] << 2) & 0xF0) | ((KB[7] >> 4) & 0x0F);
|
||||
|
||||
uint8_t SER0 = KB[3];
|
||||
uint8_t SER1 = KB[1];
|
||||
uint8_t SER2 = KB[2];
|
||||
|
||||
uint8_t total_rot = 4 + lo;
|
||||
for (uint8_t i = 0; i < total_rot; ++i) {
|
||||
uint8_t t_bit = (SER0 >> 7) & 1;
|
||||
SER0 = ((SER0 << 1) & 0xFE) | ((SER1 >> 7) & 1);
|
||||
SER1 = ((SER1 << 1) & 0xFE) | ((SER2 >> 7) & 1);
|
||||
SER2 = ((SER2 << 1) & 0xFE) | t_bit;
|
||||
}
|
||||
|
||||
uint8_t T1 = SER1 ^ REG_SH1;
|
||||
uint8_t T2 = SER2 ^ REG_SH2;
|
||||
|
||||
uint8_t hi = 0;
|
||||
if ((T1 & 0x10) == 0)
|
||||
hi |= 0x04;
|
||||
if ((T1 & 0x20) == 0)
|
||||
hi |= 0x08;
|
||||
if ((T2 & 0x80) == 0)
|
||||
hi |= 0x02;
|
||||
if ((T2 & 0x40) == 0)
|
||||
hi |= 0x01;
|
||||
if ((T1 & 0x01) == 0)
|
||||
hi |= 0x40;
|
||||
if ((T1 & 0x02) == 0)
|
||||
hi |= 0x80;
|
||||
if ((T2 & 0x08) == 0)
|
||||
hi |= 0x20;
|
||||
if ((T2 & 0x04) == 0)
|
||||
hi |= 0x10;
|
||||
*count = ((hi << 8) | lo) & 0xFFFF;
|
||||
}
|
||||
|
||||
void SubCarRecentEntryDetailView::parseProtocol() {
|
||||
btn = "";
|
||||
cnt = SD_NO_CNT;
|
||||
serial = 0;
|
||||
|
||||
if (entry_.sensorType == FPC_Invalid) return;
|
||||
|
||||
if (entry_.sensorType == FPC_SUZUKI) {
|
||||
uint32_t serial_button = (((entry_.data >> 32) & 0xFFF) << 20) | (entry_.data >> 12);
|
||||
serial = serial_button >> 4;
|
||||
uint8_t buttonid = serial_button & 0xF;
|
||||
cnt = (entry_.data >> 44) & 0xFFFF;
|
||||
btn = to_string_dec_uint(buttonid);
|
||||
return;
|
||||
}
|
||||
if (entry_.sensorType == FPC_VW) {
|
||||
// uint32_t key_high = (entry_.data >> 32) & 0xFFFFFFFF;
|
||||
uint32_t key_low = entry_.data & 0xFFFFFFFF;
|
||||
serial = key_low; // trimmed to 32 bits for VW
|
||||
uint8_t check = entry_.data2 & 0xFF;
|
||||
uint8_t btnid = (check >> 4) & 0xF;
|
||||
switch (btnid) {
|
||||
case 0x1:
|
||||
btn = "UNLOCK";
|
||||
break;
|
||||
case 0x2:
|
||||
btn = "LOCK";
|
||||
break;
|
||||
case 0x3:
|
||||
btn = "Un+Lk";
|
||||
break;
|
||||
case 0x4:
|
||||
btn = "TRUNK";
|
||||
break;
|
||||
case 0x5:
|
||||
btn = "Un+Tr";
|
||||
break;
|
||||
case 0x6:
|
||||
btn = "Lk+Tr";
|
||||
break;
|
||||
case 0x7:
|
||||
btn = "Un+Lk+Tr";
|
||||
break;
|
||||
case 0x8:
|
||||
btn = "PANIC";
|
||||
break;
|
||||
default:
|
||||
btn = "Unknown";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entry_.sensorType == FPC_SUBARU) {
|
||||
uint8_t* data_bytes = (uint8_t*)entry_.data;
|
||||
serial = ((uint32_t)data_bytes[1] << 16) | ((uint32_t)data_bytes[2] << 8) | data_bytes[3];
|
||||
uint8_t button = data_bytes[0] & 0x0F;
|
||||
btn = to_string_dec_uint(button);
|
||||
uint16_t cnttmp = 0;
|
||||
subaru_decode_count(data_bytes, &cnttmp);
|
||||
cnt = cnttmp;
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV5) {
|
||||
serial = (uint32_t)(((entry_.data >> 32) & 0x0FFFFFFF) >> 1);
|
||||
uint8_t button = (entry_.data >> 61) & 0x07;
|
||||
btn = to_string_dec_uint(button);
|
||||
cnt = (uint16_t)(entry_.data & 0xFFFF);
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV3V4) {
|
||||
// not decrypted!
|
||||
serial = SD_NO_SERIAL; //(uint32_t)entry_.data;
|
||||
// uint8_t button = entry_.data2 & 0xFF;
|
||||
btn = "?"; // to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV2) {
|
||||
serial = (uint32_t)((entry_.data >> 20) & 0xFFFFFFFF);
|
||||
uint8_t button = (uint8_t)((entry_.data >> 16) & 0x0F);
|
||||
uint16_t raw_count = (uint16_t)((entry_.data >> 4) & 0xFFF);
|
||||
cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV1) {
|
||||
serial = (uint32_t)((entry_.data >> 24) & 0xFFFFFFFF);
|
||||
uint8_t button = (uint8_t)((entry_.data >> 16) & 0xFF);
|
||||
cnt = (uint8_t)((entry_.data >> 8) & 0xFF);
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV0) {
|
||||
serial = (uint32_t)((entry_.data >> 12) & 0x0FFFFFFF);
|
||||
uint8_t button = (entry_.data >> 8) & 0x0F;
|
||||
cnt = (entry_.data >> 40) & 0xFFFF;
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
if (entry_.sensorType == FPC_FORDV0) {
|
||||
uint8_t buf[13] = {0};
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
buf[i] = (uint8_t)(entry_.data >> (56 - i * 8));
|
||||
}
|
||||
|
||||
buf[8] = (uint8_t)(entry_.data2 >> 8);
|
||||
buf[9] = (uint8_t)(entry_.data2 & 0xFF);
|
||||
uint8_t tmp = buf[8];
|
||||
uint8_t parity = 0;
|
||||
uint8_t parity_any = (tmp != 0);
|
||||
while (tmp) {
|
||||
parity ^= (tmp & 1);
|
||||
tmp >>= 1;
|
||||
}
|
||||
buf[11] = parity_any ? parity : 0;
|
||||
uint8_t xor_byte;
|
||||
uint8_t limit;
|
||||
if (buf[11]) {
|
||||
xor_byte = buf[7];
|
||||
limit = 7;
|
||||
} else {
|
||||
xor_byte = buf[6];
|
||||
limit = 6;
|
||||
}
|
||||
|
||||
for (int idx = 1; idx < limit; ++idx) {
|
||||
buf[idx] ^= xor_byte;
|
||||
}
|
||||
|
||||
if (buf[11] == 0) {
|
||||
buf[7] ^= xor_byte;
|
||||
}
|
||||
|
||||
uint8_t orig_b7 = buf[7];
|
||||
buf[7] = (orig_b7 & 0xAA) | (buf[6] & 0x55);
|
||||
uint8_t mixed = (buf[6] & 0xAA) | (orig_b7 & 0x55);
|
||||
buf[12] = mixed;
|
||||
buf[6] = mixed;
|
||||
|
||||
uint32_t serial_le = ((uint32_t)buf[1]) |
|
||||
((uint32_t)buf[2] << 8) |
|
||||
((uint32_t)buf[3] << 16) |
|
||||
((uint32_t)buf[4] << 24);
|
||||
|
||||
serial = ((serial_le & 0xFF) << 24) |
|
||||
(((serial_le >> 8) & 0xFF) << 16) |
|
||||
(((serial_le >> 16) & 0xFF) << 8) |
|
||||
((serial_le >> 24) & 0xFF);
|
||||
|
||||
uint8_t button = (buf[5] >> 4) & 0x0F;
|
||||
|
||||
cnt = ((buf[5] & 0x0F) << 16) |
|
||||
(buf[6] << 8) |
|
||||
buf[7];
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_FIATV0) {
|
||||
serial = (uint32_t)(entry_.data & 0xFFFFFFFF);
|
||||
cnt = (uint32_t)((entry_.data >> 32) & 0xFFFFFFFF);
|
||||
uint8_t button = (uint8_t)(entry_.data2 & 0xFF);
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_BMWV0) {
|
||||
serial = (uint32_t)((entry_.data >> 12) & 0x0FFFFFFF);
|
||||
uint8_t button = (entry_.data >> 8) & 0x0F;
|
||||
cnt = (entry_.data >> 40) & 0xFFFF;
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::subcarrx
|
||||
|
||||
namespace ui {
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<ui::external_app::subcarrx::SubCarRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style,
|
||||
ui::RecentEntriesColumns& columns) {
|
||||
std::string line{};
|
||||
line.reserve(30);
|
||||
|
||||
line = ui::external_app::subcarrx::SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)entry.sensorType);
|
||||
line = line + " " + to_string_hex(entry.data << 32);
|
||||
line.resize(columns.at(0).second, ' ');
|
||||
std::string ageStr = to_string_dec_uint(entry.age);
|
||||
std::string bitsStr = to_string_dec_uint(entry.bits);
|
||||
line += ui::external_app::subcarrx::SubCarView::pad_string_with_spaces(5 - bitsStr.length()) + bitsStr;
|
||||
line += ui::external_app::subcarrx::SubCarView::pad_string_with_spaces(4 - ageStr.length()) + ageStr;
|
||||
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
/*
|
||||
This and The other files related to this is based on a lot of great people's work. https://github.com/RocketGod-git/ProtoPirate Check the repo, and the credits inside.
|
||||
*/
|
||||
|
||||
#ifndef __UI_SubCar_H__
|
||||
#define __UI_SubCar_H__
|
||||
|
||||
#define SD_NO_SERIAL 0xFFFFFFFF
|
||||
#define SD_NO_BTN 0xFF
|
||||
#define SD_NO_CNT 0xFF
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
#include "../../baseband/fprotos/subcartypes.hpp"
|
||||
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::subcarrx {
|
||||
|
||||
struct SubCarRecentEntry {
|
||||
using Key = uint64_t;
|
||||
static constexpr Key invalid_key = 0x0fffffff;
|
||||
uint8_t sensorType = FPC_Invalid;
|
||||
uint16_t bits = 0;
|
||||
uint16_t age = 0; // updated on each seconds, show how long the signal was last seen
|
||||
uint64_t data = 0;
|
||||
uint64_t data2 = 0;
|
||||
SubCarRecentEntry() {}
|
||||
SubCarRecentEntry(
|
||||
uint8_t sensorType,
|
||||
uint64_t data = 0,
|
||||
uint64_t data2 = 0,
|
||||
uint16_t bits = 0)
|
||||
: sensorType{sensorType},
|
||||
bits{bits},
|
||||
data{data},
|
||||
data2{data2} {
|
||||
}
|
||||
Key key() const {
|
||||
return (data ^ ((static_cast<uint64_t>(sensorType) & 0xFF) << 0)); // should be optimized...
|
||||
}
|
||||
void inc_age(int delta) {
|
||||
if (UINT16_MAX - delta > age) age += delta;
|
||||
}
|
||||
void reset_age() {
|
||||
age = 0;
|
||||
}
|
||||
|
||||
std::string to_csv();
|
||||
};
|
||||
|
||||
class SubCarLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_data(SubCarRecentEntry& data);
|
||||
void write_header() {
|
||||
log_file.write_entry(";Type; Bits; Data;");
|
||||
}
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
using SubCarRecentEntries = RecentEntries<SubCarRecentEntry>;
|
||||
using SubCarRecentEntriesView = RecentEntriesView<SubCarRecentEntries>;
|
||||
|
||||
class SubCarView : public View {
|
||||
public:
|
||||
SubCarView(NavigationView& nav);
|
||||
~SubCarView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "SubCar"; };
|
||||
static const char* getSensorTypeName(FPROTO_SUBCAR_SENSOR type);
|
||||
static std::string pad_string_with_spaces(int snakes);
|
||||
|
||||
private:
|
||||
void on_tick_second();
|
||||
void on_data(const SubCarDataMessage* data);
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
433'920'000 /* frequency */,
|
||||
1'750'000 /* bandwidth */,
|
||||
4'000'000 /* sampling rate */,
|
||||
ReceiverModel::Mode::AMAudio};
|
||||
bool logging = false;
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_subcar",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"log"sv, &logging},
|
||||
}};
|
||||
|
||||
SubCarRecentEntries recent{};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, UI_POS_Y(0)}};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, UI_POS_Y(0)}};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, UI_POS_Y(0)}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
Channel channel{
|
||||
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
|
||||
};
|
||||
RxFrequencyField field_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
nav_};
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
Button button_clear_list{
|
||||
{0, 16, 7 * 8, 32},
|
||||
"Clear"};
|
||||
|
||||
Checkbox check_log{
|
||||
{10 * 8, 18},
|
||||
3,
|
||||
"Log",
|
||||
true};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X_RIGHT(14), UI_POS_Y(1)}, "no fm yet :(", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
static constexpr auto header_height = 3 * 16;
|
||||
|
||||
std::unique_ptr<SubCarLogger> logger{};
|
||||
|
||||
ui::RecentEntriesColumns columns{{
|
||||
{"Type", 0},
|
||||
{"Bits", 4},
|
||||
{"Age", 3},
|
||||
}};
|
||||
SubCarRecentEntriesView recent_entries_view{columns, recent};
|
||||
|
||||
void on_freqchg(int64_t freq);
|
||||
MessageHandlerRegistration message_handler_freqchg{
|
||||
Message::ID::FreqChangeCommand,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
|
||||
this->on_freqchg(message->freq);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::SubCarData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const SubCarDataMessage*>(p);
|
||||
this->on_data(message);
|
||||
}};
|
||||
};
|
||||
|
||||
class SubCarRecentEntryDetailView : public View {
|
||||
public:
|
||||
SubCarRecentEntryDetailView(NavigationView& nav, const SubCarRecentEntry& entry);
|
||||
|
||||
void update_data();
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
SubCarRecentEntry entry_{};
|
||||
|
||||
uint32_t serial = 0;
|
||||
std::string btn = "";
|
||||
uint32_t cnt = SD_NO_CNT;
|
||||
|
||||
Text text_type{{UI_POS_X(0), 1 * 16, 15 * 8, 16}, "?"};
|
||||
Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"};
|
||||
|
||||
Console console{
|
||||
{0, 4 * 16, screen_width, screen_height - (4 * 16) - 36}};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 2 * 16}, "Serial: ", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 3 * 16}, "Data:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Button button_done{
|
||||
{screen_width - 96 - 4, screen_height - 32 - 12, 96, 32},
|
||||
"Done"};
|
||||
|
||||
void parseProtocol();
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::subcarrx
|
||||
|
||||
#endif /*__UI_SubCar_H__*/
|
||||
@@ -704,6 +704,13 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PATX audio_tx)
|
||||
|
||||
### SubCar Decoders
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_subcar.cpp
|
||||
)
|
||||
DeclareTargets(PSCD subcar)
|
||||
|
||||
|
||||
### HackRF "factory" firmware
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
BMWDecoderStepReset = 0,
|
||||
BMWDecoderStepCheckPreambula,
|
||||
BMWDecoderStepSaveDuration,
|
||||
BMWDecoderStepCheckDuration,
|
||||
} BMWDecoderStep;
|
||||
|
||||
class FProtoSubCarBMWV0 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarBMWV0() {
|
||||
sensorType = FPC_BMWV0;
|
||||
te_short = 350;
|
||||
te_long = 700;
|
||||
te_delta = 120;
|
||||
min_count_bit_for_found = 61;
|
||||
}
|
||||
uint8_t subghz_protocol_bmw_crc8(uint8_t* data, size_t len) {
|
||||
uint8_t crc = 0x00;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t j = 0; j < 8; j++) {
|
||||
if (crc & 0x80)
|
||||
crc = (uint8_t)((crc << 1) ^ 0x31);
|
||||
else
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
uint16_t subghz_protocol_bmw_crc16(uint8_t* data, size_t len) {
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= ((uint16_t)data[i] << 8);
|
||||
for (uint8_t j = 0; j < 8; j++) {
|
||||
if (crc & 0x8000)
|
||||
crc = (crc << 1) ^ 0x1021;
|
||||
else
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
void subghz_protocol_decoder_bmw_reset_internal() {
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
decode_data2 = 0;
|
||||
parser_step = BMWDecoderStepReset;
|
||||
header_count = 0;
|
||||
crc_type = 0;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case BMWDecoderStepReset:
|
||||
if (level && (DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
parser_step = BMWDecoderStepCheckPreambula;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case BMWDecoderStepCheckPreambula:
|
||||
if (level) {
|
||||
if ((DURATION_DIFF(duration, te_short) <
|
||||
te_delta) ||
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
} else if (
|
||||
(DURATION_DIFF(duration, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta)) {
|
||||
header_count++;
|
||||
} else if (
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_long) <
|
||||
te_delta)) {
|
||||
if (header_count > 15) {
|
||||
parser_step = BMWDecoderStepSaveDuration;
|
||||
decode_data = 0ULL;
|
||||
decode_count_bit = 0;
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case BMWDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
if (duration >=
|
||||
(te_long + te_delta * 2UL)) {
|
||||
if (decode_count_bit >=
|
||||
min_count_bit_for_found) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
|
||||
// Perform CRC check with both CRC8 and CRC16
|
||||
uint8_t* raw_bytes = (uint8_t*)decode_data;
|
||||
size_t raw_len = (decode_count_bit + 7) / 8;
|
||||
uint8_t crc8 = subghz_protocol_bmw_crc8(raw_bytes, raw_len - 1);
|
||||
if (crc8 == raw_bytes[raw_len - 1]) {
|
||||
crc_type = 8;
|
||||
} else {
|
||||
uint16_t crc16 = subghz_protocol_bmw_crc16(raw_bytes, raw_len - 2);
|
||||
uint16_t rx_crc16 = (raw_bytes[raw_len - 2] << 8) | raw_bytes[raw_len - 1];
|
||||
if (crc16 == rx_crc16) {
|
||||
crc_type = 16;
|
||||
} else {
|
||||
crc_type = 0; // invalid
|
||||
}
|
||||
}
|
||||
|
||||
if (crc_type != 0 && callback) {
|
||||
callback(this);
|
||||
}
|
||||
}
|
||||
subghz_protocol_decoder_bmw_reset_internal();
|
||||
} else {
|
||||
te_last = duration;
|
||||
parser_step = BMWDecoderStepCheckDuration;
|
||||
}
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case BMWDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if ((DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = BMWDecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = BMWDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t header_count = 0;
|
||||
uint8_t crc_type = 0;
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
FiatV0DecoderStepReset = 0,
|
||||
FiatV0DecoderStepPreamble = 1,
|
||||
FiatV0DecoderStepData = 2,
|
||||
} FiatV0DecoderStep;
|
||||
|
||||
class FProtoSubCarFiatV0 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarFiatV0() {
|
||||
sensorType = FPC_FIATV0;
|
||||
te_short = 200;
|
||||
te_long = 400;
|
||||
te_delta = 100;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
uint32_t gap_threshold = 800;
|
||||
uint32_t diff;
|
||||
switch (decoder_state) {
|
||||
case FiatV0DecoderStepReset:
|
||||
if (!level) {
|
||||
return;
|
||||
}
|
||||
if (duration < te_short) {
|
||||
diff = te_short - duration;
|
||||
} else {
|
||||
diff = duration - te_short;
|
||||
}
|
||||
if (diff < te_delta) {
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
decoder_state = FiatV0DecoderStepPreamble;
|
||||
te_last = duration;
|
||||
preamble_count = 0;
|
||||
bit_count = 0;
|
||||
FProtoGeneral::manchester_advance(
|
||||
manchester_state,
|
||||
ManchesterEventReset,
|
||||
&manchester_state,
|
||||
NULL);
|
||||
}
|
||||
break;
|
||||
case FiatV0DecoderStepPreamble:
|
||||
if (level) {
|
||||
return;
|
||||
}
|
||||
if (duration < te_short) {
|
||||
diff = te_short - duration;
|
||||
if (diff < te_delta) {
|
||||
preamble_count++;
|
||||
te_last = duration;
|
||||
if (preamble_count >= 0x96) {
|
||||
if (duration < gap_threshold) {
|
||||
diff = gap_threshold - duration;
|
||||
} else {
|
||||
diff = duration - gap_threshold;
|
||||
}
|
||||
if (diff < te_delta) {
|
||||
decoder_state = FiatV0DecoderStepData;
|
||||
preamble_count = 0;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
bit_count = 0;
|
||||
te_last = duration;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
decoder_state = FiatV0DecoderStepReset;
|
||||
if (preamble_count >= 0x96) {
|
||||
if (duration < gap_threshold) {
|
||||
diff = gap_threshold - duration;
|
||||
} else {
|
||||
diff = duration - gap_threshold;
|
||||
}
|
||||
if (diff < te_delta) {
|
||||
decoder_state = FiatV0DecoderStepData;
|
||||
preamble_count = 0;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
bit_count = 0;
|
||||
te_last = duration;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
diff = duration - te_short;
|
||||
if (diff < te_delta) {
|
||||
preamble_count++;
|
||||
te_last = duration;
|
||||
} else {
|
||||
decoder_state = FiatV0DecoderStepReset;
|
||||
}
|
||||
if (preamble_count >= 0x96) {
|
||||
if (duration >= 799) {
|
||||
diff = duration - gap_threshold;
|
||||
} else {
|
||||
diff = gap_threshold - duration;
|
||||
}
|
||||
if (diff < te_delta) {
|
||||
decoder_state = FiatV0DecoderStepData;
|
||||
preamble_count = 0;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
bit_count = 0;
|
||||
te_last = duration;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FiatV0DecoderStepData:
|
||||
ManchesterEvent event = ManchesterEventReset;
|
||||
if (duration < te_short) {
|
||||
diff = te_short - duration;
|
||||
if (diff < te_delta) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
}
|
||||
} else {
|
||||
diff = duration - te_short;
|
||||
if (diff < te_delta) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
} else {
|
||||
if (duration < te_long) {
|
||||
diff = te_long - duration;
|
||||
} else {
|
||||
diff = duration - te_long;
|
||||
}
|
||||
if (diff < te_delta) {
|
||||
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event != ManchesterEventReset) {
|
||||
bool data_bit_bool;
|
||||
if (FProtoGeneral::manchester_advance(
|
||||
manchester_state,
|
||||
event,
|
||||
&manchester_state,
|
||||
&data_bit_bool)) {
|
||||
uint32_t new_bit = data_bit_bool ? 1 : 0;
|
||||
|
||||
uint32_t carry = (data_low >> 31) & 1;
|
||||
data_low = (data_low << 1) | new_bit;
|
||||
data_high = (data_high << 1) | carry;
|
||||
|
||||
bit_count++;
|
||||
|
||||
if (bit_count == 0x40) {
|
||||
fix = data_low;
|
||||
hop = data_high;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
}
|
||||
|
||||
if (bit_count > 0x46) {
|
||||
final_count = bit_count;
|
||||
|
||||
endbyte = (uint8_t)data_low;
|
||||
/*
|
||||
generic.data = ((uint64_t)hop << 32) | fix;
|
||||
generic.data_count_bit = 64;
|
||||
generic.serial = fix;
|
||||
generic.btn = endbyte; // still exported as btn for UI compatibility
|
||||
generic.cnt = hop;
|
||||
*/
|
||||
decode_data = ((uint64_t)hop << 32) | fix; // this is my own data passer, not the original
|
||||
decode_data2 = endbyte;
|
||||
data_count_bit = 64;
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
bit_count = 0;
|
||||
decoder_state = FiatV0DecoderStepReset;
|
||||
}
|
||||
}
|
||||
}
|
||||
te_last = duration;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
uint8_t decoder_state = 0;
|
||||
uint16_t preamble_count = 0;
|
||||
uint32_t data_low = 0;
|
||||
uint32_t data_high = 0;
|
||||
uint8_t bit_count = 0;
|
||||
uint32_t hop = 0;
|
||||
uint32_t fix = 0;
|
||||
uint8_t endbyte = 0;
|
||||
uint8_t final_count = 0;
|
||||
uint32_t te_last = 0;
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
FordV0DecoderStepReset = 0,
|
||||
FordV0DecoderStepPreamble,
|
||||
FordV0DecoderStepPreambleCheck,
|
||||
FordV0DecoderStepGap,
|
||||
FordV0DecoderStepData,
|
||||
} FordV0DecoderStep;
|
||||
|
||||
class FProtoSubCarFordV0 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarFordV0() {
|
||||
sensorType = FPC_FORDV0;
|
||||
te_short = 250;
|
||||
te_long = 500;
|
||||
te_delta = 100;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
void ford_v0_add_bit(bool bit) {
|
||||
uint32_t low = (uint32_t)data_low;
|
||||
data_low = (data_low << 1) | (bit ? 1 : 0);
|
||||
data_high = (data_high << 1) | ((low >> 31) & 1);
|
||||
bit_count++;
|
||||
}
|
||||
|
||||
bool ford_v0_process_data() {
|
||||
if (bit_count == 64) {
|
||||
uint64_t combined = ((uint64_t)data_high << 32) | data_low;
|
||||
key1 = ~combined;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bit_count == 80) {
|
||||
uint16_t key2_raw = (uint16_t)(data_low & 0xFFFF);
|
||||
uint16_t key2 = ~key2_raw;
|
||||
decode_data = key1;
|
||||
decode_data2 = key2;
|
||||
// decode_ford_v0(key1, key2, &serial, &button, &count);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
uint32_t gap_threshold = 3500;
|
||||
|
||||
switch (parser_step) {
|
||||
case FordV0DecoderStepReset:
|
||||
if (level && (DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
parser_step = FordV0DecoderStepPreamble;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
bit_count = 0;
|
||||
FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL);
|
||||
}
|
||||
break;
|
||||
|
||||
case FordV0DecoderStepPreamble:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
te_last = duration;
|
||||
parser_step = FordV0DecoderStepPreambleCheck;
|
||||
} else {
|
||||
parser_step = FordV0DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case FordV0DecoderStepPreambleCheck:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
header_count++;
|
||||
te_last = duration;
|
||||
parser_step = FordV0DecoderStepPreamble;
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
parser_step = FordV0DecoderStepGap;
|
||||
} else {
|
||||
parser_step = FordV0DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case FordV0DecoderStepGap:
|
||||
if (!level && (DURATION_DIFF(duration, gap_threshold) < 250)) {
|
||||
data_low = 1;
|
||||
data_high = 0;
|
||||
bit_count = 1;
|
||||
parser_step = FordV0DecoderStepData;
|
||||
} else if (!level && duration > gap_threshold + 250) {
|
||||
parser_step = FordV0DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case FordV0DecoderStepData: {
|
||||
ManchesterEvent event;
|
||||
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
} else if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
|
||||
} else {
|
||||
parser_step = FordV0DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
bool data_bit;
|
||||
if (FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) {
|
||||
ford_v0_add_bit(data_bit);
|
||||
if (ford_v0_process_data()) {
|
||||
/* instance->generic.data = instance->key1;
|
||||
instance->generic.data_count_bit = 64;
|
||||
instance->generic.serial = instance->serial;
|
||||
instance->generic.btn = instance->button;
|
||||
instance->generic.cnt = instance->count;
|
||||
*/
|
||||
data_count_bit = 64;
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
bit_count = 0;
|
||||
parser_step = FordV0DecoderStepReset;
|
||||
}
|
||||
}
|
||||
|
||||
te_last = duration;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
|
||||
uint64_t data_low = 0;
|
||||
uint64_t data_high = 0;
|
||||
uint8_t bit_count = 0;
|
||||
|
||||
uint16_t header_count = 0;
|
||||
uint64_t key1 = 0;
|
||||
uint16_t key2 = 0;
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KIADecoderStepReset = 0,
|
||||
KIADecoderStepCheckPreambula,
|
||||
KIADecoderStepSaveDuration,
|
||||
KIADecoderStepCheckDuration,
|
||||
} KIADecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV0 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarKiaV0() {
|
||||
sensorType = FPC_KIAV0;
|
||||
te_short = 250;
|
||||
te_long = 500;
|
||||
te_delta = 100;
|
||||
min_count_bit_for_found = 61;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KIADecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
parser_step = KIADecoderStepCheckPreambula;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
case KIADecoderStepCheckPreambula:
|
||||
if (level) {
|
||||
if ((DURATION_DIFF(duration, te_short) < te_delta) ||
|
||||
(DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
} else if (
|
||||
(DURATION_DIFF(duration, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) < te_delta)) {
|
||||
header_count++;
|
||||
break;
|
||||
} else if (
|
||||
(DURATION_DIFF(duration, te_long) < te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta)) {
|
||||
if (header_count > 15) {
|
||||
parser_step = KIADecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 1;
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
// FURI_LOG_I(TAG, "Starting data decode after %u header pulses", header_count);
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case KIADecoderStepSaveDuration:
|
||||
if (level) {
|
||||
if (duration >=
|
||||
(te_long + te_delta * 2UL)) {
|
||||
// Signal ended too early!
|
||||
// FURI_LOG_W(TAG, "Signal ended at %u bits (expected 61). Duration: %lu", decode_count_bit, duration);
|
||||
|
||||
parser_step = KIADecoderStepReset;
|
||||
if (decode_count_bit == min_count_bit_for_found) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
} else {
|
||||
// FURI_LOG_E(TAG, "Incomplete signal: only %u bits", decode_count_bit);
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
break;
|
||||
} else {
|
||||
te_last = duration;
|
||||
parser_step = KIADecoderStepCheckDuration;
|
||||
}
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case KIADecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if ((DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
if (decode_count_bit % 10 == 0) {
|
||||
// FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit);
|
||||
}
|
||||
parser_step = KIADecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
if (decode_count_bit % 10 == 0) {
|
||||
// FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit);
|
||||
}
|
||||
parser_step = KIADecoderStepSaveDuration;
|
||||
} else {
|
||||
// FURI_LOG_W(TAG, "Timing mismatch at bit %u. Last: %lu, Current: %lu", decode_count_bit, te_last, duration);
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_running = false;
|
||||
size_t preamble_count = 0;
|
||||
size_t data_bit_index = 0;
|
||||
uint8_t last_bit = 0;
|
||||
bool send_high = false;
|
||||
uint16_t header_count = 0;
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KiaV1DecoderStepReset = 0,
|
||||
KiaV1DecoderStepCheckPreamble,
|
||||
KiaV1DecoderStepFoundShortLow,
|
||||
KiaV1DecoderStepCollectRawBits,
|
||||
} KiaV1DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV1 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarKiaV1() {
|
||||
sensorType = FPC_KIAV1;
|
||||
te_short = 800;
|
||||
te_long = 1600;
|
||||
te_delta = 200;
|
||||
min_count_bit_for_found = 56;
|
||||
}
|
||||
void kia_v1_add_raw_bit(bool bit) {
|
||||
if (raw_bit_count < 192) {
|
||||
uint16_t byte_idx = raw_bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (raw_bit_count % 8);
|
||||
if (bit) {
|
||||
raw_bits[byte_idx] |= (1 << bit_idx);
|
||||
} else {
|
||||
raw_bits[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
raw_bit_count++;
|
||||
}
|
||||
}
|
||||
inline bool kia_v1_get_raw_bit(uint16_t idx) {
|
||||
uint16_t byte_idx = idx / 8;
|
||||
uint8_t bit_idx = 7 - (idx % 8);
|
||||
return (raw_bits[byte_idx] >> bit_idx) & 1;
|
||||
}
|
||||
bool kia_v1_manchester_decode() {
|
||||
if (raw_bit_count < 113) {
|
||||
// FURI_LOG_D(TAG, "Not enough raw bits: %u", raw_bit_count);
|
||||
return false;
|
||||
}
|
||||
// Try different offsets to find best alignment (RTL-433 uses -1 bit offset)
|
||||
uint16_t best_bits = 0;
|
||||
uint64_t best_data = 0;
|
||||
// uint16_t best_offset = 0;
|
||||
|
||||
for (uint16_t offset = 0; offset < 8; offset++) {
|
||||
uint64_t data = 0;
|
||||
uint16_t decoded_bits = 0;
|
||||
|
||||
for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 56; i += 2) {
|
||||
bool bit1 = kia_v1_get_raw_bit(i);
|
||||
bool bit2 = kia_v1_get_raw_bit(i + 1);
|
||||
|
||||
uint8_t two_bits = (bit1 << 1) | bit2;
|
||||
|
||||
// V1 uses: 10=1, 01=0
|
||||
if (two_bits == 0x02) { // 10 = decoded 1
|
||||
data = (data << 1) | 1;
|
||||
decoded_bits++;
|
||||
} else if (two_bits == 0x01) { // 01 = decoded 0
|
||||
data = (data << 1);
|
||||
decoded_bits++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded_bits > best_bits) {
|
||||
best_bits = decoded_bits;
|
||||
best_data = data;
|
||||
// best_offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
// FURI_LOG_I(TAG, "Best: offset=%u bits=%u data=%014llX", best_offset, best_bits, best_data);
|
||||
|
||||
decode_data = best_data;
|
||||
decode_count_bit = best_bits;
|
||||
|
||||
return best_bits >= min_count_bit_for_found;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KiaV1DecoderStepReset:
|
||||
// Preamble 0xCCCCCCCD produces alternating LONG pulses
|
||||
if ((level) && (DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
parser_step = KiaV1DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV1DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
// LOW pulse
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
// Short LOW - this is the start of sync (0xCD ends: ...long H, short L, short H)
|
||||
if (header_count > 12) {
|
||||
parser_step = KiaV1DecoderStepFoundShortLow;
|
||||
}
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV1DecoderStepFoundShortLow:
|
||||
// Expecting SHORT HIGH to complete sync
|
||||
if (level && (DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
// FURI_LOG_I(TAG, "Sync! hdr=%u", header_count);
|
||||
parser_step = KiaV1DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
// Add the sync short HIGH as first raw bit
|
||||
kia_v1_add_raw_bit(true);
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV1DecoderStepCollectRawBits:
|
||||
if (duration > 2400) {
|
||||
// FURI_LOG_I(TAG, "End! raw_bits=%u", raw_bit_count);
|
||||
|
||||
if (kia_v1_manchester_decode()) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = raw_bit_count / 8;
|
||||
|
||||
// Extract fields from 56-bit data per RTL-433:
|
||||
// Serial: bits 55-24 (32 bits)
|
||||
// Btn: bits 23-16 (8 bits)
|
||||
// Count: bits 15-8 (8 bits)
|
||||
// CRC: bits 7-0 (8 bits)
|
||||
// instance->generic.serial = (uint32_t)((instance->generic.data >> 24) & 0xFFFFFFFF);
|
||||
// instance->generic.btn = (uint8_t)((instance->generic.data >> 16) & 0xFF);
|
||||
// instance->generic.cnt = (uint8_t)((instance->generic.data >> 8) & 0xFF);
|
||||
|
||||
if (callback)
|
||||
callback(this);
|
||||
}
|
||||
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
int num_bits = 0;
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
num_bits = 1;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
num_bits = 2;
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_bits; i++) {
|
||||
kia_v1_add_raw_bit(level);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t raw_bits[24]{0};
|
||||
uint16_t raw_bit_count = 0;
|
||||
uint16_t header_count = 0;
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KiaV2DecoderStepReset = 0,
|
||||
KiaV2DecoderStepCheckPreamble,
|
||||
KiaV2DecoderStepCollectRawBits,
|
||||
} KiaV2DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV2 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarKiaV2() {
|
||||
sensorType = FPC_KIAV2;
|
||||
te_short = 500;
|
||||
te_long = 1000;
|
||||
te_delta = 160;
|
||||
min_count_bit_for_found = 51;
|
||||
}
|
||||
|
||||
void kia_v2_add_raw_bit(bool bit) {
|
||||
if (raw_bit_count < 160) {
|
||||
uint16_t byte_idx = raw_bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (raw_bit_count % 8);
|
||||
if (bit) {
|
||||
raw_bits[byte_idx] |= (1 << bit_idx);
|
||||
} else {
|
||||
raw_bits[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
raw_bit_count++;
|
||||
}
|
||||
}
|
||||
inline bool kia_v2_get_raw_bit(uint16_t idx) {
|
||||
uint16_t byte_idx = idx / 8;
|
||||
uint8_t bit_idx = 7 - (idx % 8);
|
||||
return (raw_bits[byte_idx] >> bit_idx) & 1;
|
||||
}
|
||||
bool kia_v2_manchester_decode() {
|
||||
if (raw_bit_count < 100) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t best_bits = 0;
|
||||
uint64_t best_data = 0;
|
||||
|
||||
for (uint16_t offset = 0; offset < 8; offset++) {
|
||||
uint64_t data = 0;
|
||||
uint16_t decoded_bits = 0;
|
||||
|
||||
for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 53; i += 2) {
|
||||
bool bit1 = kia_v2_get_raw_bit(i);
|
||||
bool bit2 = kia_v2_get_raw_bit(i + 1);
|
||||
|
||||
uint8_t two_bits = (bit1 << 1) | bit2;
|
||||
|
||||
if (two_bits == 0x02) {
|
||||
data = (data << 1) | 1;
|
||||
decoded_bits++;
|
||||
} else if (two_bits == 0x01) {
|
||||
data = (data << 1);
|
||||
decoded_bits++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded_bits > best_bits) {
|
||||
best_bits = decoded_bits;
|
||||
best_data = data;
|
||||
}
|
||||
}
|
||||
|
||||
decode_data = best_data;
|
||||
decode_count_bit = best_bits;
|
||||
|
||||
return best_bits >= min_count_bit_for_found;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KiaV2DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
parser_step = KiaV2DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV2DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
if (header_count > 10 &&
|
||||
DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) {
|
||||
parser_step = KiaV2DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
}
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV2DecoderStepCollectRawBits:
|
||||
if (duration > 1500) {
|
||||
if (kia_v2_manchester_decode()) {
|
||||
/*data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
|
||||
serial = (uint32_t)((data >> 20) & 0xFFFFFFFF);
|
||||
btn = (uint8_t)((data >> 16) & 0x0F);
|
||||
|
||||
uint16_t raw_count = (uint16_t)((data >> 4) & 0xFFF);
|
||||
cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
|
||||
*/
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
}
|
||||
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
int num_bits = 0;
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
num_bits = 1;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
num_bits = 2;
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_bits; i++) {
|
||||
kia_v2_add_raw_bit(level);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t raw_bits[20]{0};
|
||||
uint16_t raw_bit_count = 0;
|
||||
uint16_t header_count = 0;
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KiaV3V4DecoderStepReset = 0,
|
||||
KiaV3V4DecoderStepCheckPreamble,
|
||||
KiaV3V4DecoderStepCollectRawBits,
|
||||
} KiaV3V4DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarKiaV3V4() {
|
||||
sensorType = FPC_KIAV3V4;
|
||||
te_short = 400;
|
||||
te_long = 800;
|
||||
te_delta = 150;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
uint8_t reverse8(uint8_t byte) {
|
||||
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
|
||||
byte = (byte & 0xCC) >> 2 | (byte & 0x33) << 2;
|
||||
byte = (byte & 0xAA) >> 1 | (byte & 0x55) << 1;
|
||||
return byte;
|
||||
}
|
||||
void kia_v3_v4_add_raw_bit(bool bit) {
|
||||
if (raw_bit_count < 256) {
|
||||
uint16_t byte_idx = raw_bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (raw_bit_count % 8);
|
||||
if (bit) {
|
||||
raw_bits[byte_idx] |= (1 << bit_idx);
|
||||
} else {
|
||||
raw_bits[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
raw_bit_count++;
|
||||
}
|
||||
}
|
||||
bool kia_v3_v4_process_buffer() {
|
||||
if (raw_bit_count < 64) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t* b = raw_bits;
|
||||
|
||||
// For V3-style (long LOW sync), data is inverted
|
||||
if (is_v3_sync) {
|
||||
uint16_t num_bytes = (raw_bit_count + 7) / 8;
|
||||
for (uint16_t i = 0; i < num_bytes; i++) {
|
||||
b[i] = ~b[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract fields
|
||||
// uint32_t encrypted = ((uint32_t)reverse8(b[3]) << 24) | ((uint32_t)reverse8(b[2]) << 16) | ((uint32_t)reverse8(b[1]) << 8) | (uint32_t)reverse8(b[0]);
|
||||
uint32_t serial = ((uint32_t)reverse8(b[7] & 0xF0) << 24) | ((uint32_t)reverse8(b[6]) << 16) | ((uint32_t)reverse8(b[5]) << 8) | (uint32_t)reverse8(b[4]);
|
||||
|
||||
uint8_t btn = (reverse8(b[7]) & 0xF0) >> 4;
|
||||
decode_data = serial;
|
||||
decode_count_bit = 64;
|
||||
decode_data2 = btn;
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
// uint8_t our_serial_lsb = serial & 0xFF;
|
||||
|
||||
// Decrypt --skipped, no keeloq decoding
|
||||
/* uint32_t decrypted = keeloq_common_decrypt(encrypted, kia_mf_key);
|
||||
uint8_t dec_btn = (decrypted >> 28) & 0x0F;
|
||||
uint8_t dec_serial_lsb = (decrypted >> 16) & 0xFF;
|
||||
|
||||
// Validate
|
||||
if (dec_btn != btn || dec_serial_lsb != our_serial_lsb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Valid decode - version determined by sync type
|
||||
instance->encrypted = encrypted;
|
||||
instance->decrypted = decrypted;
|
||||
instance->generic.serial = serial;
|
||||
instance->generic.btn = btn;
|
||||
instance->generic.cnt = decrypted & 0xFFFF;
|
||||
instance->version = is_v3_sync ? 1 : 0;
|
||||
|
||||
uint64_t key_data = ((uint64_t)b[0] << 56) | ((uint64_t)b[1] << 48) | ((uint64_t)b[2] << 40) |
|
||||
((uint64_t)b[3] << 32) | ((uint64_t)b[4] << 24) | ((uint64_t)b[5] << 16) |
|
||||
((uint64_t)b[6] << 8) | (uint64_t)b[7];
|
||||
instance->generic.data = key_data;
|
||||
instance->generic.data_count_bit = 64;
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KiaV3V4DecoderStepReset:
|
||||
if (level && DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
parser_step = KiaV3V4DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV3V4DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
} else if (duration > 1000 && duration < 1500) {
|
||||
// V4 style: Sync is LONG HIGH
|
||||
if (header_count >= 8) {
|
||||
parser_step = KiaV3V4DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
is_v3_sync = false;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (duration > 1000 && duration < 1500) {
|
||||
// V3 style: Sync is LONG LOW
|
||||
if (header_count >= 8) {
|
||||
parser_step = KiaV3V4DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
is_v3_sync = true;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta &&
|
||||
DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) {
|
||||
header_count++;
|
||||
} else if (duration > 1500) {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV3V4DecoderStepCollectRawBits:
|
||||
if (level) {
|
||||
if (duration > 1000 && duration < 1500) {
|
||||
// Next sync pulse (V4 style) - end this packet
|
||||
kia_v3_v4_process_buffer();
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
kia_v3_v4_add_raw_bit(false);
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
kia_v3_v4_add_raw_bit(true);
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (duration > 1000 && duration < 1500) {
|
||||
// Next sync pulse (V3 style) - end this packet
|
||||
kia_v3_v4_process_buffer();
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
} else if (duration > 1500) {
|
||||
// Long gap - end of transmission
|
||||
kia_v3_v4_process_buffer();
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_v3_sync = false; // true = V3 (long LOW sync), false = V4 (long HIGH sync)
|
||||
uint8_t version = 0; // 0 = V4, 1 = V3
|
||||
uint8_t raw_bits[32]{0};
|
||||
uint16_t raw_bit_count = 0;
|
||||
uint16_t header_count = 0;
|
||||
|
||||
// uint32_t encrypted;
|
||||
// uint32_t decrypted;
|
||||
};
|
||||
@@ -0,0 +1,176 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
typedef enum {
|
||||
KiaV5DecoderStepReset = 0,
|
||||
KiaV5DecoderStepCheckPreamble,
|
||||
KiaV5DecoderStepCollectRawBits,
|
||||
} KiaV5DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarKiaV5() {
|
||||
sensorType = FPC_KIAV5;
|
||||
te_short = 400;
|
||||
te_long = 800;
|
||||
te_delta = 150;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
|
||||
inline bool kia_v5_get_raw_bit(uint16_t idx) {
|
||||
uint16_t byte_idx = idx / 8;
|
||||
uint8_t bit_idx = 7 - (idx % 8);
|
||||
return (raw_bits[byte_idx] >> bit_idx) & 1;
|
||||
}
|
||||
|
||||
void kia_v5_add_raw_bit(bool bit) {
|
||||
if (raw_bit_count < 256) {
|
||||
uint16_t byte_idx = raw_bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (raw_bit_count % 8);
|
||||
if (bit) {
|
||||
raw_bits[byte_idx] |= (1 << bit_idx);
|
||||
} else {
|
||||
raw_bits[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
raw_bit_count++;
|
||||
}
|
||||
}
|
||||
|
||||
bool kia_v5_manchester_decode() {
|
||||
if (raw_bit_count < 130) {
|
||||
return false;
|
||||
}
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
|
||||
// Start at offset 2 for proper Manchester alignment
|
||||
const uint16_t start_bit = 2;
|
||||
|
||||
for (uint16_t i = start_bit;
|
||||
i + 1 < raw_bit_count && decode_count_bit < 64;
|
||||
i += 2) {
|
||||
bool bit1 = kia_v5_get_raw_bit(i);
|
||||
bool bit2 = kia_v5_get_raw_bit(i + 1);
|
||||
|
||||
uint8_t two_bits = (bit1 << 1) | bit2;
|
||||
|
||||
if (two_bits == 0x01) { // 01 = decoded 1
|
||||
decode_data = (decode_data << 1) | 1;
|
||||
decode_count_bit++;
|
||||
} else if (two_bits == 0x02) { // 10 = decoded 0
|
||||
decode_data = (decode_data << 1);
|
||||
decode_count_bit++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return decode_count_bit >= min_count_bit_for_found;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KiaV5DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
parser_step = KiaV5DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV5DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if ((DURATION_DIFF(duration, te_short) <
|
||||
te_delta) ||
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if ((DURATION_DIFF(duration, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta)) {
|
||||
header_count++;
|
||||
} else if (
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta)) {
|
||||
if (header_count > 40) {
|
||||
parser_step = KiaV5DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
} else {
|
||||
header_count++;
|
||||
}
|
||||
} else if (
|
||||
DURATION_DIFF(te_last, te_long) <
|
||||
te_delta) {
|
||||
header_count++;
|
||||
} else {
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV5DecoderStepCollectRawBits:
|
||||
if (duration > 1200) {
|
||||
if (kia_v5_manchester_decode()) {
|
||||
// generic.data = decode_data;
|
||||
// generic.data_count_bit = decode_count_bit;
|
||||
data_count_bit = decode_count_bit;
|
||||
// Compute yek (bit-reverse each byte)
|
||||
uint64_t yek = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint8_t byte = (decode_data >> (i * 8)) & 0xFF;
|
||||
uint8_t reversed = 0;
|
||||
for (int b = 0; b < 8; b++) {
|
||||
if (byte & (1 << b))
|
||||
reversed |= (1 << (7 - b));
|
||||
}
|
||||
yek |= ((uint64_t)reversed << ((7 - i) * 8));
|
||||
}
|
||||
decode_data = yek;
|
||||
|
||||
// Shift serial right by 1 to correct alignment
|
||||
// generic.serial = (uint32_t)(((yek >> 32) & 0x0FFFFFFF) >> 1);
|
||||
// generic.btn = (uint8_t)((yek >> 61) & 0x07); // Shift btn too
|
||||
// generic.cnt = (uint16_t)(yek & 0xFFFF);
|
||||
|
||||
if (callback)
|
||||
callback(this);
|
||||
}
|
||||
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
int num_bits = 0;
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
num_bits = 1;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
num_bits = 2;
|
||||
} else {
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_bits; i++) {
|
||||
kia_v5_add_raw_bit(level);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t raw_bits[32]{};
|
||||
uint16_t raw_bit_count = 0;
|
||||
uint16_t header_count = 0;
|
||||
};
|
||||
@@ -0,0 +1,172 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
SubaruDecoderStepReset = 0,
|
||||
SubaruDecoderStepCheckPreamble,
|
||||
SubaruDecoderStepFoundGap,
|
||||
SubaruDecoderStepFoundSync,
|
||||
SubaruDecoderStepSaveDuration,
|
||||
SubaruDecoderStepCheckDuration,
|
||||
} SubaruDecoderStep;
|
||||
|
||||
class FProtoSubCarSubaru : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarSubaru() {
|
||||
sensorType = FPC_SUBARU;
|
||||
te_short = 800;
|
||||
te_long = 1600;
|
||||
te_delta = 260;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
void subghz_protocol_decoder_subaru_reset() {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
te_last = 0;
|
||||
header_count = 0;
|
||||
bit_count = 0;
|
||||
memset(data, 0, sizeof(data));
|
||||
}
|
||||
|
||||
void subaru_add_bit(bool bit) {
|
||||
if (bit_count < 64) {
|
||||
uint8_t byte_idx = bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (bit_count % 8);
|
||||
if (bit) {
|
||||
data[byte_idx] |= (1 << bit_idx);
|
||||
|
||||
} else {
|
||||
data[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
bit_count++;
|
||||
}
|
||||
}
|
||||
|
||||
bool subaru_process_data() {
|
||||
if (bit_count < 64) {
|
||||
return false;
|
||||
}
|
||||
uint8_t* b = data;
|
||||
|
||||
uint64_t key = ((uint64_t)b[0] << 56) | ((uint64_t)b[1] << 48) |
|
||||
((uint64_t)b[2] << 40) | ((uint64_t)b[3] << 32) |
|
||||
((uint64_t)b[4] << 24) | ((uint64_t)b[5] << 16) |
|
||||
((uint64_t)b[6] << 8) | ((uint64_t)b[7]);
|
||||
decode_data = key;
|
||||
// uint32_t serial = ((uint32_t)b[1] << 16) | ((uint32_t)b[2] << 8) | b[3];
|
||||
// uint8_t button = b[0] & 0x0F;
|
||||
// uint16_t cnt;
|
||||
// subaru_decode_count(b, &cnt);
|
||||
data_count_bit = bit_count;
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case SubaruDecoderStepReset:
|
||||
if (level && DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
parser_step = SubaruDecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case SubaruDecoderStepCheckPreamble:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
header_count++;
|
||||
} else if (duration > 2000 && duration < 3500) {
|
||||
if (header_count > 20) {
|
||||
parser_step = SubaruDecoderStepFoundGap;
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SubaruDecoderStepFoundGap:
|
||||
if (level && duration > 2000 && duration < 3500) {
|
||||
parser_step = SubaruDecoderStepFoundSync;
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case SubaruDecoderStepFoundSync:
|
||||
if (!level && DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
parser_step = SubaruDecoderStepSaveDuration;
|
||||
bit_count = 0;
|
||||
memset(data, 0, sizeof(data));
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case SubaruDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
// HIGH pulse duration encodes the bit:
|
||||
// Short HIGH (~800µs) = 1
|
||||
// Long HIGH (~1600µs) = 0
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
// Short HIGH = bit 1
|
||||
subaru_add_bit(true);
|
||||
te_last = duration;
|
||||
parser_step = SubaruDecoderStepCheckDuration;
|
||||
} else if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
// Long HIGH = bit 0
|
||||
subaru_add_bit(false);
|
||||
te_last = duration;
|
||||
parser_step = SubaruDecoderStepCheckDuration;
|
||||
} else if (duration > 3000) {
|
||||
// End of transmission
|
||||
if (bit_count >= 64) {
|
||||
subaru_process_data();
|
||||
}
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case SubaruDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
// LOW pulse - just validates timing, doesn't encode bit
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta ||
|
||||
DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
parser_step = SubaruDecoderStepSaveDuration;
|
||||
} else if (duration > 3000) {
|
||||
// Gap - end of packet
|
||||
if (bit_count >= 64) {
|
||||
subaru_process_data();
|
||||
}
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t header_count = 0;
|
||||
uint8_t data[8];
|
||||
uint8_t bit_count = 0;
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
|
||||
#define SUZUKI_GAP_TIME 2000
|
||||
#define SUZUKI_GAP_DELTA 400
|
||||
|
||||
typedef enum {
|
||||
SuzukiDecoderStepReset = 0,
|
||||
SuzukiDecoderStepFoundStartPulse,
|
||||
SuzukiDecoderStepSaveDuration,
|
||||
} SuzukiDecoderStep;
|
||||
|
||||
class FProtoSubCarSuzuki : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarSuzuki() {
|
||||
sensorType = FPC_SUZUKI;
|
||||
te_short = 250;
|
||||
te_long = 500;
|
||||
te_delta = 110;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
void suzuki_add_bit(uint32_t bit) {
|
||||
uint32_t carry = data_low >> 31;
|
||||
data_low = (data_low << 1) | bit;
|
||||
data_high = (data_high << 1) | carry;
|
||||
data_count_bit++;
|
||||
}
|
||||
void subghz_protocol_decoder_suzuki_reset() {
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
header_count = 0;
|
||||
data_count_bit = 0;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case SuzukiDecoderStepReset:
|
||||
// Wait for short HIGH pulse (~250µs) to start preamble
|
||||
if (!level)
|
||||
return;
|
||||
|
||||
if (DURATION_DIFF(duration, te_short) > te_delta) {
|
||||
return;
|
||||
}
|
||||
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
parser_step = SuzukiDecoderStepFoundStartPulse;
|
||||
header_count = 0;
|
||||
data_count_bit = 0;
|
||||
break;
|
||||
|
||||
case SuzukiDecoderStepFoundStartPulse:
|
||||
if (level) {
|
||||
// HIGH pulse
|
||||
if (header_count < 257) {
|
||||
// Still in preamble - just count
|
||||
return;
|
||||
}
|
||||
|
||||
// After preamble, look for long HIGH to start data
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
parser_step = SuzukiDecoderStepSaveDuration;
|
||||
suzuki_add_bit(1);
|
||||
}
|
||||
// Ignore short HIGHs after preamble until we see a long one
|
||||
} else {
|
||||
// LOW pulse - count as header if short
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else {
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SuzukiDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
// HIGH pulse - determines bit value
|
||||
// Long HIGH (~500µs) = 1, Short HIGH (~250µs) = 0
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
suzuki_add_bit(1);
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
suzuki_add_bit(0);
|
||||
} else {
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
}
|
||||
// Stay in this state for next bit
|
||||
} else {
|
||||
// LOW pulse - check for gap (end of transmission)
|
||||
if (DURATION_DIFF(duration, SUZUKI_GAP_TIME) < SUZUKI_GAP_DELTA) {
|
||||
// Gap found - end of transmission
|
||||
if (data_count_bit == 64) {
|
||||
data_count_bit = 64;
|
||||
decode_data = ((uint64_t)data_high << 32) | (uint64_t)data_low;
|
||||
// Check manufacturer nibble (should be 0xF)
|
||||
uint8_t manufacturer = (data_high >> 28) & 0xF;
|
||||
if (manufacturer == 0xF) {
|
||||
// Extract fields
|
||||
decode_data2 = 0; // Not used
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
}
|
||||
// Short LOW pulses are ignored - stay in this state
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t header_count = 0;
|
||||
uint32_t data_high = 0;
|
||||
uint32_t data_low = 0;
|
||||
uint8_t data_count_bit = 0;
|
||||
};
|
||||
@@ -0,0 +1,236 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
|
||||
typedef enum {
|
||||
VwDecoderStepReset = 0,
|
||||
VwDecoderStepFoundSync,
|
||||
VwDecoderStepFoundStart1,
|
||||
VwDecoderStepFoundStart2,
|
||||
VwDecoderStepFoundStart3,
|
||||
VwDecoderStepFoundData,
|
||||
} VwDecoderStep;
|
||||
|
||||
class FProtoSubCarVW : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarVW() {
|
||||
sensorType = FPC_VW;
|
||||
te_short = 500;
|
||||
te_long = 1000;
|
||||
te_delta = 130;
|
||||
min_count_bit_for_found = 80;
|
||||
}
|
||||
uint8_t vw_get_bit_index(uint8_t bit) {
|
||||
uint8_t bit_index = 0;
|
||||
if (bit < 72 && bit >= 8) {
|
||||
// use generic.data (bytes 1-8)
|
||||
bit_index = bit - 8;
|
||||
} else {
|
||||
// use data_2
|
||||
if (bit >= 72) {
|
||||
bit_index = bit - 64; // byte 0 = type
|
||||
}
|
||||
if (bit < 8) {
|
||||
bit_index = bit; // byte 9 = check digit
|
||||
}
|
||||
bit_index |= 0x80; // mark for data_2
|
||||
}
|
||||
return bit_index;
|
||||
}
|
||||
|
||||
void vw_add_bit(bool level) {
|
||||
if (data_count_bit >= min_count_bit_for_found) {
|
||||
return;
|
||||
}
|
||||
uint8_t bit_index_full = min_count_bit_for_found - 1 - data_count_bit;
|
||||
uint8_t bit_index_masked = vw_get_bit_index(bit_index_full);
|
||||
uint8_t bit_index = bit_index_masked & 0x7F;
|
||||
if (bit_index_masked & 0x80) {
|
||||
// use data_2
|
||||
if (level) {
|
||||
decode_data2 |= (1ULL << bit_index);
|
||||
} else {
|
||||
decode_data2 &= ~(1ULL << bit_index);
|
||||
}
|
||||
} else {
|
||||
// use data
|
||||
if (level) {
|
||||
decode_data |= (1ULL << bit_index);
|
||||
} else {
|
||||
decode_data &= ~(1ULL << bit_index);
|
||||
}
|
||||
}
|
||||
|
||||
data_count_bit++;
|
||||
if (data_count_bit >= min_count_bit_for_found) {
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_vw_reset() {
|
||||
parser_step = VwDecoderStepReset;
|
||||
data_count_bit = 0;
|
||||
decode_data = 0;
|
||||
decode_data2 = 0;
|
||||
manchester_state = ManchesterStateMid1;
|
||||
}
|
||||
|
||||
bool vw_manchester_advance(
|
||||
ManchesterState state,
|
||||
ManchesterEvent event,
|
||||
ManchesterState* next_state,
|
||||
bool* data) {
|
||||
bool result = false;
|
||||
ManchesterState new_state = ManchesterStateMid1;
|
||||
|
||||
if (event == ManchesterEventReset) {
|
||||
new_state = ManchesterStateMid1;
|
||||
} else if (state == ManchesterStateMid0 || state == ManchesterStateMid1) {
|
||||
if (event == ManchesterEventShortHigh) {
|
||||
new_state = ManchesterStateStart1;
|
||||
} else if (event == ManchesterEventShortLow) {
|
||||
new_state = ManchesterStateStart0;
|
||||
} else {
|
||||
new_state = ManchesterStateMid1;
|
||||
}
|
||||
} else if (state == ManchesterStateStart1) {
|
||||
if (event == ManchesterEventShortLow) {
|
||||
new_state = ManchesterStateMid1;
|
||||
result = true;
|
||||
if (data)
|
||||
*data = true;
|
||||
} else if (event == ManchesterEventLongLow) {
|
||||
new_state = ManchesterStateStart0;
|
||||
result = true;
|
||||
if (data)
|
||||
*data = true;
|
||||
} else {
|
||||
new_state = ManchesterStateMid1;
|
||||
}
|
||||
} else if (state == ManchesterStateStart0) {
|
||||
if (event == ManchesterEventShortHigh) {
|
||||
new_state = ManchesterStateMid0;
|
||||
result = true;
|
||||
if (data)
|
||||
*data = false;
|
||||
} else if (event == ManchesterEventLongHigh) {
|
||||
new_state = ManchesterStateStart1;
|
||||
result = true;
|
||||
if (data)
|
||||
*data = false;
|
||||
} else {
|
||||
new_state = ManchesterStateMid1;
|
||||
}
|
||||
}
|
||||
|
||||
*next_state = new_state;
|
||||
return result;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
uint32_t te_med = (te_long + te_short) / 2;
|
||||
uint32_t te_end = te_long * 5;
|
||||
|
||||
ManchesterEvent event = ManchesterEventReset;
|
||||
|
||||
switch (parser_step) {
|
||||
case VwDecoderStepReset:
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
parser_step = VwDecoderStepFoundSync;
|
||||
}
|
||||
break;
|
||||
|
||||
case VwDecoderStepFoundSync:
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
// Stay - sync pattern repeats ~43 times
|
||||
break;
|
||||
}
|
||||
|
||||
if (level && DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
parser_step = VwDecoderStepFoundStart1;
|
||||
break;
|
||||
}
|
||||
|
||||
parser_step = VwDecoderStepReset;
|
||||
break;
|
||||
|
||||
case VwDecoderStepFoundStart1:
|
||||
if (!level && DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
parser_step = VwDecoderStepFoundStart2;
|
||||
break;
|
||||
}
|
||||
|
||||
parser_step = VwDecoderStepReset;
|
||||
break;
|
||||
|
||||
case VwDecoderStepFoundStart2:
|
||||
if (level && DURATION_DIFF(duration, te_med) < te_delta) {
|
||||
parser_step = VwDecoderStepFoundStart3;
|
||||
break;
|
||||
}
|
||||
|
||||
parser_step = VwDecoderStepReset;
|
||||
break;
|
||||
|
||||
case VwDecoderStepFoundStart3:
|
||||
if (DURATION_DIFF(duration, te_med) < te_delta) {
|
||||
// Stay - med pattern repeats
|
||||
break;
|
||||
}
|
||||
|
||||
if (level && DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
// Start data collection
|
||||
vw_manchester_advance(
|
||||
manchester_state,
|
||||
ManchesterEventReset,
|
||||
&manchester_state,
|
||||
NULL);
|
||||
vw_manchester_advance(
|
||||
manchester_state,
|
||||
ManchesterEventShortHigh,
|
||||
&manchester_state,
|
||||
NULL);
|
||||
data_count_bit = 0;
|
||||
decode_data = 0;
|
||||
decode_data2 = 0;
|
||||
parser_step = VwDecoderStepFoundData;
|
||||
break;
|
||||
}
|
||||
|
||||
parser_step = VwDecoderStepReset;
|
||||
break;
|
||||
|
||||
case VwDecoderStepFoundData:
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = level ? ManchesterEventShortHigh : ManchesterEventShortLow;
|
||||
}
|
||||
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
event = level ? ManchesterEventLongHigh : ManchesterEventLongLow;
|
||||
}
|
||||
|
||||
// Last bit can be arbitrarily long
|
||||
if (data_count_bit == min_count_bit_for_found - 1 &&
|
||||
!level && duration > te_end) {
|
||||
event = ManchesterEventShortLow;
|
||||
}
|
||||
|
||||
if (event == ManchesterEventReset) {
|
||||
subghz_protocol_decoder_vw_reset();
|
||||
} else {
|
||||
bool new_level;
|
||||
if (vw_manchester_advance(
|
||||
manchester_state,
|
||||
event,
|
||||
&manchester_state,
|
||||
&new_level)) {
|
||||
vw_add_bit(new_level);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Base class for all weather protocols.
|
||||
This and most of the weather protocols uses code from Flipper XTreme codebase ( https://github.com/Flipper-XFW/Xtreme-Firmware/tree/dev/lib/subghz ). Thanks for their work!
|
||||
For comments in a protocol implementation check w-nexus-th.hpp
|
||||
*/
|
||||
|
||||
#ifndef __FPROTO_SCARBASE_H__
|
||||
#define __FPROTO_SCARBASE_H__
|
||||
|
||||
#include "fprotogeneral.hpp"
|
||||
#include "subcartypes.hpp"
|
||||
|
||||
#include <string>
|
||||
// default values to indicate 'no value'
|
||||
|
||||
class FProtoSubCarBase;
|
||||
typedef void (*SubCarProtocolDecoderBaseRxCallback)(FProtoSubCarBase* instance);
|
||||
|
||||
class FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarBase() {}
|
||||
virtual ~FProtoSubCarBase() {}
|
||||
virtual void feed(bool level, uint32_t duration) = 0; // need to be implemented on each protocol handler.
|
||||
void setCallback(SubCarProtocolDecoderBaseRxCallback cb) { callback = cb; } // this is called when there is a hit.
|
||||
|
||||
// General data holder, these will be passed
|
||||
uint8_t sensorType = FPC_Invalid;
|
||||
uint16_t data_count_bit = 0;
|
||||
uint64_t decode_data = 0;
|
||||
uint64_t decode_data2 = 0;
|
||||
|
||||
protected:
|
||||
// Helper functions to keep it as compatible with flipper as we can, so adding new protos will be easy.
|
||||
void subghz_protocol_blocks_add_bit(uint8_t bit) {
|
||||
decode_data = decode_data << 1 | bit;
|
||||
decode_count_bit++;
|
||||
}
|
||||
|
||||
// inner logic stuff, also for flipper compatibility.
|
||||
uint32_t te_short = UINT32_MAX;
|
||||
uint32_t te_long = UINT32_MAX;
|
||||
uint32_t te_delta = UINT32_MAX;
|
||||
uint32_t min_count_bit_for_found = UINT32_MAX;
|
||||
|
||||
SubCarProtocolDecoderBaseRxCallback callback = NULL;
|
||||
|
||||
uint8_t parser_step = 0;
|
||||
uint32_t te_last = 0;
|
||||
uint32_t decode_count_bit = 0;
|
||||
|
||||
//
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
This is the protocol list handler. It holds an instance of all known protocols.
|
||||
So include here the .hpp, and add a new element to the protos vector in the constructor. That's all you need to do here if you wanna add a new proto.
|
||||
@htotoo
|
||||
*/
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "fprotolistgeneral.hpp"
|
||||
#include "subcarbase.hpp"
|
||||
#include "c-suzuki.hpp"
|
||||
#include "c-vw.hpp"
|
||||
#include "c-subaru.hpp"
|
||||
#include "c-kia_v5.hpp"
|
||||
#include "c-kia_v3v4.hpp"
|
||||
#include "c-kia_v2.hpp"
|
||||
#include "c-kia_v1.hpp"
|
||||
#include "c-kia_v0.hpp"
|
||||
#include "c-ford_v0.hpp"
|
||||
#include "c-fiat_v0.hpp"
|
||||
#include "c-bmw_v0.hpp"
|
||||
|
||||
#ifndef __FPROTO_PROTOLISTCAR_H__
|
||||
#define __FPROTO_PROTOLISTCAR_H__
|
||||
|
||||
class SubCarProtos : public FProtoListGeneral {
|
||||
public:
|
||||
SubCarProtos(const SubCarProtos&) { SubCarProtos(); }; // won't use, but makes compiler happy
|
||||
SubCarProtos& operator=(const SubCarProtos&) { return *this; } // won't use, but makes compiler happy
|
||||
SubCarProtos() {
|
||||
// add protos
|
||||
protos[FPC_SUZUKI] = new FProtoSubCarSuzuki();
|
||||
protos[FPC_VW] = new FProtoSubCarVW();
|
||||
protos[FPC_SUBARU] = new FProtoSubCarSubaru();
|
||||
protos[FPC_KIAV5] = new FProtoSubCarKiaV5();
|
||||
protos[FPC_KIAV3V4] = new FProtoSubCarKiaV3V4();
|
||||
protos[FPC_KIAV2] = new FProtoSubCarKiaV2();
|
||||
protos[FPC_KIAV1] = new FProtoSubCarKiaV1();
|
||||
protos[FPC_KIAV0] = new FProtoSubCarKiaV0();
|
||||
protos[FPC_FORDV0] = new FProtoSubCarFordV0();
|
||||
protos[FPC_FIATV0] = new FProtoSubCarFiatV0();
|
||||
protos[FPC_BMWV0] = new FProtoSubCarBMWV0();
|
||||
|
||||
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
|
||||
if (protos[i] != NULL) protos[i]->setCallback(callbackTarget);
|
||||
}
|
||||
}
|
||||
|
||||
~SubCarProtos() { // not needed for current operation logic, but a bit more elegant :)
|
||||
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
|
||||
if (protos[i] != NULL) {
|
||||
free(protos[i]);
|
||||
protos[i] = NULL;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static void callbackTarget(FProtoSubCarBase* instance) {
|
||||
SubCarDataMessage packet_message{instance->sensorType, instance->data_count_bit, instance->decode_data, instance->decode_data2};
|
||||
shared_memory.application_queue.push(packet_message);
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
|
||||
if (protos[i] != NULL) protos[i]->feed(level, duration);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
FProtoSubCarBase* protos[FPC_COUNT] = {NULL};
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
#ifndef __FPROTO_SUBCARTYPES_H__
|
||||
#define __FPROTO_SUBCARTYPES_H__
|
||||
|
||||
/*
|
||||
Define known protocols.
|
||||
These values must be present on the protocol's constructor, like FProtoWeatherAcurite592TXR() { sensorType = FPS_ANSONIC; }
|
||||
Also it must have a switch-case element in the getSubGhzDSensorTypeName() function, to display it's name.
|
||||
*/
|
||||
|
||||
#define FPM_AM 0
|
||||
#define FPM_FM 1
|
||||
|
||||
enum FPROTO_SUBCAR_SENSOR : uint8_t {
|
||||
FPC_Invalid = 0,
|
||||
FPC_SUZUKI = 1,
|
||||
FPC_VW = 2,
|
||||
FPC_SUBARU = 3,
|
||||
FPC_KIAV5 = 4,
|
||||
FPC_KIAV3V4 = 5,
|
||||
FPC_KIAV2 = 6,
|
||||
FPC_KIAV1 = 7,
|
||||
FPC_KIAV0 = 8,
|
||||
FPC_FORDV0 = 9,
|
||||
FPC_FIATV0 = 10,
|
||||
FPC_BMWV0 = 11,
|
||||
FPC_COUNT
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -109,4 +109,4 @@ int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<ProtoViewProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -71,4 +71,4 @@ class ProtoViewProcessor : public BasebandProcessor {
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
#endif /*__PROC_PROTOVIEW_H__*/
|
||||
#endif /*__PROC_PROTOVIEW_H__*/
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
/*
|
||||
This and The other files related to this is based on a lot of great people's work. https://github.com/RocketGod-git/ProtoPirate Check the repo, and the credits inside.
|
||||
*/
|
||||
#include "proc_subcar.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "event_m4.hpp"
|
||||
|
||||
static inline int get_quadrant(int16_t i, int16_t q) {
|
||||
if (i >= 0) {
|
||||
return (q >= 0) ? 0 : 3;
|
||||
} else {
|
||||
return (q >= 0) ? 1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
void SubCarProcessor::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured) return;
|
||||
|
||||
// SR = 4Mhz , and we are decimating by /8 in total , decim1_out clock 4Mhz /8= 500khz samples/sec.
|
||||
// buffer has 2048 complex i8 I,Q signed samples
|
||||
// decim0 out: 2048/4 = 512 complex i16 I,Q signed samples
|
||||
// decim1 out: 512/2 = 256 complex i16 I,Q signed samples
|
||||
// Regarding Filters, we are re-using existing FIR filters, @4Mhz, FIR decim1 ilter, BW =+-220Khz (at -3dB's). BW = 440kHZ.
|
||||
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer); // Input:2048 complex/4 (decim factor) = 512_output complex (1024 I/Q samples)
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); // Input:512 complex/2 (decim factor) = 256_output complex ( 512 I/Q samples)
|
||||
feed_channel_stats(decim_1_out);
|
||||
|
||||
// for fm
|
||||
const int32_t DC_ALPHA = 5; // Auto-centering speed
|
||||
int32_t buffer_rotation_sum = 0;
|
||||
|
||||
for (size_t i = 0; i < decim_1_out.count; i++) {
|
||||
// am
|
||||
threshold = (low_estimate + high_estimate) / 2;
|
||||
int32_t const hysteresis = threshold / 8; // +-12%
|
||||
int16_t re = decim_1_out.p[i].real();
|
||||
int16_t im = decim_1_out.p[i].imag();
|
||||
uint32_t mag = ((uint32_t)re * (uint32_t)re) + ((uint32_t)im * (uint32_t)im);
|
||||
|
||||
mag = (mag >> 10);
|
||||
int32_t const ook_low_delta = mag - low_estimate;
|
||||
bool meashl = currentHiLow;
|
||||
if (sig_state == STATE_IDLE) {
|
||||
if (mag > (threshold + hysteresis)) { // just become high
|
||||
meashl = true;
|
||||
sig_state = STATE_PULSE;
|
||||
numg = 0;
|
||||
} else {
|
||||
meashl = false; // still low
|
||||
low_estimate += ook_low_delta / OOK_EST_LOW_RATIO;
|
||||
low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling
|
||||
// Calculate default OOK high level estimate
|
||||
high_estimate = 1.35 * low_estimate; // Default is a ratio of low level
|
||||
high_estimate = std::max(high_estimate, min_high_level);
|
||||
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
|
||||
}
|
||||
|
||||
} else if (sig_state == STATE_PULSE) {
|
||||
++numg;
|
||||
if (numg > 100) numg = 100;
|
||||
if (mag < (threshold - hysteresis)) {
|
||||
// check if really a bad value
|
||||
if (numg < 3) {
|
||||
// susp
|
||||
sig_state = STATE_GAP;
|
||||
} else {
|
||||
numg = 0;
|
||||
sig_state = STATE_GAP_START;
|
||||
}
|
||||
meashl = false; // low
|
||||
} else {
|
||||
high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO;
|
||||
high_estimate = std::max(high_estimate, min_high_level);
|
||||
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
|
||||
meashl = true; // still high
|
||||
}
|
||||
} else if (sig_state == STATE_GAP_START) {
|
||||
++numg;
|
||||
if (mag > (threshold + hysteresis)) { // New pulse?
|
||||
sig_state = STATE_PULSE;
|
||||
meashl = true;
|
||||
} else if (numg >= 3) {
|
||||
sig_state = STATE_GAP;
|
||||
meashl = false; // gap
|
||||
}
|
||||
} else if (sig_state == STATE_GAP) {
|
||||
++numg;
|
||||
if (mag > (threshold + hysteresis)) { // New pulse?
|
||||
numg = 0;
|
||||
sig_state = STATE_PULSE;
|
||||
meashl = true;
|
||||
} else {
|
||||
meashl = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal
|
||||
{
|
||||
currentDuration += nsPerDecSamp;
|
||||
} else { // called on change, so send the last duration and dir.
|
||||
if (currentDuration >= 30'000'000) sig_state = STATE_IDLE;
|
||||
if (protoList) protoList->feed(currentHiLow, currentDuration / 1000);
|
||||
currentDuration = nsPerDecSamp;
|
||||
currentHiLow = meashl;
|
||||
}
|
||||
|
||||
// fm part: -- NOT WORKING!!!! TODO FIX. AI code ;)
|
||||
int current_quad = get_quadrant(re, im);
|
||||
// Calculate Step (Current - Previous)
|
||||
int diff = current_quad - fm_state.prev_quad;
|
||||
// Handle Wrap-Around (crossing from Q3 to Q0 or Q0 to Q3)
|
||||
// 3 -> 0 should be +1 (CCW)
|
||||
// 0 -> 3 should be -1 (CW)
|
||||
if (diff == -3)
|
||||
diff = 1;
|
||||
else if (diff == 3)
|
||||
diff = -1;
|
||||
// Update History
|
||||
fm_state.prev_quad = current_quad;
|
||||
// Accumulate Rotation
|
||||
buffer_rotation_sum += diff;
|
||||
}
|
||||
|
||||
// fm finish:
|
||||
// 3. AUTO-CENTERING (DC BLOCKER)
|
||||
// Even with quadrant counting, "drift" (hand effect) makes the wheel spin
|
||||
// faster or slower. We need to subtract the average speed.
|
||||
// Update our "Average Speed" estimate
|
||||
// Note: buffer_rotation_sum is roughly proportional to frequency.
|
||||
fm_state.dc_offset = (fm_state.dc_offset * ((1 << DC_ALPHA) - 1) + buffer_rotation_sum) >> DC_ALPHA;
|
||||
// Remove the drift
|
||||
int32_t centered_rotation = buffer_rotation_sum - fm_state.dc_offset;
|
||||
// 4. LOW PASS FILTER
|
||||
const int32_t LPF_ALPHA = 4;
|
||||
fm_state.smoothed_error = (fm_state.smoothed_error * (LPF_ALPHA - 1) + centered_rotation) / LPF_ALPHA;
|
||||
// 5. DECISION LOGIC
|
||||
// Threshold is small now because we are counting quadrant steps.
|
||||
// Max steps per buffer (256 samples) is 256.
|
||||
// Typical FSK deviation might give you +/- 10 to 50 steps per buffer.
|
||||
const int32_t THRESHOLD = 3;
|
||||
bool new_level = fm_state.current_logic_level;
|
||||
if (fm_state.smoothed_error > THRESHOLD) {
|
||||
new_level = true;
|
||||
} else if (fm_state.smoothed_error < -THRESHOLD) {
|
||||
new_level = false;
|
||||
}
|
||||
// 6. TIMING OUTPUT
|
||||
if (new_level == fm_state.current_logic_level) {
|
||||
fm_state.buffer_count++;
|
||||
} else {
|
||||
// Output pulse duration
|
||||
int32_t duration_us = fm_state.buffer_count * 512;
|
||||
|
||||
if (duration_us > 250) {
|
||||
if (protoListFm) protoListFm->feed(fm_state.current_logic_level, duration_us);
|
||||
}
|
||||
fm_state.current_logic_level = new_level;
|
||||
fm_state.buffer_count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void SubCarProcessor::on_message(const Message* const message) {
|
||||
if (message->id == Message::ID::SubGhzFPRxConfigure)
|
||||
configure(*reinterpret_cast<const SubGhzFPRxConfigureMessage*>(message));
|
||||
}
|
||||
|
||||
void SubCarProcessor::configure(const SubGhzFPRxConfigureMessage& message) {
|
||||
// constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor; //unused
|
||||
// constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor; //unused
|
||||
|
||||
baseband_fs = message.sampling_rate;
|
||||
baseband_thread.set_sampling_rate(baseband_fs);
|
||||
nsPerDecSamp = 1'000'000'000 / baseband_fs * 8; // Scaled it due to less array buffer sampes due to /8 decimation. 250 nseg (4Mhz) * 8
|
||||
|
||||
decim_0.configure(taps_200k_wfm_decim_0.taps);
|
||||
decim_1.configure(taps_200k_wfm_decim_1.taps);
|
||||
|
||||
configured = true;
|
||||
}
|
||||
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<SubCarProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
/*
|
||||
This and The other files related to this is based on a lot of great people's work. https://github.com/RocketGod-git/ProtoPirate Check the repo, and the credits inside.
|
||||
*/
|
||||
|
||||
#ifndef __PROC_SUBCAR_H__
|
||||
#define __PROC_SUBCAR_H__
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
#include "message.hpp"
|
||||
#include "dsp_decimate.hpp"
|
||||
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize("Os")
|
||||
#include "fprotos/subcarprotos.hpp"
|
||||
#pragma GCC pop_options
|
||||
|
||||
#define OOK_EST_HIGH_RATIO 3 // Constant for slowness of OOK high level estimator
|
||||
#define OOK_EST_LOW_RATIO 5 // Constant for slowness of OOK low level (noise) estimator (very slow)
|
||||
#define OOK_MAX_HIGH_LEVEL 450000
|
||||
|
||||
class SubCarProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
enum {
|
||||
STATE_IDLE = 0,
|
||||
STATE_PULSE = 1,
|
||||
STATE_GAP_START = 2,
|
||||
STATE_GAP = 3,
|
||||
} sig_state = STATE_IDLE;
|
||||
uint32_t low_estimate = 100;
|
||||
uint32_t high_estimate = 12000;
|
||||
uint32_t min_high_level = 10;
|
||||
uint8_t numg = 0; // count of matched signals to filter spikes
|
||||
size_t baseband_fs = 0; // will be set later by configure message
|
||||
uint32_t nsPerDecSamp = 0;
|
||||
|
||||
/* Array Buffer aux. used in decim0 and decim1 IQ c16 signed data ; (decim0 defines the max length of the array) */
|
||||
std::array<complex16_t, 512> dst{}; // decim0 /4 , 2048/4 = 512 complex I,Q
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
/* Decimates */
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x16Decim2 decim_1{};
|
||||
|
||||
uint32_t currentDuration = 0;
|
||||
uint32_t threshold = 0x0630;
|
||||
bool currentHiLow = false;
|
||||
bool configured{false};
|
||||
uint8_t mode = 0; // 0 = am, 1 = fm
|
||||
|
||||
// fm part:
|
||||
struct DemodFMState {
|
||||
int prev_quad = 0; // Stores 0, 1, 2, or 3
|
||||
int32_t dc_offset = 0;
|
||||
int32_t smoothed_error = 0;
|
||||
bool current_logic_level = false;
|
||||
uint32_t buffer_count = 0;
|
||||
};
|
||||
DemodFMState fm_state{};
|
||||
|
||||
FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse
|
||||
FProtoListGeneral* protoListFm = new SubCarProtos(); // holds all the protocols we can parse, but for fm (dupe, bc most of it is dual)
|
||||
void configure(const SubGhzFPRxConfigureMessage& message);
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
#endif /*__PROC_WEATHER_H__*/
|
||||
@@ -23,8 +23,8 @@
|
||||
Creator: @htotoo
|
||||
*/
|
||||
|
||||
#ifndef __PROC_WEATHER_H__
|
||||
#define __PROC_WEATHER_H__
|
||||
#ifndef __PROC_SUBGHZD_H__
|
||||
#define __PROC_SUBGHZD_H__
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
|
||||
@@ -145,6 +145,7 @@ class Message {
|
||||
SSTVRXProgress = 87,
|
||||
SSTVRXPhaseSlant = 88,
|
||||
SSTVRXCalibration = 89,
|
||||
SubCarData = 90,
|
||||
MAX
|
||||
};
|
||||
|
||||
@@ -1679,4 +1680,23 @@ class FlexDebugMessage : public Message {
|
||||
char text[64];
|
||||
};
|
||||
|
||||
class SubCarDataMessage : public Message {
|
||||
public:
|
||||
constexpr SubCarDataMessage(
|
||||
uint8_t sensorType = 0,
|
||||
uint16_t bits = 0,
|
||||
uint64_t data = 0,
|
||||
uint64_t data2 = 0)
|
||||
: Message{ID::SubCarData},
|
||||
sensorType{sensorType},
|
||||
bits{bits},
|
||||
data{data},
|
||||
data2{data2} {
|
||||
}
|
||||
uint8_t sensorType = 0;
|
||||
uint16_t bits = 0;
|
||||
uint64_t data = 0;
|
||||
uint64_t data2 = 0;
|
||||
};
|
||||
|
||||
#endif /*__MESSAGE_H__*/
|
||||
|
||||
@@ -119,6 +119,7 @@ constexpr image_tag_t image_tag_usb_sd{'P', 'U', 'S', 'B'};
|
||||
|
||||
constexpr image_tag_t image_tag_weather{'P', 'W', 'T', 'H'};
|
||||
constexpr image_tag_t image_tag_subghzd{'P', 'S', 'G', 'D'};
|
||||
constexpr image_tag_t image_tag_subcar{'P', 'S', 'C', 'D'};
|
||||
constexpr image_tag_t image_tag_protoview{'P', 'P', 'V', 'W'};
|
||||
constexpr image_tag_t image_tag_wefaxrx{'P', 'W', 'F', 'X'};
|
||||
constexpr image_tag_t image_tag_noaaapt_rx{'P', 'N', 'O', 'A'};
|
||||
|
||||
+1
-1
Submodule hackrf updated: cf6815aaf9...c0b15549cb
Reference in New Issue
Block a user