diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 32a404141..b0b33d87d 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -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 ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index a6f681ed5..6ee5cc178 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -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 + + } diff --git a/firmware/application/external/subcarrx/main.cpp b/firmware/application/external/subcarrx/main.cpp new file mode 100644 index 000000000..20b0fc0f9 --- /dev/null +++ b/firmware/application/external/subcarrx/main.cpp @@ -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(); +} +} // 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 +}; +} diff --git a/firmware/application/external/subcarrx/ui_subcar.cpp b/firmware/application/external/subcarrx/ui_subcar.cpp new file mode 100644 index 000000000..cfd6bdef0 --- /dev/null +++ b/firmware/application/external/subcarrx/ui_subcar.cpp @@ -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(); + + 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(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::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 \ No newline at end of file diff --git a/firmware/application/external/subcarrx/ui_subcar.hpp b/firmware/application/external/subcarrx/ui_subcar.hpp new file mode 100644 index 000000000..a309fffa1 --- /dev/null +++ b/firmware/application/external/subcarrx/ui_subcar.hpp @@ -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(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 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; +using SubCarRecentEntriesView = RecentEntriesView; + +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 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(p); + this->on_freqchg(message->freq); + }}; + + MessageHandlerRegistration message_handler_packet{ + Message::ID::SubCarData, + [this](Message* const p) { + const auto message = static_cast(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__*/ diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index 5d967f1fb..5a92c17ed 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -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 diff --git a/firmware/baseband/fprotos/c-bmw_v0.hpp b/firmware/baseband/fprotos/c-bmw_v0.hpp new file mode 100644 index 000000000..2be444701 --- /dev/null +++ b/firmware/baseband/fprotos/c-bmw_v0.hpp @@ -0,0 +1,169 @@ +#pragma once +#include "subcarbase.hpp" +#include + +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; +}; diff --git a/firmware/baseband/fprotos/c-fiat_v0.hpp b/firmware/baseband/fprotos/c-fiat_v0.hpp new file mode 100644 index 000000000..589cfff79 --- /dev/null +++ b/firmware/baseband/fprotos/c-fiat_v0.hpp @@ -0,0 +1,204 @@ +#pragma once +#include "subcarbase.hpp" +#include + +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; +}; diff --git a/firmware/baseband/fprotos/c-ford_v0.hpp b/firmware/baseband/fprotos/c-ford_v0.hpp new file mode 100644 index 000000000..b976d9e10 --- /dev/null +++ b/firmware/baseband/fprotos/c-ford_v0.hpp @@ -0,0 +1,150 @@ +#pragma once +#include "subcarbase.hpp" +#include + +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; + */ + 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; +}; diff --git a/firmware/baseband/fprotos/c-kia_v0.hpp b/firmware/baseband/fprotos/c-kia_v0.hpp new file mode 100644 index 000000000..7b2bf1a1b --- /dev/null +++ b/firmware/baseband/fprotos/c-kia_v0.hpp @@ -0,0 +1,121 @@ +#pragma once +#include "subcarbase.hpp" +#include + +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; +}; diff --git a/firmware/baseband/fprotos/c-kia_v1.hpp b/firmware/baseband/fprotos/c-kia_v1.hpp new file mode 100644 index 000000000..6c5850855 --- /dev/null +++ b/firmware/baseband/fprotos/c-kia_v1.hpp @@ -0,0 +1,192 @@ +#pragma once +#include "subcarbase.hpp" +#include + +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 = decode_count_bit; + + // 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; +}; diff --git a/firmware/baseband/fprotos/c-kia_v2.hpp b/firmware/baseband/fprotos/c-kia_v2.hpp new file mode 100644 index 000000000..2f86d5013 --- /dev/null +++ b/firmware/baseband/fprotos/c-kia_v2.hpp @@ -0,0 +1,167 @@ +#pragma once +#include "subcarbase.hpp" +#include + +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; +}; diff --git a/firmware/baseband/fprotos/c-kia_v3v4.hpp b/firmware/baseband/fprotos/c-kia_v3v4.hpp new file mode 100644 index 000000000..bede831c1 --- /dev/null +++ b/firmware/baseband/fprotos/c-kia_v3v4.hpp @@ -0,0 +1,185 @@ +#pragma once +#include "subcarbase.hpp" +#include + +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; +}; diff --git a/firmware/baseband/fprotos/c-kia_v5.hpp b/firmware/baseband/fprotos/c-kia_v5.hpp new file mode 100644 index 000000000..d62d70dc6 --- /dev/null +++ b/firmware/baseband/fprotos/c-kia_v5.hpp @@ -0,0 +1,176 @@ +#pragma once +#include "subcarbase.hpp" +#include +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; +}; diff --git a/firmware/baseband/fprotos/c-subaru.hpp b/firmware/baseband/fprotos/c-subaru.hpp new file mode 100644 index 000000000..bfc4bc50f --- /dev/null +++ b/firmware/baseband/fprotos/c-subaru.hpp @@ -0,0 +1,172 @@ +#pragma once +#include "subcarbase.hpp" +#include + +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 = decode_count_bit; + 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; +}; diff --git a/firmware/baseband/fprotos/c-suzuki.hpp b/firmware/baseband/fprotos/c-suzuki.hpp new file mode 100644 index 000000000..93340df01 --- /dev/null +++ b/firmware/baseband/fprotos/c-suzuki.hpp @@ -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; +}; diff --git a/firmware/baseband/fprotos/c-vw.hpp b/firmware/baseband/fprotos/c-vw.hpp new file mode 100644 index 000000000..fdfbc4493 --- /dev/null +++ b/firmware/baseband/fprotos/c-vw.hpp @@ -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; +}; diff --git a/firmware/baseband/fprotos/subcarbase.hpp b/firmware/baseband/fprotos/subcarbase.hpp new file mode 100644 index 000000000..e44332133 --- /dev/null +++ b/firmware/baseband/fprotos/subcarbase.hpp @@ -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 +// 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 \ No newline at end of file diff --git a/firmware/baseband/fprotos/subcarprotos.hpp b/firmware/baseband/fprotos/subcarprotos.hpp new file mode 100644 index 000000000..d88e8d69f --- /dev/null +++ b/firmware/baseband/fprotos/subcarprotos.hpp @@ -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 +#include +#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 diff --git a/firmware/baseband/fprotos/subcartypes.hpp b/firmware/baseband/fprotos/subcartypes.hpp new file mode 100644 index 000000000..3b01cdbc7 --- /dev/null +++ b/firmware/baseband/fprotos/subcartypes.hpp @@ -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 diff --git a/firmware/baseband/proc_protoview.cpp b/firmware/baseband/proc_protoview.cpp index 7f8abd89b..9d8b5432c 100644 --- a/firmware/baseband/proc_protoview.cpp +++ b/firmware/baseband/proc_protoview.cpp @@ -109,4 +109,4 @@ int main() { EventDispatcher event_dispatcher{std::make_unique()}; event_dispatcher.run(); return 0; -} +} \ No newline at end of file diff --git a/firmware/baseband/proc_protoview.hpp b/firmware/baseband/proc_protoview.hpp index abe56d7f8..731d09b8f 100644 --- a/firmware/baseband/proc_protoview.hpp +++ b/firmware/baseband/proc_protoview.hpp @@ -71,4 +71,4 @@ class ProtoViewProcessor : public BasebandProcessor { RSSIThread rssi_thread{}; }; -#endif /*__PROC_PROTOVIEW_H__*/ +#endif /*__PROC_PROTOVIEW_H__*/ \ No newline at end of file diff --git a/firmware/baseband/proc_subcar.cpp b/firmware/baseband/proc_subcar.cpp new file mode 100644 index 000000000..846266d99 --- /dev/null +++ b/firmware/baseband/proc_subcar.cpp @@ -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(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()}; + event_dispatcher.run(); + return 0; +} diff --git a/firmware/baseband/proc_subcar.hpp b/firmware/baseband/proc_subcar.hpp new file mode 100644 index 000000000..9d16e79ba --- /dev/null +++ b/firmware/baseband/proc_subcar.hpp @@ -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 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__*/ diff --git a/firmware/baseband/proc_subghzd.hpp b/firmware/baseband/proc_subghzd.hpp index 0650b3d3b..aeef5a061 100644 --- a/firmware/baseband/proc_subghzd.hpp +++ b/firmware/baseband/proc_subghzd.hpp @@ -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" diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index c277d50cc..f9b774908 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.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__*/ diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index 6e10a87c8..6dee37495 100644 --- a/firmware/common/spi_image.hpp +++ b/firmware/common/spi_image.hpp @@ -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'};