Compare commits

...

3 Commits

Author SHA1 Message Date
gullradriel 70ac1a03a1 Make stable release (#2895)
* updated submodule
* updated versions
2025-12-21 11:39:47 +01:00
Totoo 2261e53981 fix bit counts (#2894) 2025-12-21 11:38:35 +01:00
Totoo f86d3e51f1 Subcar (#2893) 2025-12-21 11:21:55 +01:00
30 changed files with 3167 additions and 7 deletions
+1 -1
View File
@@ -1 +1 @@
v2.2.0
v2.3.1
+1 -1
View File
@@ -1 +1 @@
v2.3.1
v2.3.2
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
};
}
+459
View File
@@ -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
+222
View File
@@ -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__*/
+7
View File
@@ -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
+169
View File
@@ -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;
};
+204
View File
@@ -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;
};
+151
View File
@@ -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;
};
+121
View File
@@ -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;
};
+192
View File
@@ -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;
};
+167
View File
@@ -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;
};
+185
View File
@@ -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;
};
+176
View File
@@ -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;
};
+172
View File
@@ -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;
};
+120
View File
@@ -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;
};
+236
View File
@@ -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;
};
+54
View File
@@ -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
+30
View File
@@ -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
+1 -1
View File
@@ -109,4 +109,4 @@ int main() {
EventDispatcher event_dispatcher{std::make_unique<ProtoViewProcessor>()};
event_dispatcher.run();
return 0;
}
}
+1 -1
View File
@@ -71,4 +71,4 @@ class ProtoViewProcessor : public BasebandProcessor {
RSSIThread rssi_thread{};
};
#endif /*__PROC_PROTOVIEW_H__*/
#endif /*__PROC_PROTOVIEW_H__*/
+206
View File
@@ -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;
}
+98
View File
@@ -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__*/
+2 -2
View File
@@ -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"
+20
View File
@@ -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__*/
+1
View File
@@ -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