diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 72283c215..f79692436 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -83,6 +83,10 @@ set(EXTCPPSRC external/tpmsrx/main.cpp external/tpmsrx/tpms_app.cpp + #tpmstx 800 bytes - TPMS transmit with editable fields + external/tpmstx/main.cpp + external/tpmstx/tpms_tx_app.cpp + #protoview 8 byte external/protoview/main.cpp external/protoview/ui_protoview.cpp @@ -326,6 +330,7 @@ set(EXTAPPLIST audio_test wardrivemap tpmsrx + tpmstx protoview adsbtx #morse_tx diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 42bfe0fa7..e82df4179 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -97,6 +97,7 @@ MEMORY ram_external_app_keeloqtx (rwx) : org = 0xADF80000, len = 32k ram_external_app_rtty_rx (rwx) : org = 0xADF90000, len = 32k ram_external_app_rtty_tx (rwx) : org = 0xADFA0000, len = 32k + ram_external_app_tpmstx (rwx) : org = 0xADFB0000, len = 32k } SECTIONS @@ -215,6 +216,12 @@ SECTIONS *(*ui*external_app*tpmsrx*); } > ram_external_app_tpmsrx + .external_app_tpmstx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_tpmstx.application_information)); + *(*ui*external_app*tpmstx*); + } > ram_external_app_tpmstx + .external_app_protoview : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_protoview.application_information)); diff --git a/firmware/application/external/tpmsrx/main.cpp b/firmware/application/external/tpmsrx/main.cpp index 552a14c06..6e429b3dc 100644 --- a/firmware/application/external/tpmsrx/main.cpp +++ b/firmware/application/external/tpmsrx/main.cpp @@ -75,7 +75,7 @@ __attribute__((section(".external_app.app_tpmsrx.application_information"), used }, /*.icon_color = */ ui::Color::green().v, /*.menu_location = */ app_location_t::RX, - /*.desired_menu_position = */ -1, + /*.desired_menu_position = */ 7, /*.m4_app_tag = portapack::spi_flash::image_tag_tpms */ {'P', 'T', 'P', 'M'}, /*.m4_app_offset = */ 0x00000000, // will be filled at compile time diff --git a/firmware/application/external/tpmsrx/tpms_app.cpp b/firmware/application/external/tpmsrx/tpms_app.cpp index ebf0f537a..c75ba385c 100644 --- a/firmware/application/external/tpmsrx/tpms_app.cpp +++ b/firmware/application/external/tpmsrx/tpms_app.cpp @@ -42,6 +42,25 @@ std::string type(tpms::Reading::Type type) { return to_string_dec_uint(toUType(type), 2); } +std::string type_name(tpms::Reading::Type type) { + switch (type) { + case tpms::Reading::Type::None: + return "None"; + case tpms::Reading::Type::FLM_64: + return "FLM_64"; + case tpms::Reading::Type::FLM_72: + return "FLM_72"; + case tpms::Reading::Type::FLM_80: + return "FLM_80"; + case tpms::Reading::Type::Schrader: + return "Schrader"; + case tpms::Reading::Type::GMC_96: + return "GMC_96"; + default: + return "Unknown"; + } +} + std::string id(tpms::TransponderID id) { return to_string_hex(id.value(), 8); } @@ -101,7 +120,8 @@ void TPMSRecentEntry::update(const tpms::Reading& reading) { } } -TPMSAppView::TPMSAppView(NavigationView&) { +TPMSAppView::TPMSAppView(NavigationView& nav) + : nav_{nav} { // baseband::run_image(portapack::spi_flash::image_tag_tpms); baseband::run_prepared_image(portapack::memory::map::m4_code.base()); @@ -135,6 +155,11 @@ TPMSAppView::TPMSAppView(NavigationView&) { }; options_temperature.set_by_value(format::temp_unit); + // Add on_select handler for entries + recent_entries_view.on_select = [this](const TPMSRecentEntry& entry) { + on_show_detail(entry); + }; + logger = std::make_unique(); if (logger) { logger->append(logs_dir / u"TPMS.TXT"); @@ -178,6 +203,7 @@ void TPMSAppView::on_packet(const tpms::Packet& packet) { const auto reading = reading_opt.value(); auto& entry = ::on_packet(recent, TPMSRecentEntry::Key{reading.type(), reading.id()}); entry.update(reading); + entry.signal_type = packet.signal_type(); recent_entries_view.set_dirty(); } @@ -191,6 +217,116 @@ void TPMSAppView::on_show_list() { recent_entries_view.focus(); } +void TPMSAppView::on_show_detail(const TPMSRecentEntry& entry) { + nav_.push(entry); +} + +// Detail view implementation +TPMSRecentEntryDetailView::TPMSRecentEntryDetailView(NavigationView& nav, const TPMSRecentEntry& entry) + : nav_{nav}, + entry_{entry} { + add_children({&labels, + &text_type, + &text_id, + &text_pressure, + &text_temperature, + &text_flags, + &text_count, + &button_done, + &button_save}); + + // Display entry data + text_type.set(format::type_name(entry.type)); + text_id.set(to_string_hex(entry.id.value(), 8)); + + if (entry.last_pressure.is_valid()) { + std::string pressure_str = format::pressure(entry.last_pressure.value()); + std::string unit_str = format::pressure_unit == PRESSURE_UNIT_PSI ? " PSI" : format::pressure_unit == PRESSURE_UNIT_BAR ? " BAR" + : " kPa"; + text_pressure.set(pressure_str + unit_str); + } else { + text_pressure.set("---"); + } + + if (entry.last_temperature.is_valid()) { + text_temperature.set(to_string_dec_int(entry.last_temperature.value().celsius(), 3) + " C"); + } else { + text_temperature.set("---"); + } + + if (entry.last_flags.is_valid()) { + text_flags.set(to_string_hex(entry.last_flags.value(), 2)); + } else { + text_flags.set("--"); + } + + text_count.set(to_string_dec_uint(entry.received_count, 4)); + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + button_save.on_select = [this](Button&) { + on_save(); + }; +} + +void TPMSRecentEntryDetailView::focus() { + button_done.focus(); +} + +void TPMSRecentEntryDetailView::set_entry(const TPMSRecentEntry& entry) { + entry_ = entry; +} + +void TPMSRecentEntryDetailView::on_save() { + auto timestamp = to_string_timestamp(rtc_time::now()); + std::string file_name = "TPMS_" + timestamp + ".TXT"; + ensure_directory(tpms_dir); + auto file_path = tpms_dir / file_name; + + if (save_file(file_path)) { + nav_.display_modal("Saved", "Packet saved to:\n" + file_name); + } else { + nav_.display_modal("Error", "Failed to save\npacket"); + } +} + +bool TPMSRecentEntryDetailView::save_file(const std::filesystem::path& path) { + File f; + auto error = f.create(path); + if (error.is_valid()) + return false; + + // Save in format compatible with TX app + std::string content = "Type=" + to_string_dec_uint(toUType(entry_.type), 1) + "\n"; + content += "ID=" + to_string_hex(entry_.id.value(), 8) + "\n"; + + if (entry_.last_pressure.is_valid()) { + content += "Pressure=" + to_string_dec_int(entry_.last_pressure.value().kilopascal(), 1) + "\n"; + } else { + content += "Pressure=240\n"; // Default value + } + + if (entry_.last_temperature.is_valid()) { + content += "Temperature=" + to_string_dec_int(entry_.last_temperature.value().celsius(), 1) + "\n"; + } else { + content += "Temperature=25\n"; // Default value + } + + if (entry_.last_flags.is_valid()) { + content += "Flags=" + to_string_hex(entry_.last_flags.value(), 2) + "\n"; + } else { + content += "Flags=00\n"; + } + + // Save signal type for proper retransmission + content += "SignalType=" + to_string_dec_uint(toUType(entry_.signal_type), 1) + "\n"; + + f.write(content.c_str(), content.length()); + return true; +} + } // namespace ui::external_app::tpmsrx namespace ui { diff --git a/firmware/application/external/tpmsrx/tpms_app.hpp b/firmware/application/external/tpmsrx/tpms_app.hpp index cc5891327..3846b55ac 100644 --- a/firmware/application/external/tpmsrx/tpms_app.hpp +++ b/firmware/application/external/tpmsrx/tpms_app.hpp @@ -53,6 +53,7 @@ struct TPMSRecentEntry { tpms::Reading::Type type{invalid_key.first}; tpms::TransponderID id{invalid_key.second}; + tpms::SignalType signal_type{tpms::SignalType::OOK_8k192_Schrader}; size_t received_count{0}; @@ -89,6 +90,64 @@ class TPMSLogger { using TPMSRecentEntriesView = RecentEntriesView; +class TPMSRecentEntryDetailView : public View { + public: + TPMSRecentEntryDetailView(NavigationView& nav, const TPMSRecentEntry& entry); + + void set_entry(const TPMSRecentEntry& entry); + const TPMSRecentEntry& entry() const { return entry_; } + + void focus() override; + + private: + NavigationView& nav_; + TPMSRecentEntry entry_; + + void on_save(); + bool save_file(const std::filesystem::path& path); + + Labels labels{ + {{0 * 8, 1 * 16}, "Type:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 3 * 16}, "ID:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 5 * 16}, "Pressure:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 7 * 16}, "Temperature:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 9 * 16}, "Flags:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 11 * 16}, "Count:", Theme::getInstance()->fg_light->foreground}, + }; + + Text text_type{ + {8 * 8, 1 * 16, 20 * 8, 16}, + ""}; + + Text text_id{ + {8 * 8, 3 * 16, 20 * 8, 16}, + ""}; + + Text text_pressure{ + {12 * 8, 5 * 16, 16 * 8, 16}, + ""}; + + Text text_temperature{ + {14 * 8, 7 * 16, 14 * 8, 16}, + ""}; + + Text text_flags{ + {8 * 8, 9 * 16, 20 * 8, 16}, + ""}; + + Text text_count{ + {8 * 8, 11 * 16, 20 * 8, 16}, + ""}; + + Button button_save{ + {0 * 8, 13 * 16, 14 * 8, 32}, + "Save"}; + + Button button_done{ + {16 * 8, 13 * 16, 14 * 8, 32}, + "Done"}; +}; + class TPMSAppView : public View { public: TPMSAppView(NavigationView& nav); @@ -105,6 +164,8 @@ class TPMSAppView : public View { std::string title() const override { return "TPMS RX"; }; private: + NavigationView& nav_; + RxRadioState radio_state_{ 314900000 /* frequency*/ , @@ -188,6 +249,7 @@ class TPMSAppView : public View { void on_packet(const tpms::Packet& packet); void on_show_list(); + void on_show_detail(const TPMSRecentEntry& entry); void update_view(); }; diff --git a/firmware/application/external/tpmstx/main.cpp b/firmware/application/external/tpmstx/main.cpp new file mode 100644 index 000000000..1f323bff2 --- /dev/null +++ b/firmware/application/external/tpmstx/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2026 + * + * 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.hpp" +#include "tpms_tx_app.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::tpmstx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::tpmstx + +extern "C" { + +__attribute__((section(".external_app.app_tpmstx.application_information"), used)) application_information_t _application_information_tpmstx = { + /*.memory_location = */ (uint8_t*)0x00000000, // will be filled at compile time + /*.externalAppEntry = */ ui::external_app::tpmstx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "TPMS TX", + /*.bitmap_data = */ { + 0xC0, + 0x03, + 0xF0, + 0x0F, + 0x18, + 0x18, + 0xEC, + 0x37, + 0x36, + 0x6D, + 0x3A, + 0x59, + 0x4B, + 0xD5, + 0x8B, + 0xD3, + 0xCB, + 0xD1, + 0xAB, + 0xD2, + 0x9A, + 0x5C, + 0xB6, + 0x6C, + 0xEC, + 0x37, + 0x18, + 0x18, + 0xF0, + 0x0F, + 0xC0, + 0x03, + }, + /*.icon_color = */ ui::Color::green().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ 6, + + /*.m4_app_tag = portapack::spi_flash::image_tag_ook */ {'P', 'O', 'O', 'K'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/external/tpmstx/tpms_tx_app.cpp b/firmware/application/external/tpmstx/tpms_tx_app.cpp new file mode 100644 index 000000000..5315dc73a --- /dev/null +++ b/firmware/application/external/tpmstx/tpms_tx_app.cpp @@ -0,0 +1,812 @@ +/* + * Copyright (C) 2026 + * + * 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 "tpms_tx_app.hpp" +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "spi_image.hpp" +#include "ui_fileman.hpp" +#include "ui_freqman.hpp" +#include "manchester.hpp" +#include "rtc_time.hpp" +#include "file_path.hpp" +#include "encoders.hpp" +#include "crc.hpp" + +using namespace portapack; +using namespace tpms; + +namespace ui::external_app::tpmstx { + +void TPMSTXView::focus() { + options_packet_type.focus(); +} + +void TPMSTXView::update_signal_type_from_packet() { + // Auto-select signal type based on packet type + switch (packet_type_) { + case tpms::Reading::Type::Schrader: + signal_type_ = tpms::SignalType::OOK_8k192_Schrader; + break; + case tpms::Reading::Type::FLM_64: + case tpms::Reading::Type::FLM_72: + case tpms::Reading::Type::FLM_80: + signal_type_ = tpms::SignalType::FSK_19k2_Schrader; + break; + case tpms::Reading::Type::GMC_96: + signal_type_ = tpms::SignalType::OOK_8k4_Schrader; + break; + default: + signal_type_ = tpms::SignalType::OOK_8k192_Schrader; + break; + } + // Note: Don't call switch_baseband() here - it's called when needed +} + +void TPMSTXView::switch_baseband() { + // Switch baseband image based on signal type + // Must shutdown first to avoid BBRunning error + baseband::shutdown(); + chThdSleepMilliseconds(100); + + if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + baseband::run_image(portapack::spi_flash::image_tag_fsktx); + } else { + baseband::run_image(portapack::spi_flash::image_tag_ook); + } + + chThdSleepMilliseconds(100); +} + +void TPMSTXView::update_packet_display() { + // Only update status text - field values are already set by user interaction + // or by explicit set_value calls when loading from file + std::string status = "ID:" + to_string_hex(transponder_id_, 8); + + // Note protocol-specific limitations + if (packet_type_ == tpms::Reading::Type::Schrader) { + status = "ID:" + to_string_hex(transponder_id_ & 0x00FFFFFF, 6) + " (24bit)"; + } + + status += " " + to_string_dec_uint(pressure_kpa_) + "kPa"; + + // Check for pressure overflow and warn user + if (packet_type_ == tpms::Reading::Type::GMC_96) { + if (pressure_kpa_ > 701) { + status += " !MAX"; // GMC_96 max is 701 kPa (101.7 PSI) + } + } else { + if (pressure_kpa_ > 340) { + status += " !MAX"; // Schrader/FLM max is 340 kPa (49.3 PSI) + } + } + + // Temperature note for protocols that support it + if (packet_type_ == tpms::Reading::Type::GMC_96 || + packet_type_ == tpms::Reading::Type::FLM_64 || + packet_type_ == tpms::Reading::Type::FLM_72 || + packet_type_ == tpms::Reading::Type::FLM_80) { + status += " " + to_string_dec_int(temperature_c_) + "C"; + } else { + status += " (no temp)"; + } + + text_status.set(status); +} + +void TPMSTXView::update_field_visibility() { + // Schrader: Show Func, Hide Temp (no temperature support) + // GMC_96, FLM_xx: Hide Func, Show Temp (temperature supported, no func field) + if (packet_type_ == tpms::Reading::Type::Schrader) { + // Show Func field and label + label_flags.hidden(false); + field_flags.hidden(false); + // Hide Temp field, label, and unit selector + label_temperature.hidden(true); + field_temperature.hidden(true); + options_temperature.hidden(true); + // Show 24-bit ID field, hide 32-bit ID field + field_transponder_id_24.hidden(false); + field_transponder_id_32.hidden(true); + } else { + // Hide Func field and label + label_flags.hidden(true); + field_flags.hidden(true); + // Show Temp field, label, and unit selector + label_temperature.hidden(false); + field_temperature.hidden(false); + options_temperature.hidden(false); + // Show 32-bit ID field, hide 24-bit ID field + field_transponder_id_24.hidden(true); + field_transponder_id_32.hidden(false); + } + // Force screen refresh to clear hidden widgets + set_dirty(); +} + +void TPMSTXView::on_pressure_unit_change() { + // Convert displayed pressure to new unit and update field + units::Pressure pressure(pressure_kpa_); + int display_value = 0; + + if (format::pressure_unit == PRESSURE_UNIT_PSI) { + display_value = pressure.psi(); + } else if (format::pressure_unit == PRESSURE_UNIT_BAR) { + display_value = pressure.bar(); + } else { + display_value = pressure.kilopascal(); + } + + field_pressure.set_value(display_value); +} + +void TPMSTXView::on_temperature_unit_change() { + // Convert displayed temperature to new unit and update field + units::Temperature temperature(temperature_c_); + int display_value = 0; + + if (format::temp_unit == TEMP_UNIT_FAHRENHEIT) { + display_value = temperature.fahrenheit(); + } else { + display_value = temperature.celsius(); + } + + field_temperature.set_value(display_value); +} + +void TPMSTXView::encode_and_transmit() { + if (!is_transmitting_) return; + + // Build TPMS packet based on signal type + std::string binary_string; + uint32_t symbol_rate; + uint32_t sample_rate; + + // Set sample rate based on signal type to match transmitter config + if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + sample_rate = 2280000; // 2.28 MHz for FSK (matches transmitter_model config) + } else { + sample_rate = 2000000; // 2 MHz for OOK + } + + if (signal_type_ == tpms::SignalType::OOK_8k192_Schrader) { + // Schrader OOK 8k192 format (Type = Schrader) + // Preamble: 11*2, 01*14, 11, 10 + // Data: 3 bits function code, 24 bits ID, 8 bits pressure, 2 bits checksum + // Total: 37 Manchester symbols (74 bits after encoding) + // NOTE: This protocol does NOT support temperature transmission + // NOTE: Only lower 24 bits of ID are transmitted + // NOTE: Function code (flags_) is stored as 3-bit value (0-7) + // RX will display it combined with checksum in upper nibble + + symbol_rate = 8192; + + // Preamble as expected by RX decoder + binary_string = "1111"; // 11*2 + for (int i = 0; i < 14; i++) { + binary_string += "01"; + } + binary_string += "1110"; // 11, 10 + + // Build data bits (pre-Manchester) + uint64_t data = 0; + + // 3-bit flags (0-7, stored directly) + uint8_t flags_3bit = flags_ & 0x07; + data |= ((uint64_t)flags_3bit << 34); + + // 24-bit ID (only use lower 24 bits - protocol limitation) + uint32_t id_24bit = transponder_id_ & 0x00FFFFFF; + data |= ((uint64_t)id_24bit << 10); + + // 8-bit pressure (convert kPa to raw: kPa * 3/4) + // Clamp to prevent overflow: max raw = 255, max kPa = 255 * 4/3 = 340 (49.3 PSI) + uint16_t pressure_clamped = (pressure_kpa_ > 340) ? 340 : pressure_kpa_; + uint8_t pressure_raw = (pressure_clamped * 3 / 4); + data |= ((uint64_t)pressure_raw << 2); + + // Calculate 2-bit checksum: sum all 2-bit pairs, result & 3 must equal 3 + uint32_t checksum_calc = (data >> 36) & 1; // First bit + for (size_t i = 1; i < 37; i += 2) { + checksum_calc += (data >> (37 - i - 2)) & 3; + } + uint8_t checksum_2bit = (3 - (checksum_calc & 3)) & 3; + data |= checksum_2bit; + + // Manchester encode the 37 data bits + for (int i = 36; i >= 0; i--) { + if ((data >> i) & 1) { + binary_string += "10"; // '1' = 10 in Manchester + } else { + binary_string += "01"; // '0' = 01 in Manchester + } + } + + } else if (signal_type_ == tpms::SignalType::OOK_8k4_Schrader) { + // GMC_96 OOK 8k4 format + symbol_rate = 8400; + + // Preamble: 01*40 + for (int i = 0; i < 40; i++) { + binary_string += "01"; + } + + // First nibble of System ID (0x4) - part of preamble pattern match + // RX looks for pattern ending with: 01010101...01100101 + // The "01100101" = Manchester for 0x4, and is consumed by pattern matcher + binary_string += "01"; // 0 + binary_string += "10"; // 1 + binary_string += "01"; // 0 + binary_string += "01"; // 0 (0100 = 0x4) + + // Remaining 20 bits of system ID (padding with zeros) + // These are the first 20 bits of the 76-bit payload + for (int i = 0; i < 20; i++) { + binary_string += "01"; // All zeros for padding + } + + // 32-bit ID (Manchester encoded) + for (int i = 31; i >= 0; i--) { + if ((transponder_id_ >> i) & 1) { + binary_string += "10"; + } else { + binary_string += "01"; + } + } + + // Pressure: kPa * 4/11 + // Clamp to prevent overflow: max raw = 255, max kPa = 255 * 11/4 = 701.25 (101.7 PSI) + uint16_t pressure_clamped = (pressure_kpa_ > 701) ? 701 : pressure_kpa_; + uint8_t pressure_gmc = (pressure_clamped * 4 / 11); + for (int i = 7; i >= 0; i--) { + if ((pressure_gmc >> i) & 1) { + binary_string += "10"; + } else { + binary_string += "01"; + } + } + + // Temperature: temp + 61 + uint8_t temp_gmc = (temperature_c_ + 61) & 0xFF; + for (int i = 7; i >= 0; i--) { + if ((temp_gmc >> i) & 1) { + binary_string += "10"; + } else { + binary_string += "01"; + } + } + + // Checksum: sum of all data bytes (system ID + ID + pressure + temp) + // System ID byte 0: (0x4 << 4) | 0x0 = 0x40 + // System ID byte 1: 0x00 + // System ID byte 2: 0x00 + uint8_t checksum = 0x40; // First byte of system ID + checksum += 0x00; // Second byte of system ID + checksum += 0x00; // Third byte of system ID + + // Add all 4 bytes of the ID + checksum += (transponder_id_ >> 24) & 0xFF; + checksum += (transponder_id_ >> 16) & 0xFF; + checksum += (transponder_id_ >> 8) & 0xFF; + checksum += transponder_id_ & 0xFF; + + // Add pressure and temperature + checksum += pressure_gmc; + checksum += temp_gmc; + + for (int i = 7; i >= 0; i--) { + if ((checksum >> i) & 1) { + binary_string += "10"; + } else { + binary_string += "01"; + } + } + + } else if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + // FSK 19.2k for FLM variants + // Data is Manchester encoded: each data bit becomes two FSK symbols + // Preamble: 14 x '01' + '10' = 30 bits (matches RX pattern exactly) + // Payload: 160 Manchester-encoded bits (80 data bits max) + + symbol_rate = 19200; + + // Build preamble: 14 pairs of "01" followed by "10" + for (int i = 0; i < 14; i++) { + binary_string += "01"; + } + binary_string += "10"; + + std::array data_bytes = {0}; // 160 bits = 20 bytes max + size_t num_data_bits = 0; + + if (packet_type_ == tpms::Reading::Type::FLM_64) { + // FLM_64: 64 bits (8 bytes) + // Bytes 0-3: ID (32 bits) + // Byte 4: Pressure (8 bits) + // Byte 5: Temperature (8 bits, LSB 7 bits used) + // Byte 6: Padding + // Byte 7: Sum checksum of bytes 0-6 (low 8 bits) + + num_data_bits = 64; + + data_bytes[0] = (transponder_id_ >> 24) & 0xFF; + data_bytes[1] = (transponder_id_ >> 16) & 0xFF; + data_bytes[2] = (transponder_id_ >> 8) & 0xFF; + data_bytes[3] = transponder_id_ & 0xFF; + // Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI) + uint16_t pressure_clamped_flm64 = (pressure_kpa_ > 340) ? 340 : pressure_kpa_; + data_bytes[4] = (pressure_clamped_flm64 * 3 / 4); + data_bytes[5] = ((temperature_c_ + 56) & 0x7F); // LSB 7 bits only + data_bytes[6] = 0x00; // Padding + + // Addition checksum over bytes 0-6 + uint32_t checksum = 0; + for (int i = 0; i < 7; i++) { + checksum += data_bytes[i]; + } + data_bytes[7] = checksum & 0xFF; + + } else if (packet_type_ == tpms::Reading::Type::FLM_72) { + // FLM_72: 72 bits (9 bytes) + // Bytes 0-3: ID (32 bits) + // Byte 4: Padding + // Byte 5: Pressure (8 bits) + // Byte 6: Temperature (8 bits) + // Bytes 7-8: CRC field - RX processes ALL 9 bytes and expects CRC result = 0 + + num_data_bits = 72; + + data_bytes[0] = (transponder_id_ >> 24) & 0xFF; + data_bytes[1] = (transponder_id_ >> 16) & 0xFF; + data_bytes[2] = (transponder_id_ >> 8) & 0xFF; + data_bytes[3] = transponder_id_ & 0xFF; + data_bytes[4] = 0x00; // Padding + // Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI) + uint16_t pressure_clamped_flm = (pressure_kpa_ > 340) ? 340 : pressure_kpa_; + data_bytes[5] = (pressure_clamped_flm * 3 / 4); + data_bytes[6] = (temperature_c_ + 56) & 0xFF; + data_bytes[7] = 0x00; // Set to 0 initially + + // Calculate CRC over bytes 0-7, place result in byte 8 + // When RX calculates CRC over bytes 0-8, it should get residual 0 + CRC<8> crc{0x01, 0x00}; + for (int i = 0; i < 8; i++) { + crc.process_byte(data_bytes[i]); + } + data_bytes[8] = crc.checksum() & 0xFF; + + } else if (packet_type_ == tpms::Reading::Type::FLM_80) { + // FLM_80: 80 bits (10 bytes) + // Byte 0: Padding + // Bytes 1-4: ID (32 bits) + // Byte 5: Padding + // Byte 6: Pressure (8 bits) + // Byte 7: Temperature (8 bits) + // Bytes 8-9: CRC field - RX processes bytes 1-9 and expects CRC result = 0 + + num_data_bits = 80; + + data_bytes[0] = 0x00; // Padding (not included in CRC) + data_bytes[1] = (transponder_id_ >> 24) & 0xFF; + data_bytes[2] = (transponder_id_ >> 16) & 0xFF; + data_bytes[3] = (transponder_id_ >> 8) & 0xFF; + data_bytes[4] = transponder_id_ & 0xFF; + data_bytes[5] = 0x00; // Padding + // Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI) + uint16_t pressure_clamped_flm80 = (pressure_kpa_ > 340) ? 340 : pressure_kpa_; + data_bytes[6] = (pressure_clamped_flm80 * 3 / 4); + data_bytes[7] = (temperature_c_ + 56) & 0xFF; + data_bytes[8] = 0x00; // Padding/Reserved + data_bytes[9] = 0x00; // CRC byte + + // Calculate CRC over bytes 1-8 (all bytes except 0 and 9) + // When RX calculates CRC over bytes 1-9, it should get residual 0 + CRC<8> crc{0x01, 0x00}; + for (int i = 1; i <= 8; i++) { + crc.process_byte(data_bytes[i]); + } + data_bytes[9] = crc.checksum() & 0xFF; + } + + // Manchester-encode data bits for FSK + // RX ManchesterDecoder with sense=0: '1' = "10", '0' = "01" + size_t num_bytes = num_data_bits / 8; + for (size_t byte_idx = 0; byte_idx < num_bytes; byte_idx++) { + uint8_t byte_val = data_bytes[byte_idx]; + for (int bit_idx = 7; bit_idx >= 0; bit_idx--) { + if ((byte_val >> bit_idx) & 1) { + binary_string += "10"; // Manchester '1' + } else { + binary_string += "01"; // Manchester '0' + } + } + } + + // Pad payload to 160 bits with valid Manchester pairs ("01" = 0) + // Using proper Manchester encoding maintains clock recovery sync + while (binary_string.length() < 190) { // 30 preamble + 160 payload + binary_string += "01"; // Manchester-encoded zero + } + + } else { + text_status.set("Unknown signal type"); + stop_tx(); + return; + } + + // Convert binary string to bitstream + size_t bitstream_length = encoders::make_bitstream(binary_string); + + // Calculate samples per bit (round to nearest for best timing accuracy) + uint32_t samples_per_bit = (sample_rate + symbol_rate / 2) / symbol_rate; + + // Update status to show we're sending + text_status.set("TX: " + to_string_dec_uint(binary_string.length()) + " bits"); + + // Send via appropriate baseband (OOK or FSK) + if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + // FSK mode: 19.2 kbaud, 38.4 kHz shift + baseband::set_fsk_data( + bitstream_length, + samples_per_bit, + 38400, // 38.4 kHz shift + 256); // Progress notice interval + } else { + // OOK mode + baseband::set_ook_data( + bitstream_length, + samples_per_bit, + repeat_count_, + pause_duration_); + } +} + +void TPMSTXView::handle_tx_complete() { + // For FSK (FLM packets), handle repeats at application level + // OOK repeats are handled by the baseband processor + if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + fsk_repeat_counter_++; + + if (fsk_repeat_counter_ < repeat_count_) { + // More repeats needed - send the next one + // Update progress bar + progressbar.set_value(fsk_repeat_counter_); + + // Wait for FSK processor to fully complete and reset + // This allows the shared memory bitstream to be safely rewritten + chThdSleepMilliseconds(50); + + encode_and_transmit(); + } else { + // All FSK repeats complete + stop_tx(); + } + } else { + // OOK repeats are handled by baseband, just stop here + stop_tx(); + } +} + +void TPMSTXView::start_tx() { + if (is_transmitting_) + return; + + is_transmitting_ = true; + + progressbar.set_max(repeat_count_); + progressbar.set_value(0); + + button_transmit.set_text("STOP TX"); + text_status.set("Transmitting..."); + + // Initialize FSK repeat counter for application-level repeat handling + fsk_repeat_counter_ = 0; + + // Switch to appropriate baseband before transmission + switch_baseband(); + + // Configure transmitter based on modulation type + if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + // FSK configuration + transmitter_model.set_sampling_rate(2280000); // 2.28 MHz for FSK + transmitter_model.set_baseband_bandwidth(1750000); + } else { + // OOK configuration + transmitter_model.set_sampling_rate(2000000); // 2 MHz for OOK + transmitter_model.set_baseband_bandwidth(1750000); + } + + transmitter_model.enable(); + + // Start transmission + encode_and_transmit(); +} + +void TPMSTXView::stop_tx() { + if (!is_transmitting_) + return; + + is_transmitting_ = false; + transmitter_model.disable(); + + button_transmit.set_text("START TX"); + text_status.set("Transmission complete"); + progressbar.set_value(0); +} + +TPMSTXView::TPMSTXView(NavigationView& nav) + : nav_{nav} { + // Start with OOK baseband (will switch if needed) + baseband::run_image(portapack::spi_flash::image_tag_ook); + + add_children({&labels, + &label_temperature, + &label_flags, + &options_frequency, + &options_packet_type, + &field_transponder_id_24, + &field_transponder_id_32, + &field_pressure, + &options_pressure, + &field_temperature, + &options_temperature, + &field_flags, + &field_repeat, + &button_load, + &button_save, + &tx_view, + &button_transmit, + &text_status, + &progressbar}); + + // Set label styles to match other labels (light grey) + label_temperature.set_style(Theme::getInstance()->fg_light); + label_flags.set_style(Theme::getInstance()->fg_light); + + // Initialize values + options_frequency.set_by_value(transmitter_model.target_frequency()); + options_packet_type.set_selected_index(0); + field_repeat.set_value(repeat_count_); + + // Set initial signal type based on packet type + update_signal_type_from_packet(); + + // Initialize unit selection + options_pressure.set_by_value(format::pressure_unit); + options_temperature.set_by_value(format::temp_unit); + + // Initialize field values from default member variables + // For pressure and temperature, use unit conversion to display in selected units + field_transponder_id_24.set_value(transponder_id_ & 0x00FFFFFF); + field_transponder_id_32.set_value(transponder_id_); + on_pressure_unit_change(); + on_temperature_unit_change(); + field_flags.set_value(flags_); + + update_field_visibility(); + update_packet_display(); + + // Event handlers + options_frequency.on_change = [this](size_t, OptionsField::value_t v) { + transmitter_model.set_target_frequency(v); + }; + + options_packet_type.on_change = [this](size_t, int32_t value) { + packet_type_ = static_cast(value); + update_signal_type_from_packet(); + + // Sync field values when switching packet types + if (packet_type_ == tpms::Reading::Type::Schrader) { + // Switching to Schrader: copy from 32-bit field to 24-bit field (masked) + transponder_id_ = field_transponder_id_32.to_integer() & 0x00FFFFFF; + field_transponder_id_24.set_value(transponder_id_); + } else { + // Switching from Schrader: copy from 24-bit field to 32-bit field + transponder_id_ = field_transponder_id_24.to_integer(); + field_transponder_id_32.set_value(transponder_id_); + } + + update_field_visibility(); + update_packet_display(); + }; + + options_pressure.on_change = [this](size_t, int32_t i) { + format::pressure_unit = (uint8_t)i; + on_pressure_unit_change(); + }; + + options_temperature.on_change = [this](size_t, int32_t i) { + format::temp_unit = (uint8_t)i; + on_temperature_unit_change(); + }; + + field_transponder_id_24.on_change = [this](SymField&) { + transponder_id_ = field_transponder_id_24.to_integer() & 0x00FFFFFF; + update_packet_display(); + }; + + field_transponder_id_32.on_change = [this](SymField&) { + transponder_id_ = field_transponder_id_32.to_integer(); + update_packet_display(); + }; + + field_pressure.on_change = [this](int32_t value) { + // Convert from displayed unit to kPa for internal storage + if (format::pressure_unit == PRESSURE_UNIT_PSI) { + pressure_kpa_ = value * 6895 / 1000; + } else if (format::pressure_unit == PRESSURE_UNIT_BAR) { + pressure_kpa_ = value * 100; + } else { + pressure_kpa_ = value; + } + update_packet_display(); + }; + + field_temperature.on_change = [this](int32_t value) { + // Convert from displayed unit to Celsius for internal storage + if (format::temp_unit == TEMP_UNIT_FAHRENHEIT) { + temperature_c_ = (value - 32) * 5 / 9; + } else { + temperature_c_ = value; + } + update_packet_display(); + }; + + field_flags.on_change = [this](int32_t value) { + flags_ = value & 0x07; + update_packet_display(); + }; + + field_repeat.on_change = [this](int32_t value) { + repeat_count_ = value; + }; + + button_transmit.on_select = [this](Button&) { + if (is_transmitting_) { + stop_tx(); + } else { + start_tx(); + } + }; + + button_load.on_select = [this, &nav](Button&) { + auto open_view = nav.push(".TXT"); + open_view->on_changed = [this](std::filesystem::path file_path) { + // Load TPMS packet from file + File f; + auto error = f.open(file_path); + if (!error.is_valid()) { + char buffer[512]; + auto result = f.read(buffer, sizeof(buffer) - 1); + if (result.is_ok()) { + buffer[result.value()] = 0; + + // Parse the file format (key=value format, one per line) + std::string content(buffer); + size_t pos = 0; + + auto parse_line = [&content, &pos](const std::string& key) -> std::string { + size_t key_pos = content.find(key + "=", pos); + if (key_pos != std::string::npos) { + size_t value_start = key_pos + key.length() + 1; + size_t value_end = content.find('\n', value_start); + if (value_end == std::string::npos) value_end = content.length(); + return content.substr(value_start, value_end - value_start); + } + return ""; + }; + + std::string type_str = parse_line("Type"); + std::string id_str = parse_line("ID"); + std::string pressure_str = parse_line("Pressure"); + std::string temp_str = parse_line("Temperature"); + std::string flags_str = parse_line("Flags"); + std::string signal_type_str = parse_line("SignalType"); + + if (!type_str.empty()) { + int type = std::stoi(type_str); + if (type >= static_cast(tpms::Reading::Type::FLM_64) && + type <= static_cast(tpms::Reading::Type::GMC_96)) { + packet_type_ = static_cast(type); + options_packet_type.set_by_value(type); + } + } + + if (!id_str.empty()) { + transponder_id_ = std::stoul(id_str, nullptr, 16); + field_transponder_id_24.set_value(transponder_id_ & 0x00FFFFFF); + field_transponder_id_32.set_value(transponder_id_); + } + + if (!pressure_str.empty()) { + pressure_kpa_ = std::stoi(pressure_str); + on_pressure_unit_change(); + } + + if (!temp_str.empty()) { + temperature_c_ = std::stoi(temp_str); + field_temperature.set_value(temperature_c_); + } + + if (!flags_str.empty()) { + uint8_t loaded_flags = std::stoul(flags_str, nullptr, 16); + // Handle old format files: extract bits 6-4 if value > 7 + flags_ = (loaded_flags > 7) ? ((loaded_flags >> 4) & 0x07) : (loaded_flags & 0x07); + field_flags.set_value(flags_); + } + + // Load signal type if available, otherwise auto-select based on packet type + if (!signal_type_str.empty()) { + int signal_type = std::stoi(signal_type_str); + if (signal_type >= 1 && signal_type <= 3) { + signal_type_ = static_cast(signal_type); + } else { + // Fall back to auto-detect + update_signal_type_from_packet(); + } + } else { + // Fall back to auto-detect for backwards compatibility + update_signal_type_from_packet(); + } + + update_packet_display(); + update_field_visibility(); + text_status.set("Loaded: " + file_path.filename().string()); + } else { + text_status.set("Error reading file"); + } + } else { + text_status.set("Error opening file"); + } + }; + }; + + button_save.on_select = [this](Button&) { + // Save current TPMS packet to file + auto timestamp = to_string_timestamp(rtc_time::now()); + std::string file_name = "TPMS_" + timestamp + ".TXT"; + ensure_directory(tpms_dir); + auto file_path = tpms_dir / file_name; + + File f; + auto error = f.create(file_path); + if (!error.is_valid()) { + std::string content = "Type=" + to_string_dec_uint(toUType(packet_type_), 1) + "\n"; + content += "ID=" + to_string_hex(transponder_id_, 8) + "\n"; + content += "Pressure=" + to_string_dec_uint(pressure_kpa_) + "\n"; + content += "Temperature=" + to_string_dec_int(temperature_c_) + "\n"; + content += "Flags=" + to_string_hex(flags_ & 0x07, 1) + "\n"; + + f.write(content.c_str(), content.length()); + text_status.set("Saved: " + file_name); + } else { + text_status.set("Error saving file"); + } + }; +} + +TPMSTXView::~TPMSTXView() { + stop_tx(); + transmitter_model.disable(); + baseband::shutdown(); +} + +} // namespace ui::external_app::tpmstx diff --git a/firmware/application/external/tpmstx/tpms_tx_app.hpp b/firmware/application/external/tpmstx/tpms_tx_app.hpp new file mode 100644 index 000000000..69d0d10e1 --- /dev/null +++ b/firmware/application/external/tpmstx/tpms_tx_app.hpp @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2026 + * + * 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. + */ + +#ifndef __TPMS_TX_APP_H__ +#define __TPMS_TX_APP_H__ + +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_transmitter.hpp" +#include "transmitter_model.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "portapack.hpp" +#include "message.hpp" +#include "tpms_packet.hpp" +#include "file_path.hpp" +#include "string_format.hpp" +#include "units.hpp" + +namespace ui::external_app::tpmstx { + +namespace format { +static uint8_t pressure_unit{PRESSURE_UNIT_PSI}; +static uint8_t temp_unit{TEMP_UNIT_CELSIUS}; +} // namespace format + +class TPMSTXView : public View { + public: + TPMSTXView(NavigationView& nav); + ~TPMSTXView(); + + void focus() override; + + std::string title() const override { return "TPMS TX"; }; + + private: + NavigationView& nav_; + + TxRadioState radio_state_{ + 314900000, /* frequency */ + 1750000, /* bandwidth */ + 2457600 /* sampling rate */ + }; + + app_settings::SettingsManager settings_{ + "tx_tpms", + app_settings::Mode::TX, + { + {"pressure_unit"sv, &format::pressure_unit}, + {"temp_unit"sv, &format::temp_unit}, + }}; + + // TPMS packet data + tpms::Reading::Type packet_type_{tpms::Reading::Type::Schrader}; + uint32_t transponder_id_{0x12345678}; + uint16_t pressure_kpa_{240}; // Default ~35 PSI + int16_t temperature_c_{25}; // Default 25°C + uint8_t flags_{0x00}; // 3-bit function code (0-7), checksum is auto-calculated + tpms::SignalType signal_type_{tpms::SignalType::FSK_19k2_Schrader}; + + uint8_t repeat_count_{5}; + uint32_t pause_duration_{50}; // ms between repeats + + bool is_transmitting_{false}; + + // FSK-specific repeat tracking (OOK repeats are handled by baseband) + uint8_t fsk_repeat_counter_{0}; + uint32_t fsk_repeat_timer_id_{0}; + + void update_signal_type_from_packet(); + void switch_baseband(); + + void start_tx(); + void stop_tx(); + void send_packet(); + void encode_and_transmit(); + void update_packet_display(); + void on_pressure_unit_change(); + void on_temperature_unit_change(); + void update_field_visibility(); + + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + progressbar.set_value(message.progress); + if (message.done) { + handle_tx_complete(); + } + }}; + + void handle_tx_complete(); + + Labels labels{ + {{0 * 8, 0 * 16}, "Freq:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 1 * 16}, "Type:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 2 * 16}, "ID:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 3 * 16}, "Pres:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 5 * 16}, "Rpt:", Theme::getInstance()->fg_light->foreground}, + }; + + Text label_temperature{ + {0 * 8, 4 * 16, 5 * 8, 16}, + "Temp:"}; + + Text label_flags{ + {0 * 8, 4 * 16, 5 * 8, 16}, + "Func:"}; + + OptionsField options_frequency{ + {6 * 8, 0 * 16}, + 5, + { + {"314.9", 314900000}, + {"315.0", 315000000}, + {"433.9", 433920000}, + }}; + + OptionsField options_packet_type{ + {6 * 8, 1 * 16}, + 10, + { + {"Schrader", (int32_t)tpms::Reading::Type::Schrader}, + {"FLM_64", (int32_t)tpms::Reading::Type::FLM_64}, + {"FLM_72", (int32_t)tpms::Reading::Type::FLM_72}, + {"FLM_80", (int32_t)tpms::Reading::Type::FLM_80}, + {"GMC_96", (int32_t)tpms::Reading::Type::GMC_96}, + }}; + + OptionsField options_pressure{ + {11 * 8, 3 * 16}, + 4, + {{"kPa", PRESSURE_UNIT_KPA}, + {"PSI", PRESSURE_UNIT_PSI}, + {"BAR", PRESSURE_UNIT_BAR}}}; + + OptionsField options_temperature{ + {11 * 8, 4 * 16}, + 2, + {{STR_DEGREES_C, TEMP_UNIT_CELSIUS}, + {STR_DEGREES_F, TEMP_UNIT_FAHRENHEIT}}}; + + SymField field_transponder_id_24{ + {6 * 8, 2 * 16}, + 6, + SymField::Type::Hex}; + + SymField field_transponder_id_32{ + {6 * 8, 2 * 16}, + 8, + SymField::Type::Hex}; + + NumberField field_pressure{ + {6 * 8, 3 * 16}, + 4, + {0, 9999}, + 1, + ' '}; + + NumberField field_temperature{ + {6 * 8, 4 * 16}, + 4, + {-99, 999}, + 1, + ' '}; + + NumberField field_flags{ + {6 * 8, 4 * 16}, + 1, + {0, 7}, + 1, + ' '}; + + NumberField field_repeat{ + {6 * 8, 5 * 16}, + 3, + {1, 100}, + 1, + ' '}; + + Button button_load{ + {0 * 8, 8 * 16 + 4, 7 * 8, 24}, + "Load"}; + + Button button_save{ + {8 * 8, 8 * 16 + 4, 7 * 8, 24}, + "Save"}; + + TransmitterView2 tx_view{ + {16 * 8, 0 * 16}, + true // Short UI format + }; + + Button button_transmit{ + {0 * 8, 11 * 16, 15 * 8, 32}, + "START TX"}; + + Text text_status{ + {0 * 8, 13 * 16 + 4, 30 * 8, 16}, + "Ready"}; + + ProgressBar progressbar{ + {0 * 8, 14 * 16 + 8, 30 * 8, 16}}; +}; + +} // namespace ui::external_app::tpmstx + +#endif /*__TPMS_TX_APP_H__*/ diff --git a/firmware/application/file_path.cpp b/firmware/application/file_path.cpp index e50077a7c..942052bd6 100644 --- a/firmware/application/file_path.cpp +++ b/firmware/application/file_path.cpp @@ -51,6 +51,7 @@ const std::filesystem::path whipcalc_dir = u"WHIPCALC"; const std::filesystem::path ook_editor_dir = u"OOKFILES"; const std::filesystem::path hopper_dir = u"HOPPER"; const std::filesystem::path subghz_dir = u"SUBGHZ"; +const std::filesystem::path tpms_dir = u"TPMS"; const std::filesystem::path waterfalls_dir = u"WATERFALLS"; const std::filesystem::path macaddress_dir = u"MACADDRESS"; const std::filesystem::path splash_dot_bmp = u"/splash.bmp"; diff --git a/firmware/application/file_path.hpp b/firmware/application/file_path.hpp index 7dd956abc..bfe0e6d0b 100644 --- a/firmware/application/file_path.hpp +++ b/firmware/application/file_path.hpp @@ -53,6 +53,7 @@ extern const std::filesystem::path whipcalc_dir; extern const std::filesystem::path ook_editor_dir; extern const std::filesystem::path hopper_dir; extern const std::filesystem::path subghz_dir; +extern const std::filesystem::path tpms_dir; extern const std::filesystem::path waterfalls_dir; extern const std::filesystem::path macaddress_dir; extern const std::filesystem::path keeloq_keys_dir; diff --git a/firmware/tools/external_app_info.py b/firmware/tools/external_app_info.py index 51fe7823e..5c6fef198 100644 --- a/firmware/tools/external_app_info.py +++ b/firmware/tools/external_app_info.py @@ -24,4 +24,4 @@ # external app address ranges below must match those in linker file "external.ld" maximum_application_size = 32*1024 external_apps_address_start = 0xADB00000 -external_apps_address_end = 0xADF00000 \ No newline at end of file +external_apps_address_end = 0xADFC0000 \ No newline at end of file