This commit is contained in:
Totoo
2025-12-21 11:21:55 +01:00
committed by GitHub
parent f71f19e719
commit f86d3e51f1
27 changed files with 3163 additions and 4 deletions
+5
View File
@@ -272,6 +272,10 @@ set(EXTCPPSRC
#flex_rx
external/flex_rx/main.cpp
external/flex_rx/ui_flex_rx.cpp
#subcarrx
external/subcarrx/main.cpp
external/subcarrx/ui_subcar.cpp
)
set(EXTAPPLIST
@@ -340,4 +344,5 @@ set(EXTAPPLIST
morse_practice
adult_toys_controller
flex_rx
subcarrx
)
+9
View File
@@ -88,6 +88,7 @@ MEMORY
ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k
ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k
}
@@ -484,5 +485,13 @@ SECTIONS
KEEP(*(.external_app.app_sstvrx.application_information));
*(*ui*external_app*sstvrx*);
} > ram_external_app_sstvrx
.external_app_subcarrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_subcarrx.application_information));
*(*ui*external_app*subcarrx*);
} > ram_external_app_subcarrx
}
+81
View File
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_subcar.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::subcarrx {
void initialize_app(ui::NavigationView& nav) {
nav.push<SubCarView>();
}
} // namespace ui::external_app::subcarrx
extern "C" {
__attribute__((section(".external_app.app_subcarrx.application_information"), used)) application_information_t _application_information_subcarrx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::subcarrx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "SubCar",
/*.bitmap_data = */ {
0xC0,
0x03,
0xE0,
0x07,
0x30,
0x0C,
0x30,
0x0C,
0x30,
0x0C,
0x30,
0x0C,
0xE0,
0x07,
0xC0,
0x03,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0x80,
0x07,
0x80,
0x03,
0x80,
0x07,
0x80,
0x01,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_acars */ {'P', 'S', 'C', 'D'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+459
View File
@@ -0,0 +1,459 @@
/*
* Copyright (C) 2026 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_subcar.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "file_path.hpp"
#include "portapack_persistent_memory.hpp"
using namespace portapack;
using namespace ui;
namespace ui::external_app::subcarrx {
std::string SubCarRecentEntry::to_csv() {
std::string csv = ";";
csv += SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)sensorType);
csv += ";" + to_string_dec_uint(bits) + ";";
csv += to_string_hex(data, 64 / 4) + ";" + to_string_hex(data2, 64 / 4);
return csv;
}
void SubCarLogger::log_data(SubCarRecentEntry& data) {
log_file.write_entry(data.to_csv());
}
void SubCarRecentEntryDetailView::update_data() {
// process protocol data
parseProtocol();
// set text elements
text_type.set(SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)entry_.sensorType));
text_id.set("0x" + to_string_hex(serial));
if (entry_.bits > 0) console.writeln("Bits: " + to_string_dec_uint(entry_.bits));
if (!btn.empty()) console.writeln("Btn: " + btn);
if (cnt != SD_NO_CNT) console.writeln("Cnt: " + to_string_dec_uint(cnt));
if (entry_.data != 0) console.writeln("Data : " + to_string_hex(entry_.data));
if (entry_.data2 != 0) console.writeln("Data2: " + to_string_hex(entry_.data2));
}
SubCarRecentEntryDetailView::SubCarRecentEntryDetailView(NavigationView& nav, const SubCarRecentEntry& entry)
: nav_{nav},
entry_{entry} {
add_children({&button_done,
&text_type,
&text_id,
&console,
&labels});
button_done.on_select = [&nav](const ui::Button&) {
nav.pop();
};
update_data();
}
void SubCarRecentEntryDetailView::focus() {
button_done.focus();
}
void SubCarView::focus() {
field_frequency.focus();
}
SubCarView::SubCarView(NavigationView& nav)
: nav_{nav} {
add_children({&rssi,
&channel,
&field_rf_amp,
&field_lna,
&field_vga,
&field_frequency,
&button_clear_list,
&check_log,
&labels,
&recent_entries_view});
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
logger = std::make_unique<SubCarLogger>();
button_clear_list.on_select = [this](Button&) {
recent.clear();
recent_entries_view.set_dirty();
};
field_frequency.set_step(10000);
check_log.on_select = [this](Checkbox&, bool v) {
logging = v;
if (logger && logging) {
logger->append(logs_dir.string() + "/SubCarLOG_" + to_string_timestamp(rtc_time::now()) + ".CSV");
logger->write_header();
}
};
check_log.set_value(logging);
const Rect content_rect{0, header_height, screen_width, screen_height - header_height};
recent_entries_view.set_parent_rect(content_rect);
recent_entries_view.on_select = [this](const SubCarRecentEntry& entry) {
nav_.push<SubCarRecentEntryDetailView>(entry);
};
baseband::set_subghzd_config(0, receiver_model.sampling_rate()); // 0=am
receiver_model.enable();
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
on_tick_second();
};
}
void SubCarView::on_tick_second() {
for (auto& entry : recent) {
entry.inc_age(1);
}
recent_entries_view.set_dirty();
}
void SubCarView::on_data(const SubCarDataMessage* data) {
SubCarRecentEntry key{data->sensorType, data->data, data->data2, data->bits};
if (logger && logging) {
logger->log_data(key);
}
auto matching_recent = find(recent, key.key());
if (matching_recent != std::end(recent)) {
// Found within. Move to front of list, increment counter.
(*matching_recent).reset_age();
recent.push_front(*matching_recent);
recent.erase(matching_recent);
} else {
recent.emplace_front(key);
truncate_entries(recent, 64);
}
recent_entries_view.set_dirty();
}
SubCarView::~SubCarView() {
rtc_time::signal_tick_second -= signal_token_tick_second;
receiver_model.disable();
baseband::shutdown();
}
const char* SubCarView::getSensorTypeName(FPROTO_SUBCAR_SENSOR type) {
switch (type) {
case FPC_SUZUKI:
return "Suzuki";
case FPC_VW:
return "VW";
case FPC_SUBARU:
return "Subaru";
case FPC_KIAV5:
return "Kia V5";
case FPC_KIAV3V4:
return "Kia V3/V4";
case FPC_KIAV2:
return "Kia V2";
case FPC_KIAV1:
return "Kia V1";
case FPC_KIAV0:
return "Kia V0";
case FPC_FORDV0:
return "Ford V0";
case FPC_FIATV0:
return "Fiat V0";
case FPC_BMWV0:
return "BMW V0";
case FPC_Invalid:
default:
return "Unknown";
}
}
std::string SubCarView::pad_string_with_spaces(int snakes) {
std::string paddedStr(snakes, ' ');
return paddedStr;
}
void SubCarView::on_freqchg(int64_t freq) {
field_frequency.set_value(freq);
}
void subaru_decode_count(const uint8_t* KB, uint16_t* count) {
uint8_t lo = 0;
if ((KB[4] & 0x40) == 0)
lo |= 0x01;
if ((KB[4] & 0x80) == 0)
lo |= 0x02;
if ((KB[5] & 0x01) == 0)
lo |= 0x04;
if ((KB[5] & 0x02) == 0)
lo |= 0x08;
if ((KB[6] & 0x01) == 0)
lo |= 0x10;
if ((KB[6] & 0x02) == 0)
lo |= 0x20;
if ((KB[5] & 0x40) == 0)
lo |= 0x40;
if ((KB[5] & 0x80) == 0)
lo |= 0x80;
uint8_t REG_SH1 = (KB[7] << 4) & 0xF0;
if (KB[5] & 0x04)
REG_SH1 |= 0x04;
if (KB[5] & 0x08)
REG_SH1 |= 0x08;
if (KB[6] & 0x80)
REG_SH1 |= 0x02;
if (KB[6] & 0x40)
REG_SH1 |= 0x01;
uint8_t REG_SH2 = ((KB[6] << 2) & 0xF0) | ((KB[7] >> 4) & 0x0F);
uint8_t SER0 = KB[3];
uint8_t SER1 = KB[1];
uint8_t SER2 = KB[2];
uint8_t total_rot = 4 + lo;
for (uint8_t i = 0; i < total_rot; ++i) {
uint8_t t_bit = (SER0 >> 7) & 1;
SER0 = ((SER0 << 1) & 0xFE) | ((SER1 >> 7) & 1);
SER1 = ((SER1 << 1) & 0xFE) | ((SER2 >> 7) & 1);
SER2 = ((SER2 << 1) & 0xFE) | t_bit;
}
uint8_t T1 = SER1 ^ REG_SH1;
uint8_t T2 = SER2 ^ REG_SH2;
uint8_t hi = 0;
if ((T1 & 0x10) == 0)
hi |= 0x04;
if ((T1 & 0x20) == 0)
hi |= 0x08;
if ((T2 & 0x80) == 0)
hi |= 0x02;
if ((T2 & 0x40) == 0)
hi |= 0x01;
if ((T1 & 0x01) == 0)
hi |= 0x40;
if ((T1 & 0x02) == 0)
hi |= 0x80;
if ((T2 & 0x08) == 0)
hi |= 0x20;
if ((T2 & 0x04) == 0)
hi |= 0x10;
*count = ((hi << 8) | lo) & 0xFFFF;
}
void SubCarRecentEntryDetailView::parseProtocol() {
btn = "";
cnt = SD_NO_CNT;
serial = 0;
if (entry_.sensorType == FPC_Invalid) return;
if (entry_.sensorType == FPC_SUZUKI) {
uint32_t serial_button = (((entry_.data >> 32) & 0xFFF) << 20) | (entry_.data >> 12);
serial = serial_button >> 4;
uint8_t buttonid = serial_button & 0xF;
cnt = (entry_.data >> 44) & 0xFFFF;
btn = to_string_dec_uint(buttonid);
return;
}
if (entry_.sensorType == FPC_VW) {
// uint32_t key_high = (entry_.data >> 32) & 0xFFFFFFFF;
uint32_t key_low = entry_.data & 0xFFFFFFFF;
serial = key_low; // trimmed to 32 bits for VW
uint8_t check = entry_.data2 & 0xFF;
uint8_t btnid = (check >> 4) & 0xF;
switch (btnid) {
case 0x1:
btn = "UNLOCK";
break;
case 0x2:
btn = "LOCK";
break;
case 0x3:
btn = "Un+Lk";
break;
case 0x4:
btn = "TRUNK";
break;
case 0x5:
btn = "Un+Tr";
break;
case 0x6:
btn = "Lk+Tr";
break;
case 0x7:
btn = "Un+Lk+Tr";
break;
case 0x8:
btn = "PANIC";
break;
default:
btn = "Unknown";
break;
}
}
if (entry_.sensorType == FPC_SUBARU) {
uint8_t* data_bytes = (uint8_t*)entry_.data;
serial = ((uint32_t)data_bytes[1] << 16) | ((uint32_t)data_bytes[2] << 8) | data_bytes[3];
uint8_t button = data_bytes[0] & 0x0F;
btn = to_string_dec_uint(button);
uint16_t cnttmp = 0;
subaru_decode_count(data_bytes, &cnttmp);
cnt = cnttmp;
}
if (entry_.sensorType == FPC_KIAV5) {
serial = (uint32_t)(((entry_.data >> 32) & 0x0FFFFFFF) >> 1);
uint8_t button = (entry_.data >> 61) & 0x07;
btn = to_string_dec_uint(button);
cnt = (uint16_t)(entry_.data & 0xFFFF);
}
if (entry_.sensorType == FPC_KIAV3V4) {
// not decrypted!
serial = SD_NO_SERIAL; //(uint32_t)entry_.data;
// uint8_t button = entry_.data2 & 0xFF;
btn = "?"; // to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_KIAV2) {
serial = (uint32_t)((entry_.data >> 20) & 0xFFFFFFFF);
uint8_t button = (uint8_t)((entry_.data >> 16) & 0x0F);
uint16_t raw_count = (uint16_t)((entry_.data >> 4) & 0xFFF);
cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
btn = to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_KIAV1) {
serial = (uint32_t)((entry_.data >> 24) & 0xFFFFFFFF);
uint8_t button = (uint8_t)((entry_.data >> 16) & 0xFF);
cnt = (uint8_t)((entry_.data >> 8) & 0xFF);
btn = to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_KIAV0) {
serial = (uint32_t)((entry_.data >> 12) & 0x0FFFFFFF);
uint8_t button = (entry_.data >> 8) & 0x0F;
cnt = (entry_.data >> 40) & 0xFFFF;
btn = to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_FORDV0) {
uint8_t buf[13] = {0};
for (int i = 0; i < 8; ++i) {
buf[i] = (uint8_t)(entry_.data >> (56 - i * 8));
}
buf[8] = (uint8_t)(entry_.data2 >> 8);
buf[9] = (uint8_t)(entry_.data2 & 0xFF);
uint8_t tmp = buf[8];
uint8_t parity = 0;
uint8_t parity_any = (tmp != 0);
while (tmp) {
parity ^= (tmp & 1);
tmp >>= 1;
}
buf[11] = parity_any ? parity : 0;
uint8_t xor_byte;
uint8_t limit;
if (buf[11]) {
xor_byte = buf[7];
limit = 7;
} else {
xor_byte = buf[6];
limit = 6;
}
for (int idx = 1; idx < limit; ++idx) {
buf[idx] ^= xor_byte;
}
if (buf[11] == 0) {
buf[7] ^= xor_byte;
}
uint8_t orig_b7 = buf[7];
buf[7] = (orig_b7 & 0xAA) | (buf[6] & 0x55);
uint8_t mixed = (buf[6] & 0xAA) | (orig_b7 & 0x55);
buf[12] = mixed;
buf[6] = mixed;
uint32_t serial_le = ((uint32_t)buf[1]) |
((uint32_t)buf[2] << 8) |
((uint32_t)buf[3] << 16) |
((uint32_t)buf[4] << 24);
serial = ((serial_le & 0xFF) << 24) |
(((serial_le >> 8) & 0xFF) << 16) |
(((serial_le >> 16) & 0xFF) << 8) |
((serial_le >> 24) & 0xFF);
uint8_t button = (buf[5] >> 4) & 0x0F;
cnt = ((buf[5] & 0x0F) << 16) |
(buf[6] << 8) |
buf[7];
btn = to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_FIATV0) {
serial = (uint32_t)(entry_.data & 0xFFFFFFFF);
cnt = (uint32_t)((entry_.data >> 32) & 0xFFFFFFFF);
uint8_t button = (uint8_t)(entry_.data2 & 0xFF);
btn = to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_BMWV0) {
serial = (uint32_t)((entry_.data >> 12) & 0x0FFFFFFF);
uint8_t button = (entry_.data >> 8) & 0x0F;
cnt = (entry_.data >> 40) & 0xFFFF;
btn = to_string_dec_uint(button);
}
return;
}
} // namespace ui::external_app::subcarrx
namespace ui {
template <>
void RecentEntriesTable<ui::external_app::subcarrx::SubCarRecentEntries>::draw(
const Entry& entry,
const Rect& target_rect,
Painter& painter,
const Style& style,
ui::RecentEntriesColumns& columns) {
std::string line{};
line.reserve(30);
line = ui::external_app::subcarrx::SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)entry.sensorType);
line = line + " " + to_string_hex(entry.data << 32);
line.resize(columns.at(0).second, ' ');
std::string ageStr = to_string_dec_uint(entry.age);
std::string bitsStr = to_string_dec_uint(entry.bits);
line += ui::external_app::subcarrx::SubCarView::pad_string_with_spaces(5 - bitsStr.length()) + bitsStr;
line += ui::external_app::subcarrx::SubCarView::pad_string_with_spaces(4 - ageStr.length()) + ageStr;
line.resize(target_rect.width() / 8, ' ');
painter.draw_string(target_rect.location(), style, line);
}
} // namespace ui
+222
View File
@@ -0,0 +1,222 @@
/*
* Copyright (C) 2026 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
/*
This and The other files related to this is based on a lot of great people's work. https://github.com/RocketGod-git/ProtoPirate Check the repo, and the credits inside.
*/
#ifndef __UI_SubCar_H__
#define __UI_SubCar_H__
#define SD_NO_SERIAL 0xFFFFFFFF
#define SD_NO_BTN 0xFF
#define SD_NO_CNT 0xFF
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_freq_field.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "utility.hpp"
#include "log_file.hpp"
#include "recent_entries.hpp"
#include "../../baseband/fprotos/subcartypes.hpp"
using namespace ui;
namespace ui::external_app::subcarrx {
struct SubCarRecentEntry {
using Key = uint64_t;
static constexpr Key invalid_key = 0x0fffffff;
uint8_t sensorType = FPC_Invalid;
uint16_t bits = 0;
uint16_t age = 0; // updated on each seconds, show how long the signal was last seen
uint64_t data = 0;
uint64_t data2 = 0;
SubCarRecentEntry() {}
SubCarRecentEntry(
uint8_t sensorType,
uint64_t data = 0,
uint64_t data2 = 0,
uint16_t bits = 0)
: sensorType{sensorType},
bits{bits},
data{data},
data2{data2} {
}
Key key() const {
return (data ^ ((static_cast<uint64_t>(sensorType) & 0xFF) << 0)); // should be optimized...
}
void inc_age(int delta) {
if (UINT16_MAX - delta > age) age += delta;
}
void reset_age() {
age = 0;
}
std::string to_csv();
};
class SubCarLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void log_data(SubCarRecentEntry& data);
void write_header() {
log_file.write_entry(";Type; Bits; Data;");
}
private:
LogFile log_file{};
};
using SubCarRecentEntries = RecentEntries<SubCarRecentEntry>;
using SubCarRecentEntriesView = RecentEntriesView<SubCarRecentEntries>;
class SubCarView : public View {
public:
SubCarView(NavigationView& nav);
~SubCarView();
void focus() override;
std::string title() const override { return "SubCar"; };
static const char* getSensorTypeName(FPROTO_SUBCAR_SENSOR type);
static std::string pad_string_with_spaces(int snakes);
private:
void on_tick_second();
void on_data(const SubCarDataMessage* data);
NavigationView& nav_;
RxRadioState radio_state_{
433'920'000 /* frequency */,
1'750'000 /* bandwidth */,
4'000'000 /* sampling rate */,
ReceiverModel::Mode::AMAudio};
bool logging = false;
app_settings::SettingsManager settings_{
"rx_subcar",
app_settings::Mode::RX,
{
{"log"sv, &logging},
}};
SubCarRecentEntries recent{};
RFAmpField field_rf_amp{
{13 * 8, UI_POS_Y(0)}};
LNAGainField field_lna{
{15 * 8, UI_POS_Y(0)}};
VGAGainField field_vga{
{18 * 8, UI_POS_Y(0)}};
RSSI rssi{
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
};
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
nav_};
SignalToken signal_token_tick_second{};
Button button_clear_list{
{0, 16, 7 * 8, 32},
"Clear"};
Checkbox check_log{
{10 * 8, 18},
3,
"Log",
true};
Labels labels{
{{UI_POS_X_RIGHT(14), UI_POS_Y(1)}, "no fm yet :(", Theme::getInstance()->fg_light->foreground},
};
static constexpr auto header_height = 3 * 16;
std::unique_ptr<SubCarLogger> logger{};
ui::RecentEntriesColumns columns{{
{"Type", 0},
{"Bits", 4},
{"Age", 3},
}};
SubCarRecentEntriesView recent_entries_view{columns, recent};
void on_freqchg(int64_t freq);
MessageHandlerRegistration message_handler_freqchg{
Message::ID::FreqChangeCommand,
[this](Message* const p) {
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
this->on_freqchg(message->freq);
}};
MessageHandlerRegistration message_handler_packet{
Message::ID::SubCarData,
[this](Message* const p) {
const auto message = static_cast<const SubCarDataMessage*>(p);
this->on_data(message);
}};
};
class SubCarRecentEntryDetailView : public View {
public:
SubCarRecentEntryDetailView(NavigationView& nav, const SubCarRecentEntry& entry);
void update_data();
void focus() override;
private:
NavigationView& nav_;
SubCarRecentEntry entry_{};
uint32_t serial = 0;
std::string btn = "";
uint32_t cnt = SD_NO_CNT;
Text text_type{{UI_POS_X(0), 1 * 16, 15 * 8, 16}, "?"};
Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"};
Console console{
{0, 4 * 16, screen_width, screen_height - (4 * 16) - 36}};
Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "Type:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 2 * 16}, "Serial: ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 3 * 16}, "Data:", Theme::getInstance()->fg_light->foreground},
};
Button button_done{
{screen_width - 96 - 4, screen_height - 32 - 12, 96, 32},
"Done"};
void parseProtocol();
};
} // namespace ui::external_app::subcarrx
#endif /*__UI_SubCar_H__*/