mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-07-26 10:38:52 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 865bdee9b3 | |||
| 314e910cbc | |||
| ec6c7eff86 | |||
| 95b2e9fbc3 | |||
| 52ff442fcc | |||
| 95d246a020 | |||
| c34a6940ca | |||
| 33330e4c34 | |||
| 66f1835232 | |||
| 4dfbf666d0 |
@@ -64,4 +64,6 @@ If you are modifying an existing app, make sure the wiki page reflects your chan
|
||||
- [ ] Attached proof of testing on real PortaPack hardware *(or marked N/A as a trusted contributor)*
|
||||
- [ ] Attached proof of testing against a real emitter/receiver (if RF-related), or marked N/A with justification
|
||||
- [ ] I understand that by getting this PR merged, I am implicitly agreeing to create or update the corresponding wiki page (including a main-screen screenshot, description, controls, and limitations)
|
||||
- [ ] I own all rights to this code (i.e., all code contained in this PR), including compliant usage rights for third-party libraries, and I agree that this code is licensed under the license of this project (GPL-3.0).
|
||||
- [ ] If any third-party libraries are used, I confirm that their licenses comply with the requirements for contributing to this repository.
|
||||
- [ ] Reviewed the [Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines)
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
void GlassView::focus() {
|
||||
range_presets.focus();
|
||||
}
|
||||
@@ -44,7 +45,7 @@ GlassView::~GlassView() {
|
||||
|
||||
// Function to map the value from one range to another
|
||||
int32_t GlassView::map(int32_t value, int32_t fromLow, int32_t fromHigh, int32_t toLow, int32_t toHigh) {
|
||||
return toLow + (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow);
|
||||
return toLow + ((value - fromLow) * (toHigh - toLow) + (fromHigh - fromLow) / 2) / (fromHigh - fromLow);
|
||||
}
|
||||
|
||||
void GlassView::update_display_beep() {
|
||||
@@ -128,8 +129,9 @@ void GlassView::reset_live_view() {
|
||||
max_freq_power = -1000;
|
||||
|
||||
// Clear screen in peak mode.
|
||||
const int spectrum_height = screen_height - spectrum_y;
|
||||
if (live_frequency_view == 2)
|
||||
display.fill_rectangle({{0, 108 + 16}, {screen_width, screen_height - (108 + 16)}}, {0, 0, 0});
|
||||
display.fill_rectangle({{0, spectrum_y}, {screen_width, spectrum_height}}, {0, 0, 0});
|
||||
}
|
||||
|
||||
void GlassView::add_spectrum_pixel(uint8_t power) {
|
||||
@@ -144,11 +146,9 @@ void GlassView::add_spectrum_pixel(uint8_t power) {
|
||||
constexpr float rssi_voltage_min = 0.4;
|
||||
constexpr float rssi_voltage_max = 2.2;
|
||||
constexpr float adc_voltage_max = 3.3;
|
||||
constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max;
|
||||
constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max;
|
||||
constexpr int raw_delta = raw_max - raw_min;
|
||||
const range_t<int> y_max_range{0, screen_height - (108 + 16)};
|
||||
|
||||
constexpr int raw_min = 0.5f + rssi_sample_range * rssi_voltage_min / adc_voltage_max;
|
||||
constexpr int raw_max = 0.5f + rssi_sample_range * rssi_voltage_max / adc_voltage_max;
|
||||
const int spectrum_height = screen_height - spectrum_y;
|
||||
// drawing and keeping track of max freq
|
||||
for (uint16_t xpos = 0; xpos < screen_width; xpos++) {
|
||||
// save max powerwull freq
|
||||
@@ -156,14 +156,16 @@ void GlassView::add_spectrum_pixel(uint8_t power) {
|
||||
max_freq_power = spectrum_data[xpos];
|
||||
max_freq_hold = get_freq_from_bin_pos(xpos);
|
||||
}
|
||||
int16_t point = y_max_range.clip(((spectrum_data[xpos] - raw_min) * (screen_height - (108 + 16))) / raw_delta);
|
||||
uint8_t color_gradient = (point * 255) / 212;
|
||||
int16_t point = map(spectrum_data[xpos], 0, 255, 0, spectrum_height);
|
||||
// clear if not in peak view
|
||||
if (live_frequency_view != 2) {
|
||||
display.fill_rectangle({{xpos, 108 + 16}, {1, screen_height - point}}, {0, 0, 0});
|
||||
display.fill_rectangle({{xpos, spectrum_y}, {1, screen_height - point}}, {0, 0, 0});
|
||||
}
|
||||
display.fill_rectangle({{xpos, screen_height - point}, {1, point}}, {color_gradient, 0, uint8_t(255 - color_gradient)});
|
||||
display.fill_rectangle({{xpos, screen_height - point}, {1, point}}, gradient.lut[spectrum_data[xpos]]);
|
||||
}
|
||||
// indicate RSSI min and max power
|
||||
display.fill_rectangle({{0, screen_height - map(raw_min, 0, 255, 0, spectrum_height)}, {screen_width, 1}}, -gradient.lut[raw_min]);
|
||||
display.fill_rectangle({{0, screen_height - map(raw_max, 0, 255, 0, spectrum_height)}, {screen_width, 1}}, -gradient.lut[raw_max]);
|
||||
if (last_max_freq != max_freq_hold) {
|
||||
last_max_freq = max_freq_hold;
|
||||
freq_stats.set("MAX HOLD: " + to_string_short_freq(max_freq_hold));
|
||||
|
||||
@@ -104,6 +104,7 @@ class GlassView : public View {
|
||||
};
|
||||
|
||||
void on_freqchg(int64_t freq);
|
||||
// Function to map the value from one range to another
|
||||
int32_t map(int32_t value, int32_t fromLow, int32_t fromHigh, int32_t toLow, int32_t toHigh);
|
||||
std::vector<preset_entry> presets_db{};
|
||||
void manage_beep_audio();
|
||||
@@ -164,6 +165,9 @@ class GlassView : public View {
|
||||
uint8_t offset = 0;
|
||||
uint8_t ignore_dc = 0;
|
||||
|
||||
// start of spectrum area
|
||||
static constexpr int spectrum_y = (108 + 16);
|
||||
|
||||
Labels labels{
|
||||
{{0, UI_POS_Y(0)}, "MIN: MAX: LNA VGA ", Theme::getInstance()->fg_light->foreground},
|
||||
{{0, 1 * 16}, "RANGE: FILTER: AMP:", Theme::getInstance()->fg_light->foreground},
|
||||
|
||||
@@ -432,6 +432,10 @@ void set_epirb_tx_config(EPIRBTXDataMessage& message) {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_epirb_rx_config(EPIRBRXConfig& message) {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length) {
|
||||
const size_t max_len = sizeof(shared_memory.bb_data.data);
|
||||
if (frame_length > max_len) frame_length = max_len;
|
||||
@@ -443,6 +447,10 @@ void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length) {
|
||||
|
||||
static bool baseband_image_running = false;
|
||||
|
||||
bool is_image_running() {
|
||||
return baseband_image_running;
|
||||
}
|
||||
|
||||
void run_image(const spi_flash::image_tag_t image_tag) {
|
||||
if (baseband_image_running) {
|
||||
chDbgPanic("BBRunning");
|
||||
|
||||
@@ -118,6 +118,7 @@ void set_bitstream_config(uint32_t deviation, uint8_t mode);
|
||||
void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload = nullptr, uint16_t payload_length = 0); // baud*100
|
||||
void set_rtty_config(RTTYDataMessage& message);
|
||||
void set_epirb_tx_config(EPIRBTXDataMessage& message);
|
||||
void set_epirb_rx_config(EPIRBRXConfig& message);
|
||||
void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length);
|
||||
|
||||
void request_roger_beep();
|
||||
@@ -125,6 +126,7 @@ void request_rssi_beep();
|
||||
void request_beep_stop();
|
||||
void request_audio_beep(uint32_t freq, uint32_t sample_rate, uint32_t duration_ms);
|
||||
|
||||
bool is_image_running();
|
||||
void run_image(const portapack::spi_flash::image_tag_t image_tag);
|
||||
void run_prepared_image(const uint32_t m4_code);
|
||||
void shutdown();
|
||||
|
||||
@@ -424,17 +424,34 @@ using namespace hackrf::one;
|
||||
|
||||
void ClockManager::init_clock_generator() {
|
||||
#ifdef PRALINE
|
||||
// PRALINE: Configure clock input mux GPIO
|
||||
// GPIO0_15 (clkin_ctrl) selects GP_CLKIN source:
|
||||
// 0 = P1 connector (external)
|
||||
// 1 = P22 (internal Si5351 CLK2)
|
||||
constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15];
|
||||
gpio_clkin_ctrl.output();
|
||||
gpio_clkin_ctrl.write(1); // CLKIN_SIGNAL_P22 = 1 = internal Si5351 CLK2
|
||||
// thoese PIN can review platform_scu file
|
||||
// have many conflict and modify for clock init
|
||||
// P2_10 -> GPIO0[14] P1_CTRL0
|
||||
LPC_SCU->SFSP[2][10] = 0xF0;
|
||||
// P6_8 -> GPIO5[16] P1_CTRL1
|
||||
LPC_SCU->SFSP[6][8] = 0xF4;
|
||||
// P6_9 -> GPIO3[5] P1_CTRL2
|
||||
LPC_SCU->SFSP[6][9] = 0xF0;
|
||||
// P1_20 -> GPIO0[15] CLKIN_CTRL
|
||||
LPC_SCU->SFSP[1][20] = 0xF0;
|
||||
|
||||
constexpr GPIO gpio_p1_ctrl0 = gpio[GPIO0_14];
|
||||
constexpr GPIO gpio_p1_ctrl1 = gpio[GPIO5_16];
|
||||
constexpr GPIO gpio_p1_ctrl2 = gpio[GPIO3_5];
|
||||
constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15];
|
||||
|
||||
gpio_p1_ctrl0.output();
|
||||
gpio_p1_ctrl1.output();
|
||||
gpio_p1_ctrl2.output();
|
||||
gpio_clkin_ctrl.output();
|
||||
|
||||
// P1 control = 100 and choose P22 clock external clock on portapack
|
||||
gpio_p1_ctrl0.write(0);
|
||||
gpio_p1_ctrl1.write(0);
|
||||
gpio_p1_ctrl2.write(1);
|
||||
gpio_clkin_ctrl.write(1);
|
||||
chThdSleepMilliseconds(20);
|
||||
|
||||
// Also enable MCU clock gate (GPIO0_8)
|
||||
gpio_r9_mcu_clk_en.output();
|
||||
gpio_r9_mcu_clk_en.write(1);
|
||||
#else
|
||||
// HackRF One r9: GPIO0_8 (mcu_clk_en) gates Si5351 CLK2/CLK7 to GP_CLKIN
|
||||
if (hackrf_r9) {
|
||||
@@ -458,10 +475,23 @@ void ClockManager::init_clock_generator() {
|
||||
* 4. Reset PLLs
|
||||
* 5. Enable outputs
|
||||
*/
|
||||
clock_generator.set_pll_input_sources(si5351a_pll_input_sources);
|
||||
|
||||
/* Skip MCU CLKIN setup and reference detection for PRALINE - not applicable */
|
||||
reference = Reference{ReferenceSource::Xtal, 0}; // PLLA
|
||||
// change there detect method same as hackrf one
|
||||
// PLLA -> XTAL,PLLB -> CLKIN
|
||||
clock_generator.set_pll_input_sources(si5351c_pll_input_sources);
|
||||
// although the name a and the branch define CLK2 MCU CLOCK
|
||||
// only use CLK2
|
||||
auto si5351_clock_control_common = si5351a_clock_control_common;
|
||||
constexpr size_t clock_generator_output_pro_mcu_clkin = 2;
|
||||
// clk_src define CLKIN
|
||||
clock_generator.set_clock_control(
|
||||
clock_generator_output_pro_mcu_clkin,
|
||||
si5351_clock_control_common[clock_generator_output_pro_mcu_clkin]
|
||||
.clk_src(ClockControl::ClockSource::CLKIN)
|
||||
.clk_pdn(ClockControl::ClockPowerDown::Power_On));
|
||||
clock_generator.enable_output(clock_generator_output_pro_mcu_clkin);
|
||||
chThdSleepMilliseconds(20);
|
||||
// should be extern icon and 10MHz
|
||||
reference = choose_reference();
|
||||
|
||||
/* Clock control will be set AFTER multisynth configuration - see below */
|
||||
#else
|
||||
|
||||
+89
-105
@@ -6,11 +6,9 @@
|
||||
* ------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize("Os")
|
||||
#include "ui_battleship.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "modems.hpp"
|
||||
#include "bch_code.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
@@ -28,7 +26,6 @@ BattleshipView::BattleshipView(NavigationView& nav)
|
||||
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
|
||||
|
||||
add_children({&text_title, &text_subtitle,
|
||||
//&rect_radio_settings, &rect_audio_settings, &rect_team_selection,
|
||||
&label_radio, &button_frequency,
|
||||
&label_rf_amp, &checkbox_rf_amp,
|
||||
&label_lna, &field_lna,
|
||||
@@ -151,8 +148,6 @@ BattleshipView::BattleshipView(NavigationView& nav)
|
||||
field_tx_gain.set_parent_rect({185, 118, 32, 16});
|
||||
checkbox_sound.set_parent_rect({17, 187, 80, 24});
|
||||
field_volume.set_parent_rect({165, 187, 32, 16});
|
||||
// button_red_team.set_parent_rect({25, 242, 85, 45});
|
||||
// button_blue_team.set_parent_rect({130, 242, 85, 45});
|
||||
|
||||
// Make menu elements focusable
|
||||
button_frequency.set_focusable(true);
|
||||
@@ -196,34 +191,22 @@ void BattleshipView::init_game() {
|
||||
}
|
||||
|
||||
void BattleshipView::show_hide_menu(bool menu_vis) {
|
||||
text_title.hidden(!menu_vis);
|
||||
text_subtitle.hidden(!menu_vis);
|
||||
// rect_radio_settings.hidden(!menu_vis); rect_audio_settings.hidden(!menu_vis); rect_team_selection.hidden(!menu_vis);
|
||||
label_radio.hidden(!menu_vis);
|
||||
button_frequency.hidden(!menu_vis);
|
||||
label_rf_amp.hidden(!menu_vis);
|
||||
checkbox_rf_amp.hidden(!menu_vis);
|
||||
label_lna.hidden(!menu_vis);
|
||||
field_lna.hidden(!menu_vis);
|
||||
label_vga.hidden(!menu_vis);
|
||||
field_vga.hidden(!menu_vis);
|
||||
label_tx_gain.hidden(!menu_vis);
|
||||
field_tx_gain.hidden(!menu_vis);
|
||||
label_audio.hidden(!menu_vis);
|
||||
checkbox_sound.hidden(!menu_vis);
|
||||
label_volume.hidden(!menu_vis);
|
||||
field_volume.hidden(!menu_vis);
|
||||
label_team.hidden(!menu_vis);
|
||||
button_red_team.hidden(!menu_vis);
|
||||
button_blue_team.hidden(!menu_vis);
|
||||
rssi.hidden(menu_vis);
|
||||
button_rotate.hidden(menu_vis);
|
||||
button_place.hidden(menu_vis);
|
||||
button_menu.hidden(menu_vis);
|
||||
Widget* const menu_widgets[] = {
|
||||
&text_title, &text_subtitle, &label_radio, &button_frequency,
|
||||
&label_rf_amp, &checkbox_rf_amp, &label_lna, &field_lna,
|
||||
&label_vga, &field_vga, &label_tx_gain, &field_tx_gain,
|
||||
&label_audio, &checkbox_sound, &label_volume, &field_volume,
|
||||
&label_team, &button_red_team, &button_blue_team};
|
||||
for (auto* w : menu_widgets) {
|
||||
w->hidden(!menu_vis);
|
||||
}
|
||||
|
||||
Widget* const game_widgets[] = {
|
||||
&rssi, &button_rotate, &button_place, &button_menu};
|
||||
for (auto* w : game_widgets) {
|
||||
w->hidden(menu_vis);
|
||||
}
|
||||
|
||||
// button_rotate.set_focusable(false); //no need, since can't focus on hidden
|
||||
// button_place.set_focusable(false);
|
||||
// button_menu.set_focusable(false);
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
@@ -340,40 +323,23 @@ void BattleshipView::paint(Painter& painter) {
|
||||
if (game_state == GameState::MENU) {
|
||||
draw_menu_screen(painter);
|
||||
|
||||
// Custom paint team buttons
|
||||
if (!button_red_team.hidden()) {
|
||||
Rect r = button_red_team.screen_rect();
|
||||
painter.fill_rectangle(r, Color::dark_red());
|
||||
painter.draw_rectangle(r, Color::red());
|
||||
auto draw_team_btn = [&painter](Button& btn, Color bg, Color border, const char* name) {
|
||||
if (btn.hidden()) return;
|
||||
Rect r = btn.screen_rect();
|
||||
painter.fill_rectangle(r, bg);
|
||||
painter.draw_rectangle(r, border);
|
||||
|
||||
if (button_red_team.has_focus()) {
|
||||
if (btn.has_focus()) {
|
||||
painter.draw_rectangle({r.location().x() - 1, r.location().y() - 1, r.size().width() + 2, r.size().height() + 2}, Color::yellow());
|
||||
}
|
||||
|
||||
auto style_white = Style{
|
||||
.font = ui::font::fixed_8x16,
|
||||
.background = Color::dark_red(),
|
||||
.foreground = Color::white()};
|
||||
painter.draw_string(r.center() - Point(24, 16), style_white, "RED");
|
||||
painter.draw_string(r.center() - Point(24, 0), style_white, "TEAM");
|
||||
}
|
||||
auto style = Style{.font = ui::font::fixed_8x16, .background = bg, .foreground = Color::white()};
|
||||
painter.draw_string(r.center() - Point(24, 16), style, name);
|
||||
painter.draw_string(r.center() - Point(24, 0), style, "TEAM");
|
||||
};
|
||||
|
||||
if (!button_blue_team.hidden()) {
|
||||
Rect r = button_blue_team.screen_rect();
|
||||
painter.fill_rectangle(r, Color::dark_blue());
|
||||
painter.draw_rectangle(r, Color::blue());
|
||||
|
||||
if (button_blue_team.has_focus()) {
|
||||
painter.draw_rectangle({r.location().x() - 1, r.location().y() - 1, r.size().width() + 2, r.size().height() + 2}, Color::yellow());
|
||||
}
|
||||
|
||||
auto style_white = Style{
|
||||
.font = ui::font::fixed_8x16,
|
||||
.background = Color::dark_blue(),
|
||||
.foreground = Color::white()};
|
||||
painter.draw_string(r.center() - Point(24, 16), style_white, "BLUE");
|
||||
painter.draw_string(r.center() - Point(24, 0), style_white, "TEAM");
|
||||
}
|
||||
draw_team_btn(button_red_team, Color::dark_red(), Color::red(), "RED");
|
||||
draw_team_btn(button_blue_team, Color::dark_blue(), Color::blue(), "BLUE");
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -417,27 +383,25 @@ void BattleshipView::paint(Painter& painter) {
|
||||
void BattleshipView::draw_menu_screen(Painter& painter) {
|
||||
painter.draw_hline({12, 20 + 16}, screen_width - 24, Color::dark_cyan());
|
||||
|
||||
painter.fill_rectangle({13, 59, screen_width - 26, 116}, Color::dark_grey());
|
||||
painter.draw_hline({12, 58}, screen_width - 24, Color::cyan());
|
||||
painter.draw_hline({12, 157}, screen_width - 24, Color::cyan());
|
||||
const struct {
|
||||
Rect rect;
|
||||
Point line1_pos;
|
||||
Point line2_pos;
|
||||
} boxes[] = {
|
||||
{{13, 59, screen_width - 26, 116}, {12, 58}, {12, 157}},
|
||||
{{13, 165, screen_width - 24, 57}, {12, 164}, {12, 223}},
|
||||
{{13, 232, screen_width - 26, 67}, {12, 232}, {12, 300}}};
|
||||
|
||||
painter.fill_rectangle({13, 165, screen_width - 24, 57}, Color::dark_grey());
|
||||
painter.draw_hline({12, 164}, screen_width - 24, Color::cyan());
|
||||
painter.draw_hline({12, 223}, screen_width - 24, Color::cyan());
|
||||
|
||||
painter.fill_rectangle({13, 232, screen_width - 26, 67}, Color::dark_grey());
|
||||
painter.draw_hline({12, 232}, screen_width - 24, Color::cyan());
|
||||
painter.draw_hline({12, 300}, screen_width - 24, Color::cyan());
|
||||
for (const auto& box : boxes) {
|
||||
painter.fill_rectangle(box.rect, Color::dark_grey());
|
||||
painter.draw_hline(box.line1_pos, screen_width - 24, Color::cyan());
|
||||
painter.draw_hline(box.line2_pos, screen_width - 24, Color::cyan());
|
||||
}
|
||||
|
||||
// Radio status indicator
|
||||
Point indicator_pos{UI_POS_MAXWIDTH - 20, 59};
|
||||
if (is_transmitting) {
|
||||
painter.fill_rectangle({indicator_pos, {6, 6}}, Color::red());
|
||||
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
|
||||
} else {
|
||||
painter.fill_rectangle({indicator_pos, {6, 6}}, Color::green());
|
||||
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
|
||||
}
|
||||
painter.fill_rectangle({indicator_pos, {6, 6}}, is_transmitting ? Color::red() : Color::green());
|
||||
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
|
||||
}
|
||||
|
||||
void BattleshipView::draw_grid(Painter& painter, uint16_t grid_x, uint16_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid) {
|
||||
@@ -464,11 +428,14 @@ void BattleshipView::draw_cell(Painter& painter, uint16_t cell_x, uint16_t cell_
|
||||
|
||||
if (game_state == GameState::PLACING_SHIPS && !is_offense_grid && current_ship_index < 5) {
|
||||
uint8_t ship_size = my_ships[current_ship_index].size();
|
||||
uint8_t dx = placing_horizontal ? 1 : 0;
|
||||
uint8_t dy = placing_horizontal ? 0 : 1;
|
||||
uint16_t grid_x = (cell_x - 1) / CELL_SIZE;
|
||||
uint16_t grid_y = (cell_y - GRID_OFFSET_Y - 6) / CELL_SIZE;
|
||||
|
||||
for (uint8_t i = 0; i < ship_size; i++) {
|
||||
uint16_t preview_x = placing_horizontal ? cursor_x + i : cursor_x;
|
||||
uint16_t preview_y = placing_horizontal ? cursor_y : cursor_y + i;
|
||||
uint16_t grid_x = (cell_x - 1) / CELL_SIZE;
|
||||
uint16_t grid_y = (cell_y - GRID_OFFSET_Y - 6) / CELL_SIZE;
|
||||
uint16_t preview_x = cursor_x + (i * dx);
|
||||
uint16_t preview_y = cursor_y + (i * dy);
|
||||
if (grid_x == preview_x && grid_y == preview_y && preview_x < GRID_SIZE && preview_y < GRID_SIZE) {
|
||||
return;
|
||||
}
|
||||
@@ -540,9 +507,12 @@ void BattleshipView::draw_ship_preview(Painter& painter) {
|
||||
uint8_t size = ship.size();
|
||||
bool can_place = can_place_ship(cursor_x, cursor_y, size, placing_horizontal);
|
||||
|
||||
uint8_t dx = placing_horizontal ? 1 : 0;
|
||||
uint8_t dy = placing_horizontal ? 0 : 1;
|
||||
|
||||
for (uint8_t i = 0; i < size; i++) {
|
||||
uint8_t x = placing_horizontal ? cursor_x + i : cursor_x;
|
||||
uint8_t y = placing_horizontal ? cursor_y : cursor_y + i;
|
||||
uint8_t x = cursor_x + (i * dx);
|
||||
uint8_t y = cursor_y + (i * dy);
|
||||
|
||||
if (x < GRID_SIZE && y < GRID_SIZE) {
|
||||
uint16_t cell_x = GRID_OFFSET_X + x * CELL_SIZE + 1;
|
||||
@@ -561,18 +531,21 @@ bool BattleshipView::can_place_ship(uint8_t x, uint8_t y, uint8_t size, bool hor
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t dx = horizontal ? 1 : 0;
|
||||
uint8_t dy = horizontal ? 0 : 1;
|
||||
|
||||
for (uint8_t i = 0; i < size; i++) {
|
||||
uint8_t check_x = horizontal ? x + i : x;
|
||||
uint8_t check_y = horizontal ? y : y + i;
|
||||
uint8_t check_x = x + (i * dx);
|
||||
uint8_t check_y = y + (i * dy);
|
||||
|
||||
if (my_grid[check_y][check_x] != CellState::EMPTY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int dy = -1; dy <= 1; dy++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
int adj_x = check_x + dx;
|
||||
int adj_y = check_y + dy;
|
||||
for (int ddy = -1; ddy <= 1; ddy++) {
|
||||
for (int ddx = -1; ddx <= 1; ddx++) {
|
||||
int adj_x = check_x + ddx;
|
||||
int adj_y = check_y + ddy;
|
||||
|
||||
if (adj_x >= 0 && adj_x < GRID_SIZE &&
|
||||
adj_y >= 0 && adj_y < GRID_SIZE) {
|
||||
@@ -604,10 +577,11 @@ void BattleshipView::place_ship() {
|
||||
ship.horizontal = placing_horizontal;
|
||||
ship.placed = true;
|
||||
|
||||
uint8_t dx = placing_horizontal ? 1 : 0;
|
||||
uint8_t dy = placing_horizontal ? 0 : 1;
|
||||
|
||||
for (uint8_t i = 0; i < size; i++) {
|
||||
uint8_t x = placing_horizontal ? cursor_x + i : cursor_x;
|
||||
uint8_t y = placing_horizontal ? cursor_y : cursor_y + i;
|
||||
my_grid[y][x] = CellState::SHIP;
|
||||
my_grid[cursor_y + (i * dy)][cursor_x + (i * dx)] = CellState::SHIP;
|
||||
}
|
||||
|
||||
current_ship_index++;
|
||||
@@ -644,9 +618,14 @@ void BattleshipView::place_ship() {
|
||||
void BattleshipView::send_message(const GameMessage& msg) {
|
||||
static const char* msg_strings[] = {"READY", "SHOT:", "HIT:", "MISS:", "SUNK:", "WIN"};
|
||||
|
||||
std::string message = msg_strings[static_cast<int>(msg.type)];
|
||||
std::string message;
|
||||
message.reserve(16);
|
||||
message.append(msg_strings[static_cast<int>(msg.type)]);
|
||||
|
||||
if (msg.type != MessageType::READY && msg.type != MessageType::WIN) {
|
||||
message += to_string_dec_uint(msg.x) + "," + to_string_dec_uint(msg.y);
|
||||
message.append(to_string_dec_uint(msg.x));
|
||||
message.append(",");
|
||||
message.append(to_string_dec_uint(msg.y));
|
||||
}
|
||||
|
||||
configure_radio_tx();
|
||||
@@ -836,11 +815,12 @@ void BattleshipView::process_shot(uint8_t x, uint8_t y) {
|
||||
if (!ship.placed) continue;
|
||||
|
||||
bool hit_this_ship = false;
|
||||
for (uint8_t i = 0; i < ship.size(); i++) {
|
||||
uint8_t check_x = ship.horizontal ? ship.x + i : ship.x;
|
||||
uint8_t check_y = ship.horizontal ? ship.y : ship.y + i;
|
||||
|
||||
if (check_x == x && check_y == y) {
|
||||
uint8_t dx = ship.horizontal ? 1 : 0;
|
||||
uint8_t dy = ship.horizontal ? 0 : 1;
|
||||
|
||||
for (uint8_t i = 0; i < ship.size(); i++) {
|
||||
if ((ship.x + i * dx) == x && (ship.y + i * dy) == y) {
|
||||
hit_this_ship = true;
|
||||
ship.hits++;
|
||||
break;
|
||||
@@ -852,9 +832,7 @@ void BattleshipView::process_shot(uint8_t x, uint8_t y) {
|
||||
ships_remaining--;
|
||||
|
||||
for (uint8_t i = 0; i < ship.size(); i++) {
|
||||
uint8_t sunk_x = ship.horizontal ? ship.x + i : ship.x;
|
||||
uint8_t sunk_y = ship.horizontal ? ship.y : ship.y + i;
|
||||
my_grid[sunk_y][sunk_x] = CellState::SUNK;
|
||||
my_grid[ship.y + (i * dy)][ship.x + (i * dx)] = CellState::SUNK;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -887,7 +865,12 @@ void BattleshipView::process_shot(uint8_t x, uint8_t y) {
|
||||
}
|
||||
|
||||
void BattleshipView::update_score() {
|
||||
current_score = "W:" + to_string_dec_uint(wins) + " L:" + to_string_dec_uint(losses);
|
||||
current_score.clear();
|
||||
current_score.reserve(16);
|
||||
current_score.append("W:");
|
||||
current_score.append(to_string_dec_uint(wins));
|
||||
current_score.append(" L:");
|
||||
current_score.append(to_string_dec_uint(losses));
|
||||
}
|
||||
|
||||
bool BattleshipView::on_touch(const TouchEvent event) {
|
||||
@@ -980,3 +963,4 @@ bool BattleshipView::on_key(const KeyEvent key) {
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::battleship
|
||||
#pragma GCC pop_options
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
#ifndef __UI_BATTLESHIP_H__
|
||||
#define __UI_BATTLESHIP_H__
|
||||
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "modems.hpp"
|
||||
#include "bch_code.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 "beacon.hpp"
|
||||
#include <cstdio>
|
||||
#include "string_format.hpp"
|
||||
|
||||
// Some but not all methods of beacon class have been externalized in .cpp file
|
||||
// This has been done to try and optimize application size by preventing the compiler to inline methods
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
size_t Beacon::formatSummary(char* buffer, bool with_time) {
|
||||
size_t result = 0;
|
||||
if (with_time) {
|
||||
result += formatTime(buffer);
|
||||
buffer[result++] = '-';
|
||||
}
|
||||
result += sprintf((buffer + result), "%4s-%5s-", shortId().c_str(), getType());
|
||||
if (location.isUnknown()) {
|
||||
result += sprintf((buffer + result), " ");
|
||||
} else {
|
||||
result += location.toString((buffer + result), Location::LocationFormat::MAIDENHEAD_LOCATOR);
|
||||
}
|
||||
result += sprintf((buffer + result), "[%s%s%s]", isFrameValid() ? (bch1Corrected || bch2Corrected) ? STR_COLOR_YELLOW : STR_COLOR_GREEN : STR_COLOR_RED, getSatus().c_str(), STR_COLOR_WHITE);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Beacon::isFrameValid() {
|
||||
return isBch1Valid() && ((!hasBch2) || isBch2Valid()) && (!isEmpty);
|
||||
}
|
||||
|
||||
std::string Beacon::shortId() {
|
||||
std::string result = std::string(hexId);
|
||||
return (result.size() >= 4) ? result.substr(0, 4) : result;
|
||||
}
|
||||
|
||||
size_t Beacon::formatTime(char* buffer) {
|
||||
return sprintf(buffer, "%02d:%02d:%02d", date.hour(), date.minute(), date.second());
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
+856
@@ -0,0 +1,856 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 __BEACON_RX_H__
|
||||
#define __BEACON_RX_H__
|
||||
|
||||
#include "country.hpp"
|
||||
#include "location.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "resources.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
// Size for beacon frame buffer
|
||||
#define BEACON_DATA_SIZE 18 // Max 144 bits => 18 bytes
|
||||
|
||||
// Constants for BCH control code calculation
|
||||
#define BCH_21_POLYNOMIAL 0b1001101101100111100011UL
|
||||
#define BCH_21_POLY_LENGTH 22
|
||||
#define BCH_12_POLYNOMIAL 0b1010100111001UL
|
||||
#define BCH_12_POLY_LENGTH 13
|
||||
|
||||
// See content of sdcard/EPIRB/RES/BEACON.RES
|
||||
#define ADDITIONAL_DATA_RESOURCE_START 25
|
||||
#define LONG_ADDITIONAL_DATA_RESOURCE_START 33
|
||||
#define EMERGENCY_RESOURCE_START 39
|
||||
#define EMERGENCY_OTHER_RESOURCE_START 49
|
||||
#define AUX_DEVICE_RESOURCE_START 53
|
||||
|
||||
/**
|
||||
* This is the main class for beaon handling
|
||||
*/
|
||||
class Beacon {
|
||||
public:
|
||||
// Real beacon or test beacon ?
|
||||
enum class FrameMode { NORMAL,
|
||||
SELF_TEST,
|
||||
UNKNOWN };
|
||||
// Type of main location device
|
||||
enum class MainLocatingDevice { UNDEFINED,
|
||||
INTERNAL_NAV,
|
||||
EXTERNAL_NAV };
|
||||
// Type of aux location device
|
||||
enum class AuxLocatingDevice { UNDEFINED,
|
||||
NONE,
|
||||
NONE_OR_OTHER,
|
||||
OTHER,
|
||||
MHZ121_5,
|
||||
SART };
|
||||
|
||||
// Protcol type
|
||||
enum class ProtocolType {
|
||||
UNKNOWN,
|
||||
USER,
|
||||
STANDARD_LOCATION,
|
||||
NATIONAL_LOCATION,
|
||||
RLS_LOCATION,
|
||||
ELT_DT_LOCATION,
|
||||
SPARE
|
||||
};
|
||||
|
||||
// Protocol
|
||||
enum class Protocol {
|
||||
USER_EPIRB_MARITIME,
|
||||
USER_EPIRB_RADIO,
|
||||
USER_ELT,
|
||||
USER_SERIAL,
|
||||
USER_TEST,
|
||||
USER_ORB,
|
||||
USER_NAT,
|
||||
USER_2G,
|
||||
STD_EPIRB,
|
||||
STD_ELT_24,
|
||||
STD_ELT_SERIAL,
|
||||
STD_ELT_AIRCRAFT,
|
||||
STD_EPIRB_SERIAL,
|
||||
STD_PLB_SERIAL,
|
||||
STD_SHIP,
|
||||
STD_TEST,
|
||||
NAT_ELT,
|
||||
NAT_EPIRB,
|
||||
NAT_PLB,
|
||||
NAT_TEST,
|
||||
RLS,
|
||||
ELT_DT,
|
||||
SPARE,
|
||||
UNKNOWN
|
||||
};
|
||||
|
||||
#define UNKNOWN_LABEL "Unk."
|
||||
|
||||
// Returns true for User protocol beacons
|
||||
bool protocolIsUser() { return (protocolType == ProtocolType::USER); };
|
||||
// Returns true for National Location protocol beacons
|
||||
bool protocolIsNational() { return (protocolType == ProtocolType::NATIONAL_LOCATION); };
|
||||
// Returns true for Standard Location protocol beacons
|
||||
bool protocolIsStandard() { return (protocolType == ProtocolType::STANDARD_LOCATION); };
|
||||
// Returns true for RLS location protocol beacons
|
||||
bool protocolIsRls() { return (protocolType == ProtocolType::RLS_LOCATION); };
|
||||
// Returns true for RLS or ELT Location protocol beacons
|
||||
bool protocolIsRlsOrElt() { return (protocolType == ProtocolType::RLS_LOCATION || protocolType == ProtocolType::ELT_DT_LOCATION); };
|
||||
// Returns true for unknown protocol beacons
|
||||
bool protocolIsUnknown() { return (protocolType == ProtocolType::UNKNOWN); };
|
||||
|
||||
// Returns the protocol name
|
||||
const char* getProtocolName() {
|
||||
switch (protocolType) {
|
||||
case ProtocolType::STANDARD_LOCATION:
|
||||
return "Standard";
|
||||
case ProtocolType::NATIONAL_LOCATION:
|
||||
return "National";
|
||||
case ProtocolType::RLS_LOCATION:
|
||||
return "RLS";
|
||||
case ProtocolType::ELT_DT_LOCATION:
|
||||
return "ELT(DT)";
|
||||
case ProtocolType::USER:
|
||||
return longFrame ? "User Location" : "User";
|
||||
case ProtocolType::SPARE:
|
||||
return "Spare";
|
||||
default:
|
||||
return UNKNOWN_LABEL;
|
||||
}
|
||||
}
|
||||
|
||||
// Get protocol type for this beacon
|
||||
ProtocolType getProtocolType() {
|
||||
switch (protocol) {
|
||||
case Protocol::USER_EPIRB_MARITIME:
|
||||
case Protocol::USER_EPIRB_RADIO:
|
||||
case Protocol::USER_ELT:
|
||||
case Protocol::USER_SERIAL:
|
||||
case Protocol::USER_TEST:
|
||||
case Protocol::USER_ORB:
|
||||
case Protocol::USER_NAT:
|
||||
case Protocol::USER_2G:
|
||||
return ProtocolType::USER;
|
||||
case Protocol::STD_EPIRB:
|
||||
case Protocol::STD_ELT_24:
|
||||
case Protocol::STD_ELT_SERIAL:
|
||||
case Protocol::STD_ELT_AIRCRAFT:
|
||||
case Protocol::STD_PLB_SERIAL:
|
||||
case Protocol::STD_SHIP:
|
||||
case Protocol::STD_TEST:
|
||||
return ProtocolType::STANDARD_LOCATION;
|
||||
case Protocol::NAT_ELT:
|
||||
case Protocol::NAT_EPIRB:
|
||||
case Protocol::NAT_PLB:
|
||||
case Protocol::NAT_TEST:
|
||||
return ProtocolType::NATIONAL_LOCATION;
|
||||
case Protocol::RLS:
|
||||
return ProtocolType::RLS_LOCATION;
|
||||
case Protocol::ELT_DT:
|
||||
return ProtocolType::ELT_DT_LOCATION;
|
||||
case Protocol::SPARE:
|
||||
return ProtocolType::SPARE;
|
||||
case Protocol::UNKNOWN:
|
||||
default:
|
||||
return ProtocolType::UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// True for long frames, fals for short grames
|
||||
bool longFrame{true};
|
||||
// True if protocol flag is set for this frame
|
||||
bool protocolFlag{false};
|
||||
// Frame mode (Test or Real)
|
||||
FrameMode frameMode{FrameMode::UNKNOWN};
|
||||
// Main location device
|
||||
MainLocatingDevice mainLocatingDevice{MainLocatingDevice::UNDEFINED};
|
||||
// Axiliary location device
|
||||
AuxLocatingDevice auxLocatingDevice{AuxLocatingDevice::UNDEFINED};
|
||||
// Protocol code for this beacon
|
||||
long protocolCode{0};
|
||||
// Protocol of this beacon
|
||||
Protocol protocol{Protocol::UNKNOWN};
|
||||
// Protocol type of this beacon
|
||||
ProtocolType protocolType{ProtocolType::UNKNOWN};
|
||||
// Country of this beacon
|
||||
Country country{};
|
||||
// Frame data
|
||||
uint8_t frame[BEACON_DATA_SIZE];
|
||||
// Location of this beacon
|
||||
Location location{};
|
||||
// HexId of this beacon
|
||||
uint64_t identifier{0};
|
||||
// Date of reception of this beacon
|
||||
rtc::RTC date{};
|
||||
// Trus if this beacon has additional data
|
||||
bool hasAdditionalData{false};
|
||||
// The additional data tring
|
||||
char additionalData[48]{0};
|
||||
// True if this beacon has a serial number
|
||||
bool hasSerialNumber{false};
|
||||
// The serial number string
|
||||
char serialNumber[32]{0};
|
||||
// Beacon's Hex ID string
|
||||
char hexId[32]{0};
|
||||
// BCH1 code value
|
||||
uint32_t bch1{};
|
||||
// BCH1 calculated value
|
||||
uint32_t computedBch1{};
|
||||
// True if data has been corrected to match BCH1 code
|
||||
bool bch1Corrected{false};
|
||||
// True if this beacon has a BCH2 correction code
|
||||
bool hasBch2{false};
|
||||
// BCH2 code value
|
||||
uint32_t bch2{};
|
||||
// BCH2 calculated value
|
||||
uint32_t computedBch2{};
|
||||
// True if data has been corrected to match BCH2 code
|
||||
bool bch2Corrected{false};
|
||||
// True if this beacon is empty (not initialized)
|
||||
bool isEmpty{true};
|
||||
// True if thsi beacon contains emergency data
|
||||
bool hasEmergency{false};
|
||||
// True if emenrgency data is automatic for this beacon
|
||||
bool isAutoamticEmergency{false};
|
||||
// True if this beacon is a Maritime beacon
|
||||
bool isMaritime{false};
|
||||
// Emergency type string
|
||||
char emergencyType[32]{0};
|
||||
|
||||
/**
|
||||
* Returns the requested bits in this beacon's frame
|
||||
*/
|
||||
uint64_t getBits(int startBit, int endBit) {
|
||||
uint64_t result = 0;
|
||||
startBit--;
|
||||
int numBits = endBit - startBit;
|
||||
const uint8_t* pData = &(frame[startBit / 8]);
|
||||
uint8_t b = *pData;
|
||||
int bitOffset = 7 - (startBit % 8);
|
||||
for (int i = 0; i < numBits; ++i) {
|
||||
result <<= 1;
|
||||
result |= ((b >> bitOffset) & 0x01);
|
||||
if (--bitOffset < 0) {
|
||||
b = *(++pData);
|
||||
bitOffset = 7;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a BCH control code for the given bits and poly
|
||||
*/
|
||||
uint64_t computeBCH(int startBit, int endBit, unsigned long poly, int polyLength) {
|
||||
int dataLength = endBit - startBit + 1;
|
||||
int totalLength = dataLength + polyLength - 1;
|
||||
uint64_t result = getBits(startBit, startBit + polyLength - 1);
|
||||
for (int i = polyLength; i <= totalLength; i++) {
|
||||
bool firstBit = result >> (polyLength - 1);
|
||||
if (firstBit) result = result ^ poly;
|
||||
if (i < totalLength) {
|
||||
result = result << 1;
|
||||
if (i < dataLength) result |= getBits(startBit + i, startBit + i);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute BCH1 code
|
||||
*/
|
||||
uint64_t computeBCH1() { return computeBCH(25, 85, BCH_21_POLYNOMIAL, BCH_21_POLY_LENGTH); }
|
||||
/**
|
||||
* Compute BCH2 code
|
||||
*/
|
||||
uint64_t computeBCH2() { return computeBCH(107, 132, BCH_12_POLYNOMIAL, BCH_12_POLY_LENGTH); }
|
||||
|
||||
/**
|
||||
* Convert this frame's data to an hex string
|
||||
*/
|
||||
static size_t toHexString(char* buffer, uint8_t* frame, bool withSpace, int start, int end) {
|
||||
size_t result = 0;
|
||||
for (uint8_t i = start; i < end; i++) {
|
||||
if (withSpace && i > start) buffer[result++] = ' ';
|
||||
result += sprintf((buffer + result), "%02X", frame[i]);
|
||||
}
|
||||
buffer[result] = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
Beacon() {}
|
||||
Beacon(const Beacon& other) = delete;
|
||||
Beacon& operator=(const Beacon& other) = delete;
|
||||
|
||||
/**
|
||||
* Set the data for this Beacon and parse it
|
||||
*/
|
||||
void setFrame(const uint8_t* frameBuffer) {
|
||||
frameMode = FrameMode::UNKNOWN;
|
||||
mainLocatingDevice = MainLocatingDevice::UNDEFINED;
|
||||
auxLocatingDevice = AuxLocatingDevice::UNDEFINED;
|
||||
protocolCode = 0;
|
||||
protocol = Protocol::UNKNOWN;
|
||||
protocolType = ProtocolType::UNKNOWN;
|
||||
country.code = 0;
|
||||
country.alphaCode[0] = 0;
|
||||
country.shortName[0] = 0;
|
||||
location.clear();
|
||||
identifier = 0;
|
||||
hasAdditionalData = false;
|
||||
additionalData[0] = 0;
|
||||
hasSerialNumber = false;
|
||||
serialNumber[0] = 0;
|
||||
hexId[0] = 0;
|
||||
bch1 = 0;
|
||||
computedBch1 = 0;
|
||||
hasBch2 = false;
|
||||
bch1Corrected = false;
|
||||
bch2 = 0;
|
||||
computedBch2 = 0;
|
||||
bch2Corrected = false;
|
||||
isEmpty = true;
|
||||
hasEmergency = false;
|
||||
isAutoamticEmergency = false;
|
||||
isMaritime = false;
|
||||
emergencyType[0] = 0;
|
||||
std::memcpy(frame, (const void*)frameBuffer, BEACON_DATA_SIZE);
|
||||
parseFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flip the specified bit in this beacon's data
|
||||
*/
|
||||
void flipBit(int bit) {
|
||||
int byteIdx = (bit - 1) / 8;
|
||||
int bitIdx = 7 - ((bit - 1) % 8);
|
||||
frame[byteIdx] ^= (1 << bitIdx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Single bit error correction to match BCH1 or BCH2 code
|
||||
*/
|
||||
void simpleCorrection(bool isBCH1) {
|
||||
for (int i = (isBCH1 ? 25 : 107); i <= (isBCH1 ? 85 : 132); i++) {
|
||||
flipBit(i);
|
||||
if ((isBCH1 ? (computeBCH1() == bch1) : (computeBCH2() == bch2))) {
|
||||
if (isBCH1) {
|
||||
computedBch1 = bch1;
|
||||
bch1Corrected = true;
|
||||
} else {
|
||||
computedBch2 = bch2;
|
||||
bch2Corrected = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
flipBit(i); // Restore bit value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the string representation of this beacon's type
|
||||
*/
|
||||
const char* getType() {
|
||||
if ((protocol == Protocol::USER_EPIRB_MARITIME) || (protocol == Protocol::USER_EPIRB_RADIO) || (protocol == Protocol::STD_EPIRB) || (protocol == Protocol::STD_EPIRB_SERIAL) || (protocol == Protocol::NAT_EPIRB)) return "EPIRB";
|
||||
if ((protocol == Protocol::USER_ELT) || (protocol == Protocol::STD_ELT_24) || (protocol == Protocol::STD_ELT_SERIAL) || (protocol == Protocol::STD_ELT_AIRCRAFT) || (protocol == Protocol::NAT_ELT) || (protocol == Protocol::ELT_DT)) return "ELT";
|
||||
if ((protocol == Protocol::STD_PLB_SERIAL) || (protocol == Protocol::NAT_PLB)) return "PLB";
|
||||
if ((protocol == Protocol::USER_TEST) || (protocol == Protocol::STD_TEST) || (protocol == Protocol::NAT_TEST)) return "TEST";
|
||||
if (protocol == Protocol::USER_SERIAL) return "SRIAL";
|
||||
if (protocol == Protocol::USER_ORB) return "ORB";
|
||||
if (protocol == Protocol::USER_NAT) return "NAT";
|
||||
if (protocol == Protocol::USER_2G) return "2G";
|
||||
if (protocol == Protocol::STD_SHIP) return "SHIP";
|
||||
if (protocol == Protocol::RLS) return "RLS";
|
||||
if (protocol == Protocol::SPARE) return "SPARE";
|
||||
return UNKNOWN_LABEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this beacon has a main location device
|
||||
*/
|
||||
bool hasMainLocatingDevice() { return (mainLocatingDevice != MainLocatingDevice::UNDEFINED); }
|
||||
|
||||
/**
|
||||
* Returns this beacon's main location device string representation
|
||||
*/
|
||||
const char* getMainLocatingDeviceName() {
|
||||
if (mainLocatingDevice == MainLocatingDevice::EXTERNAL_NAV) return "Exernal";
|
||||
if (mainLocatingDevice == MainLocatingDevice::INTERNAL_NAV) return "Internal";
|
||||
return UNKNOWN_LABEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this beacon has a main location device
|
||||
*/
|
||||
bool hasAuxLocatingDevice() { return (auxLocatingDevice != AuxLocatingDevice::UNDEFINED); }
|
||||
|
||||
/**
|
||||
* Returns true if this beacon has a main location device
|
||||
*/
|
||||
const char* getAuxLocatingDeviceName() {
|
||||
return ResourceManager::get_beacon_resource(AUX_DEVICE_RESOURCE_START + ((int)auxLocatingDevice));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the serial number for this beacon
|
||||
*/
|
||||
void setSerialNumber(uint32_t serial) {
|
||||
sprintf(serialNumber, "%ld (0x%08lX)", serial, serial);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if BCH1 code is valid
|
||||
*/
|
||||
bool isBch1Valid() { return (bch1 == computedBch1); }
|
||||
|
||||
/**
|
||||
* Returns true if BCH2 code is valid
|
||||
*/
|
||||
|
||||
bool isBch2Valid() { return (bch2 == computedBch2); }
|
||||
|
||||
/**
|
||||
* Returns true if frame is valid
|
||||
*/
|
||||
bool isFrameValid();
|
||||
|
||||
/**
|
||||
* Returns true if frame is orbito
|
||||
*/
|
||||
bool isOrbito() { return (protocol == Protocol::USER_ORB); }
|
||||
|
||||
/**
|
||||
* Write the hex preresentation of this frame in the provided buffer
|
||||
*/
|
||||
size_t hexString(char* buffer, bool withHeader) {
|
||||
return toHexString(buffer, frame, false, (withHeader ? 0 : 3), (longFrame ? 18 : 14));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the short ID for this beacon
|
||||
*/
|
||||
std::string shortId();
|
||||
|
||||
/**
|
||||
* Returns the string representation of the status of this frame
|
||||
*/
|
||||
std::string getSatus() {
|
||||
if (isFrameValid())
|
||||
return "OK";
|
||||
else
|
||||
return "KO";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format this beacon's time
|
||||
*/
|
||||
size_t formatTime(char* buffer);
|
||||
|
||||
/**
|
||||
* Format this beacon's summary
|
||||
*/
|
||||
size_t formatSummary(char* buffer, bool with_time);
|
||||
|
||||
private:
|
||||
/* Baudot code matrix */
|
||||
static constexpr char BAUDOT_CODE[64] = {' ', '5', ' ', '9', ' ', ' ', ' ', ' ', ' ', ' ', '4', ' ', '8', '0', ' ', ' ',
|
||||
'3', ' ', ' ', ' ', ' ', '6', ' ', '/', '-', '2', ' ', ' ', '7', '1', ' ', ' ',
|
||||
' ', 'T', ' ', 'O', ' ', 'H', 'N', 'M', ' ', 'L', 'R', 'G', 'I', 'P', 'C', 'V',
|
||||
'E', 'Z', 'D', 'B', 'S', 'Y', 'F', 'X', 'A', 'W', 'J', ' ', 'U', 'Q', 'K', '\0'};
|
||||
|
||||
/**
|
||||
* Parse this beacon's protocol
|
||||
*/
|
||||
void parseProtocol() {
|
||||
protocolFlag = getBits(26, 26);
|
||||
if (protocolFlag)
|
||||
protocolCode = getBits(37, 39);
|
||||
else
|
||||
protocolCode = getBits(37, 40);
|
||||
|
||||
if (!longFrame || protocolFlag == 1) {
|
||||
switch (protocolCode) {
|
||||
case 0b000:
|
||||
protocol = Protocol::USER_ORB;
|
||||
break;
|
||||
case 0b001:
|
||||
protocol = Protocol::USER_ELT;
|
||||
break;
|
||||
case 0b010:
|
||||
protocol = Protocol::USER_EPIRB_MARITIME;
|
||||
isMaritime = true;
|
||||
break;
|
||||
case 0b011:
|
||||
protocol = Protocol::USER_SERIAL;
|
||||
break;
|
||||
case 0b100:
|
||||
protocol = Protocol::USER_NAT;
|
||||
break;
|
||||
case 0b101:
|
||||
protocol = Protocol::USER_2G;
|
||||
break;
|
||||
case 0b110:
|
||||
protocol = Protocol::USER_EPIRB_RADIO;
|
||||
isMaritime = true;
|
||||
break;
|
||||
case 0b111:
|
||||
protocol = Protocol::USER_TEST;
|
||||
break;
|
||||
default:
|
||||
protocol = Protocol::UNKNOWN;
|
||||
}
|
||||
} else {
|
||||
switch (protocolCode) {
|
||||
case 0b0010:
|
||||
protocol = Protocol::STD_EPIRB;
|
||||
break;
|
||||
case 0b0011:
|
||||
protocol = Protocol::STD_ELT_24;
|
||||
break;
|
||||
case 0b0100:
|
||||
protocol = Protocol::STD_ELT_SERIAL;
|
||||
break;
|
||||
case 0b0101:
|
||||
protocol = Protocol::STD_ELT_AIRCRAFT;
|
||||
break;
|
||||
case 0b0110:
|
||||
protocol = Protocol::STD_EPIRB_SERIAL;
|
||||
break;
|
||||
case 0b0111:
|
||||
protocol = Protocol::STD_PLB_SERIAL;
|
||||
break;
|
||||
case 0b1000:
|
||||
protocol = Protocol::NAT_ELT;
|
||||
break;
|
||||
case 0b1001:
|
||||
protocol = Protocol::ELT_DT;
|
||||
break;
|
||||
case 0b1010:
|
||||
protocol = Protocol::NAT_EPIRB;
|
||||
break;
|
||||
case 0b1011:
|
||||
protocol = Protocol::NAT_PLB;
|
||||
break;
|
||||
case 0b1100:
|
||||
protocol = Protocol::STD_SHIP;
|
||||
break;
|
||||
case 0b1101:
|
||||
protocol = Protocol::RLS;
|
||||
break;
|
||||
case 0b1110:
|
||||
protocol = Protocol::STD_TEST;
|
||||
break;
|
||||
case 0b1111:
|
||||
protocol = Protocol::NAT_TEST;
|
||||
break;
|
||||
default:
|
||||
protocol = Protocol::UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse this beacon's additional data
|
||||
*/
|
||||
void parseAdditionalData() {
|
||||
hasAdditionalData = false;
|
||||
hasSerialNumber = false;
|
||||
if (protocolFlag) {
|
||||
if (protocolCode == 0b011) {
|
||||
uint8_t serialUserProtocol = getBits(40, 42);
|
||||
hasAdditionalData = true;
|
||||
switch (serialUserProtocol) {
|
||||
case 0b100:
|
||||
case 0b010:
|
||||
isMaritime = true;
|
||||
// fallthrough
|
||||
case 0b000:
|
||||
case 0b001:
|
||||
case 0b011:
|
||||
case 0b110:
|
||||
strcpy(additionalData, ResourceManager::get_beacon_resource(ADDITIONAL_DATA_RESOURCE_START + serialUserProtocol));
|
||||
break;
|
||||
default:
|
||||
hasAdditionalData = false;
|
||||
}
|
||||
hasSerialNumber = true;
|
||||
setSerialNumber((serialUserProtocol == 0b011) ? getBits(44, 67) : (serialUserProtocol == 0b001) ? getBits(62, 73)
|
||||
: getBits(44, 63));
|
||||
}
|
||||
if (!longFrame) {
|
||||
hasEmergency = getBits(107, 107);
|
||||
if (hasEmergency) {
|
||||
isAutoamticEmergency = getBits(108, 108);
|
||||
if (isMaritime) {
|
||||
uint8_t emmergency = getBits(109, 112);
|
||||
switch (emmergency) {
|
||||
case 0b0001:
|
||||
case 0b0010:
|
||||
case 0b0011:
|
||||
case 0b0100:
|
||||
case 0b0101:
|
||||
case 0b0110:
|
||||
case 0b0111:
|
||||
case 0b1000:
|
||||
strcpy(emergencyType, ResourceManager::get_beacon_resource(EMERGENCY_RESOURCE_START + emmergency));
|
||||
break;
|
||||
default:
|
||||
strcpy(emergencyType, ResourceManager::get_beacon_resource(EMERGENCY_RESOURCE_START));
|
||||
}
|
||||
} else {
|
||||
bool fire = getBits(109, 109);
|
||||
bool med = getBits(110, 110);
|
||||
bool disabled = getBits(111, 111);
|
||||
char* emmergency_pointer = emergencyType;
|
||||
if (fire) emmergency_pointer += sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START));
|
||||
if (med) {
|
||||
if (fire) emmergency_pointer += sprintf(emmergency_pointer, "%c", '+');
|
||||
emmergency_pointer += sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START + 1));
|
||||
}
|
||||
if (disabled) {
|
||||
if (fire || med) emmergency_pointer += sprintf(emmergency_pointer, "%c", '+');
|
||||
sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START + 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (longFrame) {
|
||||
switch (protocolCode) {
|
||||
case 0b0010:
|
||||
hasSerialNumber = true;
|
||||
setSerialNumber(getBits(61, 64));
|
||||
break;
|
||||
case 0b1100: {
|
||||
hasAdditionalData = true;
|
||||
uint32_t mmsi = getBits(41, 60);
|
||||
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START), mmsi, mmsi);
|
||||
} break;
|
||||
case 0b0011: {
|
||||
hasAdditionalData = true;
|
||||
hasSerialNumber = true;
|
||||
setSerialNumber(getBits(41, 64));
|
||||
strcpy(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 1));
|
||||
} break;
|
||||
case 0b0100:
|
||||
case 0b0110:
|
||||
case 0b0111: {
|
||||
hasAdditionalData = true;
|
||||
hasSerialNumber = true;
|
||||
uint32_t csTaNumber = getBits(41, 50);
|
||||
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 2), csTaNumber);
|
||||
setSerialNumber(getBits(51, 64));
|
||||
} break;
|
||||
case 0b0101: {
|
||||
hasAdditionalData = true;
|
||||
hasSerialNumber = true;
|
||||
uint32_t data = getBits(41, 45);
|
||||
char char1 = BAUDOT_CODE[data + 32];
|
||||
data = getBits(46, 50);
|
||||
char char2 = BAUDOT_CODE[data + 32];
|
||||
data = getBits(51, 55);
|
||||
char char3 = BAUDOT_CODE[data + 32];
|
||||
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 3), char1, char2, char3);
|
||||
setSerialNumber(getBits(56, 64));
|
||||
} break;
|
||||
case 0b1000:
|
||||
case 0b1010:
|
||||
case 0b1011:
|
||||
case 0b1111: {
|
||||
hasSerialNumber = true;
|
||||
hasAdditionalData = true;
|
||||
setSerialNumber(getBits(41, 58));
|
||||
uint32_t natNum = getBits(127, 132);
|
||||
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 4), natNum);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse this beacon's locating device
|
||||
*/
|
||||
void parseLocatingDevices() {
|
||||
bool mainLoc;
|
||||
if (protocolIsStandard() || protocolIsNational()) {
|
||||
mainLoc = getBits(111, 111);
|
||||
mainLocatingDevice = mainLoc ? MainLocatingDevice::INTERNAL_NAV : MainLocatingDevice::EXTERNAL_NAV;
|
||||
} else if (protocolIsUser() || protocolIsRls()) {
|
||||
mainLoc = getBits(107, 107);
|
||||
mainLocatingDevice = mainLoc ? MainLocatingDevice::INTERNAL_NAV : MainLocatingDevice::EXTERNAL_NAV;
|
||||
}
|
||||
if (protocolIsStandard() || protocolIsNational()) {
|
||||
auxLocatingDevice = getBits(112, 112) ? AuxLocatingDevice::MHZ121_5 : AuxLocatingDevice::NONE_OR_OTHER;
|
||||
} else if (protocolIsRls()) {
|
||||
auxLocatingDevice = getBits(108, 108) ? AuxLocatingDevice::MHZ121_5 : AuxLocatingDevice::NONE_OR_OTHER;
|
||||
} else if (protocolIsUser() && protocolCode != 0b100) {
|
||||
uint8_t aux = getBits(84, 85);
|
||||
if (aux == 0b00)
|
||||
auxLocatingDevice = AuxLocatingDevice::NONE;
|
||||
else if (aux == 0b01)
|
||||
auxLocatingDevice = AuxLocatingDevice::MHZ121_5;
|
||||
else if (aux == 0b10)
|
||||
auxLocatingDevice = AuxLocatingDevice::SART;
|
||||
else
|
||||
auxLocatingDevice = AuxLocatingDevice::OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse this beacon's frame data
|
||||
*/
|
||||
void parseFrame() {
|
||||
long latofmin, latofsec, lonofmin, lonofsec;
|
||||
bool latoffset, lonoffset;
|
||||
|
||||
longFrame = getBits(25, 25);
|
||||
parseProtocol();
|
||||
protocolType = getProtocolType();
|
||||
CountryManager::get_country(getBits(27, 36), country);
|
||||
|
||||
if (frame[2] == 0xD0)
|
||||
frameMode = FrameMode::SELF_TEST;
|
||||
else if (frame[2] == 0x2F)
|
||||
frameMode = FrameMode::NORMAL;
|
||||
else
|
||||
frameMode = FrameMode::UNKNOWN;
|
||||
|
||||
if (longFrame) {
|
||||
if (protocolIsUser() && !isOrbito()) {
|
||||
location.latitude.orientation = (frame[13] & 0x10) >> 4;
|
||||
location.latitude.degrees = ((frame[13] & 0x0F) << 3 | (frame[14] & 0xE0) >> 5);
|
||||
location.latitude.minutes = ((frame[14] & 0x1E) >> 1) * 4;
|
||||
location.longitude.orientation = (frame[14] & 0x01);
|
||||
location.longitude.degrees = (frame[15]);
|
||||
location.longitude.minutes = ((frame[16] & 0xF0) >> 4) * 4;
|
||||
} else if (protocolIsNational()) {
|
||||
latoffset = (frame[14] & 0x80) >> 7;
|
||||
location.latitude.orientation = (frame[7] & 0x20) >> 5;
|
||||
location.latitude.degrees = ((frame[7] & 0x1F) << 2 | (frame[8] & 0xC0) >> 6);
|
||||
location.latitude.minutes = ((frame[8] & 0x3E) >> 1) * 2;
|
||||
latofmin = (frame[14] & 0x60) >> 5;
|
||||
latofsec = ((frame[14] & 0x1E) >> 1) * 4;
|
||||
location.latitude.apply_offset(latoffset, latofmin, latofsec);
|
||||
lonoffset = (frame[14] & 0x01);
|
||||
location.longitude.orientation = (frame[8] & 0x01);
|
||||
location.longitude.degrees = (frame[9]);
|
||||
location.longitude.minutes = ((frame[10] & 0xF8) >> 3) * 2;
|
||||
lonofmin = (frame[15] & 0xC0) >> 6;
|
||||
lonofsec = ((frame[15] & 0x3C) >> 2) * 4;
|
||||
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
|
||||
} else if (protocolIsStandard()) {
|
||||
latoffset = (frame[14] & 0x80) >> 7;
|
||||
location.latitude.orientation = (frame[8] & 0x80) >> 7;
|
||||
location.latitude.degrees = (frame[8] & 0x7F);
|
||||
location.latitude.minutes = ((frame[9] & 0xC0) >> 6) * 15;
|
||||
latofmin = (frame[14] & 0x7C) >> 2;
|
||||
latofsec = ((frame[14] & 0x03) << 2 | (frame[15] & 0xC0) >> 6) * 4;
|
||||
location.latitude.apply_offset(latoffset, latofmin, latofsec);
|
||||
lonoffset = (frame[15] & 0x20) >> 5;
|
||||
location.longitude.orientation = (frame[9] & 0x20) >> 5;
|
||||
location.longitude.degrees = ((frame[9] & 0x1F) << 3 | (frame[10] & 0xE0) >> 5);
|
||||
location.longitude.minutes = ((frame[10] & 0x18) >> 3) * 15;
|
||||
lonofmin = (frame[15] & 0x1F);
|
||||
lonofsec = ((frame[16] & 0xF0) >> 4) * 4;
|
||||
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
|
||||
} else if (protocolIsRlsOrElt()) {
|
||||
latoffset = (frame[14] & 0x20) >> 5;
|
||||
location.latitude.orientation = (frame[8] & 0x20) >> 5;
|
||||
location.latitude.degrees = ((frame[8] & 0x1F) << 2) | ((frame[9] & 0xC0) >> 6);
|
||||
location.latitude.minutes = ((frame[9] & 0x20) >> 5) * 30;
|
||||
latofmin = (frame[14] & 0x1E) >> 1;
|
||||
latofsec = ((frame[14] & 0x01) << 3 | (frame[15] & 0xE0) >> 5) * 4;
|
||||
location.latitude.apply_offset(latoffset, latofmin, latofsec);
|
||||
lonoffset = (frame[15] & 0x10) >> 4;
|
||||
location.longitude.orientation = (frame[9] & 0x10) >> 4;
|
||||
location.longitude.degrees = ((frame[9] & 0x0F) << 4 | (frame[10] & 0xF0) >> 4);
|
||||
location.longitude.minutes = ((frame[10] & 0x08) >> 3) * 30;
|
||||
lonofmin = (frame[15] & 0x0F);
|
||||
lonofsec = ((frame[16] & 0xF0) >> 4) * 4;
|
||||
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
|
||||
}
|
||||
}
|
||||
|
||||
parseAdditionalData();
|
||||
parseLocatingDevices();
|
||||
|
||||
identifier = getBits(26, 85);
|
||||
|
||||
uint32_t hexIdHigh = (uint32_t)(identifier >> 32);
|
||||
uint32_t hexIdLow = (uint32_t)identifier;
|
||||
|
||||
if (protocolIsStandard()) {
|
||||
// default value of bits 65 to 74 = 0 111111111 / default value of bits 75 to 85 = 0 1111111111
|
||||
hexIdLow &= 0b11111111'11100000'00000000'00000000;
|
||||
hexIdLow |= 0b00000000'00001111'11111011'11111111;
|
||||
} else if (protocolIsNational()) {
|
||||
// default value of bits 59 to 71 = 0 1111111 00000 / default value of bits 72 to 85 = 0 11111111 00000
|
||||
hexIdLow &= 0b11111000'00000000'00000000'00000000;
|
||||
hexIdLow |= 0b00000011'11111000'00011111'11100000;
|
||||
} else if (protocolIsRlsOrElt()) {
|
||||
// default value of bits 67 to 75 = 0 11111111 / default value of bits 76 to 85 = 0 111111111
|
||||
hexIdLow &= 0b11111111'11111000'00000000'00000000;
|
||||
hexIdLow |= 0b00000000'00000011'11111101'11111111;
|
||||
}
|
||||
std::sprintf(hexId, "%07lX%08lX", hexIdHigh, hexIdLow);
|
||||
|
||||
if (isOrbito() && !longFrame) {
|
||||
isEmpty = true;
|
||||
for (size_t i = 3; i < BEACON_DATA_SIZE; i++) {
|
||||
if (frame[i] != 0) {
|
||||
isEmpty = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else
|
||||
isEmpty = false;
|
||||
|
||||
bch1 = getBits(86, 106);
|
||||
computedBch1 = computeBCH1();
|
||||
// Try and correct single bit error on BCH1 (bits 25-85)
|
||||
if (computedBch1 != bch1) {
|
||||
simpleCorrection(true);
|
||||
}
|
||||
|
||||
hasBch2 = longFrame && !isOrbito();
|
||||
if (hasBch2) {
|
||||
bch2 = getBits(133, 144);
|
||||
computedBch2 = computeBCH2();
|
||||
// Try and correct single bit error on BCH2 (bits 107 - 132)
|
||||
if (computedBch2 != bch2) {
|
||||
simpleCorrection(false);
|
||||
}
|
||||
}
|
||||
static bool isCorrecting = false;
|
||||
// If BCH1 or BCH2 has been corrected, we need to parse frame again to update beacon properties
|
||||
if (!isCorrecting && (bch1Corrected || bch2Corrected)) {
|
||||
// Prevent recursion
|
||||
isCorrecting = true;
|
||||
parseFrame();
|
||||
isCorrecting = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif // __BEACON_RX_H__
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 "beacon_db.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
/**
|
||||
* Add a beacon to the database.
|
||||
* @return the added beacon reference.
|
||||
*/
|
||||
Beacon& BeaconDB::add_beacon() {
|
||||
Beacon& result = recent_beacons[recent_beacon_pos];
|
||||
recent_beacon_pos = (recent_beacon_pos + 1) % BEACON_HISTORY_SIZE;
|
||||
if (recent_beacon_pos == 0) recent_beacon_full = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the beacon database
|
||||
* @return the size of the beacon database
|
||||
*/
|
||||
size_t BeaconDB::size() {
|
||||
return recent_beacon_full ? BEACON_HISTORY_SIZE : recent_beacon_pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the database is empty
|
||||
* @return true if the database is empty
|
||||
*/
|
||||
bool BeaconDB::empty() {
|
||||
return (!recent_beacon_full && (recent_beacon_pos == 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the beacon at the provided index
|
||||
*/
|
||||
Beacon& BeaconDB::get_beacon(size_t index) {
|
||||
// Start from last beacon et go backward
|
||||
int16_t pos = (int16_t)recent_beacon_pos - 1 - index;
|
||||
while (pos < 0) pos += BEACON_HISTORY_SIZE;
|
||||
return recent_beacons[pos % BEACON_HISTORY_SIZE];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set currently selected beacon
|
||||
* @param index the selected index
|
||||
*/
|
||||
void BeaconDB::set_current_beacon(size_t index) {
|
||||
current_beacon_index = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently selected index
|
||||
* @return the currently selected index
|
||||
*/
|
||||
size_t BeaconDB::get_current_beacon_index() {
|
||||
return current_beacon_index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected beacon
|
||||
* @return the currently selected beacon
|
||||
*/
|
||||
Beacon& BeaconDB::get_current_beacon() {
|
||||
return get_beacon(current_beacon_index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear beacon database
|
||||
*/
|
||||
void BeaconDB::clear() {
|
||||
recent_beacon_pos = 0;
|
||||
recent_beacon_full = false;
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 __BEACON_DB_H__
|
||||
#define __BEACON_DB_H__
|
||||
|
||||
#include "beacon.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
// Size of beacon database calculated to match the beacon list component size
|
||||
#define BEACON_HISTORY_SIZE 13
|
||||
|
||||
class BeaconDB {
|
||||
public:
|
||||
Beacon& add_beacon();
|
||||
Beacon& get_beacon(size_t index);
|
||||
Beacon& get_current_beacon();
|
||||
size_t get_current_beacon_index();
|
||||
void set_current_beacon(size_t index);
|
||||
size_t size();
|
||||
bool empty();
|
||||
void clear();
|
||||
|
||||
private:
|
||||
// Actual beacon database array
|
||||
Beacon recent_beacons[BEACON_HISTORY_SIZE];
|
||||
// Position of the insertion pointer
|
||||
int8_t recent_beacon_pos{0};
|
||||
// Beacon selection handling
|
||||
size_t current_beacon_index{0};
|
||||
// True when database is full
|
||||
bool recent_beacon_full{false};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif /* __BEACON_DB_H__ */
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 __COUNTRY_RX_H__
|
||||
#define __COUNTRY_RX_H__
|
||||
|
||||
#include "string_format.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
#define DISABLE_COUNTRY_CACHE
|
||||
|
||||
// Country struct
|
||||
struct Country {
|
||||
int16_t code;
|
||||
char alphaCode[4]; // 3 chars + null
|
||||
char shortName[11]; // 10 chars + null
|
||||
};
|
||||
|
||||
class CountryManager {
|
||||
public:
|
||||
static void get_country(int code, Country& out) {
|
||||
#ifndef DISABLE_COUNTRY_CACHE
|
||||
// Check cache
|
||||
for (int i = 0; i < cache_count; i++) {
|
||||
if (cache[i].code == code) {
|
||||
out = cache[i];
|
||||
if (i > 0) { // Promotion
|
||||
Country tmp = cache[i];
|
||||
for (int j = i; j > 0; j--) cache[j] = cache[j - 1];
|
||||
cache[0] = tmp;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Cache miss => load from file
|
||||
if (load_from_file(code, out)) {
|
||||
#ifndef DISABLE_COUNTRY_CACHE
|
||||
// Insert in cache
|
||||
int limit = (cache_count < 16) ? cache_count : 15;
|
||||
for (int j = limit; j > 0; j--) cache[j] = cache[j - 1];
|
||||
cache[0] = out;
|
||||
if (cache_count < 16) cache_count++;
|
||||
#endif
|
||||
} else {
|
||||
out.code = code;
|
||||
out.alphaCode[0] = '\0';
|
||||
out.shortName[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
#ifndef DISABLE_COUNTRY_CACHE
|
||||
static Country cache[16];
|
||||
static int cache_count;
|
||||
#endif
|
||||
|
||||
static bool load_from_file(int target_code, Country& out) {
|
||||
FIL file;
|
||||
if (f_open(&file, (epirb_dir / u"RES/COUNTRY.RES").tchar(), FA_READ) != FR_OK) return false;
|
||||
|
||||
TCHAR line[48];
|
||||
bool found = false;
|
||||
|
||||
while (f_gets(line, sizeof(line) / sizeof(TCHAR), &file)) {
|
||||
// Light parsing method
|
||||
TCHAR* p = line;
|
||||
|
||||
// Parse CODE
|
||||
int c = 0;
|
||||
while (*p >= '0' && *p <= '9') c = c * 10 + (*p++ - '0');
|
||||
if (c != target_code || *p != ':') continue;
|
||||
p++; // Skip ':'
|
||||
|
||||
out.code = (int16_t)c;
|
||||
|
||||
// Parse ALPHA CODE (3 chars)
|
||||
for (int i = 0; i < 3 && *p != ':'; i++) out.alphaCode[i] = (char)*p++;
|
||||
out.alphaCode[3] = '\0';
|
||||
if (*p != ':') continue;
|
||||
p++; // Skip ':'
|
||||
|
||||
// Parse SHORT NAME
|
||||
int i = 0;
|
||||
while (i < static_cast<int>(sizeof(out.shortName) - 1) && *p != '\r' && *p != '\n' && *p != '\0') {
|
||||
out.shortName[i++] = (char)*p++;
|
||||
}
|
||||
out.shortName[i] = '\0';
|
||||
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
f_close(&file);
|
||||
return found;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif // __COUNTRY_RX_H__
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 "location.hpp"
|
||||
#include <cstdio>
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
void Location::Angle::clear() {
|
||||
degrees = 255;
|
||||
minutes = 0;
|
||||
seconds = 0;
|
||||
orientation = false;
|
||||
floatValue = 255;
|
||||
}
|
||||
|
||||
float Location::Angle::getFloatValue() {
|
||||
if (floatValue >= 255) {
|
||||
floatValue = (float)degrees;
|
||||
floatValue += ((float)minutes / 60.0f);
|
||||
floatValue += ((float)seconds / 3600.0f);
|
||||
if (orientation) {
|
||||
floatValue = -floatValue;
|
||||
}
|
||||
}
|
||||
return floatValue;
|
||||
}
|
||||
|
||||
void Location::Angle::apply_offset(bool positive, long ofmin, long ofsec) {
|
||||
if (positive) {
|
||||
minutes += ofmin;
|
||||
seconds += ofsec;
|
||||
} else {
|
||||
minutes -= ofmin;
|
||||
if (minutes < 0) {
|
||||
minutes += 60;
|
||||
degrees -= 1;
|
||||
}
|
||||
seconds -= ofsec;
|
||||
if (seconds < 0) {
|
||||
seconds += 60;
|
||||
minutes -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Location::clear() {
|
||||
latitude.clear();
|
||||
longitude.clear();
|
||||
}
|
||||
|
||||
bool Location::isUnknown() {
|
||||
return (latitude.degrees >= 255 || longitude.degrees >= 255);
|
||||
}
|
||||
|
||||
char Location::gps_letterize(int x) {
|
||||
return (char)x + 65;
|
||||
}
|
||||
|
||||
size_t Location::gps_compute_locator(char* buffer, float lat, float lon, int precision) {
|
||||
size_t result = 0;
|
||||
|
||||
lon += 180.0f;
|
||||
lat += 90.0f;
|
||||
|
||||
int A = lon / 20;
|
||||
int B = lat / 10;
|
||||
|
||||
lon -= A * 20;
|
||||
lat -= B * 10;
|
||||
|
||||
int C = lon / 2;
|
||||
int D = lat / 1;
|
||||
|
||||
lon -= C * 2;
|
||||
lat -= D * 1;
|
||||
|
||||
int E = lon / (5.0f / 60.0f);
|
||||
int F = lat / (2.5f / 60.0f);
|
||||
|
||||
lon -= E * (5.0f / 60.0f);
|
||||
lat -= F * (2.5f / 60.0f);
|
||||
|
||||
int G = lon / (5.0f / 600.0f);
|
||||
int H = lat / (2.5f / 600.0f);
|
||||
|
||||
buffer[result++] = char('A' + A);
|
||||
buffer[result++] = char('A' + B);
|
||||
|
||||
if (precision >= 4) {
|
||||
buffer[result++] = char('0' + C);
|
||||
buffer[result++] = char('0' + D);
|
||||
}
|
||||
|
||||
if (precision >= 6) {
|
||||
buffer[result++] = char('a' + E);
|
||||
buffer[result++] = char('a' + F);
|
||||
}
|
||||
|
||||
if (precision >= 8) {
|
||||
buffer[result++] = char('0' + G);
|
||||
buffer[result++] = char('0' + H);
|
||||
}
|
||||
buffer[result] = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t Location::toString(char* buffer, LocationFormat format, int precision) {
|
||||
if (isUnknown()) {
|
||||
return sprintf(buffer, "%s", "GPS not synchronized");
|
||||
}
|
||||
switch (format) {
|
||||
case LocationFormat::SEXAGESIMAL: {
|
||||
return sprintf(buffer, "%ld\xB0%02ld'%02ld\"%c, %ld\xB0%02ld'%02ld\"%c", // 0xB0 is degree ° symbol in our 8x16 font
|
||||
latitude.degrees, latitude.minutes, latitude.seconds, latitude.orientation ? 'S' : 'N',
|
||||
longitude.degrees, longitude.minutes, longitude.seconds, longitude.orientation ? 'W' : 'E');
|
||||
}
|
||||
case LocationFormat::MAIDENHEAD_LOCATOR:
|
||||
return gps_compute_locator(buffer, latitude.getFloatValue(), longitude.getFloatValue(), precision);
|
||||
default:
|
||||
case LocationFormat::DECIMAL:
|
||||
return sprintf(buffer, "%s, %s", to_string_decimal(latitude.getFloatValue(), 5).c_str(), to_string_decimal(longitude.getFloatValue(), 5).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Location::formatFloatLocation(char* buffer, const char* format) {
|
||||
sprintf(buffer, format, to_string_decimal(latitude.getFloatValue(), 6).c_str(), to_string_decimal(longitude.getFloatValue(), 6).c_str());
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 __LOCATION_RX_H__
|
||||
#define __LOCATION_RX_H__
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
class Location {
|
||||
public:
|
||||
class Angle {
|
||||
public:
|
||||
long degrees = 0;
|
||||
long minutes = 0;
|
||||
long seconds = 0;
|
||||
bool orientation = false; // false = N/E, true = S/W
|
||||
|
||||
Angle() {}
|
||||
Angle(long degrees)
|
||||
: degrees(degrees) {}
|
||||
|
||||
void clear();
|
||||
|
||||
float getFloatValue();
|
||||
|
||||
// Apply an offset (minutes / seconds) with sign-based borrow into minutes/degrees.
|
||||
// Out-of-line to keep parseFrame from inlining the same arithmetic 6 times.
|
||||
void apply_offset(bool positive, long ofmin, long ofsec);
|
||||
|
||||
private:
|
||||
float floatValue = 255.0f;
|
||||
};
|
||||
|
||||
enum class LocationFormat { DECIMAL,
|
||||
SEXAGESIMAL,
|
||||
MAIDENHEAD_LOCATOR };
|
||||
|
||||
Angle latitude = Angle(127);
|
||||
Angle longitude = Angle(255);
|
||||
|
||||
void clear();
|
||||
|
||||
bool isUnknown();
|
||||
|
||||
static char gps_letterize(int x);
|
||||
|
||||
// For code size optimization reasons, string manipulation is performed with char buffers
|
||||
// with no char buffer size check. Caller methods take care of providing large enough buffers
|
||||
static size_t gps_compute_locator(char* buffer, float lat, float lon, int precision = 6);
|
||||
size_t toString(char* buffer, LocationFormat format, int precision = 6);
|
||||
|
||||
void formatFloatLocation(char* buffer, const char* format);
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif // __LOCATION_RX_H__
|
||||
+1
-1
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_epirb_rx.application_information"), us
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 __RESOUCES_H__
|
||||
#define __RESOUCES_H__
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include "file_reader.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
#define UNKNOWN_LABEL "Unk."
|
||||
|
||||
extern std::vector<std::string> beacon_res;
|
||||
extern std::vector<std::string> freq;
|
||||
|
||||
/**
|
||||
* This class is used to load string resources from SD card.
|
||||
* This is done mostly to reduce application size and keep it under the 32kb limit.
|
||||
* It's also used to load frequency values, allowing users to customize frequency list by editing FREQ.TXT file on SD card.
|
||||
*/
|
||||
class ResourceManager {
|
||||
public:
|
||||
// Singleton getter
|
||||
static ResourceManager* getInstance();
|
||||
|
||||
// Singleton instance
|
||||
static ResourceManager* current;
|
||||
// Used from standalone app, to prevent memleak
|
||||
static void destroy();
|
||||
|
||||
// Get a string from BEACON.RES file
|
||||
static const char* get_beacon_resource(uint8_t line) {
|
||||
ResourceManager* rm = getInstance();
|
||||
if (rm->beacon_res.empty()) {
|
||||
load_file((epirb_dir / u"RES/BEACON.RES"), rm->beacon_res);
|
||||
}
|
||||
if (line < rm->beacon_res.size()) {
|
||||
return rm->beacon_res[line].c_str();
|
||||
}
|
||||
return UNKNOWN_LABEL;
|
||||
}
|
||||
|
||||
// Get frequency values
|
||||
static const std::vector<std::string>& get_frequencies() {
|
||||
ResourceManager* rm = getInstance();
|
||||
if (rm->freq.empty()) {
|
||||
load_file((epirb_dir / u"FREQ.TXT"), rm->freq);
|
||||
}
|
||||
return rm->freq;
|
||||
}
|
||||
|
||||
private:
|
||||
// BEACON.RES content
|
||||
std::vector<std::string> beacon_res{};
|
||||
// FREQ.TXT content
|
||||
std::vector<std::string> freq{};
|
||||
|
||||
static void load_file(const std::filesystem::path& path, std::vector<std::string>& vect) {
|
||||
FIL file;
|
||||
if (f_open(&file, path.tchar(), FA_READ) == FR_OK) {
|
||||
TCHAR line_buffer[32];
|
||||
// Read the file line by line
|
||||
while (f_gets(line_buffer, sizeof(line_buffer) / sizeof(TCHAR), &file)) {
|
||||
std::string s;
|
||||
for (int i = 0; line_buffer[i] != '\0' && line_buffer[i] != '\n' && line_buffer[i] != '\r'; i++) {
|
||||
// Convert each TCHAR in char
|
||||
s += (char)line_buffer[i];
|
||||
}
|
||||
vect.push_back(std::move(s));
|
||||
}
|
||||
f_close(&file);
|
||||
}
|
||||
if (vect.empty()) vect.push_back(UNKNOWN_LABEL);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif // __RESOUCES_H__
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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_beaconlist.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
BeaconUIList::BeaconUIList(Rect parent_rect)
|
||||
: View{parent_rect} {
|
||||
this->set_focusable(true);
|
||||
}
|
||||
|
||||
void BeaconUIList::paint(Painter& painter) {
|
||||
auto rect = screen_rect();
|
||||
// Clear view
|
||||
painter.fill_rectangle(rect, Theme::getInstance()->bg_darkest->background);
|
||||
|
||||
if (!db_ || (db_->empty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto base_style = Theme::getInstance()->bg_darkest;
|
||||
|
||||
for (auto offset = 0u; offset < BEACON_HISTORY_SIZE; ++offset) {
|
||||
// The whole frame needs to be cleared so every line 'slot'
|
||||
// is redrawn even when `text` just left empty.
|
||||
auto text = std::string{};
|
||||
auto index = start_index_ + offset;
|
||||
auto line_position = rect.location() + Point{0, 1 + (int)offset * char_height};
|
||||
auto is_selected = offset == selected_index_;
|
||||
auto style = base_style;
|
||||
|
||||
if (index < db_->size()) {
|
||||
// Get beacon entry and format it's summary
|
||||
auto& entry = db_->get_beacon(index);
|
||||
char buffer[64];
|
||||
entry.formatSummary(buffer, true);
|
||||
text = std::string(buffer);
|
||||
}
|
||||
|
||||
if (index == db_->get_current_beacon_index())
|
||||
// If this is the currently displayed beacon change color
|
||||
style = Theme::getInstance()->bg_medium;
|
||||
|
||||
// Draw entry line
|
||||
painter.draw_string(
|
||||
line_position, (is_selected ? style->invert() : *style), text);
|
||||
}
|
||||
|
||||
// Draw a bounding rectangle when focused.
|
||||
painter.draw_rectangle(rect, (has_focus() ? Theme::getInstance()->bg_darkest->foreground : Theme::getInstance()->bg_darkest->background));
|
||||
}
|
||||
|
||||
void BeaconUIList::on_show() {
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
bool BeaconUIList::on_key(const KeyEvent key) {
|
||||
if (!db_ || db_->empty())
|
||||
return false;
|
||||
|
||||
if (key == KeyEvent::Select && on_select) {
|
||||
// Select this beacon
|
||||
on_select(get_index());
|
||||
set_dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
auto delta = 0;
|
||||
// Change position in the list
|
||||
if (key == KeyEvent::Up && get_index() > 0)
|
||||
delta = -1;
|
||||
else if (key == KeyEvent::Down && get_index() < db_->size() - 1)
|
||||
delta = 1;
|
||||
else
|
||||
return false;
|
||||
|
||||
adjust_selected_index(delta);
|
||||
set_dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BeaconUIList::on_encoder(EncoderEvent delta) {
|
||||
if (!db_ || db_->empty())
|
||||
return false;
|
||||
// Change position in the list according to encoder
|
||||
adjust_selected_index(delta);
|
||||
set_dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t BeaconUIList::get_index() const {
|
||||
return start_index_ + selected_index_;
|
||||
}
|
||||
|
||||
void BeaconUIList::set_db(BeaconDB& db) {
|
||||
db_ = &db;
|
||||
start_index_ = 0;
|
||||
selected_index_ = 0;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void BeaconUIList::adjust_selected_index(int delta) {
|
||||
int32_t new_index = selected_index_ + delta;
|
||||
|
||||
// The selection went off the top of the screen, move up.
|
||||
if (new_index < 0) {
|
||||
start_index_ = std::max<int32_t>(start_index_ + new_index, 0);
|
||||
selected_index_ = 0;
|
||||
}
|
||||
|
||||
// Selection is off the bottom of the screen, move down.
|
||||
else if (new_index >= (int32_t)BEACON_HISTORY_SIZE) {
|
||||
start_index_ = std::min<int32_t>(start_index_ + delta, db_->size() - BEACON_HISTORY_SIZE);
|
||||
selected_index_ = BEACON_HISTORY_SIZE - 1;
|
||||
}
|
||||
|
||||
// Otherwise, scroll within the screen, but not past the end.
|
||||
else {
|
||||
selected_index_ = std::min<int32_t>(new_index, db_->size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 __UI_BEACONLIST_H__
|
||||
#define __UI_BEACONLIST_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "beacon_db.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
/**
|
||||
* Beacon list component
|
||||
*/
|
||||
class BeaconUIList : public View {
|
||||
public:
|
||||
std::function<void(size_t)> on_select{};
|
||||
|
||||
BeaconUIList(Rect parent_rect);
|
||||
BeaconUIList(const BeaconUIList& other) = delete;
|
||||
BeaconUIList& operator=(const BeaconUIList& other) = delete;
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void on_show() override;
|
||||
bool on_key(const KeyEvent key) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
|
||||
size_t get_index() const;
|
||||
void set_db(BeaconDB& db);
|
||||
|
||||
private:
|
||||
void adjust_selected_index(int index);
|
||||
|
||||
BeaconDB* db_{nullptr};
|
||||
size_t start_index_{0};
|
||||
size_t selected_index_{0};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif /*__UI_BEACONLIST_H__*/
|
||||
+600
-707
File diff suppressed because it is too large
Load Diff
+323
-296
@@ -1,297 +1,324 @@
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Decoder Implementation
|
||||
*
|
||||
* 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 __UI_EPIRB_RX_H__
|
||||
#define __UI_EPIRB_RX_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
|
||||
#include "event_m0.hpp"
|
||||
#include "signal.hpp"
|
||||
#include "message.hpp"
|
||||
#include "log_file.hpp"
|
||||
|
||||
#include "baseband_packet.hpp"
|
||||
|
||||
/* #include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <array> */
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
// EPIRB 406 MHz beacon types
|
||||
enum class BeaconType : uint8_t {
|
||||
OrbitingLocationBeacon = 0,
|
||||
PersonalLocatorBeacon = 1,
|
||||
EmergencyLocatorTransmitter = 2,
|
||||
SerialELT = 3,
|
||||
NationalELT = 4,
|
||||
Other = 15
|
||||
};
|
||||
|
||||
// EPIRB distress and emergency types
|
||||
enum class EmergencyType : uint8_t {
|
||||
Fire = 0,
|
||||
Flooding = 1,
|
||||
Collision = 2,
|
||||
Grounding = 3,
|
||||
Sinking = 4,
|
||||
Disabled = 5,
|
||||
Abandoning = 6,
|
||||
Piracy = 7,
|
||||
Man_Overboard = 8,
|
||||
Other = 15
|
||||
};
|
||||
|
||||
struct EPIRBLocation {
|
||||
float latitude; // degrees, -90 to +90
|
||||
float longitude; // degrees, -180 to +180
|
||||
bool valid;
|
||||
|
||||
EPIRBLocation()
|
||||
: latitude(0.0f), longitude(0.0f), valid(false) {}
|
||||
EPIRBLocation(float lat, float lon)
|
||||
: latitude(lat), longitude(lon), valid(true) {}
|
||||
};
|
||||
|
||||
enum class PacketStatus : uint8_t {
|
||||
Valid = 0,
|
||||
Corrected = 1,
|
||||
Error = 2
|
||||
};
|
||||
|
||||
struct EPIRBBeacon {
|
||||
uint32_t beacon_id;
|
||||
BeaconType beacon_type;
|
||||
EmergencyType emergency_type;
|
||||
EPIRBLocation location;
|
||||
uint32_t country_code;
|
||||
std::string vessel_name;
|
||||
rtc::RTC timestamp;
|
||||
uint32_t sequence_number;
|
||||
PacketStatus packet_status;
|
||||
uint8_t error_count;
|
||||
|
||||
EPIRBBeacon()
|
||||
: beacon_id(0), beacon_type(BeaconType::Other), emergency_type(EmergencyType::Other), location(), country_code(0), vessel_name(), timestamp(), sequence_number(0), packet_status(PacketStatus::Error), error_count(0) {}
|
||||
};
|
||||
|
||||
class EPIRBDecoder {
|
||||
public:
|
||||
static EPIRBBeacon decode_packet(const baseband::Packet& packet);
|
||||
|
||||
private:
|
||||
static EPIRBLocation decode_location(const std::array<uint8_t, 16>& data);
|
||||
static BeaconType decode_beacon_type(uint8_t type_bits);
|
||||
static EmergencyType decode_emergency_type(uint8_t emergency_bits);
|
||||
static uint32_t decode_country_code(const std::array<uint8_t, 16>& data);
|
||||
static std::string decode_vessel_name(const std::array<uint8_t, 16>& data);
|
||||
|
||||
// BCH error correction methods
|
||||
static PacketStatus perform_bch_check(std::array<uint8_t, 16>& data, uint8_t& error_count);
|
||||
static uint32_t calculate_bch_syndrome(const std::array<uint8_t, 16>& data);
|
||||
static bool correct_single_error(std::array<uint8_t, 16>& data, uint32_t syndrome);
|
||||
static uint8_t count_bit_errors(const std::array<uint8_t, 16>& original, const std::array<uint8_t, 16>& corrected);
|
||||
};
|
||||
|
||||
class EPIRBLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void on_packet(const EPIRBBeacon& beacon);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
// Forward declarations of formatting functions
|
||||
std::string format_beacon_type(BeaconType type);
|
||||
std::string format_emergency_type(EmergencyType type);
|
||||
std::string format_packet_status(PacketStatus status);
|
||||
ui::Color get_packet_status_color(PacketStatus status);
|
||||
|
||||
class EPIRBBeaconDetailView : public ui::View {
|
||||
public:
|
||||
std::function<void(void)> on_close{};
|
||||
|
||||
EPIRBBeaconDetailView(ui::NavigationView& nav);
|
||||
EPIRBBeaconDetailView(const EPIRBBeaconDetailView&) = delete;
|
||||
EPIRBBeaconDetailView& operator=(const EPIRBBeaconDetailView&) = delete;
|
||||
|
||||
void set_beacon(const EPIRBBeacon& beacon);
|
||||
const EPIRBBeacon& beacon() const { return beacon_; }
|
||||
|
||||
void focus() override;
|
||||
void paint(ui::Painter&) override;
|
||||
|
||||
ui::GeoMapView* get_geomap_view() { return geomap_view; }
|
||||
|
||||
private:
|
||||
EPIRBBeacon beacon_{};
|
||||
|
||||
ui::Button button_done{
|
||||
{125, 224, 96, 24},
|
||||
"Done"};
|
||||
ui::Button button_see_map{
|
||||
{19, 224, 96, 24},
|
||||
"See on map"};
|
||||
|
||||
ui::GeoMapView* geomap_view{nullptr};
|
||||
|
||||
ui::Rect draw_field(
|
||||
ui::Painter& painter,
|
||||
const ui::Rect& draw_rect,
|
||||
const ui::Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value);
|
||||
};
|
||||
|
||||
class EPIRBAppView : public ui::View {
|
||||
public:
|
||||
EPIRBAppView(ui::NavigationView& nav);
|
||||
~EPIRBAppView();
|
||||
|
||||
void set_parent_rect(const ui::Rect new_parent_rect) override;
|
||||
void paint(ui::Painter&) override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "EPIRB RX"; }
|
||||
|
||||
private:
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_epirb", app_settings::Mode::RX};
|
||||
|
||||
ui::NavigationView& nav_;
|
||||
|
||||
std::vector<EPIRBBeacon> recent_beacons{};
|
||||
std::unique_ptr<EPIRBLogger> logger{};
|
||||
|
||||
EPIRBBeaconDetailView beacon_detail_view{nav_};
|
||||
|
||||
static constexpr auto header_height = 4 * 16;
|
||||
|
||||
ui::Text label_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0), 4 * 8, 1 * 16},
|
||||
"Freq"};
|
||||
|
||||
ui::OptionsField options_frequency{
|
||||
{5 * 8, UI_POS_Y(0)},
|
||||
7,
|
||||
{
|
||||
{"406.028", 406028000},
|
||||
{"406.025", 406025000},
|
||||
{"406.037", 406037000},
|
||||
{"433.025", 433025000},
|
||||
{"144.875", 144875000},
|
||||
}};
|
||||
|
||||
ui::RFAmpField field_rf_amp{
|
||||
{13 * 8, UI_POS_Y(0)}};
|
||||
|
||||
ui::LNAGainField field_lna{
|
||||
{15 * 8, UI_POS_Y(0)}};
|
||||
|
||||
ui::VGAGainField field_vga{
|
||||
{18 * 8, UI_POS_Y(0)}};
|
||||
|
||||
ui::RSSI rssi{
|
||||
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
|
||||
ui::Channel channel{
|
||||
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
|
||||
ui::AudioVolumeField field_volume{
|
||||
{screen_width - 2 * 8, UI_POS_Y(0)}};
|
||||
|
||||
// Status display
|
||||
ui::Text label_status{
|
||||
{UI_POS_X(0), 1 * 16, 15 * 8, 1 * 16},
|
||||
"Listening..."};
|
||||
|
||||
ui::Text label_beacons_count{
|
||||
{16 * 8, 1 * 16, 14 * 8, 1 * 16},
|
||||
"Beacons: 0"};
|
||||
|
||||
ui::Text label_packet_stats{
|
||||
{UI_POS_X(0), 3 * 16, 29 * 8, 1 * 16},
|
||||
""};
|
||||
|
||||
// Latest beacon info display
|
||||
ui::Text label_latest{
|
||||
{UI_POS_X(0), 2 * 16, 8 * 8, 1 * 16},
|
||||
"Latest:"};
|
||||
|
||||
ui::Text text_latest_info{
|
||||
{8 * 8, 2 * 16, 22 * 8, 1 * 16},
|
||||
""};
|
||||
|
||||
// Beacon list
|
||||
ui::Console console{
|
||||
{0, 4 * 16, 240, 152}};
|
||||
|
||||
ui::Button button_map{
|
||||
{0, 224, 60, 24},
|
||||
"Map"};
|
||||
|
||||
ui::Button button_clear{
|
||||
{64, 224, 60, 24},
|
||||
"Clear"};
|
||||
|
||||
ui::Button button_log{
|
||||
{128, 224, 60, 24},
|
||||
"Log"};
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
uint32_t beacons_received = 0;
|
||||
uint32_t packets_valid = 0;
|
||||
uint32_t packets_corrected = 0;
|
||||
uint32_t packets_error = 0;
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::EPIRBPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const EPIRBPacketMessage*>(p);
|
||||
this->on_packet(message->packet);
|
||||
}};
|
||||
|
||||
void on_packet(const baseband::Packet& packet);
|
||||
void on_beacon_decoded(const EPIRBBeacon& beacon);
|
||||
void on_show_map();
|
||||
void on_clear_beacons();
|
||||
void on_toggle_log();
|
||||
void on_tick_second();
|
||||
|
||||
void update_display();
|
||||
std::string format_beacon_summary(const EPIRBBeacon& beacon);
|
||||
std::string format_location(const EPIRBLocation& location);
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Decoder Implementation
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 __UI_EPIRB_RX_H__
|
||||
#define __UI_EPIRB_RX_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
|
||||
// Specan is disable to keep application size below the 32k limit
|
||||
// #define SPECAN
|
||||
|
||||
// Comment to disable timout reset on select and save approx 200 bytes of flash
|
||||
#ifndef PRALINE
|
||||
// Application does not fit on Praline with RESET_TIMER enabled
|
||||
#define RESET_TIMER
|
||||
#endif
|
||||
// Comment to disable squelch control
|
||||
#define SQUELCH
|
||||
// Comment to disable beacon selection by encoder on detail tab
|
||||
#define DETAIL_TAB_BEACON_SEL
|
||||
// #define LOGGER
|
||||
|
||||
#ifdef SPECAN
|
||||
#include "ui_spectrum.hpp"
|
||||
#endif
|
||||
|
||||
#include "ui_tabview.hpp"
|
||||
|
||||
#include "ui_qrcode.hpp"
|
||||
|
||||
#include "event_m0.hpp"
|
||||
#include "message.hpp"
|
||||
#include "log_file.hpp"
|
||||
|
||||
#include "baseband_packet.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
|
||||
#include "beacon.hpp"
|
||||
#include "beacon_db.hpp"
|
||||
#include "ui_beaconlist.hpp"
|
||||
#include "resources.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
/**
|
||||
* Status of a packet
|
||||
*/
|
||||
enum class PacketStatus : uint8_t {
|
||||
Valid = 0,
|
||||
Corrected = 1,
|
||||
Error = 2
|
||||
};
|
||||
|
||||
// Position of tabs in tab view
|
||||
#define EPIRB_TAB_POS_Y (UI_POS_Y(4) + 3 * 8)
|
||||
// Height of tabs in tab view
|
||||
#define EPIRB_TAB_HEIGHT (screen_height - EPIRB_TAB_POS_Y - UI_POS_HEIGHT(1))
|
||||
|
||||
#ifdef LOGGER
|
||||
class EPIRBLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void on_packet(Beacon& beacon);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Dedicated TextArea component used to optimize application code size
|
||||
*/
|
||||
class TextArea : public Widget {
|
||||
public:
|
||||
TextArea(Rect parent_rect);
|
||||
|
||||
#ifdef RESET_TIMER
|
||||
std::function<void(TextArea&)> on_select{};
|
||||
bool on_key(const KeyEvent key) override;
|
||||
#endif
|
||||
|
||||
void set_content(std::string_view value);
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
std::string content{};
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
class EPIRBAppView;
|
||||
|
||||
/**
|
||||
* View for beacon detail tab
|
||||
*/
|
||||
class EPIRBDetailView : public View {
|
||||
public:
|
||||
EPIRBDetailView(Rect parent_rect, EPIRBAppView& parent);
|
||||
void set_beacon(Beacon& beacon);
|
||||
#ifdef DETAIL_TAB_BEACON_SEL
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
#endif
|
||||
|
||||
private:
|
||||
TextArea text_beacon{{UI_POS_X(0), UI_POS_Y(0), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
|
||||
EPIRBAppView& parent_app;
|
||||
};
|
||||
|
||||
#define EPIRB_RX_DEFAULT_LATITUDE 43.604f
|
||||
#define EPIRB_RX_DEFAULT_LONGITUDE 1.458f
|
||||
|
||||
/**
|
||||
* View for beacon map tab
|
||||
*/
|
||||
class EPIRBMapView : public View {
|
||||
public:
|
||||
EPIRBMapView(Rect parent_rect);
|
||||
void paint(Painter& painter) override;
|
||||
void on_show() override;
|
||||
void set_main_marker(const std::string& label, float lat, float lon);
|
||||
void clear_markers();
|
||||
void add_marker(GeoMarker& marker);
|
||||
void hide_map(bool hide);
|
||||
void repaint();
|
||||
|
||||
private:
|
||||
GeoMap geomap{{0, 0, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
|
||||
float lat_{EPIRB_RX_DEFAULT_LATITUDE};
|
||||
float lon_{EPIRB_RX_DEFAULT_LONGITUDE};
|
||||
bool map_hidden{true};
|
||||
};
|
||||
|
||||
#define QR_WIDTH 126
|
||||
#define QR_HEIGHT 127
|
||||
|
||||
/**
|
||||
* Vieaw for Beacon QR tab
|
||||
*/
|
||||
class EPRIBQRView : public View {
|
||||
public:
|
||||
EPRIBQRView(Rect parent_rect);
|
||||
EPRIBQRView(const EPRIBQRView&) = delete;
|
||||
EPRIBQRView& operator=(const EPRIBQRView&) = delete;
|
||||
|
||||
void set_beacon(Beacon* beacon);
|
||||
void update_qr();
|
||||
void update_display();
|
||||
|
||||
private:
|
||||
bool show_map{true};
|
||||
Beacon* current_beacon{nullptr};
|
||||
char qr_url[128];
|
||||
|
||||
OptionsField options_qr{
|
||||
{UI_POS_X(5), UI_POS_Y(1)},
|
||||
6,
|
||||
{{"Map", 0},
|
||||
{"Detail", 1}}};
|
||||
|
||||
QRCodeImage qr_code{
|
||||
{UI_POS_MAXWIDTH - QR_WIDTH - UI_POS_X(1), UI_POS_Y(1), QR_WIDTH, QR_HEIGHT}};
|
||||
|
||||
TextArea text_data{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT - UI_POS_Y(1)}};
|
||||
};
|
||||
|
||||
#ifdef SPECAN
|
||||
class EPIRBRxView : public spectrum::WaterfallView {
|
||||
public:
|
||||
EPIRBRxView(EPIRBAppView& parent, Rect parent_rect);
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
|
||||
private:
|
||||
EPIRBAppView& app_view;
|
||||
};
|
||||
#endif
|
||||
|
||||
class EPIRBAppView final : public ui::View {
|
||||
public:
|
||||
EPIRBAppView(ui::NavigationView& nav);
|
||||
~EPIRBAppView();
|
||||
|
||||
void focus() override;
|
||||
void refresh();
|
||||
|
||||
// Message to configure rx baseband
|
||||
EPIRBRXConfig epirb_rx_config_message{};
|
||||
void send_config();
|
||||
// Beacons database
|
||||
BeaconDB beacon_db{};
|
||||
// Update display when beacon selection changed0
|
||||
void on_beacon_change();
|
||||
|
||||
std::string title() const override { return "EPIRB RX"; }
|
||||
|
||||
private:
|
||||
uint8_t squelch{50};
|
||||
// The delay between each frame
|
||||
uint32_t countdown{50};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_epirb",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"epirb_squelch"sv, &squelch},
|
||||
{"countdown"sv, &countdown},
|
||||
}};
|
||||
|
||||
ui::NavigationView& nav_;
|
||||
|
||||
#ifdef LOGGER
|
||||
std::unique_ptr<EPIRBLogger> logger{};
|
||||
#endif
|
||||
|
||||
OptionsField options_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
7,
|
||||
{}};
|
||||
|
||||
ui::RFAmpField field_rf_amp{
|
||||
{UI_POS_X(8), UI_POS_Y(0)}};
|
||||
|
||||
ui::LNAGainField field_lna{
|
||||
{UI_POS_X(10), UI_POS_Y(0)}};
|
||||
|
||||
ui::VGAGainField field_vga{
|
||||
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||
|
||||
ui::RSSI rssi{
|
||||
{UI_POS_X(16), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(22), 4}};
|
||||
|
||||
ui::Channel channel{
|
||||
{UI_POS_X(16), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(22), 4}};
|
||||
|
||||
// ui::Audio audio{
|
||||
// {UI_POS_X(16), UI_POS_Y(0) + 10, UI_POS_WIDTH_REMAINING(22), 4}};
|
||||
|
||||
ui::AudioVolumeField field_volume{
|
||||
{UI_POS_WIDTH_REMAINING(2), UI_POS_Y(0)}};
|
||||
|
||||
#ifdef SQUELCH
|
||||
NumberField field_squelch{
|
||||
{UI_POS_WIDTH_REMAINING(5), UI_POS_Y(0)},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' '};
|
||||
#endif
|
||||
|
||||
// Status display
|
||||
TextArea text_status{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(3)}};
|
||||
TextArea text_timeout{
|
||||
{UI_POS_X(13), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}};
|
||||
SignalToken signal_token_tick_second{};
|
||||
// Timeout string
|
||||
int16_t timeout{0};
|
||||
|
||||
// Tab View
|
||||
Rect view_rect = {0, EPIRB_TAB_POS_Y, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT};
|
||||
|
||||
BeaconUIList view_list{view_rect};
|
||||
EPIRBDetailView view_detail{view_rect, (*this)};
|
||||
EPIRBMapView view_map{view_rect};
|
||||
#ifdef SPECAN
|
||||
EPIRBRxView view_rx{*this, view_rect};
|
||||
#endif
|
||||
|
||||
EPRIBQRView view_qr{view_rect};
|
||||
|
||||
TabView tab_view{
|
||||
{"List", Theme::getInstance()->fg_cyan->foreground, &view_list},
|
||||
{"Detail", Theme::getInstance()->fg_green->foreground, &view_detail},
|
||||
{"Map", Theme::getInstance()->fg_yellow->foreground, &view_map},
|
||||
#ifdef SPECAN
|
||||
{"RX", Theme::getInstance()->fg_orange->foreground, &view_rx},
|
||||
#endif
|
||||
{"QR", Theme::getInstance()->fg_orange->foreground, &view_qr}};
|
||||
|
||||
uint16_t beacons_received = 0;
|
||||
uint16_t packets_valid = 0;
|
||||
uint16_t packets_corrected = 0;
|
||||
uint16_t packets_error = 0;
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::EPIRBPacket,
|
||||
[this](Message* const p) { on_packet(p); }};
|
||||
|
||||
static void decode_packet(const baseband::Packet& packet, Beacon& beacon);
|
||||
void on_packet(Message* const p);
|
||||
void update_map();
|
||||
void on_tick_second();
|
||||
|
||||
void update_display();
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif // __UI_EPIRB_RX_H__
|
||||
+61
-57
@@ -24,7 +24,6 @@
|
||||
#include "portapack.hpp"
|
||||
#include "ui_epirb_tx.hpp"
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
|
||||
namespace ui::external_app::epirb_tx {
|
||||
@@ -37,17 +36,17 @@ namespace ui::external_app::epirb_tx {
|
||||
* @param min output minutes value
|
||||
* @param sec output seconds value
|
||||
*/
|
||||
static void decimal_to_dms(double value, bool& negative, uint16_t& deg, uint8_t& min, uint8_t& sec) {
|
||||
static void decimal_to_dms(float value, bool& negative, int16_t& deg, int8_t& min, int8_t& sec) {
|
||||
negative = value < 0;
|
||||
value = std::fabs(value);
|
||||
|
||||
deg = (uint16_t)value;
|
||||
deg = (int16_t)value;
|
||||
|
||||
double m = (value - deg) * 60.0;
|
||||
min = (uint8_t)m;
|
||||
float m = (value - deg) * 60.0f;
|
||||
min = (int8_t)m;
|
||||
|
||||
double s = (m - min) * 60.0;
|
||||
sec = (uint8_t)(s + 0.5);
|
||||
float s = (m - min) * 60.0f;
|
||||
sec = (int8_t)(s + 0.5f);
|
||||
|
||||
if (sec == 60) {
|
||||
sec = 0;
|
||||
@@ -68,8 +67,8 @@ static void decimal_to_dms(double value, bool& negative, uint16_t& deg, uint8_t&
|
||||
* @param sec seconds value
|
||||
* @return value the decimal value
|
||||
*/
|
||||
static double dms_to_decimal(bool negative, uint16_t deg, uint8_t min, uint8_t sec) {
|
||||
double v = deg + min / 60.0 + sec / 3600.0;
|
||||
static float dms_to_decimal(bool negative, int16_t deg, int8_t min, int8_t sec) {
|
||||
float v = deg + min / 60.0f + sec / 3600.0f;
|
||||
return negative ? -v : v;
|
||||
}
|
||||
|
||||
@@ -80,55 +79,60 @@ static double dms_to_decimal(bool negative, uint16_t deg, uint8_t min, uint8_t s
|
||||
* @param lon output longitude
|
||||
*/
|
||||
static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon) {
|
||||
static const double lon_step[] =
|
||||
static const float lon_step[] =
|
||||
{
|
||||
20.0,
|
||||
2.0,
|
||||
1.0 / 12.0,
|
||||
1.0 / 120.0,
|
||||
1.0 / 2880.0};
|
||||
20.0f,
|
||||
2.0f,
|
||||
1.0f / 12.0f,
|
||||
1.0f / 120.0f,
|
||||
1.0f / 2880.0f};
|
||||
|
||||
static const double lat_step[] =
|
||||
static const float lat_step[] =
|
||||
{
|
||||
10.0,
|
||||
1.0,
|
||||
1.0 / 24.0,
|
||||
1.0 / 240.0,
|
||||
1.0 / 5760.0};
|
||||
10.0f,
|
||||
1.0f,
|
||||
1.0f / 24.0f,
|
||||
1.0f / 240.0f,
|
||||
1.0f / 5760.0f};
|
||||
|
||||
lon = -180.0;
|
||||
lat = -90.0;
|
||||
lon = -180.0f;
|
||||
lat = -90.0f;
|
||||
|
||||
int pairs = loc.size() / 2;
|
||||
|
||||
if (pairs > 5)
|
||||
pairs = 5;
|
||||
|
||||
for (int i = 0; i < pairs; i++) {
|
||||
char c1 = std::toupper(loc[i * 2]);
|
||||
char c2 = std::toupper(loc[i * 2 + 1]);
|
||||
if (pairs > 0) {
|
||||
for (int i = 0; i < pairs; i++) {
|
||||
char c1 = loc[i * 2];
|
||||
char c2 = loc[i * 2 + 1];
|
||||
if (c1 >= 'a' && c1 <= 'z') c1 -= ('a' - 'A');
|
||||
if (c2 >= 'a' && c2 <= 'z') c2 -= ('a' - 'A');
|
||||
|
||||
double lon_size = lon_step[i];
|
||||
double lat_size = lat_step[i];
|
||||
float lon_size = lon_step[i];
|
||||
float lat_size = lat_step[i];
|
||||
|
||||
int v1, v2;
|
||||
int v1, v2;
|
||||
|
||||
if (i % 2 == 0) // letters
|
||||
{
|
||||
v1 = c1 - 'A';
|
||||
v2 = c2 - 'A';
|
||||
} else // digits
|
||||
{
|
||||
v1 = c1 - '0';
|
||||
v2 = c2 - '0';
|
||||
if (i % 2 == 0) // letters
|
||||
{
|
||||
v1 = c1 - 'A';
|
||||
v2 = c2 - 'A';
|
||||
} else // digits
|
||||
{
|
||||
v1 = c1 - '0';
|
||||
v2 = c2 - '0';
|
||||
}
|
||||
|
||||
lon += v1 * lon_size;
|
||||
lat += v2 * lat_size;
|
||||
}
|
||||
|
||||
lon += v1 * lon_size;
|
||||
lat += v2 * lat_size;
|
||||
// Cell center
|
||||
lon += lon_step[pairs - 1] / 2.0f;
|
||||
lat += lat_step[pairs - 1] / 2.0f;
|
||||
}
|
||||
|
||||
// Cell center
|
||||
lon += lon_step[pairs - 1] / 2.0;
|
||||
lat += lat_step[pairs - 1] / 2.0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,9 +142,9 @@ static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon
|
||||
* @param precision (optional, defaults to 8) the number of charater for the maidenhead locator
|
||||
* @return the locator
|
||||
*/
|
||||
static std::string decimal_to_maidenhead(double lat, double lon, int precision = 8) {
|
||||
lon += 180.0;
|
||||
lat += 90.0;
|
||||
static std::string decimal_to_maidenhead(float lat, float lon, int precision = 8) {
|
||||
lon += 180.0f;
|
||||
lat += 90.0f;
|
||||
|
||||
int A = lon / 20;
|
||||
int B = lat / 10;
|
||||
@@ -154,14 +158,14 @@ static std::string decimal_to_maidenhead(double lat, double lon, int precision =
|
||||
lon -= C * 2;
|
||||
lat -= D * 1;
|
||||
|
||||
int E = lon / (5.0 / 60.0);
|
||||
int F = lat / (2.5 / 60.0);
|
||||
int E = lon / (5.0f / 60.0f);
|
||||
int F = lat / (2.5f / 60.0f);
|
||||
|
||||
lon -= E * (5.0 / 60.0);
|
||||
lat -= F * (2.5 / 60.0);
|
||||
lon -= E * (5.0f / 60.0f);
|
||||
lat -= F * (2.5f / 60.0f);
|
||||
|
||||
int G = lon / (5.0 / 600.0);
|
||||
int H = lat / (2.5 / 600.0);
|
||||
int G = lon / (5.0f / 600.0f);
|
||||
int H = lat / (2.5f / 600.0f);
|
||||
|
||||
std::string locator;
|
||||
|
||||
@@ -247,9 +251,9 @@ void init_from_dms(Location& loc) {
|
||||
* Convert the provided Loaction to it's latitude string (format <xxx°yy'zz"N>)
|
||||
*/
|
||||
std::string to_latitude_string(const Location& location) {
|
||||
char buffer[16];
|
||||
char buffer[20];
|
||||
// Format : <xxx°yy'zz"N>
|
||||
snprintf(buffer, sizeof(buffer), "%3d\260%02d'%02d\"%c", location.lat_deg, location.lat_min, location.lat_sec, location.south ? 'S' : 'N');
|
||||
sprintf(buffer, "%3d\260%02d'%02d\"%c", location.lat_deg, location.lat_min, location.lat_sec, location.south ? 'S' : 'N');
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
@@ -257,12 +261,12 @@ std::string to_latitude_string(const Location& location) {
|
||||
* Convert the provided Loaction to it's longitude string (format <xxx°yy'zz"W>)
|
||||
*/
|
||||
std::string to_longitude_string(const Location& location) {
|
||||
char buffer[16];
|
||||
char buffer[20];
|
||||
// Format : <xxx°yy'zz"W>
|
||||
snprintf(buffer, sizeof(buffer), "%3d\260%02d'%02d\"%c", location.long_deg, location.long_min, location.long_sec, location.west ? 'W' : 'E');
|
||||
sprintf(buffer, "%3d\260%02d'%02d\"%c", location.long_deg, location.long_min, location.long_sec, location.west ? 'W' : 'E');
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_tx
|
||||
|
||||
#endif /*__LOCATION_H__*/
|
||||
#endif /*__LOCATION_H__*/
|
||||
|
||||
+659
-631
File diff suppressed because it is too large
Load Diff
+417
-404
@@ -1,404 +1,417 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 __EPIRB_TX_H__
|
||||
#define __EPIRB_TX_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "tonesets.hpp"
|
||||
|
||||
#define BEACON_HEXA_SIZE 36
|
||||
#define BEACON_HEXA_HALF_SIZE 18
|
||||
#define BEACON_SIZE 18
|
||||
|
||||
#define AM_TEST_FREQUENCY 121375000
|
||||
#define AM_REAL_FREQUENCY 121500000
|
||||
|
||||
#define BPSK_FREQUENCY_HAM 433025000
|
||||
#define BPSK_FREQUENCY_B 406025000
|
||||
#define BPSK_FREQUENCY_C 406028000
|
||||
#define BPSK_FREQUENCY_F 406037000
|
||||
#define BPSK_FREQUENCY_G 406040000
|
||||
#define BPSK_FREQUENCY_J 406049000
|
||||
#define BPSK_FREQUENCY_K 406052000
|
||||
#define BPSK_FREQUENCY_N 406061000
|
||||
#define BPSK_FREQUENCY_O 406064000
|
||||
|
||||
namespace ui::external_app::epirb_tx {
|
||||
|
||||
enum class BeaconMode {
|
||||
FILE = 0,
|
||||
MODE_MANUAL = 1
|
||||
};
|
||||
|
||||
enum class BeaconType {
|
||||
EPIRB = 0,
|
||||
ELT = 1,
|
||||
PLB = 2
|
||||
};
|
||||
|
||||
enum class BeaconProtocol {
|
||||
USER = 0,
|
||||
STANDARD = 1,
|
||||
NATIONAL = 2
|
||||
};
|
||||
|
||||
enum class AmChannel {
|
||||
TEST = 0,
|
||||
REAL = 1,
|
||||
MANUAL = 2
|
||||
};
|
||||
|
||||
enum class BpskChannel {
|
||||
HAM = 0,
|
||||
B = 1,
|
||||
C = 2,
|
||||
F = 3,
|
||||
G = 4,
|
||||
J = 5,
|
||||
K = 6,
|
||||
N = 7,
|
||||
O = 8,
|
||||
MANUAL = 10
|
||||
};
|
||||
|
||||
struct Location {
|
||||
std::string locator;
|
||||
bool south;
|
||||
uint16_t lat_deg;
|
||||
uint8_t lat_min;
|
||||
uint8_t lat_sec;
|
||||
float latitude;
|
||||
bool west;
|
||||
uint16_t long_deg;
|
||||
uint8_t long_min;
|
||||
uint8_t long_sec;
|
||||
float longitude;
|
||||
};
|
||||
|
||||
struct BeaconParams {
|
||||
BeaconType type;
|
||||
BeaconProtocol protocol;
|
||||
uint32_t country;
|
||||
bool is_test;
|
||||
bool is_internal;
|
||||
bool has_121_5;
|
||||
Location location;
|
||||
};
|
||||
|
||||
class EPIRBTXAppView : public View {
|
||||
public:
|
||||
EPIRBTXAppView(NavigationView& nav);
|
||||
~EPIRBTXAppView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "EPIRB TX"; };
|
||||
|
||||
private:
|
||||
void start_tx();
|
||||
void stop_tx();
|
||||
void update_config();
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void on_timer();
|
||||
void load_beacons();
|
||||
void set_tx_button_state(bool active);
|
||||
std::string frame_to_hex_string(bool start);
|
||||
void generate_frame(BeaconParams params);
|
||||
void update_frame(bool updateConfig = true);
|
||||
void update_bpsk_frequency();
|
||||
void update_am_transmission();
|
||||
void update_mode();
|
||||
void update_location(bool updateLocatorField = true);
|
||||
|
||||
struct Beacon {
|
||||
std::string title{};
|
||||
std::string description{};
|
||||
std::string frame{};
|
||||
};
|
||||
std::vector<Beacon> beacons{};
|
||||
Beacon default_beacon{"Self test", "Serial User Location Protocol", "FFFED0D6E6202820000C29FF51041775302D"};
|
||||
|
||||
BeaconParams beacon_params{BeaconType::ELT, BeaconProtocol::STANDARD, 227, true, true, true, {"JN03RO", false, 0, 0, 0, 0, false, 0, 0, 0, 0}};
|
||||
|
||||
// Currently selected beacon index (from BEACONS.TXT file / options_frame combo)
|
||||
uint32_t selected_beacon{0};
|
||||
|
||||
// Frequency of the transmitter before starting the app (used to restore frequency when leaving)
|
||||
rf::Frequency original_frequency{0};
|
||||
// Frequency of the AM emergency signal
|
||||
rf::Frequency am_frequency{AM_TEST_FREQUENCY};
|
||||
// Frequency of the 406 MHz BPSK signal
|
||||
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||
// Selected am channel
|
||||
uint8_t am_channel{(uint8_t)AmChannel::TEST};
|
||||
// Selected bpsk channel
|
||||
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
|
||||
// Manual AM frequency value
|
||||
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
|
||||
// Manual BPSK frequency value
|
||||
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||
|
||||
// True when using a beacon from the BEACONS.TXT file
|
||||
bool mode_file{false};
|
||||
// True when looping on sending beacons is enabled
|
||||
bool loop_enabled{true};
|
||||
// True if AM emergency signal transmission is enabled
|
||||
bool am_enabled{true};
|
||||
// True if we want to send a new frame each time the user changes the current beacon
|
||||
bool send_on_change{true};
|
||||
// The current locator string
|
||||
std::string locator{"JN03RO"};
|
||||
// The delay between each frame when on loop mode
|
||||
uint32_t delay{50};
|
||||
uint8_t beacon_type{(uint8_t)BeaconType::EPIRB};
|
||||
// Currently selected beacon protocol
|
||||
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
|
||||
// Currently selected beacon country
|
||||
uint32_t beacon_country{227};
|
||||
// Current beacon's internal state (true for internal location system)
|
||||
bool beacon_internal{true};
|
||||
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
TONES_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_epirb",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"sbeacon"sv, &selected_beacon},
|
||||
{"amfreq"sv, &am_frequency},
|
||||
{"bpskfreq"sv, &bpsk_frequency},
|
||||
{"amchan"sv, &am_channel},
|
||||
{"bpskchan"sv, &bpsk_channel},
|
||||
{"loop"sv, &loop_enabled},
|
||||
{"delay"sv, &delay},
|
||||
{"file"sv, &mode_file},
|
||||
{"am"sv, &am_enabled},
|
||||
{"soc"sv, &send_on_change},
|
||||
{"type"sv, &beacon_type},
|
||||
{"proto"sv, &beacon_protocol},
|
||||
{"country"sv, &beacon_country},
|
||||
{"internal"sv, &beacon_internal},
|
||||
{"locator"sv, &locator},
|
||||
}};
|
||||
|
||||
// Time of the last sent frame
|
||||
uint32_t last_frame_time{0};
|
||||
// True when transmission is enabled
|
||||
bool transmitting{false};
|
||||
// True when transmitting a BPSK frame
|
||||
bool transmitting_bpsk{false};
|
||||
// True when currently looping on sending beacons
|
||||
bool loop{false};
|
||||
|
||||
// Current EPIRBTXDataMessage for baseband
|
||||
EPIRBTXDataMessage epirb_tx_message{};
|
||||
|
||||
const size_t max_text_width = UI_POS_WIDTH_REMAINING(6) / UI_POS_DEFAULT_WIDTH;
|
||||
const size_t max_text_width_ext = UI_POS_WIDTH_REMAINING(0) / UI_POS_DEFAULT_WIDTH;
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency: MHz", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
// For file mode
|
||||
Text text_beacon{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_DEFAULT_HEIGHT},
|
||||
"Beacon:"};
|
||||
// Beacon selection from BEACONS.TXT
|
||||
OptionsField options_frame{
|
||||
{UI_POS_X(7), UI_POS_Y(1)},
|
||||
30,
|
||||
{}};
|
||||
Text text_description_label{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
"Description:"};
|
||||
Text text_description{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_description_end{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
// For manual mode
|
||||
Text text_beacon_type{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Type:"};
|
||||
Text text_beacon_country{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Country:"};
|
||||
Checkbox checkbox_beacon_internal{
|
||||
{UI_POS_X_RIGHT(12), UI_POS_Y(2)},
|
||||
12,
|
||||
"Internal",
|
||||
true};
|
||||
Text text_beacon_locator{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Locator:"};
|
||||
Text text_beacon_latitude{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Lat.:"};
|
||||
Text text_beacon_longitude{
|
||||
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Long.:"};
|
||||
Text text_beacon_latitude_value{
|
||||
{UI_POS_X(7), UI_POS_Y(4), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_beacon_longitude_value{
|
||||
{UI_POS_X(7), UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
OptionsField options_beacon_type{
|
||||
{UI_POS_X(9), UI_POS_Y(1)},
|
||||
7,
|
||||
{{"EPIRB", (uint8_t)BeaconType::EPIRB},
|
||||
{"ELT", (uint8_t)BeaconType::ELT},
|
||||
{"PLB", (uint8_t)BeaconType::PLB}}};
|
||||
OptionsField options_beacon_protocol{
|
||||
{UI_POS_X(9 + 7), UI_POS_Y(1)},
|
||||
30,
|
||||
{{"User", (uint8_t)BeaconProtocol::USER},
|
||||
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
|
||||
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
|
||||
OptionsField options_beacon_country{
|
||||
{UI_POS_X(9), UI_POS_Y(2)},
|
||||
7,
|
||||
{{"France", 227},
|
||||
{"USA", 366},
|
||||
{"Germany", 211},
|
||||
{"Russia", 273},
|
||||
{"Spain", 224},
|
||||
{"Japan", 431},
|
||||
{"UK", 232}}};
|
||||
TextField text_field_beacon_locator{
|
||||
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_DEFAULT_HEIGHT},
|
||||
"JN03RO"};
|
||||
Button button_mangps{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||
"Set pos."};
|
||||
|
||||
// Mode selection (file/manual)
|
||||
OptionsField options_mode{
|
||||
{UI_POS_X(7), UI_POS_Y(0)},
|
||||
30,
|
||||
{{"File (BEACONS.TXT)", (uint8_t)BeaconMode::FILE},
|
||||
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
|
||||
|
||||
// Frame content
|
||||
Text text_frame{
|
||||
{UI_POS_X(6), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_frame_end{
|
||||
{UI_POS_X(6), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
Text text_timeout{
|
||||
{UI_POS_X(14), UI_POS_Y(10), UI_POS_WIDTH(2), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
// Transmission settings
|
||||
Checkbox checkbox_loop{
|
||||
{UI_POS_X(0), UI_POS_Y(9)},
|
||||
10,
|
||||
"Resend every",
|
||||
true};
|
||||
NumberField field_delay{
|
||||
{UI_POS_X(15), UI_POS_Y(9)},
|
||||
2,
|
||||
{1, 99},
|
||||
1,
|
||||
' '};
|
||||
Checkbox checkbox_am{
|
||||
{UI_POS_X(0), UI_POS_Y(11)},
|
||||
10,
|
||||
"AM signal",
|
||||
true};
|
||||
FrequencyField field_am_frequency{
|
||||
{UI_POS_X(13), UI_POS_Y(12)}};
|
||||
Checkbox checkbox_send_on_change{
|
||||
{UI_POS_X(0), UI_POS_Y(13)},
|
||||
14,
|
||||
"Send on change",
|
||||
true};
|
||||
Button button_tx{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(9), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||
"START"};
|
||||
const Style& style_tx_start = *Theme::getInstance()->fg_green;
|
||||
const Style& style_tx_stop = *Theme::getInstance()->fg_red;
|
||||
OptionsField options_am_channel{
|
||||
{UI_POS_X(11), UI_POS_Y(14)},
|
||||
20,
|
||||
{{"121.375 MHz (Test)", 0},
|
||||
{"121.500 MHz /!\\Real", 1},
|
||||
{"Manual", 2}}};
|
||||
OptionsField options_bpsk_channel{
|
||||
{UI_POS_X(11), UI_POS_Y(15)},
|
||||
20,
|
||||
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
|
||||
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
|
||||
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
|
||||
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
|
||||
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
|
||||
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
|
||||
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
|
||||
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
|
||||
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
|
||||
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
|
||||
|
||||
// Transmitter view
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
10000,
|
||||
12};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
// Use frame sync for our applcation timer callback
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_tx
|
||||
|
||||
#endif /*__EPIRB_TX_H__*/
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 __EPIRB_TX_H__
|
||||
#define __EPIRB_TX_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "tonesets.hpp"
|
||||
|
||||
#define BEACON_HEXA_SIZE 36
|
||||
#define BEACON_HEXA_HALF_SIZE 18
|
||||
#define BEACON_SIZE 18
|
||||
|
||||
#define AM_TEST_FREQUENCY 121375000
|
||||
#define AM_REAL_FREQUENCY 121500000
|
||||
|
||||
#define BPSK_FREQUENCY_HAM 433025000
|
||||
#define BPSK_FREQUENCY_B 406025000
|
||||
#define BPSK_FREQUENCY_C 406028000
|
||||
#define BPSK_FREQUENCY_F 406037000
|
||||
#define BPSK_FREQUENCY_G 406040000
|
||||
#define BPSK_FREQUENCY_J 406049000
|
||||
#define BPSK_FREQUENCY_K 406052000
|
||||
#define BPSK_FREQUENCY_N 406061000
|
||||
#define BPSK_FREQUENCY_O 406064000
|
||||
|
||||
namespace ui::external_app::epirb_tx {
|
||||
|
||||
enum class BeaconMode {
|
||||
FILE = 0,
|
||||
MODE_MANUAL = 1
|
||||
};
|
||||
|
||||
enum class BeaconType {
|
||||
EPIRB = 0,
|
||||
ELT = 1,
|
||||
PLB = 2
|
||||
};
|
||||
|
||||
enum class BeaconProtocol {
|
||||
USER = 0,
|
||||
STANDARD = 1,
|
||||
NATIONAL = 2
|
||||
};
|
||||
|
||||
enum class AmChannel {
|
||||
TEST = 0,
|
||||
REAL = 1,
|
||||
MANUAL = 2
|
||||
};
|
||||
|
||||
enum class BpskChannel {
|
||||
HAM = 0,
|
||||
B = 1,
|
||||
C = 2,
|
||||
F = 3,
|
||||
G = 4,
|
||||
J = 5,
|
||||
K = 6,
|
||||
N = 7,
|
||||
O = 8,
|
||||
MANUAL = 10
|
||||
};
|
||||
|
||||
struct Location {
|
||||
std::string locator;
|
||||
bool south;
|
||||
int16_t lat_deg;
|
||||
int8_t lat_min;
|
||||
int8_t lat_sec;
|
||||
float latitude;
|
||||
bool west;
|
||||
int16_t long_deg;
|
||||
int8_t long_min;
|
||||
int8_t long_sec;
|
||||
float longitude;
|
||||
};
|
||||
|
||||
struct BeaconParams {
|
||||
BeaconType type;
|
||||
BeaconProtocol protocol;
|
||||
uint32_t country;
|
||||
bool is_test;
|
||||
bool is_internal;
|
||||
bool has_121_5;
|
||||
Location location;
|
||||
};
|
||||
|
||||
class EPIRBTXAppView : public View {
|
||||
public:
|
||||
EPIRBTXAppView(NavigationView& nav);
|
||||
~EPIRBTXAppView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "EPIRB TX"; };
|
||||
|
||||
private:
|
||||
void start_tx();
|
||||
void stop_tx();
|
||||
void update_config();
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void on_timer();
|
||||
void load_beacons();
|
||||
void set_tx_button_state(bool active);
|
||||
std::string frame_to_hex_string(bool start);
|
||||
void generate_frame(BeaconParams params);
|
||||
void update_frame(bool updateConfig = true);
|
||||
void update_bpsk_frequency();
|
||||
void update_am_transmission();
|
||||
void update_mode();
|
||||
void update_location(bool updateLocatorField = true);
|
||||
|
||||
struct Beacon {
|
||||
std::string title{};
|
||||
std::string description{};
|
||||
std::string frame{};
|
||||
};
|
||||
NavigationView& nav_;
|
||||
|
||||
std::vector<Beacon> beacons{};
|
||||
Beacon default_beacon{"Self test", "Serial User Location Protocol", "FFFED0D6E6202820000C29FF51041775302D"};
|
||||
|
||||
BeaconParams beacon_params{BeaconType::ELT, BeaconProtocol::STANDARD, 227, true, true, true, {"JN03RO", false, 0, 0, 0, 0, false, 0, 0, 0, 0}};
|
||||
|
||||
// Currently selected beacon index (from BEACONS.TXT file / options_frame combo)
|
||||
uint32_t selected_beacon{0};
|
||||
|
||||
// Frequency of the transmitter before starting the app (used to restore frequency when leaving)
|
||||
rf::Frequency original_frequency{0};
|
||||
// Frequency of the AM emergency signal
|
||||
rf::Frequency am_frequency{AM_TEST_FREQUENCY};
|
||||
// Frequency of the 406 MHz BPSK signal
|
||||
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||
// Selected am channel
|
||||
uint8_t am_channel{(uint8_t)AmChannel::TEST};
|
||||
// Selected bpsk channel
|
||||
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
|
||||
// Manual AM frequency value
|
||||
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
|
||||
// Manual BPSK frequency value
|
||||
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||
|
||||
// True when using a beacon from the BEACONS.TXT file
|
||||
bool mode_file{false};
|
||||
// True when slideshow is enabled for file mode (change beacon after every transmission)
|
||||
bool slideshow_enabled{false};
|
||||
// True when looping on sending beacons is enabled
|
||||
bool loop_enabled{true};
|
||||
// True if AM emergency signal transmission is enabled
|
||||
bool am_enabled{true};
|
||||
// True if we want to send a new frame each time the user changes the current beacon
|
||||
bool send_on_change{true};
|
||||
// The current locator string
|
||||
std::string locator{"JN03RO"};
|
||||
// The delay between each frame when on loop mode
|
||||
uint32_t delay{50};
|
||||
uint8_t beacon_type{(uint8_t)BeaconType::EPIRB};
|
||||
// Currently selected beacon protocol
|
||||
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
|
||||
// Currently selected beacon country
|
||||
uint32_t beacon_country{227};
|
||||
// Current beacon's internal state (true for internal location system)
|
||||
bool beacon_internal{true};
|
||||
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
TONES_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_epirb",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"sbeacon"sv, &selected_beacon},
|
||||
{"amfreq"sv, &am_frequency},
|
||||
{"bpskfreq"sv, &bpsk_frequency},
|
||||
{"amchan"sv, &am_channel},
|
||||
{"bpskchan"sv, &bpsk_channel},
|
||||
{"loop"sv, &loop_enabled},
|
||||
{"delay"sv, &delay},
|
||||
{"file"sv, &mode_file},
|
||||
{"slideshow"sv, &slideshow_enabled},
|
||||
{"am"sv, &am_enabled},
|
||||
{"soc"sv, &send_on_change},
|
||||
{"type"sv, &beacon_type},
|
||||
{"proto"sv, &beacon_protocol},
|
||||
{"country"sv, &beacon_country},
|
||||
{"internal"sv, &beacon_internal},
|
||||
{"locator"sv, &locator},
|
||||
}};
|
||||
|
||||
// Time of the last sent frame
|
||||
uint32_t last_frame_time{0};
|
||||
// True when transmission is enabled
|
||||
bool transmitting{false};
|
||||
// True when transmitting a BPSK frame
|
||||
bool transmitting_bpsk{false};
|
||||
// True when currently looping on sending beacons
|
||||
bool loop{false};
|
||||
|
||||
// Current EPIRBTXDataMessage for baseband
|
||||
EPIRBTXDataMessage epirb_tx_message{};
|
||||
|
||||
const size_t max_text_width = UI_POS_WIDTH_REMAINING(6) / UI_POS_DEFAULT_WIDTH;
|
||||
const size_t max_text_width_ext = UI_POS_WIDTH_REMAINING(0) / UI_POS_DEFAULT_WIDTH;
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency: MHz", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
// For file mode
|
||||
struct FileModeWidgets {
|
||||
Text text_beacon{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_DEFAULT_HEIGHT},
|
||||
"Beacon:"};
|
||||
// Beacon selection from BEACONS.TXT
|
||||
OptionsField options_frame{
|
||||
{UI_POS_X(7), UI_POS_Y(1)},
|
||||
30,
|
||||
{}};
|
||||
Text text_description_label{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
"Description:"};
|
||||
Text text_description{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_description_end{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Checkbox checkbox_slideshow{
|
||||
{UI_POS_X(0), UI_POS_Y(5)},
|
||||
9,
|
||||
"Slideshow",
|
||||
true};
|
||||
};
|
||||
std::unique_ptr<FileModeWidgets> file_mode_ui{};
|
||||
|
||||
// For manual mode
|
||||
Text text_beacon_type{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Type:"};
|
||||
Text text_beacon_country{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Country:"};
|
||||
Checkbox checkbox_beacon_internal{
|
||||
{UI_POS_X_RIGHT(12), UI_POS_Y(2)},
|
||||
12,
|
||||
"Internal",
|
||||
true};
|
||||
Text text_beacon_locator{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Locator:"};
|
||||
Text text_beacon_latitude{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Lat.:"};
|
||||
Text text_beacon_longitude{
|
||||
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Long.:"};
|
||||
Text text_beacon_latitude_value{
|
||||
{UI_POS_X(7), UI_POS_Y(4), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_beacon_longitude_value{
|
||||
{UI_POS_X(7), UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
OptionsField options_beacon_type{
|
||||
{UI_POS_X(9), UI_POS_Y(1)},
|
||||
7,
|
||||
{{"EPIRB", (uint8_t)BeaconType::EPIRB},
|
||||
{"ELT", (uint8_t)BeaconType::ELT},
|
||||
{"PLB", (uint8_t)BeaconType::PLB}}};
|
||||
OptionsField options_beacon_protocol{
|
||||
{UI_POS_X(9 + 7), UI_POS_Y(1)},
|
||||
30,
|
||||
{{"User", (uint8_t)BeaconProtocol::USER},
|
||||
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
|
||||
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
|
||||
OptionsField options_beacon_country{
|
||||
{UI_POS_X(9), UI_POS_Y(2)},
|
||||
7,
|
||||
{{"France", 227},
|
||||
{"USA", 366},
|
||||
{"Germany", 211},
|
||||
{"Russia", 273},
|
||||
{"Spain", 224},
|
||||
{"Japan", 431},
|
||||
{"UK", 232}}};
|
||||
TextField text_field_beacon_locator{
|
||||
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_DEFAULT_HEIGHT},
|
||||
"JN03RO"};
|
||||
Button button_mangps{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||
"Set pos."};
|
||||
|
||||
// Mode selection (file/manual)
|
||||
OptionsField options_mode{
|
||||
{UI_POS_X(7), UI_POS_Y(0)},
|
||||
30,
|
||||
{{"File (BEACONS.TXT)", (uint8_t)BeaconMode::FILE},
|
||||
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
|
||||
|
||||
// Frame content
|
||||
Text text_frame{
|
||||
{UI_POS_X(6), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_frame_end{
|
||||
{UI_POS_X(6), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
Text text_timeout{
|
||||
{UI_POS_X(14), UI_POS_Y(10), UI_POS_WIDTH(2), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
// Transmission settings
|
||||
Checkbox checkbox_loop{
|
||||
{UI_POS_X(0), UI_POS_Y(9)},
|
||||
10,
|
||||
"Resend every",
|
||||
true};
|
||||
NumberField field_delay{
|
||||
{UI_POS_X(15), UI_POS_Y(9)},
|
||||
2,
|
||||
{1, 99},
|
||||
1,
|
||||
' '};
|
||||
Checkbox checkbox_am{
|
||||
{UI_POS_X(0), UI_POS_Y(11)},
|
||||
10,
|
||||
"AM signal",
|
||||
true};
|
||||
FrequencyField field_am_frequency{
|
||||
{UI_POS_X(13), UI_POS_Y(12)}};
|
||||
Checkbox checkbox_send_on_change{
|
||||
{UI_POS_X(0), UI_POS_Y(13)},
|
||||
14,
|
||||
"Send on change",
|
||||
true};
|
||||
Button button_tx{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(9), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||
"START"};
|
||||
const Style& style_tx_start = *Theme::getInstance()->fg_green;
|
||||
const Style& style_tx_stop = *Theme::getInstance()->fg_red;
|
||||
OptionsField options_am_channel{
|
||||
{UI_POS_X(11), UI_POS_Y(14)},
|
||||
20,
|
||||
{{"121.375 MHz (Test)", 0},
|
||||
{"121.500 MHz /!\\Real", 1},
|
||||
{"Manual", 2}}};
|
||||
OptionsField options_bpsk_channel{
|
||||
{UI_POS_X(11), UI_POS_Y(15)},
|
||||
20,
|
||||
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
|
||||
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
|
||||
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
|
||||
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
|
||||
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
|
||||
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
|
||||
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
|
||||
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
|
||||
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
|
||||
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
|
||||
|
||||
// Transmitter view
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
10000,
|
||||
12};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
// Use frame sync for our applcation timer callback
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_tx
|
||||
|
||||
#endif /*__EPIRB_TX_H__*/
|
||||
|
||||
+10
-1
@@ -263,6 +263,10 @@ set(EXTCPPSRC
|
||||
#epirb_rx 168 byte flash
|
||||
external/epirb_rx/main.cpp
|
||||
external/epirb_rx/ui_epirb_rx.cpp
|
||||
external/epirb_rx/ui_beaconlist.cpp
|
||||
external/epirb_rx/beacon_db.cpp
|
||||
external/epirb_rx/beacon.cpp
|
||||
external/epirb_rx/location.cpp
|
||||
|
||||
#epirb_tx
|
||||
external/epirb_tx/main.cpp
|
||||
@@ -353,7 +357,11 @@ set(EXTCPPSRC
|
||||
|
||||
#two_tone_rx
|
||||
external/two_tone_rx/main.cpp
|
||||
external/two_tone_rx/ui_two_tone_rx.cpp
|
||||
external/two_tone_rx/ui_two_tone_rx.cpp
|
||||
|
||||
#hard_reset
|
||||
external/hard_reset/main.cpp
|
||||
external/hard_reset/ui_hard_reset.cpp
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -442,6 +450,7 @@ set(EXTAPPLIST
|
||||
p25_tx
|
||||
two_tone_pager
|
||||
two_tone_rx
|
||||
hard_reset
|
||||
)
|
||||
|
||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
||||
|
||||
+7
@@ -109,6 +109,7 @@ MEMORY
|
||||
ram_external_app_two_tone_pager (rwx) : org = 0xAE040000, len = 32k
|
||||
ram_external_app_two_tone_rx (rwx) : org = 0xAE050000, len = 32k
|
||||
ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k
|
||||
ram_external_app_hard_reset (rwx) : org = 0xAE070000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -628,4 +629,10 @@ SECTIONS
|
||||
KEEP(*(.external_app.app_flex_tx.application_information));
|
||||
*(*ui*external_app*flex_tx*);
|
||||
} > ram_external_app_flex_tx
|
||||
|
||||
.external_app_hard_reset : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_hard_reset.application_information));
|
||||
*(*ui*external_app*hard_reset*);
|
||||
} > ram_external_app_hard_reset
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* 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 "ui_hard_reset.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::hard_reset {
|
||||
|
||||
void initialize_app(NavigationView& nav) {
|
||||
nav.push<HardResetView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::hard_reset
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_hard_reset.application_information"), used))
|
||||
application_information_t _application_information_hard_reset = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::hard_reset::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "Hard Reset",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xC0,
|
||||
0x01,
|
||||
0xFF,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0x7F,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0xFE,
|
||||
0x3F,
|
||||
0xFE,
|
||||
0x3F,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::red().v,
|
||||
/*.menu_location = */ app_location_t::SETTINGS,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {0, 0, 0, 0},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* 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_hard_reset.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "file.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "ui_touch_calibration.hpp"
|
||||
#include "ui_external_items_menu_loader.hpp"
|
||||
#include "app_settings.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
namespace ui::external_app::hard_reset {
|
||||
HardResetView::HardResetView(ui::NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
add_children({&chk_settings_file,
|
||||
&chk_bad_apps,
|
||||
&chk_pmem,
|
||||
&txt_settings_file_count,
|
||||
&txt_bad_apps_count,
|
||||
&btn_yes,
|
||||
&btn_no,
|
||||
&text,
|
||||
&txt_wait});
|
||||
|
||||
txt_wait.set_style(Theme::getInstance()->warning_dark);
|
||||
|
||||
btn_yes.on_select = [this](Button&) {
|
||||
{ // to free painter after draw
|
||||
Painter painter;
|
||||
txt_wait.hidden(false);
|
||||
text.hidden(true);
|
||||
painter.fill_rectangle(text.screen_rect(), this->style().background);
|
||||
txt_wait.paint(painter);
|
||||
}
|
||||
if (chk_settings_file.value()) {
|
||||
clear_settings_folder();
|
||||
}
|
||||
if (chk_bad_apps.value()) {
|
||||
delete_bad_apps(apps_dir);
|
||||
}
|
||||
if (chk_pmem.value()) {
|
||||
pmem::cache::defaults();
|
||||
StatusRefreshMessage message{};
|
||||
EventDispatcher::send_message(message);
|
||||
nav_.replace<TouchCalibrationView>();
|
||||
} else {
|
||||
nav_.pop();
|
||||
}
|
||||
};
|
||||
|
||||
btn_no.on_select = [this](Button&) {
|
||||
nav_.pop();
|
||||
};
|
||||
txt_settings_file_count.set_style(Theme::getInstance()->fg_magenta);
|
||||
txt_bad_apps_count.set_style(Theme::getInstance()->fg_magenta);
|
||||
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
this->calculate();
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
};
|
||||
}
|
||||
|
||||
HardResetView::~HardResetView() {
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
}
|
||||
|
||||
void HardResetView::calculate() {
|
||||
settings_file_count = count_ini_files_in_directory(settings_dir);
|
||||
txt_settings_file_count.set(to_string_dec_uint(settings_file_count));
|
||||
|
||||
bad_apps_count = count_bad_apps(apps_dir);
|
||||
txt_bad_apps_count.set(to_string_dec_uint(bad_apps_count));
|
||||
|
||||
if (settings_file_count > 0) {
|
||||
chk_settings_file.set_value(true);
|
||||
}
|
||||
if (bad_apps_count > 0) {
|
||||
chk_bad_apps.set_value(true);
|
||||
}
|
||||
chk_pmem.set_value(true);
|
||||
txt_wait.hidden(true);
|
||||
text.set("Warning! Checked items will beerased (bad apps, P.Mem, settings). Uncheck to keep. Touch calibration is required only if P.Mem is reset.");
|
||||
}
|
||||
|
||||
void HardResetView::focus() {
|
||||
btn_no.focus();
|
||||
}
|
||||
|
||||
uint16_t HardResetView::count_ini_files_in_directory(const std::filesystem::path& dir_path) {
|
||||
int count = 0;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir_path, u"*.ini")) {
|
||||
if (std::filesystem::is_regular_file(entry.status())) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void HardResetView::delete_ini_files_in_directory(const std::filesystem::path& dir_path) {
|
||||
bool found_and_deleted;
|
||||
do {
|
||||
found_and_deleted = false;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir_path, u"*.ini")) {
|
||||
if (std::filesystem::is_regular_file(entry.status())) {
|
||||
auto full_path = dir_path / entry.path().filename();
|
||||
delete_file(full_path);
|
||||
found_and_deleted = true;
|
||||
break; // iterator can become invalid if the dir is modified, so break and restart the loop after deletion
|
||||
}
|
||||
}
|
||||
} while (found_and_deleted);
|
||||
}
|
||||
|
||||
void HardResetView::clear_settings_folder() {
|
||||
delete_ini_files_in_directory(settings_dir);
|
||||
}
|
||||
|
||||
bool HardResetView::is_bad_app(const std::filesystem::path& file_path, bool is_ppma) {
|
||||
File app;
|
||||
if (app.open(file_path)) {
|
||||
return true;
|
||||
}
|
||||
bool is_bad = false;
|
||||
if (is_ppma) {
|
||||
application_information_t app_info = {};
|
||||
auto readResult = app.read(&app_info, sizeof(application_information_t));
|
||||
if (!readResult || readResult.value() != sizeof(application_information_t)) {
|
||||
is_bad = true;
|
||||
} else if (app_info.header_version != CURRENT_HEADER_VERSION) {
|
||||
is_bad = true;
|
||||
} else if (VERSION_MD5 != app_info.app_version) {
|
||||
is_bad = true;
|
||||
} else {
|
||||
// --- CHECKSUM VALIDATION ---
|
||||
// Similar to run_external_app: read through the file and calculate the checksum
|
||||
app.seek(0);
|
||||
uint32_t checksum = 0;
|
||||
uint8_t buffer[512]; // 512-byte buffer to conserve RAM
|
||||
|
||||
while (true) {
|
||||
auto res = app.read(buffer, sizeof(buffer));
|
||||
if (!res) break;
|
||||
checksum += simple_checksum((uint32_t)buffer, res.value());
|
||||
if (res.value() < sizeof(buffer)) break; // End of file
|
||||
}
|
||||
|
||||
if (checksum != EXT_APP_EXPECTED_CHECKSUM) {
|
||||
is_bad = true; // The file body is corrupted / truncated!
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// PPMP validation
|
||||
standalone_application_information_t app_info = {};
|
||||
auto readResult = app.read(&app_info, sizeof(standalone_application_information_t));
|
||||
if (!readResult || readResult.value() != sizeof(standalone_application_information_t)) {
|
||||
is_bad = true;
|
||||
} else if (app_info.header_version > CURRENT_STANDALONE_APPLICATION_API_VERSION) {
|
||||
is_bad = true;
|
||||
}
|
||||
}
|
||||
|
||||
app.close();
|
||||
return is_bad;
|
||||
}
|
||||
|
||||
uint16_t HardResetView::count_bad_apps(const std::filesystem::path& apps_dir) {
|
||||
int bad_count = 0;
|
||||
|
||||
// Check .ppma files
|
||||
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppma")) {
|
||||
auto file_path = apps_dir / entry.path();
|
||||
if (is_bad_app(file_path, true)) {
|
||||
bad_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check .ppmp files
|
||||
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppmp")) {
|
||||
auto file_path = apps_dir / entry.path();
|
||||
if (is_bad_app(file_path, false)) {
|
||||
bad_count++;
|
||||
}
|
||||
}
|
||||
|
||||
return bad_count;
|
||||
}
|
||||
|
||||
void HardResetView::delete_bad_apps(const std::filesystem::path& apps_dir) {
|
||||
// Delete bad .ppma files
|
||||
bool deleted_something;
|
||||
do {
|
||||
deleted_something = false;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppma")) {
|
||||
auto file_path = apps_dir / entry.path();
|
||||
if (is_bad_app(file_path, true)) {
|
||||
delete_file(file_path);
|
||||
deleted_something = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (deleted_something);
|
||||
|
||||
// Delete bad .ppmp files
|
||||
do {
|
||||
deleted_something = false;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppmp")) {
|
||||
auto file_path = apps_dir / entry.path();
|
||||
if (is_bad_app(file_path, false)) {
|
||||
delete_file(file_path);
|
||||
deleted_something = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (deleted_something);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::hard_reset
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* 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 __UI_HARD_RESET_H__
|
||||
#define __UI_HARD_RESET_H__
|
||||
|
||||
#include "ui_navigation.hpp"
|
||||
#include "signal.hpp"
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::hard_reset {
|
||||
|
||||
class HardResetView : public ui::View {
|
||||
public:
|
||||
HardResetView(ui::NavigationView& nav);
|
||||
~HardResetView();
|
||||
|
||||
std::string title() const override { return "Hard Reset"; }
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
ui::NavigationView& nav_;
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
ui::Button btn_yes{{UI_POS_X(1), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(2)}, "Erase sel."};
|
||||
ui::Button btn_no{{UI_POS_X_RIGHT(11), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)}, "No"};
|
||||
ui::Text text{{UI_POS_X(0), UI_POS_Y(6), UI_POS_MAXWIDTH, UI_POS_HEIGHT(5)}, ""};
|
||||
Checkbox chk_settings_file{{UI_POS_X(1), UI_POS_Y(1)}, 14, "Settings file:", true};
|
||||
Checkbox chk_bad_apps{{UI_POS_X(1), UI_POS_Y(2.5)}, 9, "Bad apps:", true};
|
||||
Checkbox chk_pmem{{UI_POS_X(1), UI_POS_Y(4)}, 11, "P.mem reset", true};
|
||||
|
||||
Text txt_settings_file_count{{UI_POS_X(19), UI_POS_Y(1), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "?"};
|
||||
Text txt_bad_apps_count{{UI_POS_X(14), UI_POS_Y(2.5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "?"};
|
||||
|
||||
Text txt_wait{{UI_POS_X_CENTER(14), UI_POS_Y(8), UI_POS_WIDTH(14), UI_POS_HEIGHT(1)}, "Please wait..."};
|
||||
|
||||
uint16_t count_ini_files_in_directory(const std::filesystem::path& dir_path);
|
||||
void delete_ini_files_in_directory(const std::filesystem::path& dir_path);
|
||||
void clear_settings_folder();
|
||||
void calculate();
|
||||
bool is_bad_app(const std::filesystem::path& file_path, bool is_ppma);
|
||||
uint16_t count_bad_apps(const std::filesystem::path& apps_dir);
|
||||
void delete_bad_apps(const std::filesystem::path& apps_dir);
|
||||
|
||||
uint16_t settings_file_count = 0;
|
||||
uint16_t bad_apps_count = 0;
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::hard_reset
|
||||
|
||||
#endif // __UI_HARD_RESET_H__
|
||||
@@ -50,7 +50,7 @@ using namespace hackrf::one;
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "hal.h" // For LPC_SGPIO
|
||||
|
||||
#include <array>
|
||||
@@ -413,6 +413,19 @@ void set_rx_max283x_iq_phase_calibration(const size_t v) {
|
||||
}
|
||||
|
||||
void disable() {
|
||||
if (direction == rf::Direction::Transmit && baseband::is_image_running()) {
|
||||
static constexpr uint32_t radio_tx_drain_timeout_ms = 100;
|
||||
shared_memory.radio_tx_drain = 1; // Request drain of the current DMA queue.
|
||||
for (uint32_t waited_ms = 0;
|
||||
shared_memory.radio_tx_drain && (waited_ms < radio_tx_drain_timeout_ms);
|
||||
++waited_ms) {
|
||||
chThdSleepMilliseconds(1);
|
||||
}
|
||||
}
|
||||
/* Never allow shutdown to block indefinitely waiting for a drain
|
||||
* acknowledgement that may never arrive in normal operation. */
|
||||
shared_memory.radio_tx_drain = 0;
|
||||
|
||||
set_antenna_bias(false);
|
||||
baseband_codec.set_mode(max5864::Mode::Shutdown);
|
||||
#ifdef PRALINE
|
||||
|
||||
@@ -20,9 +20,31 @@
|
||||
*/
|
||||
|
||||
#include "usb_serial_cdc.h"
|
||||
#include "usb_serial_descriptor.h"
|
||||
#include "usb_serial_endpoints.h"
|
||||
#include "usb_serial_event.hpp"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* Defined in hackrf/firmware/hackrf_usb/usb_device.c. After upstream commit
|
||||
* 85dfacf6 ("Universalize: firmware/hackrf_usb"), the global `usb_device` is
|
||||
* left zero-initialized and is populated at runtime from per-board
|
||||
* `usb_device_*` constants inside hackrf_usb.c. Mayhem does not link
|
||||
* hackrf_usb.c, so we must populate `usb_device` ourselves before usb_run().
|
||||
* The descriptor fields are const-qualified, so we follow the upstream
|
||||
* pattern of memcpy from a const template. */
|
||||
extern usb_configuration_t* usb_configurations[];
|
||||
|
||||
static const usb_device_t usb_device_mayhem = {
|
||||
.descriptor = usb_descriptor_device,
|
||||
.descriptor_strings = usb_descriptor_strings,
|
||||
.qualifier_descriptor = usb_descriptor_device_qualifier,
|
||||
.configurations = &usb_configurations,
|
||||
.configuration = 0,
|
||||
.wcid_string_descriptor = wcid_string_descriptor,
|
||||
.wcid_feature_descriptor = wcid_feature_descriptor,
|
||||
};
|
||||
|
||||
uint32_t EVT_MASK_USB = EVENT_MASK(8);
|
||||
|
||||
extern void usb0_isr(void);
|
||||
@@ -85,6 +107,8 @@ void usb_configuration_changed(usb_device_t* const device) {
|
||||
}
|
||||
|
||||
void setup_usb_serial_controller(void) {
|
||||
memcpy(&usb_device, &usb_device_mayhem, sizeof(usb_device_mayhem));
|
||||
|
||||
usb_set_configuration_changed_cb(usb_configuration_changed);
|
||||
usb_peripheral_reset();
|
||||
|
||||
@@ -97,7 +121,10 @@ void setup_usb_serial_controller(void) {
|
||||
usb_queue_init(&usb_endpoint_bulk_in_queue);
|
||||
|
||||
usb_endpoint_init(&usb_endpoint_control_out, false);
|
||||
usb_endpoint_init(&usb_endpoint_control_in, false);
|
||||
/* Control IN needs ZLP for descriptor reads whose length is a multiple of
|
||||
* the max packet size (otherwise the host hangs waiting for the transfer
|
||||
* to terminate). Matches upstream hackrf_usb.c after commit db73ecbf. */
|
||||
usb_endpoint_init(&usb_endpoint_control_in, true);
|
||||
|
||||
usb_run(&usb_device);
|
||||
}
|
||||
|
||||
@@ -217,7 +217,25 @@ set(CPPWARN "-Wall -Wextra")
|
||||
if(NOT DEFINED BOARD)
|
||||
set(BOARD "HACKRF_ONE")
|
||||
endif()
|
||||
set(DDEFS "-DLPC43XX -DLPC43XX_M4 -D__NEWLIB__ -D${BOARD} -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"'")
|
||||
|
||||
# Upstream hackrf headers now gate on IS_* macros normally populated by
|
||||
# hackrf/firmware/platform-detect.cmake. We don't include that here, so derive
|
||||
# the IS_* set from BOARD ourselves. IS_H1_R9 is forced to 0 (compile-time
|
||||
# constant) so `if (IS_H1_R9)` branches resolve without needing the runtime
|
||||
# detected_platform() symbol, which mayhem doesn't link in.
|
||||
if(BOARD STREQUAL "HACKRF_ONE")
|
||||
set(BOARD_IS_DEFS "-DIS_HACKRF_ONE=1 -DIS_NOT_PRALINE=1 -DIS_H1_R9=0 -DIS_NOT_H1_R9=1 -DIS_NOT_RAD1O=1 -DIS_NOT_JAWBREAKER=1 -DIS_H1_OR_PRALINE=1 -DIS_H1_OR_RAD1O=1 -DIS_H1_OR_JAWBREAKER=1 -DIS_EXPANSION_COMPATIBLE=1")
|
||||
elseif(BOARD STREQUAL "PRALINE")
|
||||
set(BOARD_IS_DEFS "-DIS_PRALINE=1 -DIS_NOT_HACKRF_ONE=1 -DIS_NOT_H1_R9=1 -DIS_NOT_RAD1O=1 -DIS_NOT_JAWBREAKER=1 -DIS_H1_OR_PRALINE=1 -DIS_FOUR_LEDS=1 -DIS_EXPANSION_COMPATIBLE=1")
|
||||
elseif(BOARD STREQUAL "RAD1O")
|
||||
set(BOARD_IS_DEFS "-DIS_RAD1O=1 -DIS_NOT_PRALINE=1 -DIS_NOT_HACKRF_ONE=1 -DIS_NOT_H1_R9=1 -DIS_NOT_JAWBREAKER=1 -DIS_H1_OR_RAD1O=1 -DIS_FOUR_LEDS=1")
|
||||
elseif(BOARD STREQUAL "JAWBREAKER")
|
||||
set(BOARD_IS_DEFS "-DIS_JAWBREAKER=1 -DIS_NOT_PRALINE=1 -DIS_NOT_HACKRF_ONE=1 -DIS_NOT_H1_R9=1 -DIS_NOT_RAD1O=1 -DIS_H1_OR_JAWBREAKER=1")
|
||||
else()
|
||||
set(BOARD_IS_DEFS "")
|
||||
endif()
|
||||
|
||||
set(DDEFS "-DLPC43XX -DLPC43XX_M4 -D__NEWLIB__ -D${BOARD} ${BOARD_IS_DEFS} -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"'")
|
||||
|
||||
# List all default ASM defines here, like -D_DEBUG=1
|
||||
set(DADEFS)
|
||||
@@ -765,7 +783,6 @@ set(MODE_CPPSRC
|
||||
${HACKRF_PATH}/firmware/common/si5351c.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_bus.c
|
||||
${HACKRF_PATH}/firmware/common/mixer.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/spi_bus.c
|
||||
${HACKRF_PATH}/firmware/common/sgpio.c
|
||||
${HACKRF_PATH}/firmware/common/rf_path.c
|
||||
@@ -773,7 +790,6 @@ set(MODE_CPPSRC
|
||||
${HACKRF_PATH}/firmware/common/spi_ssp.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071_spi.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/gpdma.c
|
||||
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c
|
||||
|
||||
@@ -90,7 +90,7 @@ void BasebandThread::run() {
|
||||
#ifdef PRALINE
|
||||
shared_memory.m4_streaming_marker = 0xAA; // Phase 0 instrumentation
|
||||
#endif
|
||||
|
||||
uint8_t buffer_drained = 0; // to count how many dma buffers we already emptied
|
||||
while (!chThdShouldTerminate()) {
|
||||
#ifdef PRALINE
|
||||
shared_memory.m4_baseband_loops++; // Phase 0 instrumentation
|
||||
@@ -117,11 +117,20 @@ void BasebandThread::run() {
|
||||
}
|
||||
|
||||
if (baseband_processor_) {
|
||||
baseband_processor_->execute(buffer);
|
||||
if (shared_memory.radio_tx_drain == 0) { // only generate if not draining.
|
||||
baseband_processor_->execute(buffer);
|
||||
}
|
||||
}
|
||||
if (shared_memory.radio_tx_drain == 1) {
|
||||
buffer_drained++;
|
||||
if (buffer_drained >= 4) { // We have 4 buffers, so after draining 4 we should be safe to disable.
|
||||
shared_memory.radio_tx_drain = 0; // Clear the drain request to allow normal operation to resume.
|
||||
buffer_drained = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_memory.radio_tx_drain = 0;
|
||||
i2s::i2s0::tx_mute();
|
||||
baseband::dma::disable();
|
||||
baseband_sgpio.streaming_disable();
|
||||
|
||||
@@ -1,96 +1,272 @@
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||
*
|
||||
* 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 "proc_epirb.hpp"
|
||||
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "dsp_fir_taps.hpp"
|
||||
|
||||
#include "event_m4.hpp"
|
||||
#include <ch.h>
|
||||
|
||||
EPIRBProcessor::EPIRBProcessor() {
|
||||
// Configure the decimation filters for narrowband EPIRB signal
|
||||
// Target: Reduce 2.457600 MHz to ~38.4 kHz for 400 bps processing
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
||||
/* 2.4576MHz, 2048 samples */
|
||||
|
||||
// First decimation stage: 2.4576 MHz -> 307.2 kHz
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
|
||||
// Second decimation stage: 307.2 kHz -> 38.4 kHz
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
const auto decimator_out = decim_1_out;
|
||||
|
||||
/* 38.4kHz, 32 samples (approximately) */
|
||||
feed_channel_stats(decimator_out);
|
||||
|
||||
// Process each decimated sample through the matched filter
|
||||
for (size_t i = 0; i < decimator_out.count; i++) {
|
||||
// Apply matched filter for BPSK demodulation
|
||||
if (mf.execute_once(decimator_out.p[i])) {
|
||||
// Feed symbol to clock recovery when matched filter triggers
|
||||
clock_recovery(mf.get_output());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBProcessor::consume_symbol(const float raw_symbol) {
|
||||
// BPSK demodulation: positive = 1, negative = 0
|
||||
const uint_fast8_t sliced_symbol = (raw_symbol >= 0.0f) ? 1 : 0;
|
||||
|
||||
// Decode bi-phase L encoding manually
|
||||
// In bi-phase L: 0 = no transition, 1 = transition
|
||||
// This is a simple edge detector
|
||||
const auto decoded_symbol = sliced_symbol ^ last_symbol;
|
||||
last_symbol = sliced_symbol;
|
||||
|
||||
// Build packet from decoded symbols
|
||||
packet_builder.execute(decoded_symbol);
|
||||
}
|
||||
|
||||
void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
|
||||
// EPIRB packet received - validate and process
|
||||
if (packet.size() >= 112) { // Minimum EPIRB data payload size (112 bits)
|
||||
packets_received++;
|
||||
last_packet_timestamp = Timestamp::now();
|
||||
|
||||
// Create and send EPIRB packet message to application layer
|
||||
const EPIRBPacketMessage message{packet};
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBProcessor::on_message(const Message* const message) {
|
||||
(void)message; // Unused in this processor
|
||||
}
|
||||
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<EPIRBProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 "proc_epirb.hpp"
|
||||
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "dsp_fir_taps.hpp"
|
||||
|
||||
#include "audio_dma.hpp"
|
||||
|
||||
#include "event_m4.hpp"
|
||||
#include <ch.h>
|
||||
|
||||
EPIRBProcessor::EPIRBProcessor() {
|
||||
// Configure the decimation filters for narrowband EPIRB signal
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
// Configure channel filter for audio filtering
|
||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||
// Configure demodulation for audio output
|
||||
demod.configure(SAMPLE_RATE, 5000);
|
||||
// Configure audio output (+squelch level)
|
||||
configure_audio();
|
||||
#ifdef SPECAN
|
||||
channel_spectrum.set_decimation_factor(1);
|
||||
#endif
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
void EPIRBProcessor::configure_audio() {
|
||||
// UI sends an squelch value ranging from 0 to 99, 0 disables squelch, dividing UI value by 40 gives a valid UI threashold around 50
|
||||
audio_output.configure(audio_24k_hpf_300hz_config, audio_24k_deemph_300_6_config, ((float)squelch_level) / 40.0f);
|
||||
}
|
||||
|
||||
float EPIRBProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
|
||||
// Calculate the phase difference between two samples
|
||||
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
|
||||
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
|
||||
float phase_diff = atan2f(dQ, dI);
|
||||
// Prevent phase diff from wrapping around
|
||||
if (phase_diff > M_PI) phase_diff -= 2.0f * M_PI;
|
||||
if (phase_diff < -M_PI) phase_diff += 2.0f * M_PI;
|
||||
return phase_diff;
|
||||
}
|
||||
|
||||
bool EPIRBProcessor::filtered_rise_detect(bool condition) {
|
||||
bool result = false;
|
||||
if (condition) {
|
||||
// If rise condition is matched, filter peaks that last less than 3 samples
|
||||
rise_detection_count++;
|
||||
if (rise_detection_count >= RISE_FILTER_SAMPLES) {
|
||||
result = true;
|
||||
rise_detection_count = 0;
|
||||
}
|
||||
} else {
|
||||
rise_detection_count = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
||||
// First decimation stage: 3.072000 MHz / 8 -> 384 kHz
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
|
||||
// Second decimation stage: 384 kHz / 8 -> 48 kHz
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
// We use decim1 output as decimator output
|
||||
const auto decimator_out = decim_1_out;
|
||||
|
||||
#ifdef SPECAN
|
||||
// Feed IQ data into spectrum collector for the RF waterfall.
|
||||
if (spectrum_on) channel_spectrum.feed(decim_1_out, -5500, 5500, 3400);
|
||||
#endif
|
||||
|
||||
feed_channel_stats(decimator_out);
|
||||
|
||||
// if (audio_on) {
|
||||
// Channel filter for audio out
|
||||
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||
auto audio = demod.execute(channel_out, audio_buffer);
|
||||
audio_output.write(audio);
|
||||
//}
|
||||
|
||||
// Process each decimated sample through state machine
|
||||
for (size_t i = 0; i < decimator_out.count; i++) {
|
||||
// Track sample count since last symbol and since begining of the frame
|
||||
sample_count++;
|
||||
frame_sample_count++;
|
||||
// Compute phase delta since last sample
|
||||
float phase_delta = get_phase_diff(last_sample, decimator_out.p[i]);
|
||||
last_sample = decimator_out.p[i];
|
||||
|
||||
// Let's sum phase delta over a 12 sample window to get the full phase jump
|
||||
phase_delta_acc -= phase_delta_buffer[pahse_delta_index];
|
||||
phase_delta_buffer[pahse_delta_index] = phase_delta;
|
||||
phase_delta_acc += phase_delta_buffer[pahse_delta_index];
|
||||
pahse_delta_index = (pahse_delta_index + 1) % PHASE_DELTA_ACC_SIZE;
|
||||
|
||||
// Use accumulated delta
|
||||
phase_delta = phase_delta_acc;
|
||||
|
||||
// State machine for COSPAS frame detection
|
||||
switch (current_state) {
|
||||
case IDLE:
|
||||
// We are waiting for a 160ms empty carrier => phase shouls be stable during this period
|
||||
// We accept a 0.6 phase shift since phase may drift durring carrier if carrier frequency is not alligned with tuner frequency
|
||||
if (filtered_rise_detect(phase_delta >= 0.6f)) {
|
||||
stability_counter = 0;
|
||||
} else {
|
||||
stability_counter++;
|
||||
if (stability_counter > CARRIER_SAMPLES_THRESHOLD) {
|
||||
// Carrier has been stable long enought, go to locked state
|
||||
current_state = CARRIER_LOCKED;
|
||||
frame_sample_count = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CARRIER_LOCKED:
|
||||
// Carrier is locked, we now wait for a phase 1.1 rad phase jump corresponding to the befining of the frame
|
||||
// Let's use a 0.7 phase jump threshold
|
||||
if (filtered_rise_detect(phase_delta >= 0.7f)) {
|
||||
// Jump detected, frame starts now
|
||||
frame_sample_count = 0;
|
||||
// Go to data sync state
|
||||
current_state = DATA_SYNC;
|
||||
// Frame should always start with a positive phase shift
|
||||
last_phase_positive = true;
|
||||
// And a 1 value
|
||||
last_bit = true;
|
||||
} else if (frame_sample_count > CARRIER_MAX_SAMPLES) {
|
||||
// We missed sync pattern
|
||||
frame_end();
|
||||
}
|
||||
break;
|
||||
|
||||
case DATA_SYNC: {
|
||||
float abs_phase_delta = fabsf(phase_delta);
|
||||
|
||||
if (abs_phase_delta >= 1.6f) {
|
||||
// Phase should jump from 1.1 rad to -1.1 rad or the other way around
|
||||
// Absolute phase jump is expected to be 2.2 rad
|
||||
// Phase jump is either positive or negative
|
||||
bool phase_positive = (phase_delta >= 0.0f);
|
||||
|
||||
if (phase_positive != last_phase_positive) {
|
||||
// Phase jumped to the opposit direction of last jump
|
||||
last_phase_positive = phase_positive;
|
||||
bool cur_bit;
|
||||
// Phase change => how long since last change ?
|
||||
if ((frame_sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (frame_sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
||||
// Frame start
|
||||
if (!phase_positive) {
|
||||
// Symbol detection is made on falling edge
|
||||
cur_bit = true;
|
||||
} else {
|
||||
// Ignore rising edge
|
||||
continue;
|
||||
}
|
||||
} else if (sample_count > (SAMPLES_PER_SYMBOL * 2 + SAMPLES_MARGIN)) {
|
||||
// We missed something...
|
||||
// Let's keep same value for current bit
|
||||
cur_bit = last_bit;
|
||||
} else if (sample_count >= (SAMPLES_PER_SYMBOL * 2 - SAMPLES_MARGIN)) {
|
||||
// 2 symbols since last change => bit value changes
|
||||
cur_bit = !last_bit;
|
||||
} else if ((sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
||||
// Phase change occured in first half bit => we keep the same value
|
||||
if ((phase_positive && last_bit) || (!phase_positive && !last_bit)) {
|
||||
sample_count = 0;
|
||||
// Ignore rising edge if current value is 1 and falling edge if current value is 0 and move to next symbol
|
||||
continue;
|
||||
}
|
||||
// Same value on falling/rising edge
|
||||
cur_bit = last_bit;
|
||||
} else {
|
||||
// Filter the rest
|
||||
continue;
|
||||
}
|
||||
// Store new bit and move to next symbol
|
||||
sample_count = 0;
|
||||
packet_builder.execute(cur_bit);
|
||||
last_bit = cur_bit;
|
||||
}
|
||||
}
|
||||
if (frame_sample_count > FRAME_MAX_SAMPLES) {
|
||||
// End of frame
|
||||
current_state = POST_FRAME;
|
||||
packet_builder.flush();
|
||||
}
|
||||
} break;
|
||||
case POST_FRAME:
|
||||
if (frame_sample_count > CARRIER_MAX_SAMPLES) {
|
||||
// End of carrier
|
||||
frame_end();
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBProcessor::frame_end() {
|
||||
sample_count = 0;
|
||||
frame_sample_count = 0;
|
||||
stability_counter = 0;
|
||||
last_phase_positive = false;
|
||||
last_bit = false;
|
||||
current_state = IDLE;
|
||||
packet_builder.reset_state();
|
||||
}
|
||||
|
||||
void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
|
||||
// EPIRB packet received: create and send EPIRB packet message to application layer
|
||||
const EPIRBPacketMessage message{packet};
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
|
||||
void EPIRBProcessor::on_message(const Message* const msg) {
|
||||
// Configure the processor
|
||||
switch (msg->id) {
|
||||
#ifdef SPECAN
|
||||
case Message::ID::UpdateSpectrum:
|
||||
case Message::ID::SpectrumStreamingConfig:
|
||||
channel_spectrum.on_message(msg);
|
||||
break;
|
||||
#endif
|
||||
case Message::ID::EPIRBRXConfig: {
|
||||
const EPIRBRXConfig message = *reinterpret_cast<const EPIRBRXConfig*>(msg);
|
||||
// audio_on = message.audio_on;
|
||||
#ifdef SPECAN
|
||||
spectrum_on = message.spectrum_on;
|
||||
#endif
|
||||
if (message.squelch != squelch_level) {
|
||||
// Update squelch config
|
||||
squelch_level = message.squelch;
|
||||
configure_audio();
|
||||
}
|
||||
} break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
audio::dma::init_audio_out();
|
||||
|
||||
EventDispatcher event_dispatcher{std::make_unique<EPIRBProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
+267
-134
@@ -1,135 +1,268 @@
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||
*
|
||||
* 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 __PROC_EPIRB_H__
|
||||
#define __PROC_EPIRB_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <complex>
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
#include "channel_decimator.hpp"
|
||||
#include "matched_filter.hpp"
|
||||
#include "clock_recovery.hpp"
|
||||
#include "symbol_coding.hpp"
|
||||
#include "packet_builder.hpp"
|
||||
#include "baseband_packet.hpp"
|
||||
#include "message.hpp"
|
||||
#include "buffer.hpp"
|
||||
|
||||
// Forward declarations for types only used as pointers/references
|
||||
class Message;
|
||||
namespace baseband {
|
||||
class Packet;
|
||||
}
|
||||
|
||||
// EPIRB 406 MHz Emergency Position Indicating Radio Beacon
|
||||
// Signal characteristics:
|
||||
// - Frequency: 406.025 - 406.028 MHz (typically 406.028 MHz)
|
||||
// - Modulation: BPSK (Binary Phase Shift Keying)
|
||||
// - Data rate: 400 bps
|
||||
// - Encoding: Bi-phase L (Manchester)
|
||||
// - Transmission: Every 50 seconds ± 2.5 seconds
|
||||
// - Power: 5W ± 2dB
|
||||
// - Message length: 144 bits (including sync pattern)
|
||||
|
||||
// Matched filter for BPSK demodulation at 400 bps
|
||||
// Using raised cosine filter taps optimized for 400 bps BPSK
|
||||
static constexpr std::array<std::complex<float>, 64> bpsk_taps = {{// Raised cosine filter coefficients for BPSK 400 bps
|
||||
-5, -8, -12, -15, -17, -17, -15, -11,
|
||||
-5, 2, 11, 20, 29, 37, 43, 47,
|
||||
48, 46, 42, 35, 26, 16, 4, -8,
|
||||
-21, -33, -44, -53, -59, -62, -62, -58,
|
||||
-51, -41, -28, -13, 3, 19, 36, 51,
|
||||
64, 74, 80, 82, 80, 74, 64, 51,
|
||||
36, 19, 3, -13, -28, -41, -51, -58,
|
||||
-62, -62, -59, -53, -44, -33, -21, -8}};
|
||||
|
||||
class EPIRBProcessor : public BasebandProcessor {
|
||||
public:
|
||||
EPIRBProcessor();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
// EPIRB operates at 406 MHz with narrow bandwidth
|
||||
static constexpr size_t baseband_fs = 2457600;
|
||||
static constexpr uint32_t epirb_center_freq = 406028000; // 406.028 MHz
|
||||
static constexpr uint32_t symbol_rate = 400; // 400 bps
|
||||
static constexpr size_t decimation_factor = 64; // Decimate to ~38.4kHz
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
// Decimation chain for 406 MHz EPIRB signal processing
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
|
||||
dsp::matched_filter::MatchedFilter mf{bpsk_taps, 2};
|
||||
|
||||
// Clock recovery for 400 bps symbol rate
|
||||
// Sampling rate after decimation: ~38.4kHz
|
||||
// Symbols per sample: 38400 / 400 = 96 samples per symbol
|
||||
clock_recovery::ClockRecovery<clock_recovery::FixedErrorFilter> clock_recovery{
|
||||
38400, // sampling_rate
|
||||
400, // symbol_rate (400 bps)
|
||||
{0.0555f}, // error_filter coefficient
|
||||
[this](const float symbol) { this->consume_symbol(symbol); }};
|
||||
|
||||
// Simple bi-phase L decoder state
|
||||
uint_fast8_t last_symbol = 0;
|
||||
|
||||
// EPIRB packet structure:
|
||||
// - Sync pattern: 000101010101... (15 bits)
|
||||
// - Frame sync: 0111110 (7 bits)
|
||||
// - Data: 112 bits
|
||||
// - BCH error correction: 10 bits
|
||||
// Total: 144 bits
|
||||
PacketBuilder<BitPattern, BitPattern, BitPattern> packet_builder{
|
||||
{0b010101010101010, 15, 1}, // Preamble pattern
|
||||
{0b0111110, 7}, // Frame sync pattern
|
||||
{0b0111110, 7}, // End pattern (same as sync for simplicity)
|
||||
[this](const baseband::Packet& packet) {
|
||||
this->payload_handler(packet);
|
||||
}};
|
||||
|
||||
void consume_symbol(const float symbol);
|
||||
void payload_handler(const baseband::Packet& packet);
|
||||
|
||||
// Statistics
|
||||
uint32_t packets_received = 0;
|
||||
Timestamp last_packet_timestamp{};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* 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 __PROC_EPIRB_H__
|
||||
#define __PROC_EPIRB_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <complex>
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
#include "channel_decimator.hpp"
|
||||
#include "matched_filter.hpp"
|
||||
#include "packet_builder.hpp"
|
||||
#include "baseband_packet.hpp"
|
||||
#include "message.hpp"
|
||||
#include "buffer.hpp"
|
||||
|
||||
// Specan is disable to keep application size below the 32k limit
|
||||
// #define SPECAN
|
||||
|
||||
#ifdef SPECAN
|
||||
#include "spectrum_collector.hpp"
|
||||
#endif
|
||||
|
||||
#include "audio_output.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
|
||||
// Forward declarations for types only used as pointers/references
|
||||
class Message;
|
||||
namespace baseband {
|
||||
class Packet;
|
||||
}
|
||||
|
||||
// COSPAS / SARSAT 406 frame constants
|
||||
// Size of preamble (bits)
|
||||
#define COSPAS_PREAMBLE_SIZE 24
|
||||
// Size of long frame (bits)
|
||||
#define COSPAS_LONG_FRAME_SIZE 144
|
||||
// Siz of short frame (bits)
|
||||
#define COSPAS_SHORT_FRAME_SIZE 112
|
||||
// Preamble for real frames
|
||||
#define COSPAS_REAL_PREAMBLE 0b1111'1111'1111'1110'0010'1111
|
||||
// Preable for test frames
|
||||
#define COSPAS_TEST_PREAMBLE 0b1111'1111'1111'1110'1101'0000
|
||||
|
||||
// Dedicated EPIRB PacketBuilder
|
||||
// Usees diedicated preamble detection logic to find both real and test frames
|
||||
// Also as a dedicated packet size detection based on frame's size bit
|
||||
class EPIRBPacketBuilder {
|
||||
public:
|
||||
using EPIRBHandler = void (*)(void* context, const baseband::Packet& packet);
|
||||
|
||||
EPIRBPacketBuilder(
|
||||
void* context,
|
||||
EPIRBHandler handler)
|
||||
: context(context),
|
||||
handler(handler) {
|
||||
}
|
||||
|
||||
void execute(
|
||||
const uint_fast8_t symbol) {
|
||||
bit_history.add(symbol);
|
||||
|
||||
switch (state) {
|
||||
case State::Preamble: {
|
||||
// Detect both real and test fram preambles
|
||||
bool is_real = real_sync_matcher(bit_history, packet.size());
|
||||
bool is_test = test_sync_matcher(bit_history, packet.size());
|
||||
if (is_real || is_test) {
|
||||
// Append preamble to the begining of the packet
|
||||
uint64_t preamble = is_real ? COSPAS_REAL_PREAMBLE : COSPAS_TEST_PREAMBLE;
|
||||
for (int8_t i = (COSPAS_PREAMBLE_SIZE - 1); i >= 0; i--) {
|
||||
packet.add((preamble >> i) & 0x1);
|
||||
}
|
||||
state = State::Format;
|
||||
}
|
||||
} break;
|
||||
case State::Format:
|
||||
packet.add(symbol);
|
||||
// 144 bits for long frames and 112 for short frames
|
||||
size = symbol ? COSPAS_LONG_FRAME_SIZE : COSPAS_SHORT_FRAME_SIZE;
|
||||
state = State::Payload;
|
||||
|
||||
break;
|
||||
case State::Payload:
|
||||
packet.add(symbol);
|
||||
|
||||
if (packet.size() >= size) {
|
||||
flush();
|
||||
} else {
|
||||
if (packet.size() >= packet.capacity()) {
|
||||
reset_state();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
reset_state();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void flush() {
|
||||
// Timestamp is not set here to save some app space (not used on ui side)
|
||||
// packet.set_timestamp(Timestamp::now());
|
||||
if (handler) handler(context, packet);
|
||||
reset_state();
|
||||
}
|
||||
|
||||
void reset_state() {
|
||||
packet.clear();
|
||||
bit_history = BitHistory();
|
||||
state = State::Preamble;
|
||||
}
|
||||
|
||||
private:
|
||||
enum State {
|
||||
Preamble,
|
||||
Format,
|
||||
Payload,
|
||||
};
|
||||
|
||||
BitHistory bit_history{};
|
||||
BitPattern real_sync_matcher{COSPAS_REAL_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||
BitPattern test_sync_matcher{COSPAS_TEST_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||
void* context;
|
||||
EPIRBHandler handler;
|
||||
uint8_t size{0};
|
||||
|
||||
State state{State::Preamble};
|
||||
baseband::Packet packet{};
|
||||
};
|
||||
|
||||
class EPIRBProcessor : public BasebandProcessor {
|
||||
public:
|
||||
EPIRBProcessor();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
// Baseband frequency is set to 3,072,000 samples / sec
|
||||
static constexpr uint32_t BASEBAND_SAMPLE_RATE = 3072000;
|
||||
static constexpr uint32_t SAMPLE_RATE = BASEBAND_SAMPLE_RATE / 8 / 8; // We use to decimators with factor 8 each
|
||||
static constexpr uint32_t SYMBOL_RATE = 800; // 400 bps + Manchester (2 1/2 bits per symbol) => 800
|
||||
static constexpr size_t SAMPLES_PER_SYMBOL = SAMPLE_RATE / SYMBOL_RATE; // = 60 samples per symbol
|
||||
static constexpr size_t SAMPLES_PER_BIT = SAMPLES_PER_SYMBOL * 2; // = 120 samples per bit
|
||||
static constexpr size_t SAMPLES_MARGIN = SAMPLES_PER_SYMBOL / 3; // = Allow 20 sample drift
|
||||
static constexpr size_t SAMPLES_ACCUMUMLATOR = SAMPLES_PER_SYMBOL / 5; // Accumulate phase change across 12 samples
|
||||
static constexpr size_t RISE_FILTER_SAMPLES = SAMPLES_PER_SYMBOL / 20; // Filter peaks of less than 3 samples
|
||||
|
||||
static constexpr size_t CARRIER_SAMPLES_THRESHOLD = 0.080f * SAMPLE_RATE; // Carrier before frame lasts 160ms, require at least 80ms
|
||||
static constexpr size_t CARRIER_MAX_SAMPLES = 0.900f * SAMPLE_RATE; // Carrier + frame lasts 160ms + 520ms + 100ms post carrier = 880ms
|
||||
static constexpr size_t FRAME_MAX_SAMPLES = SAMPLES_PER_BIT * (144 * 1.1f); // Frame max length (add 1% error margin)
|
||||
|
||||
AudioOutput audio_output{};
|
||||
|
||||
// Config
|
||||
uint8_t squelch_level{50};
|
||||
// Audio on/off logic is disabled to save app space
|
||||
// bool audio_on{true};
|
||||
#ifdef SPECAN
|
||||
bool spectrum_on{false};
|
||||
#endif
|
||||
|
||||
std::array<float, 32> audio{};
|
||||
const buffer_f32_t audio_buffer{
|
||||
audio.data(),
|
||||
audio.size()};
|
||||
|
||||
// Last received bit (for manchester deconding)
|
||||
bool last_bit = false;
|
||||
// Sample count since last symbol
|
||||
uint16_t sample_count{0};
|
||||
// Sample count since frame start
|
||||
uint16_t frame_sample_count{0};
|
||||
// True if last phase shift was positive, false otherwise
|
||||
bool last_phase_positive = false;
|
||||
// Counter used for peak filtering
|
||||
uint16_t rise_detection_count{0};
|
||||
|
||||
// Frame detection state machine states
|
||||
enum State { IDLE,
|
||||
CARRIER_LOCKED,
|
||||
DATA_SYNC,
|
||||
POST_FRAME };
|
||||
// Current state for frame detection state machine
|
||||
State current_state = IDLE;
|
||||
// Carrier detection counter
|
||||
uint32_t stability_counter = 0;
|
||||
|
||||
// Phase delta accumulator (6 samples)
|
||||
static constexpr size_t PHASE_DELTA_ACC_SIZE = SAMPLES_ACCUMUMLATOR;
|
||||
float phase_delta_buffer[PHASE_DELTA_ACC_SIZE] = {0.0f};
|
||||
size_t pahse_delta_index = 0;
|
||||
float phase_delta_acc = 0.0f;
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
// Decimation chain for 406 MHz EPIRB signal processing
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
// Audio filtering
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
// Audio demodulation
|
||||
dsp::demodulate::FM demod{};
|
||||
#ifdef SPECAN
|
||||
SpectrumCollector channel_spectrum{};
|
||||
#endif
|
||||
// Store last stample for phase delta calculation
|
||||
complex16_t last_sample{};
|
||||
|
||||
// EPIRB packet structure:
|
||||
// - Sync pattern: 111111111111111 (15 bits)
|
||||
// - Frame sync: 000101111(real) / 011010000(test) (9 bits)
|
||||
// - Data: 120 bits (long frame) / // bits (short frame)
|
||||
// - BCH error correction: 10 bits
|
||||
// Total: 144 bits (long frame) / 112 bits (short frame)
|
||||
EPIRBPacketBuilder packet_builder{
|
||||
this,
|
||||
[](void* ctx, const baseband::Packet& p) {
|
||||
static_cast<EPIRBProcessor*>(ctx)->payload_handler(p);
|
||||
}};
|
||||
|
||||
void payload_handler(const baseband::Packet& packet);
|
||||
// Compute phase diff between two samples
|
||||
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
|
||||
// End current frame
|
||||
void frame_end();
|
||||
// Rise detection with peak filtering
|
||||
bool filtered_rise_detect(bool condition);
|
||||
// Configure audio processing
|
||||
void configure_audio();
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
BASEBAND_SAMPLE_RATE, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
#endif /*__PROC_EPIRB_H__*/
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "platform_scu.h"
|
||||
#include "fixed_point.h"
|
||||
#include "clkin.h"
|
||||
#include "usb_type.h"
|
||||
#include <libopencm3/lpc43xx/cgu.h>
|
||||
#include <libopencm3/lpc43xx/ccu.h>
|
||||
#include <libopencm3/lpc43xx/scu.h>
|
||||
@@ -147,34 +148,16 @@ static struct gpio gpio_h1r9_hw_sync_enable = GPIO(5, 5);
|
||||
#endif
|
||||
// clang-format on
|
||||
|
||||
i2c_bus_t i2c0 = {
|
||||
.obj = (void*)I2C0_BASE,
|
||||
.start = i2c_lpc_start,
|
||||
.stop = i2c_lpc_stop,
|
||||
.transfer = i2c_lpc_transfer,
|
||||
};
|
||||
|
||||
i2c_bus_t i2c1 = {
|
||||
.obj = (void*)I2C1_BASE,
|
||||
.start = i2c_lpc_start,
|
||||
.stop = i2c_lpc_stop,
|
||||
.transfer = i2c_lpc_transfer,
|
||||
};
|
||||
|
||||
// const i2c_lpc_config_t i2c_config_si5351c_slow_clock = {
|
||||
// .duty_cycle_count = 15,
|
||||
// };
|
||||
|
||||
const i2c_lpc_config_t i2c_config_si5351c_fast_clock = {
|
||||
.duty_cycle_count = 255,
|
||||
};
|
||||
/* i2c0, i2c1, and i2c_config_si5351c_fast_clock are defined in
|
||||
* hackrf/firmware/common/i2c_bus.c and si5351c.c; use those instead of
|
||||
* redefining them here to avoid multiple-definition link errors. */
|
||||
|
||||
si5351c_driver_t clock_gen = {
|
||||
.bus = &i2c0,
|
||||
.i2c_address = 0x60,
|
||||
};
|
||||
|
||||
const ssp_config_t ssp_config_max283x = {
|
||||
ssp_config_t ssp_config_max283x = {
|
||||
/* FIXME speed up once everything is working reliably */
|
||||
/*
|
||||
// Freq About 0.0498MHz / 49.8KHz => Freq = PCLK / (CPSDVSR * [SCR+1]) with PCLK=PLL1=204MHz
|
||||
@@ -188,7 +171,7 @@ const ssp_config_t ssp_config_max283x = {
|
||||
.gpio_select = &gpio_max283x_select,
|
||||
};
|
||||
|
||||
const ssp_config_t ssp_config_max5864 = {
|
||||
ssp_config_t ssp_config_max5864 = {
|
||||
/* FIXME speed up once everything is working reliably */
|
||||
/*
|
||||
// Freq About 0.0498MHz / 49.8KHz => Freq = PCLK / (CPSDVSR * [SCR+1]) with PCLK=PLL1=204MHz
|
||||
@@ -202,14 +185,8 @@ const ssp_config_t ssp_config_max5864 = {
|
||||
.gpio_select = &gpio_max5864_select,
|
||||
};
|
||||
|
||||
spi_bus_t spi_bus_ssp1 = {
|
||||
.obj = (void*)SSP1_BASE,
|
||||
.config = &ssp_config_max5864,
|
||||
.start = spi_ssp_start,
|
||||
.stop = spi_ssp_stop,
|
||||
.transfer = spi_ssp_transfer,
|
||||
.transfer_gather = spi_ssp_transfer_gather,
|
||||
};
|
||||
/* spi_bus_ssp1 is defined in hackrf/firmware/common/spi_bus.c. We update
|
||||
* its .config at runtime in clock_init() to point at ssp_config_max5864. */
|
||||
|
||||
max283x_driver_t max283x = {};
|
||||
|
||||
@@ -224,14 +201,8 @@ ssp_config_t ssp_config_w25q80bv = {
|
||||
.clock_prescale_rate = 2,
|
||||
};
|
||||
|
||||
spi_bus_t spi_bus_ssp0 = {
|
||||
.obj = (void*)SSP0_BASE,
|
||||
.config = &ssp_config_w25q80bv,
|
||||
.start = spi_ssp_start,
|
||||
.stop = spi_ssp_stop,
|
||||
.transfer = spi_ssp_transfer,
|
||||
.transfer_gather = spi_ssp_transfer_gather,
|
||||
};
|
||||
/* spi_bus_ssp0 is defined in hackrf/firmware/common/spi_bus.c. We update
|
||||
* its .config at runtime in clock_init() to point at ssp_config_w25q80bv. */
|
||||
|
||||
w25q80bv_driver_t spi_flash = {
|
||||
.bus = &spi_bus_ssp0,
|
||||
@@ -240,43 +211,8 @@ w25q80bv_driver_t spi_flash = {
|
||||
.target_init = w25q80bv_target_init,
|
||||
};
|
||||
|
||||
sgpio_config_t sgpio_config = {
|
||||
.gpio_q_invert = &gpio_q_invert,
|
||||
.gpio_trigger_enable = &gpio_hw_sync_enable,
|
||||
.slice_mode_multislice = true,
|
||||
};
|
||||
|
||||
rf_path_t rf_path = {
|
||||
.switchctrl = 0,
|
||||
#ifdef HACKRF_ONE
|
||||
.gpio_hp = &gpio_hp,
|
||||
.gpio_lp = &gpio_lp,
|
||||
.gpio_tx_mix_bp = &gpio_tx_mix_bp,
|
||||
.gpio_no_mix_bypass = &gpio_no_mix_bypass,
|
||||
.gpio_rx_mix_bp = &gpio_rx_mix_bp,
|
||||
.gpio_tx_amp = &gpio_tx_amp,
|
||||
.gpio_tx = &gpio_tx,
|
||||
.gpio_mix_bypass = &gpio_mix_bypass,
|
||||
.gpio_rx = &gpio_rx,
|
||||
.gpio_no_tx_amp_pwr = &gpio_no_tx_amp_pwr,
|
||||
.gpio_amp_bypass = &gpio_amp_bypass,
|
||||
.gpio_rx_amp = &gpio_rx_amp,
|
||||
.gpio_no_rx_amp_pwr = &gpio_no_rx_amp_pwr,
|
||||
#endif
|
||||
#ifdef RAD1O
|
||||
.gpio_tx_rx_n = &gpio_tx_rx_n,
|
||||
.gpio_tx_rx = &gpio_tx_rx,
|
||||
.gpio_by_mix = &gpio_by_mix,
|
||||
.gpio_by_mix_n = &gpio_by_mix_n,
|
||||
.gpio_by_amp = &gpio_by_amp,
|
||||
.gpio_by_amp_n = &gpio_by_amp_n,
|
||||
.gpio_mixer_en = &gpio_mixer_en,
|
||||
.gpio_low_high_filt = &gpio_low_high_filt,
|
||||
.gpio_low_high_filt_n = &gpio_low_high_filt_n,
|
||||
.gpio_tx_amp = &gpio_tx_amp,
|
||||
.gpio_rx_lna = &gpio_rx_lna,
|
||||
#endif
|
||||
};
|
||||
/* sgpio_config and rf_path are defined in hackrf/firmware/common/sgpio.c and
|
||||
* rf_path.c. Their gpio_* fields are populated at runtime in pin_setup(). */
|
||||
|
||||
jtag_gpio_t jtag_gpio_cpld = {
|
||||
.gpio_tms = &gpio_cpld_tms,
|
||||
@@ -681,7 +617,7 @@ void cpu_clock_init(void) {
|
||||
|
||||
si5351c_set_clock_source(&clock_gen, PLL_SOURCE_XTAL);
|
||||
// soft reset
|
||||
si5351c_reset_pll(&clock_gen);
|
||||
si5351c_reset_pll(&clock_gen, SI5351C_PLL_BOTH);
|
||||
si5351c_enable_clock_outputs(&clock_gen);
|
||||
|
||||
// FIXME disable I2C
|
||||
@@ -866,6 +802,55 @@ void ssp1_set_mode_max5864(void) {
|
||||
void pin_setup(void) {
|
||||
const platform_scu_t* scu = platform_scu();
|
||||
|
||||
/* spi_bus_ssp0/1 are defined in hackrf/firmware/common/spi_bus.c with no
|
||||
* .config. Patch in our SSP configs and SPI driver entry points here. */
|
||||
spi_bus_ssp0.config = &ssp_config_w25q80bv;
|
||||
spi_bus_ssp0.start = spi_ssp_start;
|
||||
spi_bus_ssp0.stop = spi_ssp_stop;
|
||||
spi_bus_ssp0.transfer = spi_ssp_transfer;
|
||||
spi_bus_ssp0.transfer_gather = spi_ssp_transfer_gather;
|
||||
|
||||
spi_bus_ssp1.config = &ssp_config_max5864;
|
||||
spi_bus_ssp1.start = spi_ssp_start;
|
||||
spi_bus_ssp1.stop = spi_ssp_stop;
|
||||
spi_bus_ssp1.transfer = spi_ssp_transfer;
|
||||
spi_bus_ssp1.transfer_gather = spi_ssp_transfer_gather;
|
||||
|
||||
/* sgpio_config / rf_path are defined in common/sgpio.c and rf_path.c
|
||||
* with minimal initializers. Populate the gpio_* fields here. */
|
||||
sgpio_config.gpio_q_invert = &gpio_q_invert;
|
||||
sgpio_config.gpio_trigger_enable = &gpio_hw_sync_enable;
|
||||
sgpio_config.slice_mode_multislice = true;
|
||||
|
||||
#ifdef HACKRF_ONE
|
||||
rf_path.gpio_hp = &gpio_hp;
|
||||
rf_path.gpio_lp = &gpio_lp;
|
||||
rf_path.gpio_tx_mix_bp = &gpio_tx_mix_bp;
|
||||
rf_path.gpio_no_mix_bypass = &gpio_no_mix_bypass;
|
||||
rf_path.gpio_rx_mix_bp = &gpio_rx_mix_bp;
|
||||
rf_path.gpio_tx_amp = &gpio_tx_amp;
|
||||
rf_path.gpio_tx = &gpio_tx;
|
||||
rf_path.gpio_mix_bypass = &gpio_mix_bypass;
|
||||
rf_path.gpio_rx = &gpio_rx;
|
||||
rf_path.gpio_no_tx_amp_pwr = &gpio_no_tx_amp_pwr;
|
||||
rf_path.gpio_amp_bypass = &gpio_amp_bypass;
|
||||
rf_path.gpio_rx_amp = &gpio_rx_amp;
|
||||
rf_path.gpio_no_rx_amp_pwr = &gpio_no_rx_amp_pwr;
|
||||
#endif
|
||||
#ifdef RAD1O
|
||||
rf_path.gpio_tx_rx_n = &gpio_tx_rx_n;
|
||||
rf_path.gpio_tx_rx = &gpio_tx_rx;
|
||||
rf_path.gpio_by_mix = &gpio_by_mix;
|
||||
rf_path.gpio_by_mix_n = &gpio_by_mix_n;
|
||||
rf_path.gpio_by_amp = &gpio_by_amp;
|
||||
rf_path.gpio_by_amp_n = &gpio_by_amp_n;
|
||||
rf_path.gpio_mixer_en = &gpio_mixer_en;
|
||||
rf_path.gpio_low_high_filt = &gpio_low_high_filt;
|
||||
rf_path.gpio_low_high_filt_n = &gpio_low_high_filt_n;
|
||||
rf_path.gpio_tx_amp = &gpio_tx_amp;
|
||||
rf_path.gpio_rx_lna = &gpio_rx_lna;
|
||||
#endif
|
||||
|
||||
/* Configure all GPIO as Input (safe state) */
|
||||
// gpio_init();
|
||||
|
||||
@@ -1070,3 +1055,17 @@ void halt_and_flash(const uint32_t duration) {
|
||||
delay(duration);
|
||||
}
|
||||
}
|
||||
|
||||
/* Upstream commit 85dfacf6 ("Universalize: firmware/hackrf_usb") wired
|
||||
* usb_endpoint_control_out.setup_complete to transceiver_usb_setup_complete
|
||||
* (defined in hackrf_usb/usb_api_transceiver.c). That function dispatches
|
||||
* transceiver-specific SETUP packets and falls through to usb_setup_complete
|
||||
* for everything else. sd_over_usb doesn't link the transceiver API, so
|
||||
* provide a shim that always delegates to the standard handler. Without
|
||||
* this, EP0 SETUP packets get a no-op completion and enumeration times out
|
||||
* (host reports "device descriptor read/64, error -110"). */
|
||||
void usb_setup_complete(usb_endpoint_t* const endpoint);
|
||||
void transceiver_usb_setup_complete(usb_endpoint_t* const endpoint);
|
||||
void transceiver_usb_setup_complete(usb_endpoint_t* const endpoint) {
|
||||
usb_setup_complete(endpoint);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,28 @@
|
||||
|
||||
#include "sd_over_usb.h"
|
||||
#include "scsi.h"
|
||||
#include "usb_descriptor.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* Defined in hackrf/firmware/hackrf_usb/usb_device.c. After upstream commit
|
||||
* 85dfacf6 ("Universalize: firmware/hackrf_usb"), the global `usb_device` is
|
||||
* left zero-initialized and is populated at runtime from per-board
|
||||
* `usb_device_*` constants inside hackrf_usb.c. The sd_over_usb baseband
|
||||
* does not link hackrf_usb.c, so populate `usb_device` ourselves before
|
||||
* usb_run(). The descriptor fields are const-qualified, so use the upstream
|
||||
* memcpy-from-template pattern. */
|
||||
extern usb_configuration_t* usb_configurations[];
|
||||
|
||||
static const usb_device_t usb_device_sd_over_usb = {
|
||||
.descriptor = usb_descriptor_device,
|
||||
.descriptor_strings = usb_descriptor_strings,
|
||||
.qualifier_descriptor = usb_descriptor_device_qualifier,
|
||||
.configurations = &usb_configurations,
|
||||
.configuration = 0,
|
||||
.wcid_string_descriptor = wcid_string_descriptor,
|
||||
.wcid_feature_descriptor = wcid_feature_descriptor,
|
||||
};
|
||||
|
||||
volatile bool scsi_running = false;
|
||||
|
||||
@@ -74,6 +96,8 @@ void start_usb(void) {
|
||||
pin_setup();
|
||||
cpu_clock_init();
|
||||
|
||||
memcpy(&usb_device, &usb_device_sd_over_usb, sizeof(usb_device_sd_over_usb));
|
||||
|
||||
usb_set_configuration_changed_cb(usb_configuration_changed);
|
||||
usb_peripheral_reset();
|
||||
|
||||
@@ -85,7 +109,10 @@ void start_usb(void) {
|
||||
usb_queue_init(&usb_endpoint_bulk_in_queue);
|
||||
|
||||
usb_endpoint_init(&usb_endpoint_control_out, false);
|
||||
usb_endpoint_init(&usb_endpoint_control_in, false);
|
||||
/* Match the new usb_endpoint_init() contract introduced upstream by
|
||||
* db73ecbf, control IN needs ZLP for transfers whose length is a
|
||||
* multiple of the EP0 max packet size, otherwise the host hangs. */
|
||||
usb_endpoint_init(&usb_endpoint_control_in, true);
|
||||
|
||||
nvic_set_priority(NVIC_USB0_IRQ, 255);
|
||||
|
||||
|
||||
@@ -551,6 +551,15 @@ bool I2cDev_MAX17055::setModelCfg(const uint8_t _Model_ID) {
|
||||
}
|
||||
|
||||
bool I2cDev_MAX17055::setHibCFG(const uint16_t _Config) {
|
||||
uint16_t config1_reg = read_register(0x1D);
|
||||
|
||||
// (SHDN: 0x80) and (COMMSH: 0x40) Otherwise it will go into sleep mode after 45 seconds.
|
||||
config1_reg &= ~(0x0080 | 0x0040);
|
||||
|
||||
if (!write_register(0x1D, config1_reg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return write_register(0xBA, _Config);
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ class Message {
|
||||
ToneDetectData = 103,
|
||||
ToneDetectConfig = 104,
|
||||
FlexTosend = 105,
|
||||
EPIRBRXConfig = 106,
|
||||
MAX
|
||||
};
|
||||
|
||||
@@ -390,6 +391,16 @@ class EPIRBPacketMessage : public Message {
|
||||
baseband::Packet packet;
|
||||
};
|
||||
|
||||
class EPIRBRXConfig : public Message {
|
||||
public:
|
||||
constexpr EPIRBRXConfig()
|
||||
: Message{ID::EPIRBRXConfig} {
|
||||
}
|
||||
bool spectrum_on = false;
|
||||
bool audio_on = true;
|
||||
uint8_t squelch{50};
|
||||
};
|
||||
|
||||
class TPMSPacketMessage : public Message {
|
||||
public:
|
||||
constexpr TPMSPacketMessage(
|
||||
|
||||
@@ -85,7 +85,7 @@ struct SharedMemory {
|
||||
uint16_t volatile m4_stack_usage{0};
|
||||
uint32_t volatile m4_heap_usage{0};
|
||||
uint16_t volatile m4_buffer_missed{0};
|
||||
|
||||
uint8_t volatile radio_tx_drain{0}; // to indicate the baseband thread to drain the tx buffer, and wait for it, before radio::disable()
|
||||
#ifdef PRALINE
|
||||
// Phase 0 instrumentation counters for PRALINE radio debugging
|
||||
uint32_t volatile m4_dma_xfr_count{0}; // DMA transfer_complete() calls
|
||||
|
||||
+1
-1
Submodule hackrf updated: 442d95e23d...0bf478ed25
@@ -13,7 +13,7 @@ EPIRB; Standard Location Protocol EPIRB-MMSI; FFFED0A0D205F260850F3DC9E0B70B6E4F
|
||||
PLB; Standard Location Protocol - PLB (Serial); FFFED0A3E7B10016150D364D8B3689C09437
|
||||
SAAS; Std. Location ship security protocol (SSAS); FFFED0A5DC19E1A07FDFFE5C803483E0FCCA
|
||||
ELT 24; Serial user Location (ELT with Aircraft 24-bit Address); FFFED0CE46E76EF8C00C2BAA31CFE0FF0F61
|
||||
MMSO; Maritime User Protocol MMSI - EPIRB; FFFED0D9D4EB28140AA6893F16A2C67282EC
|
||||
MMSI; Maritime User Protocol MMSI - EPIRB; FFFED0D9D4EB28140AA6893F16A2C67282EC
|
||||
Float Free EPIRB; Float Free EPIRB with Serial Identification Number; FFFED0DF76A9A9C800174DE27BE1F0277E45
|
||||
Non Float Free EPIRB; Non Float Free EPIRB with Serial Identification; FFFED0DF77200000001168C610AFE0FF0146
|
||||
Radio Call Sign; Radio Call Sign - EPIRB; FFFED0E0DDAE599508268E4C05E054651307
|
||||
@@ -22,4 +22,13 @@ ELT Aircraft; ELT with Aircraft Operator Designator & Serial Number; FFFED0EDF67
|
||||
PLB Serial; Standard Location Protocol - PLB (Serial); FFFED0A157B081437FDFF8B4833783E0F66C
|
||||
RLS Location; RLS Location Protocol; FFFED096ED09900149D4D467EE0851A3B2E8
|
||||
Orbitography; Orbitography Protocol (from Toulouse FMCC's LUT); FFFE2FCE3000000000000DBD0E4022417500
|
||||
ELT 24 2; ELT 24 bits; FFFED08DB345B146202DDF3C71F59BAB7072
|
||||
ELT 24 2; ELT 24 bits; FFFED08DB345B146202DDF3C71F59BAB7072
|
||||
Selftest BCH1 ERR; Serial user Location Protocol; FFFED0D7E6202820000C29FF51041775302D
|
||||
Selftest BCH2 ERR; Serial user Location Protocol; FFFED0D6E6202820000C29FF51041765302D
|
||||
Selftest BCH12 ERR; Serial user Location Protocol; FFFED0D7E6202820000C29FF51041765302D
|
||||
Emergency Cap;EPIRB Emergency Capsizing;FFFED056E68040022021A4CDC675
|
||||
Emergency No Mar.;Emergency non maritime;FFFED056EE8040022021A5623CFE
|
||||
Emergency PLB;Emergency PLB no fire+med+dis;FFFED056E7804002202123F39E36
|
||||
Emergency PLB fire;Emergency PLB fire;FFFED056E7804002202123F39E39
|
||||
EPIRB Sinking;Emergency EPIRB Sinking;FFFED056E58040022021342FAD76
|
||||
EPIRB Call Sign Col.;EPIRB Radio Call Sign Emergency collision;FFFED056ED80400220217185E933
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
406025000
|
||||
406028000
|
||||
406037000
|
||||
406040000
|
||||
406049000
|
||||
406052000
|
||||
406061000
|
||||
406064000
|
||||
433025000
|
||||
144875000
|
||||
406022000
|
||||
@@ -0,0 +1,59 @@
|
||||
EPIRB - Maritime
|
||||
EPIRB - Radio Call Sign
|
||||
ELT - Aviation
|
||||
Serial User Protocol
|
||||
Test User Protocol
|
||||
Orbitography Protocol
|
||||
National User Protocol
|
||||
2nd Generation Beacons
|
||||
EPIRB - MMSI / Location
|
||||
ELT-24-bit Address / Location
|
||||
ELT Serial Location
|
||||
ELT Serial Aircraft Location
|
||||
EPIRB Serial Location
|
||||
PLB Serial Location
|
||||
Ship Security Location
|
||||
Test Standard Location
|
||||
ELT National Location
|
||||
EPIRB National Location
|
||||
PLB National Location
|
||||
Test National Location
|
||||
RLS Location Protocol
|
||||
ELT(DT) Location Protocol
|
||||
Spare
|
||||
Unknown
|
||||
# L. 25: additional data
|
||||
ELTs/SN
|
||||
ELTs/AC op & SN
|
||||
FF EPIRBs/SN
|
||||
ELTs+AC 24-bit ad.
|
||||
NFF EPIRBs/SN
|
||||
Not Used
|
||||
PLBs/SN
|
||||
# L. 33: long additinal data
|
||||
MMSI=0x%08lX MID=%ld
|
||||
24 bits AC ad.
|
||||
C/S TA #=%ld
|
||||
Op Design.=%c%c%c
|
||||
Nati. data=%ld
|
||||
# L. 39: emmergency
|
||||
Other
|
||||
Fire
|
||||
Flooding
|
||||
Collision
|
||||
Grounding
|
||||
Capsizing
|
||||
Sinking
|
||||
Disabled
|
||||
Abandoning
|
||||
# L. 49: other emmergency
|
||||
Fire
|
||||
Med.
|
||||
Dis.
|
||||
# L. 53:
|
||||
Undefined
|
||||
No device
|
||||
Other/no device
|
||||
Other device
|
||||
121.5 MHz
|
||||
SART
|
||||
@@ -0,0 +1,292 @@
|
||||
501:ADE:AdelieLand
|
||||
201:ALB:Albania
|
||||
202:AND:Andorra
|
||||
203:AUT:Austria
|
||||
204:AZC:Azores
|
||||
205:BEL:Belgium
|
||||
206:BLR:Belarus
|
||||
207:BUL:Bulgaria
|
||||
208:VAT:Vatican
|
||||
209:CYP:Cyprus
|
||||
210:CYP:Cyprus
|
||||
211:GER:Germany
|
||||
212:CYP:Cyprus
|
||||
213:GOG:Georgia
|
||||
214:MOL:Moldova
|
||||
215:MAL:MALTA
|
||||
216:ARM:Armenia
|
||||
218:GER:Germany
|
||||
219:DEN:Denmark
|
||||
220:DEN:Denmark
|
||||
224:SPA:Spain
|
||||
225:SPA:Spain
|
||||
226:FRA:France
|
||||
227:FRA:France
|
||||
228:FRA:France
|
||||
229:MAL:Malta
|
||||
230:FIN:Finland
|
||||
231:FAR:Faro Isle
|
||||
232:UKM:G Britain
|
||||
233:UKM:G Britain
|
||||
234:UKM:G Britain
|
||||
235:UKM:G Britain
|
||||
236:GIB:Gibraltar
|
||||
237:GRE:Greece
|
||||
238:CRT:Croatia
|
||||
239:GRE:Greece
|
||||
240:GRE:Greece
|
||||
241:GRE:Greece
|
||||
242:MOR:Morocco
|
||||
243:HUN:Hungary
|
||||
244:NET:Netherland
|
||||
245:NET:Netherland
|
||||
246:NET:Netherland
|
||||
247:ITA:Italy
|
||||
248:MAL:Malta
|
||||
249:MAL:Malta
|
||||
250:IRE:Ireland
|
||||
251:ICE:Iceland
|
||||
252:LIE:Liechten
|
||||
253:LUX:Luxembourg
|
||||
254:MON:Monaco
|
||||
255:MAE:Madeira
|
||||
256:MAL:Malta
|
||||
257:NOR:Norway
|
||||
258:NOR:Norway
|
||||
259:NOR:Norway
|
||||
261:POL:Poland
|
||||
262:MNT:Montenegro
|
||||
263:POR:Portugal
|
||||
264:ROM:Romania
|
||||
265:SWE:Sweden
|
||||
266:SWE:Sweden
|
||||
267:SLV:Slovakia
|
||||
268:SAN:San Marino
|
||||
269:SWT:Swiss
|
||||
270:CZH:Czech Rep
|
||||
271:TUR:Turkiye
|
||||
272:UKR:Ukraine
|
||||
273:RUS:Russia
|
||||
274:MKD:North Mac
|
||||
275:LAT:Latvia
|
||||
276:EST:Estonia
|
||||
277:LIT:Lithuania
|
||||
278:SVN:Slovenia
|
||||
279:SER:Serbia
|
||||
301:ANA:Anguilla
|
||||
303:ALA:Alaska
|
||||
304:ANT:Antigua
|
||||
305:ANT:Antigua
|
||||
306:NEA:N Antilles
|
||||
307:ARU:Aruba
|
||||
308:BAA:Bahamas
|
||||
309:BAA:Bahamas
|
||||
310:BER:Bermuda
|
||||
311:BAA:Bahamas
|
||||
312:BEZ:Belize
|
||||
314:BAR:Barbados
|
||||
316:CAN:Canada
|
||||
319:CAY:Cayman Is
|
||||
321:COS:Costa Rica
|
||||
323:CUB:Cuba
|
||||
325:DOM:Dominica
|
||||
327:DOR:Dominican
|
||||
329:GUA:Guadeloupe
|
||||
330:GRA:Grenada
|
||||
331:GRN:Greenland
|
||||
332:GUT:Guatemala
|
||||
334:HON:Honduras
|
||||
336:HAI:Haiti
|
||||
338:USA:USA
|
||||
339:JAM:Jamaica
|
||||
341:SKN:St Kitts
|
||||
343:SLU:St Lucia
|
||||
345:MEX:Mexico
|
||||
347:MTQ:Martinique
|
||||
348:MOT:Montserrat
|
||||
350:NIC:Nicaragua
|
||||
351:PAN:Panama
|
||||
352:PAN:Panama
|
||||
353:PAN:Panama
|
||||
354:PAN:Panama
|
||||
355:PAN:Panama
|
||||
356:PAN:Panama
|
||||
357:PAN:Panama
|
||||
358:PUE:PuertoRico
|
||||
359:ELS:ElSalvador
|
||||
361:SPI:St Pierre
|
||||
362:TAT:Trinidad
|
||||
364:TUK:Caicos Is
|
||||
366:USA:USA
|
||||
367:USA:USA
|
||||
368:USA:USA
|
||||
369:USA:USA
|
||||
370:PAN:Panama
|
||||
371:PAN:Panama
|
||||
372:PAN:Panama
|
||||
373:PAN:Panama
|
||||
374:PAN:Panama
|
||||
375:SVG:St Vincent
|
||||
376:SVG:St Vincent
|
||||
377:SVG:St Vincent
|
||||
378:BVI:Virgin GB
|
||||
379:USV:Virgin US
|
||||
401:AFG:Afghan
|
||||
403:SAU:Saudi
|
||||
405:BAN:Bangladesh
|
||||
408:BAH:Bahrain
|
||||
410:BHU:Bhutan
|
||||
412:CHN:China
|
||||
413:CHN:China
|
||||
414:CHN:CHINA
|
||||
416:TAI:Taipei
|
||||
417:SRI:Sri Lanka
|
||||
419:IND:India
|
||||
422:IRN:Iran
|
||||
423:AZR:Azerbaijan
|
||||
425:IRQ:Iraq
|
||||
428:ISR:Israel
|
||||
431:JPN:Japan
|
||||
432:JPN:Japan
|
||||
434:TKM:Turkmenist
|
||||
436:KAZ:Kazakhstan
|
||||
437:UZB:Uzbekistan
|
||||
438:JOR:Jordan
|
||||
440:KOR:Korea Sou
|
||||
441:KOR:Korea Sou
|
||||
443:PAA:Palestine
|
||||
445:KDR:Korea Nor
|
||||
447:KUW:Kuwait
|
||||
450:LEB:Lebanon
|
||||
451:KYR:Kyrgyzia
|
||||
453:MAC:Macao
|
||||
455:MAV:Maldives
|
||||
457:MNG:Mongolia
|
||||
459:NEP:Nepal
|
||||
461:OMN:Oman
|
||||
463:PAK:Pakistan
|
||||
466:QAT:Qatar
|
||||
468:SYR:Syria
|
||||
470:UAE:UAE
|
||||
471:UAE:UAE
|
||||
472:TJK:TAJIKISTAN
|
||||
473:YEM:Yemen
|
||||
475:YEM:Yemen
|
||||
477:HKG:Hong Kong
|
||||
478:BOS:Bosniaherz
|
||||
503:AUS:Australia
|
||||
506:BUR:Burma
|
||||
508:BRU:Brunei
|
||||
510:MIC:Micronesia
|
||||
511:PAL:Palau
|
||||
512:NZL:NewZealand
|
||||
514:CMB:Cambodia
|
||||
515:CMB:Cambodia
|
||||
516:CHR:Christmas
|
||||
518:COO:Cook Isles
|
||||
520:FIJ:Fiji
|
||||
523:COC:Cocos Isle
|
||||
525:INO:Indonesia
|
||||
529:KIR:Kiribati
|
||||
531:LAO:Lao
|
||||
533:MLY:Malaysia
|
||||
536:MAI:Mariana Is
|
||||
538:MAR:Marshall I
|
||||
540:NCA:Caledonia
|
||||
542:NIU:Niue Isle
|
||||
544:NAU:Nauru
|
||||
546:PLY:Polynesia
|
||||
548:PHI:Philippine
|
||||
550:TIM:TimorLeste
|
||||
553:PAP:Papua NG
|
||||
555:PIT:Pitcairn I
|
||||
557:SOL:Solomon Is
|
||||
559:ASA:Samoa USA
|
||||
561:WSA:West Samoa
|
||||
563:SIN:Singapore
|
||||
564:SIN:Singapore
|
||||
565:SIN:Singapore
|
||||
566:SIN:SINGAPORE
|
||||
567:THA:Thailand
|
||||
570:TON:Tonga
|
||||
572:TUV:Tuvalu Is
|
||||
574:VIE:Vietnam
|
||||
576:VAN:Vanuatu
|
||||
577:VAN:Vanuatu
|
||||
578:WAL:Wallis Is
|
||||
601:SAF:So Africa
|
||||
603:ANG:Angola
|
||||
605:ALG:Algeria
|
||||
607:SPL:St Paul
|
||||
608:ASC:Ascension
|
||||
609:BUI:Burundi
|
||||
610:BEN:Benin
|
||||
611:BOT:Botswana
|
||||
612:CAR:CenAfr Rep
|
||||
613:CAM:Cameroon
|
||||
615:CON:Congo
|
||||
616:COM:Comoros
|
||||
617:CAP:Cape Verde
|
||||
618:CRP:Crozet
|
||||
619:IVO:IvoryCoast
|
||||
620:COM:Comoros
|
||||
621:DJI:Djibouti
|
||||
622:EGY:Egypt
|
||||
624:ETH:Ethiopia
|
||||
625:ERT:Eritrea
|
||||
626:GAB:Gabon Rep
|
||||
627:GHA:Ghana
|
||||
629:GAM:Gambia
|
||||
630:GUB:Guinea Bis
|
||||
631:EQG:Eq Guinea
|
||||
632:GUN:Guinea Rep
|
||||
633:BUF:Burkina FS
|
||||
634:KEN:Kenya
|
||||
635:KER:Kerguelen
|
||||
636:LIB:Liberia
|
||||
637:LIB:Liberia
|
||||
638:SSD:Southsudan
|
||||
642:LBY:Libya
|
||||
644:LES:Lesotho
|
||||
645:MAU:Mauritius
|
||||
647:MAD:Madagascar
|
||||
649:MLI:Mali
|
||||
650:MOZ:Mozambique
|
||||
654:MAA:Mauritania
|
||||
655:MAW:Malawi
|
||||
656:NIG:Niger
|
||||
657:NIA:Nigeria
|
||||
659:NAM:Namibia
|
||||
660:REU:Reunion
|
||||
661:RWA:Rwanda
|
||||
662:SUD:Sudan
|
||||
663:SEN:Senegal
|
||||
664:SEY:Seychelle
|
||||
665:SHE:St Helena
|
||||
666:SOM:Somali
|
||||
667:SIL:Sierra Leo
|
||||
668:SAO:Sao Tome
|
||||
669:SWZ:Eswatini
|
||||
670:CHA:Chad
|
||||
671:TOG:Togo
|
||||
672:TUN:Tunisia
|
||||
674:TAN:Tanzania
|
||||
675:UGA:Uganda
|
||||
676:ZAI:Zaire
|
||||
677:TAN:Tanzania
|
||||
678:ZAM:Zambia
|
||||
679:ZIM:Zimbabwe
|
||||
701:ARG:Argentina
|
||||
710:BRA:Brazil
|
||||
720:BOL:Bolivia
|
||||
725:CHI:Chile
|
||||
730:COL:Colombia
|
||||
735:ECU:Ecuador
|
||||
740:FAL:Falkland I
|
||||
745:GUI:Guiana
|
||||
750:GUY:Guyana
|
||||
755:PAR:Paraguay
|
||||
760:PER:Peru
|
||||
765:SUR:Suriname
|
||||
770:URU:Uruguay
|
||||
775:VEN:Venezuela
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'apricity' colormap from cmyt
|
||||
0,89,105,175
|
||||
8,90,111,171
|
||||
16,91,116,169
|
||||
25,93,121,167
|
||||
33,94,127,166
|
||||
41,96,132,165
|
||||
49,97,137,164
|
||||
58,99,141,164
|
||||
66,101,147,164
|
||||
74,103,151,163
|
||||
82,106,156,163
|
||||
90,108,161,163
|
||||
99,111,166,162
|
||||
107,114,171,161
|
||||
115,118,175,160
|
||||
123,121,180,159
|
||||
132,125,185,157
|
||||
140,128,190,155
|
||||
148,132,195,152
|
||||
156,137,199,149
|
||||
165,142,205,144
|
||||
173,147,209,139
|
||||
181,153,214,134
|
||||
189,160,218,129
|
||||
197,170,223,122
|
||||
206,179,227,115
|
||||
214,188,230,108
|
||||
222,199,234,100
|
||||
230,211,237,91
|
||||
239,223,240,81
|
||||
247,234,243,70
|
||||
255,246,246,55
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'arbre' colormap from cmyt
|
||||
0,113,14,13
|
||||
8,117,22,34
|
||||
16,121,29,54
|
||||
25,125,35,73
|
||||
33,129,41,95
|
||||
41,133,45,115
|
||||
49,136,49,136
|
||||
58,139,52,158
|
||||
66,141,56,184
|
||||
74,141,60,208
|
||||
82,138,69,228
|
||||
90,130,82,241
|
||||
99,118,100,245
|
||||
107,105,116,241
|
||||
115,92,129,234
|
||||
123,82,141,225
|
||||
132,72,153,217
|
||||
140,65,162,209
|
||||
148,60,171,203
|
||||
156,54,179,196
|
||||
165,48,189,189
|
||||
173,43,197,182
|
||||
181,40,206,173
|
||||
189,45,214,162
|
||||
197,62,222,147
|
||||
206,84,228,131
|
||||
214,110,234,113
|
||||
222,139,238,93
|
||||
230,173,240,69
|
||||
239,203,241,50
|
||||
247,231,241,44
|
||||
255,254,242,69
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'cividis' colormap from matplotlib
|
||||
0,0,34,78
|
||||
8,0,40,91
|
||||
16,0,46,106
|
||||
25,5,51,113
|
||||
33,28,57,111
|
||||
41,41,63,110
|
||||
49,51,68,109
|
||||
58,60,74,108
|
||||
66,69,80,108
|
||||
74,77,85,108
|
||||
82,85,91,109
|
||||
90,92,97,110
|
||||
99,100,103,112
|
||||
107,107,109,114
|
||||
115,114,114,116
|
||||
123,120,120,119
|
||||
132,128,127,120
|
||||
140,136,133,120
|
||||
148,144,139,120
|
||||
156,151,145,119
|
||||
165,160,152,117
|
||||
173,168,158,115
|
||||
181,176,165,113
|
||||
189,185,171,109
|
||||
197,194,179,105
|
||||
206,203,185,101
|
||||
214,211,192,95
|
||||
222,220,200,89
|
||||
230,230,208,81
|
||||
239,239,215,72
|
||||
247,248,223,60
|
||||
255,254,232,56
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'dusk' colormap from cmyt
|
||||
0,6,3,5
|
||||
8,11,10,15
|
||||
16,15,18,27
|
||||
25,18,27,38
|
||||
33,16,36,52
|
||||
41,7,45,62
|
||||
49,17,53,64
|
||||
58,35,59,64
|
||||
66,50,65,66
|
||||
74,63,70,69
|
||||
82,74,75,72
|
||||
90,86,81,73
|
||||
99,101,85,73
|
||||
107,115,89,73
|
||||
115,130,92,72
|
||||
123,145,95,70
|
||||
132,163,97,68
|
||||
140,179,99,64
|
||||
148,195,100,60
|
||||
156,213,100,54
|
||||
165,231,101,42
|
||||
173,239,111,31
|
||||
181,240,126,49
|
||||
189,241,140,73
|
||||
197,241,155,98
|
||||
206,242,168,119
|
||||
214,242,180,140
|
||||
222,243,192,161
|
||||
230,244,206,184
|
||||
239,245,218,204
|
||||
247,246,230,223
|
||||
255,249,241,241
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'inferno' colormap from matplotlib
|
||||
0,0,0,4
|
||||
8,4,3,18
|
||||
16,11,7,36
|
||||
25,21,11,55
|
||||
33,35,12,76
|
||||
41,49,10,92
|
||||
49,62,9,102
|
||||
58,76,12,107
|
||||
66,90,17,110
|
||||
74,103,22,110
|
||||
82,116,26,110
|
||||
90,128,31,108
|
||||
99,143,36,105
|
||||
107,155,41,100
|
||||
115,168,46,95
|
||||
123,180,51,89
|
||||
132,193,58,80
|
||||
140,204,66,72
|
||||
148,215,75,63
|
||||
156,224,85,54
|
||||
165,233,97,43
|
||||
173,239,110,33
|
||||
181,245,123,23
|
||||
189,248,137,12
|
||||
197,251,153,6
|
||||
206,252,168,13
|
||||
214,251,184,29
|
||||
222,249,199,47
|
||||
230,245,217,73
|
||||
239,242,232,101
|
||||
247,243,245,134
|
||||
255,252,255,164
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'kelp' colormap from cmyt
|
||||
0,20,6,117
|
||||
8,32,14,122
|
||||
16,43,20,126
|
||||
25,54,26,130
|
||||
33,66,32,133
|
||||
41,76,38,135
|
||||
49,85,44,135
|
||||
58,94,50,134
|
||||
66,103,57,133
|
||||
74,111,64,132
|
||||
82,117,71,130
|
||||
90,124,78,129
|
||||
99,130,87,128
|
||||
107,135,95,128
|
||||
115,139,103,128
|
||||
123,143,111,129
|
||||
132,146,120,132
|
||||
140,150,128,135
|
||||
148,153,137,139
|
||||
156,156,145,142
|
||||
165,158,154,147
|
||||
173,161,163,152
|
||||
181,163,172,156
|
||||
189,165,181,159
|
||||
197,168,191,161
|
||||
206,174,199,159
|
||||
214,182,207,157
|
||||
222,193,214,153
|
||||
230,207,222,149
|
||||
239,220,229,144
|
||||
247,235,235,139
|
||||
255,250,242,133
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'magma' colormap from matplotlib
|
||||
0,0,0,4
|
||||
8,3,3,18
|
||||
16,10,8,34
|
||||
25,19,13,52
|
||||
33,30,17,73
|
||||
41,42,17,92
|
||||
49,56,16,108
|
||||
58,69,16,119
|
||||
66,84,19,125
|
||||
74,96,24,128
|
||||
82,109,29,129
|
||||
90,121,34,130
|
||||
99,136,39,129
|
||||
107,148,44,128
|
||||
115,161,48,126
|
||||
123,174,52,123
|
||||
132,189,57,119
|
||||
140,202,62,114
|
||||
148,214,69,108
|
||||
156,226,77,102
|
||||
165,236,88,96
|
||||
173,243,101,92
|
||||
181,248,116,92
|
||||
189,251,131,95
|
||||
197,253,148,103
|
||||
206,254,163,111
|
||||
214,254,178,122
|
||||
222,254,193,133
|
||||
230,254,209,148
|
||||
239,253,224,161
|
||||
247,252,238,176
|
||||
255,252,253,191
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'mako' colormap from seaborn
|
||||
0,11,4,5
|
||||
8,20,9,16
|
||||
16,28,16,28
|
||||
25,36,22,40
|
||||
33,44,28,54
|
||||
41,50,34,67
|
||||
49,55,40,81
|
||||
58,59,47,95
|
||||
66,63,54,111
|
||||
74,65,62,125
|
||||
82,64,70,138
|
||||
90,62,79,148
|
||||
99,58,89,154
|
||||
107,56,99,157
|
||||
115,54,108,160
|
||||
123,53,117,161
|
||||
132,52,127,164
|
||||
140,52,136,166
|
||||
148,52,145,168
|
||||
156,52,154,170
|
||||
165,54,164,171
|
||||
173,58,173,172
|
||||
181,63,182,173
|
||||
189,71,191,173
|
||||
197,83,201,173
|
||||
206,101,208,173
|
||||
214,124,214,175
|
||||
222,148,220,181
|
||||
230,171,226,190
|
||||
239,190,232,202
|
||||
247,206,238,215
|
||||
255,222,245,229
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'octarine' colormap from cmyt
|
||||
0,5,37,81
|
||||
8,8,45,75
|
||||
16,15,51,70
|
||||
25,20,57,67
|
||||
33,25,63,65
|
||||
41,29,69,63
|
||||
49,32,75,60
|
||||
58,38,80,55
|
||||
66,48,85,47
|
||||
74,61,89,40
|
||||
82,76,91,33
|
||||
90,92,93,27
|
||||
99,108,94,23
|
||||
107,123,94,21
|
||||
115,137,94,21
|
||||
123,152,93,21
|
||||
132,168,91,24
|
||||
140,182,89,27
|
||||
148,196,85,31
|
||||
156,211,81,37
|
||||
165,227,75,47
|
||||
173,238,71,63
|
||||
181,244,77,85
|
||||
189,244,89,108
|
||||
197,243,103,131
|
||||
206,241,115,151
|
||||
214,239,127,169
|
||||
222,236,138,186
|
||||
230,233,149,204
|
||||
239,232,159,220
|
||||
247,231,167,236
|
||||
255,238,172,250
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'plasma' colormap from matplotlib
|
||||
0,13,8,135
|
||||
8,34,6,144
|
||||
16,49,5,151
|
||||
25,63,4,156
|
||||
33,78,2,162
|
||||
41,91,1,165
|
||||
49,103,0,168
|
||||
58,116,1,168
|
||||
66,129,4,167
|
||||
74,141,11,165
|
||||
82,152,20,160
|
||||
90,162,29,154
|
||||
99,173,39,147
|
||||
107,182,48,139
|
||||
115,191,57,132
|
||||
123,199,66,124
|
||||
132,207,76,116
|
||||
140,214,85,109
|
||||
148,221,94,102
|
||||
156,227,104,95
|
||||
165,233,114,87
|
||||
173,239,124,81
|
||||
181,243,135,74
|
||||
189,247,145,67
|
||||
197,250,158,59
|
||||
206,252,169,52
|
||||
214,253,181,46
|
||||
222,253,194,41
|
||||
230,252,208,37
|
||||
239,249,221,37
|
||||
247,245,235,39
|
||||
255,240,249,33
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'viridis' colormap from matplotlib
|
||||
0,68,1,84
|
||||
8,71,13,96
|
||||
16,72,24,106
|
||||
25,72,35,116
|
||||
33,71,46,124
|
||||
41,69,56,130
|
||||
49,66,65,134
|
||||
58,62,74,137
|
||||
66,58,84,140
|
||||
74,54,93,141
|
||||
82,50,101,142
|
||||
90,46,109,142
|
||||
99,43,117,142
|
||||
107,40,125,142
|
||||
115,37,132,142
|
||||
123,34,140,141
|
||||
132,31,148,140
|
||||
140,30,156,137
|
||||
148,32,163,134
|
||||
156,37,171,130
|
||||
165,46,179,124
|
||||
173,58,186,118
|
||||
181,72,193,110
|
||||
189,88,199,101
|
||||
197,108,205,90
|
||||
206,127,211,78
|
||||
214,147,215,65
|
||||
222,168,219,52
|
||||
230,192,223,37
|
||||
239,213,226,26
|
||||
247,234,229,26
|
||||
255,253,231,37
|
||||
Reference in New Issue
Block a user