mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-15 19:13:00 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0685fb7a9f | |||
| 6498f5a1b2 | |||
| 77f747aab2 | |||
| 819b4d0fed | |||
| 15df51f976 | |||
| e9e78609b7 | |||
| 785bd06938 | |||
| 300a4b03c6 | |||
| eaa07f8369 | |||
| c31a978e6f |
+1
-1
@@ -43,7 +43,7 @@ if(NOT DEFINED FLASH_MB_LIMIT_SIZE)
|
||||
set(FLASH_MB_LIMIT_SIZE 3.5) #PRALINE 4 MB = 3.5 firmware + 0.5 MB fpga bitstream.
|
||||
else()
|
||||
set(FLASH_MB_LIMIT_SIZE 1) #TODO: should be ${FLASH_MB_SIZE} remove once we got a stable build for all devices we support with bigger than 1 mb flash
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
message("Configuring for FLASH_MB_LIMIT_SIZE=${FLASH_MB_LIMIT_SIZE}MB")
|
||||
|
||||
|
||||
@@ -258,6 +258,7 @@ set(CPPSRC
|
||||
ook_file.cpp
|
||||
ui_baseband_stats_view.cpp
|
||||
ui_navigation.cpp
|
||||
ui_notifications.cpp
|
||||
ui_record_view.cpp
|
||||
ui_sd_card_status_view.cpp
|
||||
ui/ui_alphanum.cpp
|
||||
|
||||
@@ -225,7 +225,7 @@ void BLETxView::send_packet() {
|
||||
uint8_t min = 0x00;
|
||||
uint8_t max = 0x0F;
|
||||
|
||||
hexDigit = min + std::rand() % (max - min + 1);
|
||||
hexDigit = min + rand() % (max - min + 1);
|
||||
} break;
|
||||
default:
|
||||
hexDigit = 0;
|
||||
|
||||
@@ -320,6 +320,47 @@ void ADSBRxDetailsView::update(const AircraftRecentEntry& entry) {
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::get_altitude_color(int32_t alt_ft, uint8_t* r, uint8_t* g, uint8_t* b) {
|
||||
static const struct {
|
||||
int32_t alt;
|
||||
uint8_t r, g, b;
|
||||
} stops[] = {
|
||||
{0, 220, 220, 220}, // 0 ft: White/Grey (Ground)
|
||||
{2000, 255, 255, 0}, // 2k ft: Yellow
|
||||
{10000, 0, 255, 0}, // 10k ft: Green
|
||||
{20000, 0, 255, 255}, // 20k ft: Cyan
|
||||
{30000, 60, 60, 255}, // 30k ft: Blue (Lightened for visibility on Black)
|
||||
{40000, 255, 0, 255} // 40k+ ft: Magenta
|
||||
};
|
||||
if (alt_ft <= stops[0].alt) {
|
||||
*r = stops[0].r;
|
||||
*g = stops[0].g;
|
||||
*b = stops[0].b;
|
||||
return;
|
||||
}
|
||||
const int count = sizeof(stops) / sizeof(stops[0]);
|
||||
if (alt_ft >= stops[count - 1].alt) {
|
||||
*r = stops[count - 1].r;
|
||||
*g = stops[count - 1].g;
|
||||
*b = stops[count - 1].b;
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < count - 1; i++) {
|
||||
if (alt_ft < stops[i + 1].alt) {
|
||||
float t = (float)(alt_ft - stops[i].alt) / (stops[i + 1].alt - stops[i].alt);
|
||||
|
||||
*r = (uint8_t)(stops[i].r + (stops[i + 1].r - stops[i].r) * t);
|
||||
*g = (uint8_t)(stops[i].g + (stops[i + 1].g - stops[i].g) * t);
|
||||
*b = (uint8_t)(stops[i].b + (stops[i + 1].b - stops[i].b) * t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fallback: in case no interval matched, default to ground color.
|
||||
*r = stops[0].r;
|
||||
*g = stops[0].g;
|
||||
*b = stops[0].b;
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::clear_map_markers() {
|
||||
if (geomap_view_)
|
||||
geomap_view_->clear_markers();
|
||||
@@ -335,6 +376,9 @@ bool ADSBRxDetailsView::add_map_marker(const AircraftRecentEntry& entry) {
|
||||
marker.lat = entry.pos.latitude;
|
||||
marker.angle = entry.velo.heading;
|
||||
marker.tag = get_map_tag(entry);
|
||||
uint8_t r, g, b;
|
||||
get_altitude_color(entry.pos.altitude, &r, &g, &b);
|
||||
marker.color = Color(r, g, b);
|
||||
|
||||
auto markerStored = geomap_view_->store_marker(marker);
|
||||
return markerStored == MARKER_STORED;
|
||||
|
||||
@@ -270,6 +270,7 @@ class ADSBRxDetailsView : public View {
|
||||
void focus() override;
|
||||
void update(const AircraftRecentEntry& entry);
|
||||
|
||||
void get_altitude_color(int32_t alt_ft, uint8_t* r, uint8_t* g, uint8_t* b);
|
||||
/* Calls forwarded to map view if shown. */
|
||||
bool map_active() const { return geomap_view_; }
|
||||
void clear_map_markers();
|
||||
|
||||
@@ -1499,6 +1499,10 @@ SignalPathStatusView::SignalPathStatusView(NavigationView& nav)
|
||||
&text_max_mode,
|
||||
&text_lbl_rf_path,
|
||||
&text_rf_path,
|
||||
&text_lbl_filter,
|
||||
&text_filter,
|
||||
&text_lbl_mixer,
|
||||
&text_mixer,
|
||||
&text_lbl_rf_amp,
|
||||
&text_rf_amp,
|
||||
&text_lbl_lna,
|
||||
@@ -1507,8 +1511,13 @@ SignalPathStatusView::SignalPathStatusView(NavigationView& nav)
|
||||
&text_vga,
|
||||
&text_lbl_fpga_decim,
|
||||
&text_fpga_decim,
|
||||
&text_lbl_fpga_ctrl_dc_q,
|
||||
&text_fpga_ctrl_dc_q,
|
||||
&text_lbl_fpga_ctrl_qs,
|
||||
&text_fpga_ctrl_qs,
|
||||
&text_status,
|
||||
&button_refresh,
|
||||
&button_toggle_q,
|
||||
&button_done,
|
||||
});
|
||||
|
||||
@@ -1518,6 +1527,21 @@ SignalPathStatusView::SignalPathStatusView(NavigationView& nav)
|
||||
refresh_status();
|
||||
};
|
||||
|
||||
button_toggle_q.on_select = [this](Button&) {
|
||||
static bool q_override = false;
|
||||
q_override = !q_override;
|
||||
|
||||
uint8_t ctrl_reg = 0x01; // DC_BLOCK always on
|
||||
if (q_override) {
|
||||
ctrl_reg |= 0x02; // Force Q_INVERT ON
|
||||
}
|
||||
|
||||
radio::debug::fpga::register_write(1, ctrl_reg);
|
||||
radio::invalidate_spi_config();
|
||||
|
||||
refresh_status();
|
||||
};
|
||||
|
||||
button_done.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
@@ -1537,6 +1561,37 @@ void SignalPathStatusView::refresh_status() {
|
||||
int_fast8_t cached_lna = radio::debug::get_cached_lna_gain();
|
||||
int_fast8_t cached_vga = radio::debug::get_cached_vga_gain();
|
||||
|
||||
// Get current band.
|
||||
auto current_band = radio::debug::rf_path_info::get_current_band();
|
||||
switch (current_band) {
|
||||
case rf::path::Band::Low:
|
||||
text_filter.set("LOW PASS");
|
||||
text_filter.set_style(Theme::getInstance()->fg_green);
|
||||
text_mixer.set("ENABLED");
|
||||
text_mixer.set_style(Theme::getInstance()->fg_green);
|
||||
break;
|
||||
|
||||
case rf::path::Band::Mid:
|
||||
text_filter.set("BYPASS");
|
||||
text_filter.set_style(Theme::getInstance()->fg_green);
|
||||
text_mixer.set("DISABLED");
|
||||
text_mixer.set_style(Theme::getInstance()->fg_orange);
|
||||
break;
|
||||
|
||||
case rf::path::Band::High:
|
||||
text_filter.set("HIGH PASS");
|
||||
text_filter.set_style(Theme::getInstance()->fg_green);
|
||||
text_mixer.set("ENABLED");
|
||||
text_mixer.set_style(Theme::getInstance()->fg_green);
|
||||
break;
|
||||
|
||||
default:
|
||||
text_filter.set("UNKNOWN");
|
||||
text_filter.set_style(Theme::getInstance()->fg_red);
|
||||
text_mixer.set("UNKNOWN");
|
||||
text_mixer.set_style(Theme::getInstance()->fg_red);
|
||||
}
|
||||
|
||||
// Read actual register values to verify
|
||||
uint32_t max_r11 = radio::debug::second_if::register_read(11);
|
||||
|
||||
@@ -1622,6 +1677,25 @@ void SignalPathStatusView::refresh_status() {
|
||||
text_fpga_decim.set("n=" + to_string_dec_uint(fpga_decim) +
|
||||
" (/" + to_string_dec_uint(1 << fpga_decim) + ")");
|
||||
|
||||
// FPGA Register Ctrl Info
|
||||
uint32_t fpga_ctrl = radio::debug::fpga::register_read(1);
|
||||
// text_fpga_ctrl_dc_q.set(to_string_hex(fpga_ctrl, 2));
|
||||
// text_fpga_ctrl_qs.set(to_string_hex(fpga_ctrl, 2));
|
||||
|
||||
// Decode bits
|
||||
bool dc_block = fpga_ctrl & 0x01;
|
||||
bool q_invert = fpga_ctrl & 0x02;
|
||||
uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03;
|
||||
|
||||
// Display human-readable
|
||||
std::string fpga_status_dc_q = "DC:" + std::string(dc_block ? "ON" : "OFF") +
|
||||
" Q:" + std::string(q_invert ? "INV" : "NOR");
|
||||
|
||||
std::string fpga_status_qs = "QS:" + to_string_dec_uint(quarter_shift);
|
||||
|
||||
text_fpga_ctrl_dc_q.set(fpga_status_dc_q);
|
||||
text_fpga_ctrl_qs.set(fpga_status_qs);
|
||||
|
||||
// Summary status
|
||||
// Summary status - update to account for rounding tolerance
|
||||
int lna_diff = (actual_lna_db > cached_lna) ? (actual_lna_db - cached_lna) : (cached_lna - actual_lna_db);
|
||||
@@ -1643,6 +1717,355 @@ void SignalPathStatusView::refresh_status() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
/* SystemDiagnosticsView *************************************************/
|
||||
SystemDiagnosticsView::SystemDiagnosticsView(NavigationView& nav) {
|
||||
add_children({
|
||||
&text_title,
|
||||
&text_lbl_sample,
|
||||
&text_sample_rate,
|
||||
&text_lbl_band,
|
||||
&text_band,
|
||||
&button_sample_2m,
|
||||
&button_sample_4m,
|
||||
&button_sample_8m,
|
||||
&button_sample_20m,
|
||||
&text_lbl_bb_filter,
|
||||
&text_bb_filter,
|
||||
&text_lbl_reg8,
|
||||
&text_reg8,
|
||||
&text_lbl_gpio,
|
||||
&text_lbl_lpf,
|
||||
&text_gpio_lpf,
|
||||
&text_lbl_mix,
|
||||
&text_gpio_mix,
|
||||
&text_lbl_amp,
|
||||
&text_gpio_amp,
|
||||
&text_lbl_fpga,
|
||||
&text_lbl_fpga_ctrl,
|
||||
&text_fpga_ctrl,
|
||||
&text_lbl_fpga_decode,
|
||||
&button_toggle_q,
|
||||
&button_toggle_dc,
|
||||
&button_refresh,
|
||||
&button_done,
|
||||
});
|
||||
|
||||
text_title.set_style(Theme::getInstance()->fg_yellow);
|
||||
text_lbl_gpio.set_style(Theme::getInstance()->fg_yellow);
|
||||
text_lbl_fpga.set_style(Theme::getInstance()->fg_yellow);
|
||||
|
||||
// Sample rate buttons
|
||||
button_sample_2m.on_select = [this](Button&) {
|
||||
set_sample_rate(2000000);
|
||||
};
|
||||
|
||||
button_sample_4m.on_select = [this](Button&) {
|
||||
set_sample_rate(4000000);
|
||||
};
|
||||
|
||||
button_sample_8m.on_select = [this](Button&) {
|
||||
set_sample_rate(8000000);
|
||||
};
|
||||
|
||||
button_sample_20m.on_select = [this](Button&) {
|
||||
set_sample_rate(20000000);
|
||||
};
|
||||
|
||||
// Q inversion toggle
|
||||
button_toggle_q.on_select = [this](Button&) {
|
||||
uint32_t current = radio::debug::fpga::register_read(1);
|
||||
uint8_t new_val = current ^ 0x02; // Toggle Q_INVERT bit
|
||||
radio::debug::fpga::register_write(1, new_val);
|
||||
radio::invalidate_spi_config();
|
||||
refresh();
|
||||
};
|
||||
|
||||
// DC block toggle
|
||||
button_toggle_dc.on_select = [this](Button&) {
|
||||
uint32_t current = radio::debug::fpga::register_read(1);
|
||||
uint8_t new_val = current ^ 0x01; // Toggle DC_BLOCK bit
|
||||
radio::debug::fpga::register_write(1, new_val);
|
||||
radio::invalidate_spi_config();
|
||||
refresh();
|
||||
};
|
||||
|
||||
button_refresh.on_select = [this](Button&) {
|
||||
refresh();
|
||||
};
|
||||
|
||||
button_done.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SystemDiagnosticsView::focus() {
|
||||
button_refresh.focus();
|
||||
}
|
||||
|
||||
void SystemDiagnosticsView::set_sample_rate(uint32_t rate) {
|
||||
portapack::clock_manager.set_sampling_frequency(rate);
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SystemDiagnosticsView::read_gpio_states() {
|
||||
// Read actual GPIO port states
|
||||
uint32_t gpio3_state = LPC_GPIO->PIN[3]; // GPIO3 for mixer
|
||||
uint32_t gpio4_state = LPC_GPIO->PIN[4]; // GPIO4 for LPF and amp
|
||||
|
||||
// Extract specific pins based on hackrf_gpio.hpp definitions:
|
||||
// gpio_mix_enable_n = GPIO3_2 (P6_3) - bit 2 of port 3, INVERTED
|
||||
// gpio_lpf_enable = GPIO4_8 (PA_1) - bit 8 of port 4
|
||||
// gpio_rf_amp_enable = GPIO4_9 (PA_2) - bit 9 of port 4
|
||||
|
||||
bool mix_n_actual = (gpio3_state >> 2) & 1; // GPIO3[2]
|
||||
bool lpf_actual = (gpio4_state >> 8) & 1; // GPIO4[8]
|
||||
bool amp_actual = (gpio4_state >> 9) & 1; // GPIO4[9]
|
||||
|
||||
// Mixer is active LOW, so invert for display
|
||||
bool mixer_enabled = !mix_n_actual;
|
||||
|
||||
// Display with GPIO pin numbers
|
||||
text_gpio_lpf.set(
|
||||
std::string(lpf_actual ? "ON" : "OFF") +
|
||||
" (GPIO4[8]=" + to_string_dec_uint(lpf_actual ? 1 : 0) + ")");
|
||||
|
||||
text_gpio_mix.set(
|
||||
std::string(mixer_enabled ? "ENABLED" : "BYPASSED") +
|
||||
" (GPIO3[2]=" + to_string_dec_uint(mix_n_actual ? 1 : 0) + ")");
|
||||
|
||||
text_gpio_amp.set(
|
||||
std::string(amp_actual ? "ON" : "OFF") +
|
||||
" (GPIO4[9]=" + to_string_dec_uint(amp_actual ? 1 : 0) + ")");
|
||||
|
||||
// Color code
|
||||
text_gpio_lpf.set_style(lpf_actual ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red);
|
||||
text_gpio_mix.set_style(mixer_enabled ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red);
|
||||
text_gpio_amp.set_style(amp_actual ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_orange);
|
||||
}
|
||||
|
||||
void SystemDiagnosticsView::refresh() {
|
||||
// Sample rate
|
||||
uint32_t sample_rate = portapack::clock_manager.get_sampling_frequency();
|
||||
if (sample_rate >= 1000000) {
|
||||
text_sample_rate.set(to_string_dec_uint(sample_rate / 1000000) + " MSS");
|
||||
} else {
|
||||
text_sample_rate.set(to_string_dec_uint(sample_rate / 1000) + " kSS");
|
||||
}
|
||||
|
||||
// Baseband filter bandwidth
|
||||
uint32_t reg8 = radio::debug::second_if::register_read(8);
|
||||
text_reg8.set("0x" + to_string_hex(reg8, 4));
|
||||
|
||||
uint8_t lpf_coarse = reg8 & 0x03; // Bits 1:0
|
||||
const char* bw_names[] = {"7.5 MHz", "8.5 MHz", "15 MHz", "18 MHz"};
|
||||
text_bb_filter.set(bw_names[lpf_coarse]);
|
||||
|
||||
// Color code - green if >= 8 MHz, red otherwise
|
||||
if (lpf_coarse >= 1) {
|
||||
text_bb_filter.set_style(Theme::getInstance()->fg_green);
|
||||
} else {
|
||||
text_bb_filter.set_style(Theme::getInstance()->fg_red);
|
||||
}
|
||||
|
||||
// Read actual GPIO pin states
|
||||
read_gpio_states();
|
||||
|
||||
uint32_t gpio6_pin = LPC_GPIO->PIN[6];
|
||||
bool rffc_locked = (gpio6_pin >> 25) & 1;
|
||||
|
||||
// Display it by changing one of the existing fields temporarily
|
||||
// For example, modify the band display to show lock status:
|
||||
|
||||
auto current_band = radio::debug::rf_path_info::get_current_band();
|
||||
switch (current_band) {
|
||||
case rf::path::Band::Low:
|
||||
text_band.set("LOW(0-2320MHz)|" + std::string(rffc_locked ? "LCK)" : "ULCK)"));
|
||||
text_band.set_style(rffc_locked ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red);
|
||||
break;
|
||||
case rf::path::Band::Mid:
|
||||
text_band.set("MID(2320-2740MHz)");
|
||||
text_band.set_style(Theme::getInstance()->fg_green);
|
||||
break;
|
||||
case rf::path::Band::High:
|
||||
text_band.set("HIGH(2740-7250MHz)");
|
||||
text_band.set_style(Theme::getInstance()->fg_green);
|
||||
break;
|
||||
}
|
||||
// FPGA control register
|
||||
uint32_t fpga_ctrl = radio::debug::fpga::register_read(1);
|
||||
text_fpga_ctrl.set("0x" + to_string_hex(fpga_ctrl, 2));
|
||||
|
||||
// Decode FPGA register bits
|
||||
bool dc_block = fpga_ctrl & 0x01;
|
||||
bool q_invert = fpga_ctrl & 0x02;
|
||||
uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03;
|
||||
|
||||
text_lbl_fpga_decode.set(
|
||||
"|DC:" + std::string(dc_block ? "ON" : "OFF") +
|
||||
" Q:" + std::string(q_invert ? "INV" : "NOR") +
|
||||
" QS:" + to_string_dec_uint(quarter_shift));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
/* GPIODebugView *************************************************/
|
||||
GPIODebugView::GPIODebugView(NavigationView& nav) {
|
||||
add_children({
|
||||
&text_lbl_gpio4,
|
||||
&text_lbl_dir4,
|
||||
&text_dir4,
|
||||
&text_lbl_pin4,
|
||||
&text_pin4,
|
||||
&text_lbl_set4,
|
||||
&text_set4,
|
||||
&text_lbl_lpf_bit,
|
||||
&text_lpf_dir,
|
||||
&text_lpf_pin,
|
||||
&text_lpf_set,
|
||||
&button_lpf_toggle,
|
||||
&button_lpf_on,
|
||||
&button_lpf_off,
|
||||
&text_lbl_amp_bit,
|
||||
&text_amp_dir,
|
||||
&text_amp_pin,
|
||||
&text_amp_set,
|
||||
&button_amp_toggle,
|
||||
&button_amp_on,
|
||||
&button_amp_off,
|
||||
&text_lbl_gpio3,
|
||||
&text_lbl_mix_bit,
|
||||
&text_mix_dir,
|
||||
&text_mix_pin,
|
||||
&text_mix_set,
|
||||
&button_refresh,
|
||||
&button_done,
|
||||
});
|
||||
|
||||
text_lbl_gpio4.set_style(Theme::getInstance()->fg_yellow);
|
||||
text_lbl_gpio3.set_style(Theme::getInstance()->fg_yellow);
|
||||
|
||||
// LPF control buttons
|
||||
button_lpf_toggle.on_select = [this](Button&) {
|
||||
// Read current state
|
||||
uint32_t current = LPC_GPIO->PIN[4];
|
||||
bool current_state = (current >> 8) & 1;
|
||||
|
||||
// Toggle
|
||||
if (current_state) {
|
||||
LPC_GPIO->CLR[4] = (1 << 8); // Clear bit 8
|
||||
} else {
|
||||
LPC_GPIO->SET[4] = (1 << 8); // Set bit 8
|
||||
}
|
||||
|
||||
refresh();
|
||||
};
|
||||
|
||||
button_lpf_on.on_select = [this](Button&) {
|
||||
LPC_GPIO->SET[4] = (1 << 8); // Force ON
|
||||
refresh();
|
||||
};
|
||||
|
||||
button_lpf_off.on_select = [this](Button&) {
|
||||
LPC_GPIO->CLR[4] = (1 << 8); // Force OFF
|
||||
refresh();
|
||||
};
|
||||
|
||||
// RF Amp control buttons
|
||||
button_amp_toggle.on_select = [this](Button&) {
|
||||
uint32_t current = LPC_GPIO->PIN[4];
|
||||
bool current_state = (current >> 9) & 1;
|
||||
|
||||
if (current_state) {
|
||||
LPC_GPIO->CLR[4] = (1 << 9);
|
||||
} else {
|
||||
LPC_GPIO->SET[4] = (1 << 9);
|
||||
}
|
||||
|
||||
refresh();
|
||||
};
|
||||
|
||||
button_amp_on.on_select = [this](Button&) {
|
||||
LPC_GPIO->SET[4] = (1 << 9); // Force ON
|
||||
refresh();
|
||||
};
|
||||
|
||||
button_amp_off.on_select = [this](Button&) {
|
||||
LPC_GPIO->CLR[4] = (1 << 9); // Force OFF
|
||||
refresh();
|
||||
};
|
||||
|
||||
button_refresh.on_select = [this](Button&) {
|
||||
refresh();
|
||||
};
|
||||
|
||||
button_done.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void GPIODebugView::focus() {
|
||||
button_refresh.focus();
|
||||
}
|
||||
|
||||
void GPIODebugView::refresh() {
|
||||
// Read GPIO4 registers
|
||||
uint32_t gpio4_dir = LPC_GPIO->DIR[4]; // Direction: 1=output, 0=input
|
||||
uint32_t gpio4_pin = LPC_GPIO->PIN[4]; // Actual pin state
|
||||
uint32_t gpio4_set = LPC_GPIO->SET[4]; // What we're trying to output
|
||||
|
||||
// Display full registers
|
||||
text_dir4.set("0x" + to_string_hex(gpio4_dir, 8));
|
||||
text_pin4.set("0x" + to_string_hex(gpio4_pin, 8));
|
||||
text_set4.set("0x" + to_string_hex(gpio4_set, 8));
|
||||
|
||||
// Extract bit 8 (LPF)
|
||||
bool lpf_dir = (gpio4_dir >> 8) & 1;
|
||||
bool lpf_pin = (gpio4_pin >> 8) & 1;
|
||||
bool lpf_set = (gpio4_set >> 8) & 1;
|
||||
|
||||
text_lpf_dir.set("DIR: " + std::string(lpf_dir ? "OUT" : "IN"));
|
||||
text_lpf_pin.set("PIN: " + std::string(lpf_pin ? "1" : "0"));
|
||||
text_lpf_set.set("SET: " + std::string(lpf_set ? "1" : "0"));
|
||||
|
||||
// Color code
|
||||
text_lpf_dir.set_style(lpf_dir ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red);
|
||||
text_lpf_pin.set_style(lpf_pin ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red);
|
||||
|
||||
// Extract bit 9 (RF Amp)
|
||||
bool amp_dir = (gpio4_dir >> 9) & 1;
|
||||
bool amp_pin = (gpio4_pin >> 9) & 1;
|
||||
bool amp_set = (gpio4_set >> 9) & 1;
|
||||
|
||||
text_amp_dir.set("DIR: " + std::string(amp_dir ? "OUT" : "IN"));
|
||||
text_amp_pin.set("PIN: " + std::string(amp_pin ? "1" : "0"));
|
||||
text_amp_set.set("SET: " + std::string(amp_set ? "1" : "0"));
|
||||
|
||||
text_amp_dir.set_style(amp_dir ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red);
|
||||
text_amp_pin.set_style(amp_pin ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red);
|
||||
|
||||
// Read GPIO3 (Mixer) - bit 2
|
||||
uint32_t gpio3_dir = LPC_GPIO->DIR[3];
|
||||
uint32_t gpio3_pin = LPC_GPIO->PIN[3];
|
||||
uint32_t gpio3_set = LPC_GPIO->SET[3];
|
||||
|
||||
bool mix_dir = (gpio3_dir >> 2) & 1;
|
||||
bool mix_pin = (gpio3_pin >> 2) & 1;
|
||||
bool mix_set = (gpio3_set >> 2) & 1;
|
||||
|
||||
text_mix_dir.set("DIR: " + std::string(mix_dir ? "OUT" : "IN"));
|
||||
text_mix_pin.set("PIN: " + std::string(mix_pin ? "1" : "0"));
|
||||
text_mix_set.set("SET: " + std::string(mix_set ? "1" : "0"));
|
||||
|
||||
text_mix_dir.set_style(mix_dir ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red);
|
||||
text_mix_pin.set_style(mix_pin ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
/* RFFC5072StatusView *************************************************/
|
||||
|
||||
@@ -1650,6 +2073,10 @@ RFFC5072StatusView::RFFC5072StatusView(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
add_children({
|
||||
&text_title,
|
||||
&text_lbl_lock,
|
||||
&text_lock,
|
||||
&text_lbl_ctrl,
|
||||
&text_ctrl,
|
||||
&text_lbl_enabled,
|
||||
&text_enabled,
|
||||
&text_lbl_freq,
|
||||
@@ -1664,7 +2091,6 @@ RFFC5072StatusView::RFFC5072StatusView(NavigationView& nav)
|
||||
&text_r1,
|
||||
&text_lbl_r2,
|
||||
&text_r2,
|
||||
&text_lbl_decode,
|
||||
&text_lbl_n,
|
||||
&text_n,
|
||||
&text_lbl_lodiv,
|
||||
@@ -1672,12 +2098,13 @@ RFFC5072StatusView::RFFC5072StatusView(NavigationView& nav)
|
||||
&text_lbl_calc,
|
||||
&text_calc,
|
||||
&text_status,
|
||||
&text_lbl_regs_status,
|
||||
&text_regs_status,
|
||||
&button_refresh,
|
||||
&button_done,
|
||||
});
|
||||
|
||||
text_title.set_style(Theme::getInstance()->fg_yellow);
|
||||
text_lbl_decode.set_style(Theme::getInstance()->fg_yellow);
|
||||
|
||||
button_refresh.on_select = [this](Button&) {
|
||||
refresh_status();
|
||||
@@ -1696,11 +2123,47 @@ void RFFC5072StatusView::focus() {
|
||||
}
|
||||
|
||||
void RFFC5072StatusView::refresh_status() {
|
||||
// === READ RAW GPIO STATES FOR DEBUGGING ===
|
||||
uint32_t gpio2_dir = LPC_GPIO->DIR[2];
|
||||
uint32_t gpio2_pin = LPC_GPIO->PIN[2];
|
||||
|
||||
// Check if the pins are even configured as outputs
|
||||
bool enx_is_output = (gpio2_dir >> 13) & 1;
|
||||
bool resetx_is_output = (gpio2_dir >> 14) & 1;
|
||||
|
||||
// === LOCK DETECT ===
|
||||
uint32_t gpio6_pin = LPC_GPIO->PIN[6];
|
||||
bool rffc_locked = (gpio6_pin >> 25) & 1;
|
||||
|
||||
text_lock.set(rffc_locked ? "LOCKED" : "UNLOCKED");
|
||||
text_lock.set_style(rffc_locked ? Theme::getInstance()->fg_green
|
||||
: Theme::getInstance()->fg_red);
|
||||
|
||||
uint8_t fpga_reg1 = radio::debug::fpga::register_read(1); // CTRL register
|
||||
uint8_t fpga_reg2 = radio::debug::fpga::register_read(2); // RX_DECIM
|
||||
|
||||
text_regs_status.set(
|
||||
"FPGA R1:" + to_string_hex(fpga_reg1, 2) +
|
||||
" R2:" + to_string_hex(fpga_reg2, 2));
|
||||
|
||||
// === CONTROL PINS ===
|
||||
// ENX = GPIO2[13] (P5_4) - active LOW (0 = enabled)
|
||||
// RESETX = GPIO2[14] (P5_5) - active LOW (0 = reset)
|
||||
bool enx = (gpio2_pin >> 13) & 1;
|
||||
bool resetx = (gpio2_pin >> 14) & 1;
|
||||
|
||||
text_ctrl.set(std::string(enx ? "DIS" : "EN") + " " +
|
||||
std::string(resetx ? "RUN" : "RST") + " " +
|
||||
"O:" + std::string(resetx_is_output ? "Y" : "N"));
|
||||
|
||||
text_ctrl.set_style((enx == 0 && resetx == 1) ? Theme::getInstance()->fg_green
|
||||
: Theme::getInstance()->fg_red);
|
||||
|
||||
// === REGISTERS ===
|
||||
// Read CORRECT registers for Path 2 (active path!)
|
||||
uint32_t r0 = radio::debug::first_if::register_read(0); // Control
|
||||
uint32_t r15 = radio::debug::first_if::register_read(15); // P2_FREQ1
|
||||
uint32_t r16 = radio::debug::first_if::register_read(16); // P2_FREQ2
|
||||
uint32_t r17 = radio::debug::first_if::register_read(17); // P2_FREQ3
|
||||
|
||||
// Display
|
||||
text_r0.set(to_string_hex(r0, 4));
|
||||
@@ -1713,35 +2176,22 @@ void RFFC5072StatusView::refresh_status() {
|
||||
text_enabled.set_style(enabled ? Theme::getInstance()->fg_green
|
||||
: Theme::getInstance()->fg_red);
|
||||
|
||||
// Decode from P2_FREQ1 (R15) using struct layout:
|
||||
// bits [1:0] = p2vcosel
|
||||
// bits [3:2] = p2presc (prescaler)
|
||||
// bits [6:4] = p2lodiv (LO divider)
|
||||
// bits [15:7] = p2n (N divider integer)
|
||||
|
||||
// Decode from P2_FREQ1 (R15)
|
||||
uint16_t n_int = (r15 >> 7) & 0x1FF; // 9 bits
|
||||
uint8_t lodiv_sel = (r15 >> 4) & 0x07; // 3 bits
|
||||
uint8_t presc_sel = (r15 >> 2) & 0x03; // 2 bits
|
||||
uint8_t vcosel = r15 & 0x03; // 2 bits
|
||||
|
||||
text_n.set(to_string_dec_uint(n_int));
|
||||
|
||||
// LO divider: 0=÷2, 1=÷4, 2=÷8, 3=÷16, 4=÷32, 5=÷64
|
||||
uint16_t lodiv_val = 1u << lodiv_sel;
|
||||
|
||||
// Prescaler: 0=÷2, 1=÷4 (but code only uses 1 or 2 per rffc507x.cpp)
|
||||
uint16_t presc_val = 1u << presc_sel;
|
||||
|
||||
text_lodiv.set("/" + to_string_dec_uint(lodiv_val) +
|
||||
" (P: /" + to_string_dec_uint(presc_val) + ")");
|
||||
" (P:/" + to_string_dec_uint(presc_val) + ")");
|
||||
|
||||
// Calculate frequencies
|
||||
// F_VCO = (F_ref × N) / Prescaler
|
||||
// F_LO = F_VCO / LODIV
|
||||
const uint32_t f_ref_mhz = 40;
|
||||
|
||||
// N divider is 24-bit fractional
|
||||
// For quick calc, use integer part only
|
||||
uint32_t f_vco_mhz = (f_ref_mhz * n_int) / presc_val;
|
||||
uint32_t f_lo_mhz = f_vco_mhz / lodiv_val;
|
||||
|
||||
@@ -1757,32 +2207,37 @@ void RFFC5072StatusView::refresh_status() {
|
||||
text_freq.set_style(vco_ok ? Theme::getInstance()->fg_green
|
||||
: Theme::getInstance()->fg_red);
|
||||
|
||||
// Check mixer mode (R0 bit 5 = MODE, 0=path1, 1=path2)
|
||||
// Check mixer mode
|
||||
bool path2_active = (r0 & 0x0020) != 0;
|
||||
text_path.set(path2_active ? "PATH2" : "PATH1");
|
||||
text_mixer.set(path2_active ? "ACTIVE" : "INACTIVE");
|
||||
text_mixer.set_style(path2_active ? Theme::getInstance()->fg_green
|
||||
: Theme::getInstance()->fg_orange);
|
||||
|
||||
// Summary
|
||||
if (!enabled) {
|
||||
text_status.set("DISABLED!");
|
||||
// === SUMMARY STATUS ===
|
||||
if (!rffc_locked) {
|
||||
text_status.set("PLL UNLOCKED!");
|
||||
text_status.set_style(Theme::getInstance()->fg_red);
|
||||
} else if (enx == 1) {
|
||||
text_status.set("DISABLED (ENX=1)!");
|
||||
text_status.set_style(Theme::getInstance()->fg_red);
|
||||
} else if (resetx == 0) {
|
||||
text_status.set("IN RESET (RESETX=0)!");
|
||||
text_status.set_style(Theme::getInstance()->fg_red);
|
||||
} else if (!enabled) {
|
||||
text_status.set("R0 bit 4 = 0 (disabled)");
|
||||
text_status.set_style(Theme::getInstance()->fg_red);
|
||||
} else if (!path2_active) {
|
||||
text_status.set("Path 2 not selected!");
|
||||
text_status.set_style(Theme::getInstance()->fg_red);
|
||||
} else if (!vco_ok) {
|
||||
text_status.set("VCO " + to_string_dec_uint(f_vco_mhz) + "MHz OOR");
|
||||
text_status.set("VCO out of range!");
|
||||
text_status.set_style(Theme::getInstance()->fg_red);
|
||||
} else if (!lo_ok) {
|
||||
text_status.set("LO " + to_string_dec_uint(f_lo_mhz) + "MHz OOR");
|
||||
text_status.set("LO out of range!");
|
||||
text_status.set_style(Theme::getInstance()->fg_red);
|
||||
} else {
|
||||
text_status.set(to_string_dec_uint(f_ref_mhz) + "x" +
|
||||
to_string_dec_uint(n_int) + "/" +
|
||||
to_string_dec_uint(presc_val) + "/" +
|
||||
to_string_dec_uint(lodiv_val) + "=" +
|
||||
to_string_dec_uint(f_lo_mhz) + "MHz.");
|
||||
text_status.set("All checks passed!");
|
||||
text_status.set_style(Theme::getInstance()->fg_green);
|
||||
}
|
||||
}
|
||||
@@ -1893,6 +2348,125 @@ void RFFCTuningDebugView::refresh() {
|
||||
}
|
||||
}
|
||||
|
||||
/* MAX2831DebugView *************************************************/
|
||||
MAX2831DebugView::MAX2831DebugView(NavigationView& nav) {
|
||||
add_children({
|
||||
&text_title,
|
||||
&text_lbl_called,
|
||||
&text_called,
|
||||
&text_lbl_valid,
|
||||
&text_valid,
|
||||
&text_lbl_req,
|
||||
&text_req,
|
||||
&text_lbl_calc_n,
|
||||
&text_calc_n,
|
||||
&text_lbl_calc_frac,
|
||||
&text_calc_frac,
|
||||
&text_spacer,
|
||||
&text_lbl_r3,
|
||||
&text_r3,
|
||||
&text_lbl_r4,
|
||||
&text_r4,
|
||||
&text_lbl_act_n,
|
||||
&text_act_n,
|
||||
&text_lbl_act_frac,
|
||||
&text_act_frac,
|
||||
&text_lbl_calc_freq,
|
||||
&text_calc_freq,
|
||||
&text_status,
|
||||
&button_refresh,
|
||||
&button_done,
|
||||
});
|
||||
|
||||
text_spacer.set_style(Theme::getInstance()->fg_yellow);
|
||||
|
||||
button_refresh.on_select = [this](Button&) {
|
||||
refresh();
|
||||
};
|
||||
|
||||
button_done.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void MAX2831DebugView::focus() {
|
||||
button_refresh.focus();
|
||||
}
|
||||
|
||||
void MAX2831DebugView::refresh() {
|
||||
auto info = radio::debug::second_if::get_max2831_info();
|
||||
|
||||
// Show if set_frequency was called
|
||||
if (info.set_frequency_called) {
|
||||
text_called.set("YES");
|
||||
text_called.set_style(Theme::getInstance()->fg_green);
|
||||
|
||||
if (info.frequency_valid) {
|
||||
text_valid.set("YES (2.3-2.6G)");
|
||||
text_valid.set_style(Theme::getInstance()->fg_green);
|
||||
} else {
|
||||
text_valid.set("NO - OUT OF RANGE!");
|
||||
text_valid.set_style(Theme::getInstance()->fg_red);
|
||||
}
|
||||
|
||||
text_req.set(to_string_dec_uint(info.requested_freq_mhz) + " MHz");
|
||||
text_calc_n.set(to_string_dec_uint(info.calculated_n));
|
||||
text_calc_frac.set(to_string_hex(info.calculated_frac, 5));
|
||||
} else {
|
||||
text_called.set("NO");
|
||||
text_called.set_style(Theme::getInstance()->fg_red);
|
||||
text_valid.set("---");
|
||||
text_req.set("---");
|
||||
text_calc_n.set("---");
|
||||
text_calc_frac.set("---");
|
||||
}
|
||||
|
||||
// Read actual hardware registers
|
||||
uint32_t r3 = radio::debug::second_if::register_read(3);
|
||||
uint32_t r4 = radio::debug::second_if::register_read(4);
|
||||
|
||||
text_r3.set(to_string_hex(r3, 4));
|
||||
text_r4.set(to_string_hex(r4, 4));
|
||||
|
||||
// Decode actual values from registers
|
||||
uint16_t act_n = r3 & 0xFF;
|
||||
uint32_t act_frac_lo = (r3 >> 8) & 0x3F;
|
||||
uint32_t act_frac_hi = r4 & 0x3FFF;
|
||||
uint32_t act_frac = (act_frac_hi << 6) | act_frac_lo;
|
||||
|
||||
text_act_n.set(to_string_dec_uint(act_n));
|
||||
text_act_frac.set(to_string_hex(act_frac, 5));
|
||||
|
||||
// Calculate actual frequency from registers
|
||||
// F_LO = 20 MHz × (N + Frac/2^20)
|
||||
// For display, show integer part only
|
||||
uint32_t calc_freq_mhz = 20 * act_n;
|
||||
// Add fractional contribution (approximate)
|
||||
uint32_t frac_contribution = (act_frac * 20) >> 20;
|
||||
calc_freq_mhz += frac_contribution;
|
||||
|
||||
text_calc_freq.set(to_string_dec_uint(calc_freq_mhz) + " MHz");
|
||||
|
||||
// Status
|
||||
if (!info.set_frequency_called) {
|
||||
text_status.set("MAX2831 set_frequency\nNEVER called!");
|
||||
text_status.set_style(Theme::getInstance()->fg_red);
|
||||
} else if (!info.frequency_valid) {
|
||||
text_status.set("Freq " + to_string_dec_uint(info.requested_freq_mhz) +
|
||||
" MHz OUT OF RANGE!\n(need 2300-2600)");
|
||||
text_status.set_style(Theme::getInstance()->fg_red);
|
||||
} else if (act_n == info.calculated_n && act_frac == info.calculated_frac) {
|
||||
text_status.set("MATCH!\nHardware = Expected");
|
||||
text_status.set_style(Theme::getInstance()->fg_green);
|
||||
} else {
|
||||
text_status.set("MISMATCH!\nN: exp=" + to_string_dec_uint(info.calculated_n) +
|
||||
" act=" + to_string_dec_uint(act_n));
|
||||
text_status.set_style(Theme::getInstance()->fg_red);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1956,14 +2530,17 @@ void DebugMenuView::on_populate() {
|
||||
}
|
||||
add_items({
|
||||
#ifdef PRALINE
|
||||
{"System Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SystemDiagnosticsView>(); }},
|
||||
{"Radio Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RadioDiagnosticsView>(); }},
|
||||
{"Baseband Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<BasebandStatusView>(); }},
|
||||
{"SGPIO Live", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIOLiveMonitorView>(); }},
|
||||
{"SGPIO8 Clock", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIO8ClockDetectorView>(); }},
|
||||
{"Si5351 Clocks", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351DebugView>(); }},
|
||||
{"Signal Path", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SignalPathStatusView>(); }},
|
||||
{"GPIO Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<GPIODebugView>(); }},
|
||||
{"RFFC Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RFFC5072StatusView>(); }},
|
||||
{"RFFC Tuning", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RFFCTuningDebugView>(); }},
|
||||
{"MAX2831 Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<MAX2831DebugView>(); }},
|
||||
{"Si5351 Clocks", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351DebugView>(); }},
|
||||
{"SGPIO8 Clock", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIO8ClockDetectorView>(); }},
|
||||
{"Baseband Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<BasebandStatusView>(); }},
|
||||
{"SGPIO Live", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIOLiveMonitorView>(); }},
|
||||
{"RX Test", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RadioRxTestView>(); }},
|
||||
#endif
|
||||
{"Buttons Test", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_controls, [this]() { nav_.push<DebugControlsView>(); }},
|
||||
@@ -2063,7 +2640,7 @@ void DebugPmemView::update() {
|
||||
DebugScreenTest::DebugScreenTest(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
set_focusable(true);
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
}
|
||||
|
||||
bool DebugScreenTest::on_key(const KeyEvent key) {
|
||||
@@ -2073,10 +2650,10 @@ bool DebugScreenTest::on_key(const KeyEvent key) {
|
||||
nav_.pop();
|
||||
break;
|
||||
case KeyEvent::Down:
|
||||
painter.fill_rectangle({0, 0, screen_width, screen_height}, std::rand());
|
||||
painter.fill_rectangle({0, 0, screen_width, screen_height}, rand());
|
||||
break;
|
||||
case KeyEvent::Left:
|
||||
pen_color = std::rand();
|
||||
pen_color = rand();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -2099,7 +2676,7 @@ bool DebugScreenTest::on_touch(const TouchEvent event) {
|
||||
void DebugScreenTest::paint(Painter& painter) {
|
||||
painter.fill_rectangle({0, 16, screen_width, screen_height - 16}, Theme::getInstance()->bg_darkest->foreground);
|
||||
painter.draw_string({10 * 8, screen_height / 2}, *Theme::getInstance()->bg_darkest, "Use Stylus");
|
||||
pen_color = std::rand();
|
||||
pen_color = rand();
|
||||
}
|
||||
|
||||
/* DebugLCRView *******************************************************/
|
||||
|
||||
@@ -703,20 +703,88 @@ class SignalPathStatusView : public View {
|
||||
Text text_lbl_rf_path{{0, 52, 114, 16}, "RF Path:"};
|
||||
Text text_rf_path{{116, 52, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_rf_amp{{0, 68, 114, 16}, "RF Amp:"};
|
||||
Text text_rf_amp{{116, 68, 124, 16}, "---"};
|
||||
Text text_lbl_filter{{0, 68, 114, 16}, "Filter Band:"};
|
||||
Text text_filter{{116, 68, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_lna{{0, 84, 114, 16}, "LNA Gain:"};
|
||||
Text text_lna{{116, 84, 124, 16}, "---"};
|
||||
Text text_lbl_mixer{{0, 84, 114, 16}, "Mixer Enable:"};
|
||||
Text text_mixer{{116, 84, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_vga{{0, 100, 114, 16}, "VGA Gain:"};
|
||||
Text text_vga{{116, 100, 124, 16}, "---"};
|
||||
Text text_lbl_rf_amp{{0, 100, 114, 16}, "RF Amp:"};
|
||||
Text text_rf_amp{{116, 100, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_decim{{0, 116, 114, 16}, "FPGA Decim:"};
|
||||
Text text_fpga_decim{{116, 116, 124, 16}, "---"};
|
||||
Text text_lbl_lna{{0, 116, 114, 16}, "LNA Gain:"};
|
||||
Text text_lna{{116, 116, 124, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 140, 240, 32}, ""};
|
||||
Text text_lbl_vga{{0, 132, 114, 16}, "VGA Gain:"};
|
||||
Text text_vga{{116, 132, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_decim{{0, 148, 114, 16}, "FPGA Decim:"};
|
||||
Text text_fpga_decim{{116, 148, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_ctrl_dc_q{{0, 164, 114, 16}, "FPGA Ctrl:"};
|
||||
Text text_fpga_ctrl_dc_q{{116, 164, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_ctrl_qs{{0, 180, 114, 16}, ""};
|
||||
Text text_fpga_ctrl_qs{{116, 180, 124, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 200, 240, 32}, ""};
|
||||
|
||||
Button button_refresh{{0, 280, 72, 24}, "Refresh"};
|
||||
Button button_toggle_q{{88, 280, 72, 24}, "Q Inv"};
|
||||
Button button_done{{176, 280, 64, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
class GPIODebugView : public View {
|
||||
public:
|
||||
GPIODebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "GPIO Debug"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
// GPIO4 (LPF and RF Amp)
|
||||
Text text_lbl_gpio4{{0, 0, 240, 16}, "Pin Diag: GPIO4 (LPF|Amp|Mix)"};
|
||||
|
||||
Text text_lbl_dir4{{0, 18, 114, 16}, "DIR[4]:"};
|
||||
Text text_dir4{{116, 18, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_pin4{{0, 36, 114, 16}, "PIN[4] (read):"};
|
||||
Text text_pin4{{116, 36, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_set4{{0, 54, 114, 16}, "SET[4] (write):"};
|
||||
Text text_set4{{116, 54, 124, 16}, "---"};
|
||||
|
||||
// Bit 8 (LPF)
|
||||
Text text_lbl_lpf_bit{{0, 78, 240, 16}, "Bit 8 (LPF - GPIO4[8]):"};
|
||||
Text text_lpf_dir{{0, 96, 80, 16}, "DIR: ?"};
|
||||
Text text_lpf_pin{{82, 96, 78, 16}, "PIN: ?"};
|
||||
Text text_lpf_set{{162, 96, 78, 16}, "SET: ?"};
|
||||
|
||||
Button button_lpf_toggle{{8, 114, 110, 24}, "Toggle LPF"};
|
||||
Button button_lpf_on{{122, 114, 50, 24}, "ON"};
|
||||
Button button_lpf_off{{176, 114, 50, 24}, "OFF"};
|
||||
|
||||
// Bit 9 (RF Amp)
|
||||
Text text_lbl_amp_bit{{0, 146, 240, 16}, "Bit 9 (Amp - GPIO4[9]):"};
|
||||
Text text_amp_dir{{0, 164, 80, 16}, "DIR: ?"};
|
||||
Text text_amp_pin{{82, 164, 78, 16}, "PIN: ?"};
|
||||
Text text_amp_set{{162, 164, 78, 16}, "SET: ?"};
|
||||
|
||||
Button button_amp_toggle{{8, 182, 110, 24}, "Toggle Amp"};
|
||||
Button button_amp_on{{122, 182, 50, 24}, "ON"};
|
||||
Button button_amp_off{{176, 182, 50, 24}, "OFF"};
|
||||
|
||||
// GPIO3 (Mixer)
|
||||
Text text_lbl_gpio3{{0, 214, 240, 16}, "--- GPIO3 (Mixer) ---"};
|
||||
Text text_lbl_mix_bit{{0, 232, 240, 16}, "Bit 2 (MIX - GPIO3[2]):"};
|
||||
Text text_mix_dir{{0, 250, 80, 16}, "DIR: ?"};
|
||||
Text text_mix_pin{{82, 250, 78, 16}, "PIN: ?"};
|
||||
Text text_mix_set{{162, 250, 78, 16}, "SET: ?"};
|
||||
|
||||
// Controls
|
||||
Button button_refresh{{8, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
};
|
||||
@@ -736,42 +804,49 @@ class RFFC5072StatusView : public View {
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "=== RFFC5072 (1st IF) ==="};
|
||||
|
||||
Text text_lbl_enabled{{0, 20, 114, 16}, "Status:"};
|
||||
Text text_enabled{{116, 20, 124, 16}, "---"};
|
||||
Text text_lbl_lock{{0, 16, 114, 16}, "Lock Detect:"};
|
||||
Text text_lock{{116, 16, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_freq{{0, 36, 114, 16}, "LO Freq:"};
|
||||
Text text_freq{{116, 36, 124, 16}, "---"};
|
||||
Text text_lbl_ctrl{{0, 32, 114, 16}, "Control:"};
|
||||
Text text_ctrl{{116, 32, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_path{{0, 52, 114, 16}, "Path:"};
|
||||
Text text_path{{116, 52, 124, 16}, "---"};
|
||||
Text text_lbl_enabled{{0, 48, 114, 16}, "Status:"};
|
||||
Text text_enabled{{116, 48, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_mixer{{0, 68, 114, 16}, "Mixer:"};
|
||||
Text text_mixer{{116, 68, 124, 16}, "---"};
|
||||
Text text_lbl_freq{{0, 64, 114, 16}, "LO Freq:"};
|
||||
Text text_freq{{116, 64, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_r0{{0, 92, 114, 16}, "Reg 0:"};
|
||||
Text text_r0{{116, 92, 124, 16}, "---"};
|
||||
Text text_lbl_path{{0, 80, 114, 16}, "Path:"};
|
||||
Text text_path{{116, 80, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_r1{{0, 108, 114, 16}, "Reg 1 (N):"};
|
||||
Text text_r1{{116, 108, 124, 16}, "---"};
|
||||
Text text_lbl_mixer{{0, 96, 114, 16}, "Mixer:"};
|
||||
Text text_mixer{{116, 96, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_r2{{0, 124, 114, 16}, "Reg 2:"};
|
||||
Text text_r2{{116, 124, 124, 16}, "---"};
|
||||
Text text_lbl_r0{{0, 112, 114, 16}, "Reg 0:"};
|
||||
Text text_r0{{116, 112, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_decode{{0, 148, 240, 16}, "--- Decoded Values ---"};
|
||||
Text text_lbl_r1{{0, 128, 114, 16}, "Reg 1 (N):"};
|
||||
Text text_r1{{116, 128, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_n{{0, 168, 114, 16}, "N divider:"};
|
||||
Text text_n{{116, 168, 124, 16}, "---"};
|
||||
Text text_lbl_r2{{0, 144, 114, 16}, "Reg 2:"};
|
||||
Text text_r2{{116, 144, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_lodiv{{0, 184, 114, 16}, "LO divider:"};
|
||||
Text text_lodiv{{116, 184, 124, 16}, "---"};
|
||||
Text text_lbl_n{{0, 160, 114, 16}, "N divider:"};
|
||||
Text text_n{{116, 160, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc{{0, 200, 114, 16}, "Calc freq:"};
|
||||
Text text_calc{{116, 200, 124, 16}, "---"};
|
||||
Text text_lbl_lodiv{{0, 176, 114, 16}, "LO divider:"};
|
||||
Text text_lodiv{{116, 176, 124, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 224, 240, 32}, ""};
|
||||
Text text_lbl_calc{{0, 192, 114, 16}, "Calc freq:"};
|
||||
Text text_calc{{116, 192, 124, 16}, "---"};
|
||||
|
||||
Button button_refresh{{8, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
Text text_status{{0, 208, 240, 16}, ""};
|
||||
|
||||
Text text_lbl_regs_status{{0, 224, 48, 16}, "Regs:"};
|
||||
Text text_regs_status{{50, 224, 190, 16}, "---"};
|
||||
|
||||
Button button_refresh{{2, 280, 56, 24}, "Rfrsh"};
|
||||
Button button_done{{182, 280, 56, 24}, "Done"};
|
||||
};
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -828,6 +903,120 @@ class RFFCTuningDebugView : public View {
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
/* MAX2831DebugView *************************************************/
|
||||
|
||||
class MAX2831DebugView : public View {
|
||||
public:
|
||||
MAX2831DebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "MAX2831 Debug"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "MAX2831 (2nd IF) Debug"};
|
||||
|
||||
Text text_lbl_called{{0, 24, 120, 16}, "Freq Set:"};
|
||||
Text text_called{{122, 24, 118, 16}, "NO"};
|
||||
|
||||
Text text_lbl_valid{{0, 42, 120, 16}, "In Range:"};
|
||||
Text text_valid{{122, 42, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_req{{0, 60, 120, 16}, "Requested:"};
|
||||
Text text_req{{122, 60, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc_n{{0, 78, 120, 16}, "Calc N:"};
|
||||
Text text_calc_n{{122, 78, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc_frac{{0, 96, 120, 16}, "Calc Frac:"};
|
||||
Text text_calc_frac{{122, 96, 118, 16}, "---"};
|
||||
|
||||
Text text_spacer{{0, 114, 240, 16}, "--- Hardware Regs ---"};
|
||||
|
||||
Text text_lbl_r3{{0, 132, 120, 16}, "Reg 3:"};
|
||||
Text text_r3{{122, 132, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_r4{{0, 150, 120, 16}, "Reg 4:"};
|
||||
Text text_r4{{122, 150, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_act_n{{0, 168, 120, 16}, "Actual N:"};
|
||||
Text text_act_n{{122, 168, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_act_frac{{0, 186, 120, 16}, "Actual Frac:"};
|
||||
Text text_act_frac{{122, 186, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc_freq{{0, 204, 120, 16}, "Calc Freq:"};
|
||||
Text text_calc_freq{{122, 204, 118, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 228, 240, 44}, ""};
|
||||
|
||||
Button button_refresh{{8, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
class SystemDiagnosticsView : public View {
|
||||
public:
|
||||
SystemDiagnosticsView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "System Diagnostics"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
void read_gpio_states();
|
||||
void set_sample_rate(uint32_t rate);
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "System Diagnostics"};
|
||||
|
||||
// Sample Rate Section
|
||||
Text text_lbl_sample{{0, 24, 42, 16}, "Rate:"};
|
||||
Text text_sample_rate{{44, 24, 50, 16}, "---"};
|
||||
Text text_lbl_fpga_decode{{96, 24, 144, 16}, "DC:? Q:? QS:?"};
|
||||
|
||||
Button button_sample_2m{{2, 44, 56, 24}, "2 MSPS"};
|
||||
Button button_sample_4m{{62, 44, 56, 24}, "4 MSPS"};
|
||||
Button button_sample_8m{{122, 44, 56, 24}, "8 MSPS"};
|
||||
Button button_sample_20m{{182, 44, 56, 24}, "20 MSPS"};
|
||||
|
||||
// Baseband Filter Section
|
||||
Text text_lbl_bb_filter{{0, 76, 114, 16}, "BB Filter BW:"};
|
||||
Text text_bb_filter{{116, 76, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_reg8{{0, 94, 114, 16}, "MAX2831 R8:"};
|
||||
Text text_reg8{{116, 94, 124, 16}, "---"};
|
||||
|
||||
// GPIO States Section
|
||||
Text text_lbl_gpio{{0, 118, 240, 16}, "--- GPIO Pin States ---"};
|
||||
|
||||
Text text_lbl_lpf{{0, 136, 56, 16}, "LPF:"};
|
||||
Text text_gpio_lpf{{58, 136, 181, 16}, "---"};
|
||||
|
||||
Text text_lbl_mix{{0, 154, 56, 16}, "Mixer:"};
|
||||
Text text_gpio_mix{{58, 154, 181, 16}, "---"};
|
||||
|
||||
Text text_lbl_amp{{0, 172, 56, 16}, "RF Amp:"};
|
||||
Text text_gpio_amp{{58, 172, 181, 16}, "---"};
|
||||
|
||||
// FPGA Control Section
|
||||
Text text_lbl_fpga{{0, 196, 240, 16}, "--- FPGA Control ---"};
|
||||
|
||||
Text text_lbl_fpga_ctrl{{0, 214, 114, 16}, "FPGA Reg 1:"};
|
||||
Text text_fpga_ctrl{{116, 214, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_band{{0, 232, 48, 16}, "Band:"};
|
||||
Text text_band{{50, 232, 190, 16}, "---"};
|
||||
|
||||
Button button_toggle_q{{2, 252, 110, 24}, "Toggle Q Inv"};
|
||||
Button button_toggle_dc{{122, 252, 110, 24}, "Toggle DC Blk"};
|
||||
|
||||
// Control Buttons
|
||||
Button button_refresh{{2, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
class DebugPeripheralsMenuView : public BtnGridView {
|
||||
|
||||
@@ -46,11 +46,10 @@ bool valid_firmware_file(std::filesystem::path::string_type path) {
|
||||
auto result = firmware_file.open(path.c_str());
|
||||
if (!result.is_valid()) {
|
||||
uint64_t file_size = firmware_file.size();
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK && file_size > 1 * 1024 * 1024) {
|
||||
// Portapack firmware files must not be larger than 1MB
|
||||
if (file_size > FLASH_ROM_SIZE) {
|
||||
// Firmware file is larger than the flash size for this device
|
||||
return false;
|
||||
}
|
||||
// May need to add a check to portarf and portarf pro too.
|
||||
checksum = 0;
|
||||
for (uint64_t offset = 0; offset < FLASH_ROM_SIZE && offset < file_size; offset += sizeof(read_buffer)) {
|
||||
auto readResult = firmware_file.read(&read_buffer, sizeof(read_buffer));
|
||||
|
||||
@@ -61,6 +61,19 @@ void SubGhzDRecentEntryDetailView::update_data() {
|
||||
|
||||
if (entry_.data != 0) console.writeln("Data: " + to_string_hex(entry_.data));
|
||||
|
||||
if (entry_.sensorType == FPS_RESTAURANT_PAGER) {
|
||||
uint8_t pager_addr = (entry_.data >> 5) & 0x0F;
|
||||
uint8_t func_code = (entry_.data >> 1) & 0x0F;
|
||||
console.writeln("Station: 0x" + to_string_hex(serial) + " (" + to_string_dec_uint(serial) + ")");
|
||||
console.writeln("Pager: " + to_string_dec_uint(pager_addr));
|
||||
if (func_code == 0x0D)
|
||||
console.writeln("Action: Buzz");
|
||||
else if (func_code == 0x0F)
|
||||
console.writeln("Action: Sync");
|
||||
else
|
||||
console.writeln("Action: 0x" + to_string_hex(func_code));
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPS_KEELOQ) {
|
||||
console.writeln("Fix: " + to_string_hex(fix));
|
||||
console.writeln("Encrypted: " + to_string_hex(encrypted));
|
||||
@@ -280,6 +293,8 @@ const char* SubGhzDView::getSensorTypeName(FPROTO_SUBGHZD_SENSOR type) {
|
||||
return "Marantec24";
|
||||
case FPS_HOLTEKHT6P20B:
|
||||
return "Holtek HT6P20B";
|
||||
case FPS_RESTAURANT_PAGER:
|
||||
return "Rest. Pager";
|
||||
case FPS_Invalid:
|
||||
default:
|
||||
return "Unknown";
|
||||
@@ -306,7 +321,14 @@ void RecentEntriesTable<ui::SubGhzDRecentEntries>::draw(
|
||||
line.reserve(30);
|
||||
|
||||
line = SubGhzDView::getSensorTypeName((FPROTO_SUBGHZD_SENSOR)entry.sensorType);
|
||||
line = line + " " + to_string_hex(entry.data << 32);
|
||||
if (entry.sensorType == FPS_RESTAURANT_PAGER) {
|
||||
uint8_t pgr = (entry.data >> 5) & 0x0F;
|
||||
uint8_t func = (entry.data >> 1) & 0x0F;
|
||||
line += " P:" + to_string_dec_uint(pgr) + (func == 0x0D ? " Buzz" : func == 0x0F ? " Sync"
|
||||
: "");
|
||||
} else {
|
||||
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);
|
||||
@@ -873,5 +895,12 @@ void SubGhzDRecentEntryDetailView::parseProtocol() {
|
||||
btn = (entry_.data >> 4) & 0xF;
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPS_RESTAURANT_PAGER) {
|
||||
// 25-bit EV1527-variant: [sysid:16][pager:4][func:4][stop:1]
|
||||
serial = (entry_.data >> 9) & 0xFFFF; // System ID
|
||||
// btn/cnt left as SD_NO values; friendly output in update_data()
|
||||
return;
|
||||
}
|
||||
}
|
||||
} // namespace ui
|
||||
|
||||
@@ -659,6 +659,8 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
|
||||
/* PRALINE: Match HackRF USB sample_rate_frac_set()
|
||||
* Reference: hackrf_usb radio.c lines 29-91, hackrf_core.c lines 501-685 */
|
||||
|
||||
_base_band_frequency = frequency;
|
||||
|
||||
// Set FPGA decimation to 0 (no decimation) for direct passthrough
|
||||
fpga_debug_register_write(2, 0);
|
||||
radio::invalidate_spi_config();
|
||||
|
||||
@@ -70,6 +70,10 @@ class ClockManager {
|
||||
|
||||
void set_sampling_frequency(const uint32_t frequency);
|
||||
|
||||
uint32_t get_sampling_frequency() const {
|
||||
return _base_band_frequency;
|
||||
};
|
||||
|
||||
void set_reference_ppb(const int32_t ppb);
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -92,6 +96,8 @@ class ClockManager {
|
||||
si5351::Si5351& clock_generator;
|
||||
Reference reference;
|
||||
|
||||
uint32_t _base_band_frequency{20000000};
|
||||
|
||||
void set_gp_clkin_to_clkin_direct();
|
||||
|
||||
void start_frequency_monitor_measurement(const cgu::CLK_SEL clk_sel);
|
||||
|
||||
@@ -256,7 +256,9 @@ ui::Widget* EventDispatcher::touch_widget(ui::Widget* const w, ui::TouchEvent ev
|
||||
if (!w->hidden()) {
|
||||
// To achieve reverse depth ordering (last object drawn is
|
||||
// considered "top"), descend first.
|
||||
for (const auto child : w->children()) {
|
||||
auto& children = w->children();
|
||||
for (auto it = children.rbegin(); it != children.rend(); ++it) { // reverse, bc the lastly added will be "top" if overlaps
|
||||
const auto& child = *it;
|
||||
const auto touched_widget = touch_widget(child, event);
|
||||
if (touched_widget) {
|
||||
return touched_widget;
|
||||
|
||||
+1
-1
@@ -339,7 +339,7 @@ void AdultToysView::randomizeMac() {
|
||||
}
|
||||
|
||||
void AdultToysView::randomChn() {
|
||||
channel_number = 37 + std::rand() % (39 - 37 + 1);
|
||||
channel_number = 37 + rand() % (39 - 37 + 1);
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ void BlackjackView::paint(Painter& painter) {
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_deck();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ void BLESpamView::reset() {
|
||||
}
|
||||
|
||||
void BLESpamView::randomChn() {
|
||||
channel_number = 37 + std::rand() % (39 - 37 + 1);
|
||||
channel_number = 37 + rand() % (39 - 37 + 1);
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -655,7 +655,7 @@ void BreakoutView::paint(Painter& painter) {
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_game();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ void DinoGameView::paint(Painter& painter) {
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_game();
|
||||
}
|
||||
}
|
||||
|
||||
+13
-13
@@ -204,11 +204,11 @@ void spawn_entity(uint8_t type, uint8_t x, uint8_t y) {
|
||||
|
||||
void spawn_random_entity(uint8_t type) {
|
||||
if (num_entities >= MAX_ENTITIES) return;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
uint8_t spawn_x, spawn_y;
|
||||
do {
|
||||
spawn_x = std::rand() % LEVEL_WIDTH;
|
||||
spawn_y = std::rand() % LEVEL_HEIGHT;
|
||||
spawn_x = rand() % LEVEL_WIDTH;
|
||||
spawn_y = rand() % LEVEL_HEIGHT;
|
||||
} while (get_block_at(spawn_x, spawn_y) == 0xF ||
|
||||
(spawn_x == (uint8_t)player.pos.x && spawn_y == (uint8_t)player.pos.y));
|
||||
|
||||
@@ -227,9 +227,9 @@ void remove_entity(uint8_t index) {
|
||||
bool spawned = false;
|
||||
|
||||
while (!spawned && attempts < 50) {
|
||||
std::srand(LPC_RTC->CTIME0 + attempts);
|
||||
uint8_t spawn_x = std::rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = std::rand() % LEVEL_HEIGHT;
|
||||
srand(LPC_RTC->CTIME0 + attempts);
|
||||
uint8_t spawn_x = rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = rand() % LEVEL_HEIGHT;
|
||||
|
||||
int16_t dx = (int16_t)spawn_x - (int16_t)player.pos.x;
|
||||
int16_t dy = (int16_t)spawn_y - (int16_t)player.pos.y;
|
||||
@@ -270,9 +270,9 @@ void initialize_level() {
|
||||
uint8_t attempts = 0;
|
||||
|
||||
while (initial_enemies < 2 && num_entities < MAX_ENTITIES && attempts < 50) {
|
||||
std::srand(LPC_RTC->CTIME0 + attempts);
|
||||
int8_t offset_x = (std::rand() % 11) - 5;
|
||||
int8_t offset_y = (std::rand() % 11) - 5;
|
||||
srand(LPC_RTC->CTIME0 + attempts);
|
||||
int8_t offset_x = (rand() % 11) - 5;
|
||||
int8_t offset_y = (rand() % 11) - 5;
|
||||
|
||||
if (abs(offset_x) < 3 && abs(offset_y) < 3) {
|
||||
attempts++;
|
||||
@@ -307,10 +307,10 @@ void initialize_level() {
|
||||
|
||||
attempts = 0;
|
||||
while (num_entities < MIN_ENTITIES && attempts < 100) {
|
||||
std::srand(LPC_RTC->CTIME0 + attempts + 100);
|
||||
srand(LPC_RTC->CTIME0 + attempts + 100);
|
||||
|
||||
uint8_t spawn_x = std::rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = std::rand() % LEVEL_HEIGHT;
|
||||
uint8_t spawn_x = rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = rand() % LEVEL_HEIGHT;
|
||||
|
||||
int16_t dx = (int16_t)spawn_x - (int16_t)player.pos.x;
|
||||
int16_t dy = (int16_t)spawn_y - (int16_t)player.pos.y;
|
||||
@@ -1045,7 +1045,7 @@ void DoomView::on_show() {
|
||||
void DoomView::paint(Painter& painter) {
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
scene = 0;
|
||||
up = down = left = right = fired = false;
|
||||
jogging = view_height = 0;
|
||||
|
||||
@@ -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
|
||||
|
||||
+7
@@ -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));
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ Game2048View::Game2048View(NavigationView& nav)
|
||||
|
||||
void Game2048View::on_show() {
|
||||
if (!initialized) {
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
reset_game();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
@@ -239,10 +239,10 @@ void RandomPasswordView::on_freqchg(int64_t freq) {
|
||||
}
|
||||
|
||||
void RandomPasswordView::set_random_freq() {
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
// this is only for seed to visit random freq, the radio is still real random
|
||||
|
||||
auto random_freq = 100000000 + (std::rand() % 900000000); // 100mhz to 1ghz
|
||||
auto random_freq = 100000000 + (rand() % 900000000); // 100mhz to 1ghz
|
||||
receiver_model.set_target_frequency(random_freq);
|
||||
field_frequency.set_value(random_freq);
|
||||
}
|
||||
@@ -297,12 +297,12 @@ void RandomPasswordView::new_password() {
|
||||
/// roll worker
|
||||
for (int i = 0; i < password_length * 2; i += 2) {
|
||||
unsigned int seed = seeds_deque[i];
|
||||
std::srand(seed);
|
||||
srand(seed);
|
||||
uint8_t rollnum = (uint8_t)(seeds_deque[i + 1] % 128);
|
||||
uint8_t nu = 0;
|
||||
for (uint8_t o = 0; o < rollnum; ++o) nu = std::rand();
|
||||
for (uint8_t o = 0; o < rollnum; ++o) nu = rand();
|
||||
nu++;
|
||||
char c = charset[std::rand() % charset.length()];
|
||||
char c = charset[rand() % charset.length()];
|
||||
initial_password += c;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -221,7 +221,7 @@ void SnakeView::paint(Painter& painter) {
|
||||
(void)painter;
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_game();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -605,7 +605,7 @@ void pause_game() {
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::srand(GenerateRandomSeed());
|
||||
srand(GenerateRandomSeed());
|
||||
Init();
|
||||
ShowLevelMenu();
|
||||
joystick.attach(&ReadJoystickForLevel, 0.3);
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+137
-1
@@ -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<TPMSLogger>();
|
||||
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<TPMSRecentEntryDetailView>(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 {
|
||||
|
||||
@@ -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<TPMSRecentEntries>;
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
|
||||
+83
@@ -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<TPMSTXView>();
|
||||
}
|
||||
} // 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
|
||||
};
|
||||
}
|
||||
@@ -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<uint8_t, 20> 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<tpms::Reading::Type>(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<FileLoadView>(".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<int>(tpms::Reading::Type::FLM_64) &&
|
||||
type <= static_cast<int>(tpms::Reading::Type::GMC_96)) {
|
||||
packet_type_ = static_cast<tpms::Reading::Type>(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<tpms::SignalType>(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
|
||||
@@ -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<const TXProgressMessage*>(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__*/
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
#include "../../hackrf/firmware/common/firmware_info.h"
|
||||
#include "../../hackrf/firmware/common/platform_detect.h"
|
||||
|
||||
// Use the same logic as official HackRF firmware
|
||||
#ifdef JAWBREAKER
|
||||
#define SUPPORTED_PLATFORM PLATFORM_JAWBREAKER
|
||||
#elif HACKRF_ONE
|
||||
#define SUPPORTED_PLATFORM (PLATFORM_HACKRF1_OG | PLATFORM_HACKRF1_R9)
|
||||
#elif RAD1O
|
||||
#define SUPPORTED_PLATFORM PLATFORM_RAD1O
|
||||
#elif PRALINE
|
||||
#define SUPPORTED_PLATFORM PLATFORM_PRALINE
|
||||
#else
|
||||
#define SUPPORTED_PLATFORM 0
|
||||
#endif
|
||||
|
||||
#define DFU_MODE_VALUE 0
|
||||
|
||||
__attribute__((section(".firmware_info"))) const struct firmware_info_t firmware_info = {
|
||||
|
||||
@@ -38,6 +38,16 @@ using namespace hackrf::one;
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
// Global debug tracking for MAX2831
|
||||
struct max2831_debug_t {
|
||||
uint32_t requested_freq_mhz;
|
||||
uint32_t calculated_n;
|
||||
uint32_t calculated_frac;
|
||||
bool set_frequency_called;
|
||||
bool frequency_valid;
|
||||
};
|
||||
extern "C" max2831_debug_t max2831_debug_info = {0, 0, 0, false, false};
|
||||
|
||||
namespace max2831 {
|
||||
|
||||
using namespace max283x;
|
||||
@@ -120,7 +130,8 @@ void MAX2831::init() {
|
||||
// set_reg_field(11, REG11_RXVGA_GAIN_MASK, 0x1F); // 62 dB VGA = MAX
|
||||
|
||||
/* Configure baseband filter for 8 MHz TX - matches GSG reference */
|
||||
set_reg_field(8, REG8_LPF_COARSE_MASK, REG8_RX_LPF_7_5M);
|
||||
// set_reg_field(8, REG8_LPF_COARSE_MASK, REG8_RX_LPF_7_5M);
|
||||
set_reg_field(8, REG8_LPF_COARSE_MASK, REG8_RX_LPF_15M);
|
||||
set_reg_field(7, REG7_RX_LPF_FINE_MASK, REG7_RX_LPF_FINE_100);
|
||||
set_reg_field(7, REG7_TX_LPF_FINE_MASK, REG7_TX_LPF_FINE_100);
|
||||
|
||||
@@ -329,9 +340,21 @@ uint32_t MAX2831::set_lpf_bandwidth_internal(const uint32_t bandwidth_hz) {
|
||||
|
||||
void MAX2831::set_lpf_rf_bandwidth_rx(const uint32_t bandwidth_minimum) {
|
||||
_desired_lpf_bw = bandwidth_minimum;
|
||||
#ifdef PRALINE
|
||||
uint32_t actual_bw = bandwidth_minimum;
|
||||
if (actual_bw < 22000000) {
|
||||
actual_bw = 22000000; // Never go below 15 MHz, set by choosing 22.6 MHz bandwidth
|
||||
}
|
||||
|
||||
_desired_lpf_bw = actual_bw;
|
||||
if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) {
|
||||
set_lpf_bandwidth_internal(actual_bw);
|
||||
}
|
||||
#else
|
||||
if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) {
|
||||
set_lpf_bandwidth_internal(bandwidth_minimum);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MAX2831::set_lpf_rf_bandwidth_tx(const uint32_t bandwidth_minimum) {
|
||||
@@ -355,7 +378,20 @@ bool MAX2831::set_frequency(const rf::Frequency lo_frequency) {
|
||||
*/
|
||||
|
||||
/* MAX2831 supports 2.3-2.6 GHz */
|
||||
if (lo_frequency < 2300000000ULL || lo_frequency > 2600000000ULL) {
|
||||
// if (lo_frequency < 2300000000ULL || lo_frequency > 2600000000ULL) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
bool valid = (lo_frequency >= 2300000000ULL && lo_frequency <= 2600000000ULL);
|
||||
|
||||
// TRACK REQUEST IMMEDIATELY
|
||||
max2831_debug_info.requested_freq_mhz = lo_frequency / 1000000;
|
||||
max2831_debug_info.set_frequency_called = true;
|
||||
max2831_debug_info.frequency_valid = valid;
|
||||
|
||||
if (!valid) {
|
||||
max2831_debug_info.calculated_n = 0;
|
||||
max2831_debug_info.calculated_frac = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -377,6 +413,10 @@ bool MAX2831::set_frequency(const rf::Frequency lo_frequency) {
|
||||
}
|
||||
}
|
||||
|
||||
// TRACK CALCULATED VALUES
|
||||
max2831_debug_info.calculated_n = div_int;
|
||||
max2831_debug_info.calculated_frac = div_frac;
|
||||
|
||||
/* Write order matters - matches GSG reference */
|
||||
/* REG 3: SYN_INT (bits 7:0) and SYN_FRAC_LO (bits 13:8) */
|
||||
uint16_t reg3_val = (div_int & 0xFF) | ((div_frac & 0x3F) << 8);
|
||||
|
||||
@@ -197,9 +197,11 @@ struct SynthConfig {
|
||||
*/
|
||||
|
||||
void RFFC507x::init() {
|
||||
#ifndef PRALINE
|
||||
gpio_rffc5072_resetx.set();
|
||||
gpio_rffc5072_resetx.output();
|
||||
reset();
|
||||
#endif
|
||||
|
||||
_bus.init();
|
||||
|
||||
@@ -211,10 +213,12 @@ void RFFC507x::reset() {
|
||||
/* TODO: Is RESETB pin ignored if sdi_ctrl.sipin=1? Programming guide
|
||||
* description of sdi_ctrl.sipin suggests the pin is not ignored.
|
||||
*/
|
||||
#ifndef PRALINE
|
||||
gpio_rffc5072_resetx.clear();
|
||||
halPolledDelay(ticks_during_reset);
|
||||
gpio_rffc5072_resetx.set();
|
||||
halPolledDelay(ticks_after_reset);
|
||||
#endif
|
||||
}
|
||||
|
||||
void RFFC507x::flush() {
|
||||
|
||||
@@ -145,6 +145,21 @@ Continuous (Fox-oring)
|
||||
rffc507x::RFFC507x first_if;
|
||||
ui::SystemView* system_view_ptr;
|
||||
|
||||
static uint32_t random_seed_state = 123456789;
|
||||
extern "C" int rand(void) {
|
||||
random_seed_state ^= random_seed_state << 13;
|
||||
random_seed_state ^= random_seed_state >> 17;
|
||||
random_seed_state ^= random_seed_state << 5;
|
||||
return (int)(random_seed_state & 0x7FFFFFFF);
|
||||
}
|
||||
extern "C" void srand(unsigned int seed) {
|
||||
if (seed == 0) {
|
||||
random_seed_state = 123456789;
|
||||
} else {
|
||||
random_seed_state = seed;
|
||||
}
|
||||
}
|
||||
|
||||
static void event_loop() {
|
||||
static ui::Context context;
|
||||
static ui::SystemView system_view{
|
||||
|
||||
@@ -56,11 +56,9 @@ using asahi_kasei::ak4951::AK4951;
|
||||
#include "i2cdevmanager.hpp"
|
||||
#include "battery.hpp"
|
||||
|
||||
#ifndef PRALINE
|
||||
extern "C" {
|
||||
#include "platform_detect.h"
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace portapack {
|
||||
|
||||
@@ -555,12 +553,10 @@ init_status_t init() {
|
||||
|
||||
chThdSleepMilliseconds(100);
|
||||
|
||||
#ifndef PRALINE
|
||||
detect_hardware_platform();
|
||||
finalize_detect_hardware_platform();
|
||||
|
||||
chThdSleepMilliseconds(100);
|
||||
#endif
|
||||
|
||||
configure_pins_portapack();
|
||||
|
||||
|
||||
@@ -205,8 +205,10 @@ void set_direction(const rf::Direction new_direction) {
|
||||
#ifndef PRALINE
|
||||
baseband_cpld.set_invert(mixer_invert ^ baseband_invert);
|
||||
#else
|
||||
// TEST: Force baseband invert for Praline (like r9)
|
||||
// baseband_invert = (direction == rf::Direction::Receive);
|
||||
|
||||
// Praline: Control Q inversion via FPGA register
|
||||
// Assuming register 1 bit 1 controls Q inversion
|
||||
uint8_t ctrl_reg = 0x01; // DC_BLOCK enabled
|
||||
if (mixer_invert ^ baseband_invert) {
|
||||
ctrl_reg |= 0x02; // Set Q_INVERT bit
|
||||
@@ -224,16 +226,6 @@ void set_direction(const rf::Direction new_direction) {
|
||||
led_rx.on();
|
||||
else
|
||||
led_tx.on();
|
||||
|
||||
// #ifdef PRALINE
|
||||
// Try with Q inversion OFF
|
||||
// fpga_debug_register_write(1, 0x01); // DC_BLOCK=1, Q_INVERT=0
|
||||
// ssp1_arbiter.invalidate();
|
||||
|
||||
// If no signals, try with Q inversion ON
|
||||
// fpga_debug_register_write(1, 0x03); // DC_BLOCK=1, Q_INVERT=1
|
||||
// ssp1_arbiter.invalidate();
|
||||
// #endif
|
||||
}
|
||||
|
||||
bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
@@ -278,6 +270,17 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
mixer_invert = tuning_config.mixer_invert;
|
||||
#ifndef PRALINE
|
||||
baseband_cpld.set_invert(mixer_invert ^ baseband_invert);
|
||||
#else
|
||||
// TEST: Force baseband invert for Praline (like r9)
|
||||
// baseband_invert = (direction == rf::Direction::Receive);
|
||||
|
||||
// PRALINE: Update FPGA Q inversion when tuning changes
|
||||
uint8_t ctrl_reg = 0x01; // DC_BLOCK enabled
|
||||
if (mixer_invert ^ baseband_invert) {
|
||||
ctrl_reg |= 0x02; // Set Q_INVERT bit
|
||||
}
|
||||
fpga_debug_register_write(1, ctrl_reg);
|
||||
ssp1_arbiter.invalidate();
|
||||
#endif
|
||||
|
||||
return result_second_if;
|
||||
@@ -454,6 +457,27 @@ TuningInfo get_tuning_info() {
|
||||
|
||||
namespace second_if {
|
||||
|
||||
#ifdef PRALINE
|
||||
extern "C" {
|
||||
extern struct max2831_debug_t {
|
||||
uint32_t requested_freq_mhz;
|
||||
uint32_t calculated_n;
|
||||
uint32_t calculated_frac;
|
||||
bool set_frequency_called;
|
||||
bool frequency_valid;
|
||||
} max2831_debug_info;
|
||||
}
|
||||
|
||||
MAX2831Info get_max2831_info() {
|
||||
return {
|
||||
max2831_debug_info.requested_freq_mhz,
|
||||
max2831_debug_info.calculated_n,
|
||||
max2831_debug_info.calculated_frac,
|
||||
max2831_debug_info.set_frequency_called,
|
||||
max2831_debug_info.frequency_valid};
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t register_read(const size_t register_number) {
|
||||
return radio::second_if->read(register_number);
|
||||
}
|
||||
@@ -468,6 +492,12 @@ int8_t temp_sense() {
|
||||
|
||||
} /* namespace second_if */
|
||||
|
||||
namespace rf_path_info {
|
||||
rf::path::Band get_current_band() {
|
||||
return radio::rf_path.get_band();
|
||||
}
|
||||
} /* namespace rf_path_info */
|
||||
|
||||
#ifdef PRALINE
|
||||
namespace fpga {
|
||||
|
||||
|
||||
@@ -95,6 +95,17 @@ TuningInfo get_tuning_info();
|
||||
|
||||
namespace second_if {
|
||||
|
||||
#ifdef PRALINE
|
||||
struct MAX2831Info {
|
||||
uint32_t requested_freq_mhz;
|
||||
uint32_t calculated_n;
|
||||
uint32_t calculated_frac;
|
||||
bool set_frequency_called;
|
||||
bool frequency_valid;
|
||||
};
|
||||
MAX2831Info get_max2831_info();
|
||||
#endif
|
||||
|
||||
uint32_t register_read(const size_t register_number);
|
||||
void register_write(const size_t register_number, uint32_t value);
|
||||
|
||||
@@ -103,6 +114,12 @@ int8_t temp_sense();
|
||||
|
||||
} /* namespace second_if */
|
||||
|
||||
namespace rf_path_info {
|
||||
|
||||
rf::path::Band get_current_band();
|
||||
|
||||
} /* namespace rf_path_info */
|
||||
|
||||
#ifdef PRALINE
|
||||
namespace fpga {
|
||||
|
||||
|
||||
@@ -258,6 +258,7 @@ void Path::set_direction(const Direction new_direction) {
|
||||
|
||||
void Path::set_band(const Band new_band) {
|
||||
band = new_band;
|
||||
_band = new_band;
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -298,6 +299,7 @@ void Path::update() {
|
||||
|
||||
/* Move to the final state by turning on required signals. */
|
||||
/* LPF for low band */
|
||||
|
||||
config.lpf_en = (band == Band::Low);
|
||||
|
||||
/* RF amp when amplification requested */
|
||||
@@ -307,6 +309,7 @@ void Path::update() {
|
||||
config.ant_bias_en_n = true;
|
||||
|
||||
config.apply();
|
||||
|
||||
#else
|
||||
/* HackRF One RF path control */
|
||||
const auto config = get_config(direction, band, rf_amp);
|
||||
|
||||
@@ -58,12 +58,16 @@ class Path {
|
||||
void set_band(const Band band);
|
||||
void set_rf_amp(const bool rf_amp);
|
||||
|
||||
Band get_band() const { return _band; } //_band is used solely for debugging purposes.
|
||||
|
||||
private:
|
||||
Direction direction{Direction::Receive};
|
||||
Band band{Band::Mid};
|
||||
bool rf_amp{false};
|
||||
|
||||
void update();
|
||||
|
||||
Band _band{Band::Mid}; //_band is solely used of debugging purposes
|
||||
};
|
||||
|
||||
} // namespace path
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "shell.hpp"
|
||||
#include "chprintf.h"
|
||||
#include "portapack.hpp"
|
||||
#include "../flashsize.h"
|
||||
|
||||
extern "C" {
|
||||
#include "platform_detect.h"
|
||||
@@ -93,6 +94,22 @@ static const char* get_board_revision_string(board_rev_t rev) {
|
||||
return "GSG HackRF R9";
|
||||
case BOARD_REV_GSG_HACKRF1_R10:
|
||||
return "GSG HackRF R10";
|
||||
case BOARD_REV_PRALINE_R0_1:
|
||||
case BOARD_REV_PRALINE_R0_2:
|
||||
case BOARD_REV_PRALINE_R0_3:
|
||||
return "HackRF Pro R0";
|
||||
case BOARD_REV_PRALINE_R1_0:
|
||||
case BOARD_REV_PRALINE_R1_1:
|
||||
case BOARD_REV_PRALINE_R1_2:
|
||||
return "HackRF Pro R1";
|
||||
case BOARD_REV_GSG_PRALINE_R0_1:
|
||||
case BOARD_REV_GSG_PRALINE_R0_2:
|
||||
case BOARD_REV_GSG_PRALINE_R0_3:
|
||||
return "GSG HackRF Pro R0";
|
||||
case BOARD_REV_GSG_PRALINE_R1_0:
|
||||
case BOARD_REV_GSG_PRALINE_R1_1:
|
||||
case BOARD_REV_GSG_PRALINE_R1_2:
|
||||
return "GSG HackRF Pro R1";
|
||||
case BOARD_REV_UNRECOGNIZED:
|
||||
return "Unrecognized";
|
||||
case BOARD_REV_UNDETECTED:
|
||||
@@ -134,6 +151,36 @@ static void cmd_info(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
board_rev_t revision = detected_revision();
|
||||
const char* revision_string = get_board_revision_string(revision);
|
||||
chprintf(chp, "HackRF Board Rev: %s\r\n", revision_string);
|
||||
|
||||
// Determine device type string
|
||||
const char* device_str;
|
||||
#ifdef PRALINE
|
||||
device_str = "HackRF Pro";
|
||||
#else
|
||||
if (portapack::device_type == portapack::DEV_PORTARF) {
|
||||
device_str = "PortaRF";
|
||||
} else {
|
||||
device_str = "HackRF One";
|
||||
}
|
||||
#endif
|
||||
chprintf(chp, "Device: %s\r\n", device_str);
|
||||
|
||||
// Flash info: build-time allowed MB, runtime-detected limit, current firmware size
|
||||
// Use explicit uint32_t cast: FLASH_SIZE_LIMIT_MB may be a float (e.g. 3.5 for HPro)
|
||||
uint8_t flash_allowrun;
|
||||
#ifdef PRALINE
|
||||
flash_allowrun = 4; // HackRF Pro
|
||||
#else
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK) {
|
||||
flash_allowrun = 1; // PortaPack classic
|
||||
} else {
|
||||
flash_allowrun = FLASH_SIZE_MB; // PortaRF
|
||||
}
|
||||
#endif
|
||||
chprintf(chp, "Flash Allowed: %dMB\r\n", (uint32_t)FLASH_SIZE_MB);
|
||||
chprintf(chp, "Flash Runtime: %dMB\r\n", (uint32_t)flash_allowrun);
|
||||
chprintf(chp, "Flash Current: %dMB\r\n", (uint32_t)FLASH_SIZE_LIMIT_MB);
|
||||
|
||||
chprintf(chp, "Reference Source: %s\r\n", portapack::clock_manager.get_source().c_str());
|
||||
chprintf(chp, "Reference Freq: %s\r\n", portapack::clock_manager.get_freq().c_str());
|
||||
#ifdef __DATE__
|
||||
|
||||
@@ -265,7 +265,7 @@ void GeoMap::map_read_line_bin(ui::Color* buffer, uint16_t pixels) {
|
||||
|
||||
void GeoMap::draw_markers(Painter& painter) {
|
||||
for (int i = 0; i < markerListLen; ++i) {
|
||||
draw_marker_item(painter, markerList[i], Color::blue(), Color::blue(), Color::magenta());
|
||||
draw_marker_item(painter, markerList[i], markerList[i].color, markerList[i].color, Color::black());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,7 +826,7 @@ void GeoMap::update_my_position(float lat, float lon, int32_t altitude) {
|
||||
my_pos.lat = lat;
|
||||
my_pos.lon = lon;
|
||||
my_altitude = altitude;
|
||||
redraw_map = is_changed;
|
||||
redraw_map |= is_changed;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
|
||||
@@ -62,13 +62,14 @@ struct GeoMarker {
|
||||
float lon{0};
|
||||
uint16_t angle{0};
|
||||
std::string tag{""};
|
||||
Color color{Color::blue()};
|
||||
|
||||
GeoMarker& operator=(GeoMarker& rhs) {
|
||||
GeoMarker& operator=(const GeoMarker& rhs) {
|
||||
lat = rhs.lat;
|
||||
lon = rhs.lon;
|
||||
angle = rhs.angle;
|
||||
tag = rhs.tag;
|
||||
|
||||
color = rhs.color;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -607,7 +607,14 @@ bool InformationView::firmware_checksum_error() {
|
||||
|
||||
// only checking firmware checksum once per boot
|
||||
if (!fw_checksum_checked) {
|
||||
#ifdef PRALINE
|
||||
fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, 4 * 1024 * 1024) != FLASH_EXPECTED_CHECKSUM);
|
||||
// TODO: This is a minimal workaround to fix the FLASH ERR checksum, bc GSG's cmake doesn't define the PortaRF board,
|
||||
// so if we do, we need to patch many codes. so we can't define the PortaRF board currently in our cmake.
|
||||
// discuss needed to find a better way but currently we need CI works and make hackrf pro works as much as possible.
|
||||
#else
|
||||
fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, FLASH_SIZE_LIMIT_MB * 1024 * 1024) != FLASH_EXPECTED_CHECKSUM);
|
||||
#endif
|
||||
}
|
||||
return fw_checksum_error;
|
||||
}
|
||||
@@ -969,6 +976,8 @@ SystemView::SystemView(
|
||||
|
||||
navigation_view.push<SystemMenuView>();
|
||||
|
||||
add_child(¬ification_view);
|
||||
|
||||
if (pmem::config_splash()) {
|
||||
navigation_view.push<SplashScreenView>();
|
||||
}
|
||||
@@ -1054,20 +1063,10 @@ SplashScreenView::SplashScreenView(NavigationView& nav)
|
||||
};
|
||||
}
|
||||
|
||||
uint32_t SplashScreenView::myrand(uint32_t* state) {
|
||||
uint32_t x = *state;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
*state = x;
|
||||
return x;
|
||||
}
|
||||
|
||||
void SplashScreenView::get_random_splash_file(std::filesystem::path& path) {
|
||||
path = u"";
|
||||
|
||||
uint32_t rng_state = LPC_RTC->CTIME0;
|
||||
if (rng_state == 0) rng_state = 0xABBACAFE;
|
||||
srand(LPC_RTC->CTIME0);
|
||||
|
||||
DIR dir;
|
||||
FILINFO fno;
|
||||
@@ -1089,7 +1088,7 @@ void SplashScreenView::get_random_splash_file(std::filesystem::path& path) {
|
||||
(ext[3] == 'P' || ext[3] == 'p')) {
|
||||
valid_count++;
|
||||
// Reservoir Sampling:
|
||||
if ((myrand(&rng_state) % valid_count) == 0) {
|
||||
if ((rand() % valid_count) == 0) {
|
||||
path = splash_dir / fno.fname;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
#include "ui_audio.hpp"
|
||||
#include "ui_sd_card_status_view.hpp"
|
||||
#include "ui_dfu_menu.hpp"
|
||||
|
||||
#include "ui_notifications.hpp"
|
||||
#include "bitmap.hpp"
|
||||
#include "ui_bmpview.hpp"
|
||||
#include "ff.h"
|
||||
@@ -366,7 +366,6 @@ class SplashScreenView : public View {
|
||||
Button button_done{
|
||||
{screen_width, 0, 1, 1},
|
||||
""};
|
||||
uint32_t myrand(uint32_t* state);
|
||||
void get_random_splash_file(std::filesystem::path& path);
|
||||
};
|
||||
|
||||
@@ -447,11 +446,12 @@ class SystemView : public View {
|
||||
private:
|
||||
uint8_t overlay_active{0};
|
||||
|
||||
NavigationView navigation_view{};
|
||||
SystemStatusView status_view{navigation_view};
|
||||
InformationView info_view{navigation_view};
|
||||
NotificationView notification_view{navigation_view};
|
||||
DfuMenu overlay{navigation_view};
|
||||
DfuMenu2 overlay2{navigation_view};
|
||||
NavigationView navigation_view{};
|
||||
Context& context_;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "ui_notifications.hpp"
|
||||
|
||||
#include "ui_navigation.hpp"
|
||||
|
||||
extern ui::SystemView* system_view_ptr;
|
||||
|
||||
namespace ui {
|
||||
|
||||
NotificationEntryView::NotificationEntryView(const NotificationEntry& entry, NotificationView* notifhandler)
|
||||
: entry_(entry), notifhandler_(notifhandler) {
|
||||
add_children({&background, &border, &title_text, &message_text, &close_button});
|
||||
border.set_outline(true);
|
||||
if (entry_.icon != NOTIF_ICON_NONE) {
|
||||
add_child(&icon_image);
|
||||
icon_image.set_parent_rect({UI_POS_X(0) + 2, UI_POS_Y(1), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)});
|
||||
message_text.set_parent_rect({UI_POS_X(2) + 2, UI_POS_Y(1), UI_POS_WIDTH_REMAINING(6) - 2, UI_POS_HEIGHT(2)});
|
||||
if (entry_.icon == NOTIF_ICON_MESSAGE) {
|
||||
icon_image.set_bitmap(&bitmap_icon_pocsag);
|
||||
}
|
||||
|
||||
} else {
|
||||
message_text.set_parent_rect({UI_POS_X(1), UI_POS_Y(1), UI_POS_WIDTH_REMAINING(5) - 1, UI_POS_HEIGHT(2)});
|
||||
}
|
||||
title_text.set_style(Theme::getInstance()->bg_dark);
|
||||
title_text.set(entry_.title);
|
||||
message_text.set_style(Theme::getInstance()->bg_darkest);
|
||||
message_text.set(entry_.message);
|
||||
|
||||
close_button.on_select = [this](Button&) {
|
||||
if (notifhandler_) {
|
||||
notifhandler_->remove_notification(entry_.id);
|
||||
}
|
||||
};
|
||||
|
||||
message_text.on_select = [this](Text&) {
|
||||
if (!entry_.source_app.empty() && notifhandler_) {
|
||||
notifhandler_->open_notification(entry_.source_app);
|
||||
// notifhandler_->remove_notification(entry_.id);
|
||||
}
|
||||
};
|
||||
title_text.on_select = message_text.on_select;
|
||||
}
|
||||
|
||||
NotificationView::NotificationView(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
this->on_tick_second();
|
||||
};
|
||||
}
|
||||
|
||||
NotificationView::~NotificationView() {
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
}
|
||||
|
||||
void NotificationView::on_tick_second() {
|
||||
bool changed = false;
|
||||
for (size_t i = 0; i < notification_views_.size();) {
|
||||
if (notification_views_[i]->entry()->increase_time(1000)) {
|
||||
notification_views_.erase(notification_views_.begin() + i);
|
||||
changed = true;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
rearrange_notifications();
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationView::rearrange_notifications() {
|
||||
size_t count = children().size();
|
||||
while (children().size() > 0) {
|
||||
remove_child(children().back());
|
||||
}
|
||||
|
||||
const int entry_height = UI_POS_HEIGHT(3) + 1;
|
||||
int index = 0;
|
||||
|
||||
for (auto& view_ptr : notification_views_) {
|
||||
view_ptr->set_parent_rect({UI_POS_X(0),
|
||||
(Coord)(UI_POS_Y(0) + (index * entry_height)),
|
||||
UI_POS_MAXWIDTH,
|
||||
entry_height});
|
||||
add_child(view_ptr.get());
|
||||
index++;
|
||||
}
|
||||
|
||||
set_parent_rect({UI_POS_X(0), UI_POS_Y(0), UI_POS_MAXWIDTH, (Coord)(notification_views_.size() * entry_height)});
|
||||
set_dirty();
|
||||
if (count != children().size()) {
|
||||
// refresh screen!!!
|
||||
system_view_ptr->set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationView::add_notification(NotificationEntry& entry) {
|
||||
if (notification_views_.size() >= max_notifications) {
|
||||
notification_views_.erase(notification_views_.begin());
|
||||
}
|
||||
|
||||
entry.id = ++curr_not_id;
|
||||
notification_views_.push_back(std::make_unique<NotificationEntryView>(entry, this));
|
||||
|
||||
rearrange_notifications();
|
||||
}
|
||||
|
||||
void NotificationView::remove_notification(uint16_t id) {
|
||||
bool found = false;
|
||||
size_t found_index = 0;
|
||||
|
||||
for (size_t i = 0; i < notification_views_.size(); i++) {
|
||||
if (notification_views_[i]->id() == id) {
|
||||
found = true;
|
||||
found_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
notification_views_.erase(notification_views_.begin() + found_index);
|
||||
rearrange_notifications();
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationView::open_notification(std::string app_name) {
|
||||
if (app_name.empty()) return;
|
||||
nav_.StartAppByName(app_name.c_str());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "signal.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "event_m0.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class NotificationView;
|
||||
class NavigationView;
|
||||
|
||||
enum notification_icon_t : uint8_t {
|
||||
NOTIF_ICON_NONE = 0,
|
||||
NOTIF_ICON_MESSAGE
|
||||
};
|
||||
|
||||
class NotificationEntry {
|
||||
public:
|
||||
std::string source_app{};
|
||||
std::string title{};
|
||||
std::string message{};
|
||||
notification_icon_t icon = NOTIF_ICON_NONE;
|
||||
uint16_t timeout = 10000;
|
||||
uint16_t id = 0;
|
||||
|
||||
NotificationEntry() = default;
|
||||
NotificationEntry(const std::string& source_app, const std::string& title, const std::string& message, notification_icon_t icon = NOTIF_ICON_NONE, uint16_t timeout = 10000, uint16_t id = 0)
|
||||
: source_app(source_app), title(title), message(message), icon(icon), timeout(timeout), id(id) {}
|
||||
|
||||
static NotificationEntry build(const std::string& source_app, const std::string& title, const std::string& message, notification_icon_t icon = NOTIF_ICON_NONE, uint16_t timeout = 10000) {
|
||||
return NotificationEntry{source_app, title, message, icon, timeout, 0};
|
||||
}
|
||||
|
||||
bool increase_time(uint16_t delta) {
|
||||
current_time += delta;
|
||||
return (current_time >= timeout);
|
||||
}
|
||||
|
||||
private:
|
||||
uint16_t current_time = 0;
|
||||
};
|
||||
|
||||
class NotificationEntryView : public View {
|
||||
public:
|
||||
NotificationEntryView(const NotificationEntry& entry, NotificationView* notifhandler);
|
||||
|
||||
NotificationEntryView(const NotificationEntryView&) = delete;
|
||||
NotificationEntryView& operator=(const NotificationEntryView&) = delete;
|
||||
|
||||
ui::Rectangle background{{1, 1, UI_POS_MAXWIDTH - 2, UI_POS_HEIGHT(3) - 2}, Theme::getInstance()->bg_darkest->background};
|
||||
ui::Rectangle border{{0, 0, UI_POS_MAXWIDTH, UI_POS_HEIGHT(3) + 1}, Theme::getInstance()->bg_light->background};
|
||||
ui::Text title_text{{UI_POS_X(0) + 1, UI_POS_Y(0) + 1, UI_POS_WIDTH_REMAINING(4) - 3, UI_POS_HEIGHT(1)}, ""};
|
||||
ui::Text message_text{{UI_POS_X(1), UI_POS_Y(1), UI_POS_WIDTH_REMAINING(5) - 1, UI_POS_HEIGHT(2)}, ""};
|
||||
ui::Image icon_image{}; // 16*16 bitmap
|
||||
ui::Button close_button{{UI_POS_X_RIGHT(4) - 2, UI_POS_Y(0) + 1, UI_POS_WIDTH(4), UI_POS_HEIGHT(2)}, "X"};
|
||||
|
||||
uint16_t id() const { return entry_.id; }
|
||||
NotificationEntry* entry() { return &entry_; }
|
||||
|
||||
private:
|
||||
NotificationEntry entry_;
|
||||
NotificationView* notifhandler_;
|
||||
};
|
||||
|
||||
class NotificationView : public View {
|
||||
public:
|
||||
NotificationView(NavigationView& nav);
|
||||
~NotificationView();
|
||||
|
||||
void add_notification(NotificationEntry& entry);
|
||||
void remove_notification(uint16_t id);
|
||||
void open_notification(std::string app_name);
|
||||
|
||||
private:
|
||||
void on_tick_second();
|
||||
void rearrange_notifications();
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
// Changed to unique_ptr to handle non-copyable Views safely
|
||||
std::vector<std::unique_ptr<NotificationEntryView>> notification_views_{};
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
uint16_t curr_not_id = 0;
|
||||
constexpr static uint8_t max_notifications = 4;
|
||||
|
||||
MessageHandlerRegistration message_handler_notifs{
|
||||
Message::ID::NotificationData, [this](Message* const p) {
|
||||
const auto message = static_cast<const NotificationDataMessage*>(p);
|
||||
NotificationEntry entry = NotificationEntry::build(message->source_app, message->title, message->message, static_cast<notification_icon_t>(message->icon), message->timeout);
|
||||
this->add_notification(entry);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
@@ -1386,22 +1386,76 @@ static void cmd_getres(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
static void cmd_getflash(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
uint8_t allowrun = FLASH_SIZE_MB;
|
||||
// FLASH_SIZE_LIMIT_MB may be a float (e.g. 3.5 for HPro), always cast explicitly.
|
||||
uint8_t allowrun;
|
||||
#ifdef PRALINE
|
||||
allowrun = 4; // HackRF Pro
|
||||
#else
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK) {
|
||||
allowrun = 1;
|
||||
allowrun = 1; // PortaPack classic
|
||||
} else {
|
||||
allowrun = FLASH_SIZE_MB; // PortaRF
|
||||
}
|
||||
std::string res = "ALLOWEDFW:" + to_string_dec_uint(FLASH_SIZE_MB) + "\r\nALLOWEDRUNTIME:" + to_string_dec_uint(allowrun) + "\r\nCURRENT:" + to_string_dec_uint(FLASH_SIZE_LIMIT_MB) + "\r\nok\r\n";
|
||||
#endif
|
||||
std::string res = "ALLOWEDFW:" + to_string_dec_uint((uint32_t)FLASH_SIZE_MB) + "\r\nALLOWEDRUNTIME:" + to_string_dec_uint((uint32_t)allowrun) + "\r\nCURRENT:" + to_string_dec_uint((uint32_t)FLASH_SIZE_LIMIT_MB) + "\r\nok\r\n";
|
||||
chprintf(chp, res.c_str());
|
||||
}
|
||||
|
||||
static void cmd_getdevtype(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
std::string res = portapack::device_type == portapack::DeviceType::DEV_PORTARF ? "PORTARF" : "PORTAPACK";
|
||||
std::string res;
|
||||
#ifdef PRALINE
|
||||
res = "HPRO";
|
||||
#else
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTARF) {
|
||||
res = "PORTARF";
|
||||
} else {
|
||||
res = "PORTAPACK";
|
||||
}
|
||||
#endif
|
||||
res += "\r\nok\r\n";
|
||||
chprintf(chp, res.c_str());
|
||||
}
|
||||
|
||||
static void cmd_notification(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
const char* usage = "usage: notif <iconindex> [appname]\r\n";
|
||||
if (argc < 1) {
|
||||
chprintf(chp, usage);
|
||||
return;
|
||||
}
|
||||
int iconindex = atoi(argv[0]);
|
||||
std::string appname = (argc > 1) ? argv[1] : "";
|
||||
chprintf(chp, "Send title, and a <CR>\r\n");
|
||||
std::string title{};
|
||||
uint8_t msg[1]{0};
|
||||
do {
|
||||
size_t bytes_read = chSequentialStreamRead(chp, &msg[0], 1);
|
||||
if (bytes_read != 1)
|
||||
break;
|
||||
if (msg[0] == '\r') // end of title
|
||||
break;
|
||||
if (msg[0] == '\n') continue;
|
||||
title += (char)msg[0];
|
||||
} while (title.size() < 48);
|
||||
std::string message{};
|
||||
chprintf(chp, "Send message, and a <CR>\r\n");
|
||||
do {
|
||||
size_t bytes_read = chSequentialStreamRead(chp, &msg[0], 1);
|
||||
if (bytes_read != 1)
|
||||
break;
|
||||
if (msg[0] == '\r') // end of message
|
||||
break;
|
||||
if (msg[0] == '\n') continue;
|
||||
message += (char)msg[0];
|
||||
} while (message.size() < 298);
|
||||
|
||||
NotificationDataMessage msgn{appname.c_str(), title.c_str(), message.c_str(), (uint8_t)iconindex};
|
||||
EventDispatcher::send_message(msgn);
|
||||
|
||||
chprintf(chp, "ok\r\n");
|
||||
}
|
||||
|
||||
static const ShellCommand commands[] = {
|
||||
{"reboot", cmd_reboot},
|
||||
{"dfu", cmd_dfu},
|
||||
@@ -1440,6 +1494,7 @@ static const ShellCommand commands[] = {
|
||||
{"getres", cmd_getres},
|
||||
{"getflash", cmd_getflash},
|
||||
{"getdevtype", cmd_getdevtype},
|
||||
{"notif", cmd_notification},
|
||||
{NULL, NULL}};
|
||||
|
||||
static const ShellConfig shell_cfg1 = {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
#ifndef __FPROTO_RESTAURANT_PAGER_H__
|
||||
#define __FPROTO_RESTAURANT_PAGER_H__
|
||||
|
||||
#include "subghzdbase.hpp"
|
||||
|
||||
typedef enum : uint8_t {
|
||||
RestaurantPagerDecoderStepReset = 0,
|
||||
RestaurantPagerDecoderStepSaveDuration,
|
||||
RestaurantPagerDecoderStepCheckDuration,
|
||||
} RestaurantPagerDecoderStep;
|
||||
|
||||
class FProtoSubGhzDRestaurantPager : public FProtoSubGhzDBase {
|
||||
public:
|
||||
FProtoSubGhzDRestaurantPager() {
|
||||
sensorType = FPS_RESTAURANT_PAGER;
|
||||
te_short = 204;
|
||||
te_long = 636;
|
||||
te_delta = 100;
|
||||
min_count_bit_for_found = 25;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case RestaurantPagerDecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short * 36) < te_delta * 36)) {
|
||||
// Found preamble
|
||||
parser_step = RestaurantPagerDecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
case RestaurantPagerDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = RestaurantPagerDecoderStepCheckDuration;
|
||||
}
|
||||
break;
|
||||
case RestaurantPagerDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (duration >= ((uint32_t)te_long * 2)) {
|
||||
parser_step = RestaurantPagerDecoderStepSaveDuration;
|
||||
if (decode_count_bit == min_count_bit_for_found) {
|
||||
// Skip all-ones preamble frames
|
||||
if ((decode_data >> 1) != 0xFFFFFF) {
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback) callback(this);
|
||||
}
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) < te_delta * 3)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = RestaurantPagerDecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta * 3) &&
|
||||
(DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = RestaurantPagerDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = RestaurantPagerDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = RestaurantPagerDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -55,6 +55,7 @@ So include here the .hpp, and add a new element to the protos vector in the cons
|
||||
#include "s-marantec24.hpp"
|
||||
// GENIE FROM PR
|
||||
#include "s-holtek_ht6p20b.hpp"
|
||||
#include "s-restaurant_pager.hpp"
|
||||
|
||||
#ifndef __FPROTO_PROTOLISTSGZ_H__
|
||||
#define __FPROTO_PROTOLISTSGZ_H__
|
||||
@@ -109,6 +110,7 @@ class SubGhzDProtos : public FProtoListGeneral {
|
||||
protos[FPS_GANGQI] = new FProtoSubGhzDGangqi();
|
||||
protos[FPS_MARANTEC24] = new FProtoSubGhzDMarantec24();
|
||||
protos[FPS_HOLTEKHT6P20B] = new FProtoSubGhzDHoltekHt6p20b();
|
||||
protos[FPS_RESTAURANT_PAGER] = new FProtoSubGhzDRestaurantPager();
|
||||
|
||||
for (uint8_t i = 0; i < FPS_COUNT; ++i) {
|
||||
if (protos[i] != NULL) protos[i]->setCallback(callbackTarget);
|
||||
|
||||
@@ -59,6 +59,7 @@ enum FPROTO_SUBGHZD_SENSOR : uint8_t {
|
||||
FPS_GANGQI,
|
||||
FPS_MARANTEC24,
|
||||
FPS_HOLTEKHT6P20B,
|
||||
FPS_RESTAURANT_PAGER,
|
||||
FPS_COUNT
|
||||
};
|
||||
|
||||
|
||||
@@ -927,14 +927,29 @@ extern "C" void boardInit(void) {
|
||||
|
||||
/* Configure Port A pins for RF path control */
|
||||
/* PA_1 = GPIO4[8] LPF enable */
|
||||
LPC_SCU->SFSP[0xA][1] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */
|
||||
LPC_SCU->SFSP[0xA][1] = 0xF0; /* SCU_GPIO_FAST | FUNCTION0 */
|
||||
LPC_GPIO->SET[4] = (1 << 8); /* LPF enabled by default (low band) */
|
||||
LPC_GPIO->DIR[4] |= (1 << 8); /* Output */
|
||||
/* PA_2 = GPIO4[9] RF amp enable */
|
||||
LPC_SCU->SFSP[0xA][2] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */
|
||||
LPC_SCU->SFSP[0xA][2] = 0xF0; /* SCU_GPIO_FAST | FUNCTION0 */
|
||||
LPC_GPIO->CLR[4] = (1 << 9); /* RF amp off by default */
|
||||
LPC_GPIO->DIR[4] |= (1 << 9); /* Output */
|
||||
|
||||
/* Configure RFFC5072 control pins for PRALINE */
|
||||
/* P5_4 = GPIO2[13] RFFC5072 ENX (active low: 0=enabled) */
|
||||
LPC_SCU->SFSP[5][4] = 0xF0; /* FUNCTION0 (GPIO), no pulls */
|
||||
LPC_GPIO->DIR[2] &= ~(1 << 13); /* ENX: INPUT (let FPGA control) */
|
||||
|
||||
/* P5_5 = GPIO2[14] RFFC5072 RESETX - FPGA controlled, MCU should not touch */
|
||||
LPC_SCU->SFSP[5][5] = 0xF0; /* FUNCTION0 (GPIO), no pulls */
|
||||
LPC_GPIO->DIR[2] &= ~(1 << 14); /* RESETX: INPUT (let FPGA control) */
|
||||
|
||||
/* PD_11 = GPIO6[25] RFFC5072 Lock Detect (input) */
|
||||
LPC_SCU->SFSP[0xD][11] = 0xF0; /* FUNCTION0 (GPIO), no pulls */
|
||||
|
||||
/* Small delay for signals to stabilize */
|
||||
for (volatile int i = 0; i < 10000; i++) {}
|
||||
|
||||
/* Configure PRALINE-specific SGPIO pins for FPGA sample interface.
|
||||
* These override the HackRF One pin config from pins_setup.
|
||||
* PRALINE uses different pins than HackRF One for SGPIO4/8/9/10.
|
||||
|
||||
@@ -154,6 +154,7 @@ class Message {
|
||||
MorseTXkey = 96,
|
||||
StreamTXConfiguration = 97,
|
||||
RTTYData = 98,
|
||||
NotificationData = 99,
|
||||
MAX
|
||||
};
|
||||
|
||||
@@ -1793,4 +1794,33 @@ class RTTYDataMessage : public Message {
|
||||
static constexpr uint16_t max_len = 490;
|
||||
};
|
||||
|
||||
class NotificationDataMessage : public Message {
|
||||
public:
|
||||
constexpr NotificationDataMessage(const char* source_app, const char* title, const char* message, uint8_t icon = 0, uint16_t timeout = 10000)
|
||||
: Message{ID::NotificationData},
|
||||
icon(icon),
|
||||
timeout(timeout) {
|
||||
if (source_app) {
|
||||
size_t len = std::min(strlen(source_app), (size_t)19);
|
||||
memcpy(this->source_app, source_app, len);
|
||||
this->source_app[len] = '\0';
|
||||
}
|
||||
if (title) {
|
||||
size_t len = std::min(strlen(title), (size_t)49);
|
||||
memcpy(this->title, title, len);
|
||||
this->title[len] = '\0';
|
||||
}
|
||||
if (message) {
|
||||
size_t len = std::min(strlen(message), (size_t)299);
|
||||
memcpy(this->message, message, len);
|
||||
this->message[len] = '\0';
|
||||
}
|
||||
}
|
||||
char source_app[20]{0}; // source application name, null-terminated, max 19 chars + null
|
||||
char title[50]{0}; // title, null-terminated, max 49 chars + null
|
||||
char message[300]{0}; // message, null-terminated, max 299 chars + null
|
||||
uint8_t icon = 0;
|
||||
uint16_t timeout = 10000;
|
||||
};
|
||||
|
||||
#endif /*__MESSAGE_H__*/
|
||||
|
||||
@@ -397,18 +397,35 @@ void Text::getWidgetName(std::string& result) {
|
||||
void Text::paint(Painter& painter) {
|
||||
const auto rect = screen_rect();
|
||||
auto s = has_focus() ? style().invert() : style();
|
||||
auto max_len = (unsigned)rect.width() / s.font.char_width();
|
||||
auto text_view = std::string_view{text};
|
||||
|
||||
painter.fill_rectangle(rect, s.background);
|
||||
const int char_width = s.font.char_width();
|
||||
const int line_height = s.font.line_height();
|
||||
if (char_width == 0 || line_height == 0) return;
|
||||
const size_t chars_per_line = rect.width() / char_width;
|
||||
if (chars_per_line == 0) return;
|
||||
size_t lines_capacity = rect.height() / line_height;
|
||||
if (lines_capacity == 0) lines_capacity = 1; // at least one line, even if it overflows vertically
|
||||
auto text_view = std::string_view{text};
|
||||
size_t current_offset = 0;
|
||||
for (size_t line_idx = 0; line_idx < lines_capacity; ++line_idx) {
|
||||
if (current_offset >= text_view.length()) break;
|
||||
size_t chunk_len = std::min(chars_per_line, text_view.length() - current_offset);
|
||||
painter.draw_string(
|
||||
rect.location() + Point(0, line_idx * line_height),
|
||||
s,
|
||||
text_view.substr(current_offset, chunk_len));
|
||||
current_offset += chunk_len;
|
||||
}
|
||||
}
|
||||
|
||||
if (text_view.length() > max_len)
|
||||
text_view = text_view.substr(0, max_len);
|
||||
|
||||
painter.draw_string(
|
||||
rect.location(),
|
||||
s,
|
||||
text_view);
|
||||
bool Text::on_touch(const TouchEvent event) {
|
||||
if (event.type == TouchEvent::Type::Start) {
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Labels ****************************************************************/
|
||||
|
||||
@@ -209,6 +209,7 @@ class Rectangle : public Widget {
|
||||
|
||||
class Text : public Widget {
|
||||
public:
|
||||
std::function<void(Text&)> on_select{};
|
||||
Text()
|
||||
: text{""} {
|
||||
}
|
||||
@@ -223,6 +224,8 @@ class Text : public Widget {
|
||||
void getAccessibilityText(std::string& result) override;
|
||||
void getWidgetName(std::string& result) override;
|
||||
|
||||
bool on_touch(const TouchEvent event) override;
|
||||
|
||||
protected:
|
||||
// NB: Don't truncate this string. The UI will only render
|
||||
// as many characters as will fit in its rectange.
|
||||
|
||||
@@ -386,7 +386,9 @@ void Text::set(std::string_view value) {
|
||||
text = std::string{value};
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
std::string Text::get() {
|
||||
return text;
|
||||
}
|
||||
void Text::getAccessibilityText(std::string& result) {
|
||||
result = text;
|
||||
}
|
||||
@@ -396,18 +398,35 @@ void Text::getWidgetName(std::string& result) {
|
||||
void Text::paint(Painter& painter) {
|
||||
const auto rect = screen_rect();
|
||||
auto s = has_focus() ? style().invert() : style();
|
||||
auto max_len = (unsigned)rect.width() / s.font.char_width();
|
||||
auto text_view = std::string_view{text};
|
||||
|
||||
painter.fill_rectangle(rect, s.background);
|
||||
const int char_width = s.font.char_width();
|
||||
const int line_height = s.font.line_height();
|
||||
if (char_width == 0 || line_height == 0) return;
|
||||
const size_t chars_per_line = rect.width() / char_width;
|
||||
if (chars_per_line == 0) return;
|
||||
size_t lines_capacity = rect.height() / line_height;
|
||||
if (lines_capacity == 0) lines_capacity = 1; // at least one line, even if it overflows vertically
|
||||
auto text_view = std::string_view{text};
|
||||
size_t current_offset = 0;
|
||||
for (size_t line_idx = 0; line_idx < lines_capacity; ++line_idx) {
|
||||
if (current_offset >= text_view.length()) break;
|
||||
size_t chunk_len = std::min(chars_per_line, text_view.length() - current_offset);
|
||||
painter.draw_string(
|
||||
rect.location() + Point(0, line_idx * line_height),
|
||||
s,
|
||||
text_view.substr(current_offset, chunk_len));
|
||||
current_offset += chunk_len;
|
||||
}
|
||||
}
|
||||
|
||||
if (text_view.length() > max_len)
|
||||
text_view = text_view.substr(0, max_len);
|
||||
|
||||
painter.draw_string(
|
||||
rect.location(),
|
||||
s,
|
||||
text_view);
|
||||
bool Text::on_touch(const TouchEvent event) {
|
||||
if (event.type == TouchEvent::Type::Start) {
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Labels ****************************************************************/
|
||||
|
||||
@@ -210,6 +210,7 @@ class Rectangle : public Widget {
|
||||
|
||||
class Text : public Widget {
|
||||
public:
|
||||
std::function<void(Text&)> on_select{};
|
||||
Text()
|
||||
: text{""} {
|
||||
}
|
||||
@@ -218,11 +219,14 @@ class Text : public Widget {
|
||||
Text(Rect parent_rect);
|
||||
|
||||
void set(std::string_view value);
|
||||
std::string get();
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void getAccessibilityText(std::string& result) override;
|
||||
void getWidgetName(std::string& result) override;
|
||||
|
||||
bool on_touch(const TouchEvent event) override;
|
||||
|
||||
protected:
|
||||
// NB: Don't truncate this string. The UI will only render
|
||||
// as many characters as will fit in its rectange.
|
||||
|
||||
@@ -125,7 +125,7 @@ class DigitalRain {
|
||||
|
||||
public:
|
||||
DigitalRain() {
|
||||
std::srand(0);
|
||||
srand(0);
|
||||
WIDTH = screen_width;
|
||||
HEIGHT = screen_height + 5;
|
||||
COLS = WIDTH / CHAR_WIDTH;
|
||||
|
||||
@@ -6996,7 +6996,7 @@ int Context::run() {
|
||||
} else if (p->order_by.compare("name", true) == 0) {
|
||||
std::sort(testArray.begin(), testArray.end(), nameOrderComparator);
|
||||
} else if (p->order_by.compare("rand", true) == 0) {
|
||||
std::srand(p->rand_seed);
|
||||
srand(p->rand_seed);
|
||||
|
||||
// random_shuffle implementation
|
||||
const auto first = &testArray[0];
|
||||
|
||||
@@ -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
|
||||
external_apps_address_end = 0xADFC0000
|
||||
+1
-1
Submodule hackrf updated: 8a416fc913...f56e711b6b
Reference in New Issue
Block a user