Compare commits

...

25 Commits

Author SHA1 Message Date
gullradriel 2bb3b415d1 update submodule (#3199) 2026-05-30 20:31:01 +02:00
Pezsma 6e0394fb6b Filename scroll (#3193) 2026-05-30 08:24:20 +02:00
Totoo da21ea7866 Fix flashing (#3188)
Fixed some bugs
2026-05-30 01:49:49 +02:00
未来方舟 2c434946cd Display if IQ inverted in DFU overlay menu while the algo decides to (#3196)
* init

* expose more info

* copilot suggest
2026-05-29 12:06:17 +02:00
gullradriel f3aee44e3b update submodule (#3187)
* update submodule
* fix iwyu CI errors
* fix sd over usb with hackrf submodule si5351c refactored functions
2026-05-21 17:48:53 +02:00
未来方舟 165c190d92 Use html comments to make som text only visible by PR creator (#3186)
* Use html comments to make som text only visible by PR creator

* reformat a bit
2026-05-21 15:51:56 +02:00
Totoo 8f9246c1ea Enhance transmitter model and UI for better handling of TX and RF ampstates (#3185) 2026-05-21 15:50:47 +02:00
Pezsma f1d52ae78f expanding channels (#3183) 2026-05-21 15:50:10 +02:00
qwer123 865bdee9b3 add external clock detect method (#3180) 2026-05-20 22:40:39 +02:00
Totoo 314e910cbc make battleship smaller (#3181) 2026-05-20 18:39:21 +02:00
Pezsma ec6c7eff86 Hard reset app (#3182) 2026-05-20 18:38:13 +02:00
Pezsma 95b2e9fbc3 Charge sleep bug (#3177) 2026-05-15 09:24:30 +00:00
未来方舟 52ff442fcc Tune pull_request_template a bit (#3176) 2026-05-14 00:48:27 +08:00
gullradriel 95d246a020 Update hackrf submodule (#3174)
* updated submodule

* usb_serial: populate usb_device before usb_run. Upstream now leaves the global usb_device zero-initialized and expects hackrf_usb.c to memcpy a per-board const into it. We don't link hackrf_usb.c, so do the memcpy here. Also flip control_in to enable_zlp=true to match the new endpoint API.

* baseband: derive IS_* board defines from BOARD. Upstream moved platform IS_* macros from platform_detect.h to cmake. Set them per BOARD here since we don't include platform-detect.cmake. Drop the duplicate clkin.c entry.

* sd_over_usb: drop duplicate driver objects, fix setup callback. Upstream now defines i2c0/i2c1, spi_bus_ssp0/1, sgpio_config and rf_path in the common driver files, so stop redefining them here and patch the runtime fields in pin_setup() instead. Update si5351c_reset_pll() call to the two-arg form. Replace the no-op transceiver_usb_setup_complete stub with one that delegates to usb_setup_complete.

* sd_over_usb: populate usb_device before usb_run. Same fix as the application side: memcpy a const template into usb_device before usb_device_init/usb_run, otherwise the descriptor pointer is NULL. Also pass enable_zlp=true on control_in to match the new endpoint API.

* fix unicode in comment

* remove lz4 bufs reservation in M0
2026-05-13 14:53:54 +02:00
Frederic BORRY c34a6940ca EPIRB RX app rework + EPIRB TX app fixes (#3172)
* First step to EPIRB rx app fix
* Added spike filtering on carrier detection for more robustness.
* Created dedicated packet builder
* First step to fixing UI.
* First step to epirb-rx ui rework.
* Made static methods static members
* Code optim
* Code cleanup + first step to new UI
* Fixed display
* Added BeaconList widget + BeaconDB
* Epirb TX app memory optim
* Fixed OOM when opening Map Display or Locator editor on EPIRB TX app
* First step to tabs + code cleanup to free some space...
* Added spectrum analyzer + fixed display
* Fixed display refresh issue after leaving spectrum.
* More code optim + first step to detail view.
* Fixed float coord display.
* Fixed protocol names
* Fixed location display
* Externalized country base to sd card + more code optim
* Fixed country display
* Fixed path
* Added protocol description and resource manager
* Added main/aux loc device
* Removed specan to save space
* First step to adding QR code
* Used a custom TextArea component instead of Console
* Fixed QR code display for map URL
* Added beacon selection management + fixes
* First step to map/data qr.
* QR tab : added data + detail/map toggle
* Removed sqhelch to save space
* Removed audio to save space
* Removed freq label to save space
* Removed packet timestamp to save space
* Moved frequency values to resource file
* Fixed detection parameters to suite real beacons (slower phase shift time).
* Fixed countdown field size
* Fixed frequency list.
* Fixed detail view on select.
* First step to fix settings.
* Fixed squelch for a 0-99 range.
* Added bch code correction (single bit error).
* More code optim, disable country cache, removed inline from location to reduce code size.
* Removed inline, code formatting.
* Added bch code correction display.
* Fixed focus bug, fixed packet size check. Added BCH1/BCH2 correction test cases in BEACONS.TXT file for EPIRB TX application.
* Fixed merge
* Added countdown setting.
* Added selection display to beacon list
* Restored squelch control. Added countdown reset.
* Code optim on formatSummary
* More code optim
* Made ResourceManager methods static
* Externalized beacon strings to res file
*Removed frame parameter from methods
* Added test beacons for emergency. Removed old res file.
* Fixed resource manager
* Fixed resources. Added emergency display.
* BCH + res fixes
* Fixed beacons
* Fixed app color. Fixed HexId and Serial calculation.
* Fixed frequency list. Comments.
* Code cleanup and comments
* Added slideshow mode to EPIRB TX application. Code comments / cleanup.
* Fixed frame end after wrong code cleanup
* More code optim and comments. Removed touch from beacon list.
* Added beacon selection with encoder on detail view.
* Added beacon paging in header. Fixed timeout display
* Fixed build for PRALINE
* Fixed formatting
* Potential fix for pull request finding
* stop on carriage return
* empty string if unknown
* zeroing data buffer before use
* fix typo
* add include
* fix case if pair == 0
* fix scpectrum_on to spectrum_on
* fix typos
* replaced 3 inlined offset blocks with apply_offset calls
* set_beacon consolidation
2026-05-12 14:00:47 +02:00
Synray 33330e4c34 Use gradient for level view, fix height scaling (#3173)
* Use gradient for level view, fix height scaling
* Add RSSI indicators
2026-05-12 12:48:17 +02:00
Pezsma 66f1835232 Wait tx complete (#3165)
* Implement radio transmission drain mechanism and update shared memory structure
* don't wait for timeout if baseband already in shut down state
2026-05-12 12:45:02 +02:00
Synray 4dfbf666d0 Add more waterfalls (#3170) 2026-05-10 09:37:08 +02:00
Synray 245a36459f UI and bug fixes (#3164)
* Fix BLE RX RSSI and Channel scale

* Properly reset gradient index 0

* Use move constructor in MenuView::add_item

* Avoid erasing during iteration

* Fix gradient rounding. Extend to end of table

* Add comment to add_item
2026-05-10 00:08:05 +02:00
janhaverkamp 590f19062b Add support for larger battery capacities (#3168) 2026-05-09 20:54:25 +02:00
Totoo f38ef4ebc8 Added CU8 playback support (#3166) 2026-05-09 12:23:43 +02:00
gullradriel 900d2f2482 fix missing header (#3167) 2026-05-09 12:23:10 +02:00
Totoo 5aff5ac277 Fix text color multiline handling (#3161) 2026-05-08 09:57:06 +02:00
Totoo cf4d245964 Fixes #3159 (#3160) 2026-05-07 23:54:30 +08:00
Pezsma c7f7173958 sleep warning fix (#3162) 2026-05-07 15:26:34 +02:00
90 changed files with 6477 additions and 2857 deletions
+46 -22
View File
@@ -1,58 +1,80 @@
<!--
👋 Thank you for your contribution!
⚠️ Before you start, please review our Contributing Guidelines:
https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines
-->
## Brief description of what you did
<!--
Replace this line with a description of your changes. What problem does this PR solve? What does it add or fix?
Don't forget to describe how maintainers can test your modifications!
-->
<!-- Replace this line with a description of your changes. What problem does this PR solve? What does it add or fix?
Don't forget to describe how maintainers can test your modifications! -->
---
## Proof that your changes work
<!--
1. PRs without proof will not be merged. Please provide evidence for every section below that applies to your change.
2. Trusted contributors: If you have a track record of merged PRs that were properly tested and documented, maintainers may waive one or more of the proof sections below at their discretion. You are still encouraged to include whatever evidence you have, but sections may be marked `N/A — trusted contributor` with a short note.
3. First-time contributors must provide full proof. See the Contributing Guidelines(https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines) and Trusted Contributors (https://github.com/portapack-mayhem/mayhem-firmware/wiki/Trusted-Contributors) articles for details.
-->
> ⚠️ **PRs without proof will not be merged.** Please provide evidence for every section below that applies to your change.
>
> 🛈 **Trusted contributors:** If you have a track record of merged PRs that were properly tested and documented, maintainers may waive one or more of the proof sections below at their discretion. You are still encouraged to include whatever evidence you have, but sections may be marked `N/A — trusted contributor` with a short note. First-time contributors must provide full proof. See the [Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines) and [Trusted Contributors](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Trusted-Contributors) articles for details.
### 🖥️ Proof it compiles
<!--
Attach a log snippet showing a successful build from the final firmware size/space summary in the build output. Example: `Space remaining in flash ROM: 19552 bytes ( 1.9 % )`.
Paste log below.
-->
<!-- Paste log below -->
### 📱 Proof of testing on a real device
<!--
Attach photos, screenshots, or a short video of your changes running on actual PortaPack hardware. Emulator or simulator output is not a substitute for real-device testing.
Paste photo / video / screenshot below
-->
Attach photos, screenshots, or a short video of your changes running on actual PortaPack hardware. **Emulator or simulator output is not a substitute for real-device testing.**
<!-- Paste photo / video / screenshot below -->
### 📡 Proof against a real emitter/receiver (if applicable)
<!--
1. If your PR involves RX/TX, protocol decoding, signal generation, or any RF-related functionality, you **must** demonstrate it working against a real emitter or receiver. Include photos, videos, logs, or waterfall/spectrum screenshots.
2. If this section does not apply to your PR, write `N/A` below and briefly explain why.
3. Paste RF evidence, or write N/A with a short justification
-->
If your PR involves RX/TX, protocol decoding, signal generation, or any RF-related functionality, you **must** demonstrate it working against a real emitter or receiver. Include photos, videos, logs, or waterfall/spectrum screenshots.
If this section does not apply to your PR, write `N/A` below and briefly explain why.
<!-- Paste RF evidence, or write N/A with a short justification -->
---
## 📚 Wiki documentation commitment
**By submitting this PR, you acknowledge that once it is merged you are responsible for creating or updating the corresponding page on the [project wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki).**
<!--
By submitting this PR, you acknowledge that once it is merged you are responsible for creating or updating the corresponding page on the project wiki(https://github.com/portapack-mayhem/mayhem-firmware/wiki).
Your wiki article should include:
- A **screenshot of the main app screen** (strongly recommended)
- A clear **description** of what the app does
- An explanation of the **controls** and UI elements
- Any known **limitations**, quirks, or caveats
- A screenshot of the main app screen (strongly recommended)
- A clear description of what the app does
- An explanation of the controls and UI elements
- Any known limitations, quirks, or caveats
- Anything else a user should know to use the app effectively (dependencies, required hardware, file formats, etc.)
- If you are modifying an existing app, make sure the wiki page reflects your changes.
-->
- [ ] I will (or already) create(d) wiki document for my newly added feature
If you are modifying an existing app, make sure the wiki page reflects your changes.
---
@@ -64,4 +86,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)
+1
View File
@@ -332,6 +332,7 @@ set(CPPSRC
${CPLD_H4M_DATA_CPP}
${HACKRF_CPLD_DATA_CPP}
ui_external_items_menu_loader.cpp
view_factory_base.cpp
)
set_source_files_properties(${CPPSRC} PROPERTIES COMPILE_FLAGS -flto) # Add lto flag to the non-external sources only
+2 -2
View File
@@ -322,10 +322,10 @@ class BLERxView : public View {
{UI_POS_X(21), UI_POS_Y(0)}};
RSSI rssi{
{UI_POS_X(24), 0, UI_POS_WIDTH_REMAINING(6), 4}};
{UI_POS_X(24), 0, UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{
{UI_POS_X(24), 5, UI_POS_WIDTH_REMAINING(6), 4}};
{UI_POS_X(24), 5, UI_POS_WIDTH_REMAINING(24), 4}};
Labels label_sort{
{{UI_POS_X(0), UI_POS_Y(1)}, "Sort:", Theme::getInstance()->fg_light->foreground}};
+62 -53
View File
@@ -116,7 +116,7 @@ static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
break;
default:
freq_hz = UINT64_MAX;
freq_hz = 2'402'000'000ull; // Default to channel 37 frequency if invalid channel number
}
return freq_hz;
@@ -254,7 +254,7 @@ void BLETxView::send_packet() {
}
void BLETxView::start() {
if (file_path.empty()) {
if (file_path.empty() || num_packets == 0) {
file_error();
check_loop.set_value(false);
return;
@@ -266,6 +266,8 @@ void BLETxView::start() {
button_play.set_bitmap(&bitmap_stop);
is_running = true;
advCount = 0;
send_packet();
}
@@ -296,55 +298,61 @@ void BLETxView::on_timer() {
void BLETxView::on_tx_progress(const bool done, uint32_t progress) {
// TODO: make use of progress variable
(void)progress;
if (!is_active() || num_packets == 0) {
return;
}
if (done) {
if (is_active()) {
transmitter_model.disable();
if (auto_channel) {
switch (advCount) {
case 0:
channel_number = 37;
break;
case 1:
channel_number = 38;
break;
case 2:
channel_number = 39;
break;
}
field_frequency.set_value(get_freq_by_channel_number(channel_number));
advCount++;
if (advCount == 3) {
channel_number = 37;
packet_counter--;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
if (packet_counter > 0) packet_counter--;
packetDone = true;
advCount = 0;
} else {
channel_number = 37 + advCount;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
send_packet();
}
} else if (all_channels) {
advCount++;
if (advCount == 40) {
channel_number = 0;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
if (packet_counter > 0) packet_counter--;
packetDone = true;
advCount = 0;
} else {
channel_number = advCount;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
send_packet();
advCount++;
}
} else {
packet_counter--;
if (packet_counter > 0) packet_counter--;
packetDone = true;
}
}
}
// Reached end of current packet repeats.
if (packet_counter == 0) {
// Done sending all packets.
if (current_packet == (num_packets - 1)) {
current_packet = 0;
// Reached end of current packet repeats.
if (packet_counter == 0) {
// Done sending all packets.
current_packet++;
if (current_packet >= num_packets) {
current_packet = 0;
// If looping, restart from beginning.
if (check_loop.value()) {
update_current_packet(packets[current_packet], current_packet);
} else {
stop();
}
} else {
current_packet++;
// If looping, restart from beginning.
if (check_loop.value()) {
update_current_packet(packets[current_packet], current_packet);
} else {
stop();
}
} else {
update_current_packet(packets[current_packet], current_packet);
}
}
}
@@ -385,16 +393,20 @@ BLETxView::BLETxView(NavigationView& nav)
};
options_channel.on_change = [this](size_t, int32_t i) {
// If we selected Auto don't do anything and Auto will handle changing.
if (i == 40) {
auto_channel = true;
return;
auto_channel = (i == BLT_CHAN_AUTO);
all_channels = (i == BLT_CHAN_ALL);
advCount = 0;
if (auto_channel) {
channel_number = 37;
} else if (all_channels) {
channel_number = 0;
} else {
auto_channel = false;
channel_number = i;
}
field_frequency.set_value(get_freq_by_channel_number(i));
channel_number = i;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
};
options_speed.on_change = [this](size_t, int32_t i) {
@@ -403,7 +415,7 @@ BLETxView::BLETxView(NavigationView& nav)
};
options_speed.set_selected_index(0);
options_channel.set_selected_index(3);
options_channel.set_by_value(BLT_CHAN_AUTO);
check_rand_mac.set_value(false);
check_rand_mac.on_select = [this](Checkbox&, bool v) {
@@ -413,8 +425,7 @@ BLETxView::BLETxView(NavigationView& nav)
button_open.on_select = [this](Button&) {
auto open_view = nav_.push<FileLoadView>(".TXT");
open_view->on_changed = [this](std::filesystem::path new_file_path) {
on_file_changed(new_file_path);
this->on_file_changed(new_file_path);
nav_.set_on_pop([this]() { button_play.focus(); });
};
};
@@ -427,7 +438,7 @@ BLETxView::BLETxView(NavigationView& nav)
64,
ENTER_KEYBOARD_MODE_ALPHA,
[this](std::string& buffer) {
on_save_file(buffer);
this->on_save_file(buffer);
});
};
@@ -506,10 +517,10 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
}
do {
readUntil(data_file, packets[num_packets].macAddress, mac_address_size_str, ' ');
readUntil(data_file, packets[num_packets].advertisementData, max_packet_size_str, ' ');
readUntil(data_file, packets[num_packets].packetType, max_packet_type_str, ' ');
readUntil(data_file, packets[num_packets].packetCount, max_packet_repeat_str, '\n');
readUntil(data_file, packets[num_packets].macAddress, sizeof(packets[num_packets].macAddress), ' ');
readUntil(data_file, packets[num_packets].advertisementData, sizeof(packets[num_packets].advertisementData), ' ');
readUntil(data_file, packets[num_packets].packetType, sizeof(packets[num_packets].packetType), ' ');
readUntil(data_file, packets[num_packets].packetCount, sizeof(packets[num_packets].packetCount), '\n');
uint64_t macAddressSize = strlen(packets[num_packets].macAddress);
uint64_t advertisementDataSize = strlen(packets[num_packets].advertisementData);
@@ -542,10 +553,8 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
num_packets++;
} while (num_packets < max_num_packets);
update_current_packet(packets[0], 0);
data_file.close();
update_current_packet(packets[0], 0);
}
}
@@ -570,6 +579,9 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
std::vector<std::string> strings = splitIntoStrings(packet.advertisementData);
packet_counter = packet.packet_count;
current_packet = currentIndex;
text_packet_index.set(to_string_dec_uint(current_packet));
text_packets_sent.set(to_string_dec_uint(packet.packet_count));
@@ -578,9 +590,6 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
progressbar.set_max(packet.packet_count);
packet_counter = packet.packet_count;
current_packet = currentIndex;
dataFile.create(dataTempFilePath);
for (const std::string& str : strings) {
+49 -7
View File
@@ -45,6 +45,9 @@
#include <string>
#include <memory>
#define BLT_CHAN_AUTO 40
#define BLT_CHAN_ALL 41
class BLELoggerTx {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
@@ -94,7 +97,7 @@ struct BLETxPacket {
char advertisementData[63];
char packetCount[11];
char packetType[17];
uint32_t packet_count;
uint32_t packet_count; // repeat the same packet count times
PKT_TYPE pduType;
};
@@ -145,6 +148,7 @@ class BLETxView : public View {
std::filesystem::path packet_save_path{bletx_dir / u"BLETX_????.TXT"};
uint8_t channel_number = 37;
bool auto_channel = false;
bool all_channels = false;
char randomMac[13] = "010203040506";
@@ -156,10 +160,10 @@ class BLETxView : public View {
int16_t auto_channel_period{6};
bool repeatLoop = false;
uint32_t packet_counter{0};
uint32_t num_packets{0};
uint32_t current_packet{0};
uint8_t packetTxCount{0};
uint32_t packet_counter{0}; // the counter (decr) of the actual packet as it can be sent multiple times
uint32_t num_packets{0}; // how many packets in the file
uint32_t current_packet{0}; // the actual packet's num, i'm sending if i have multiple packets
uint8_t packetTxCount{0}; // not used
bool packetDone = false;
bool random_mac = false;
bool file_override = false;
@@ -241,10 +245,48 @@ class BLETxView : public View {
OptionsField options_channel{
{11 * 8, 6 * 8},
5,
{{"Ch.37 ", 37},
{{"Ch.0", 0},
{"Ch.1", 1},
{"Ch.2", 2},
{"Ch.3", 3},
{"Ch.4", 4},
{"Ch.5", 5},
{"Ch.6", 6},
{"Ch.7", 7},
{"Ch.8", 8},
{"Ch.9", 9},
{"Ch.10", 10},
{"Ch.11", 11},
{"Ch.12", 12},
{"Ch.13", 13},
{"Ch.14", 14},
{"Ch.15", 15},
{"Ch.16", 16},
{"Ch.17", 17},
{"Ch.18", 18},
{"Ch.19", 19},
{"Ch.20", 20},
{"Ch.21", 21},
{"Ch.22", 22},
{"Ch.23", 23},
{"Ch.24", 24},
{"Ch.25", 25},
{"Ch.26", 26},
{"Ch.27", 27},
{"Ch.28", 28},
{"Ch.29", 29},
{"Ch.30", 30},
{"Ch.31", 31},
{"Ch.32", 32},
{"Ch.33", 33},
{"Ch.34", 34},
{"Ch.35", 35},
{"Ch.36", 36},
{"Ch.37", 37},
{"Ch.38", 38},
{"Ch.39", 39},
{"Auto", 40}}};
{"Auto", BLT_CHAN_AUTO},
{"All", BLT_CHAN_ALL}}};
OptionsField options_adv_type{
{17 * 8, 6 * 8},
+6 -2
View File
@@ -103,7 +103,8 @@ DfuMenu2::DfuMenu2(NavigationView& nav)
&text_info_line_8,
&text_info_line_9,
&text_info_line_10,
&text_info_line_11});
&text_info_line_11,
&text_info_line_12});
}
void DfuMenu2::paint(Painter& painter) {
@@ -118,9 +119,12 @@ void DfuMenu2::paint(Painter& painter) {
text_info_line_9.set(to_string_dec_uint(portapack::transmitter_model.target_frequency(), 10));
text_info_line_10.set(to_string_dec_uint(portapack::transmitter_model.baseband_bandwidth(), 10));
text_info_line_11.set(to_string_dec_uint(portapack::transmitter_model.sampling_rate(), 10));
text_info_line_12.set(std::string("M") + (radio::get_mixer_invert() ? "Y" : "N") +
" B" + (radio::get_baseband_invert() ? "Y" : "N") +
" IQ" + ((radio::get_mixer_invert() ^ radio::get_baseband_invert()) ? "Y" : "N"));
constexpr auto margin = 5;
constexpr auto lines = 11 + 2;
constexpr auto lines = 12 + 2;
painter.fill_rectangle(
{{5 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin},
@@ -96,6 +96,7 @@ class DfuMenu2 : public View {
{{5 * CHARACTER_WIDTH, 13 * LINE_HEIGHT}, "TX Freq:", Theme::getInstance()->fg_darkcyan->foreground},
{{5 * CHARACTER_WIDTH, 14 * LINE_HEIGHT}, "TX BW:", Theme::getInstance()->fg_darkcyan->foreground},
{{5 * CHARACTER_WIDTH, 15 * LINE_HEIGHT}, "TX SampR:", Theme::getInstance()->fg_darkcyan->foreground},
{{5 * CHARACTER_WIDTH, 16 * LINE_HEIGHT}, "IQ Inv:", Theme::getInstance()->fg_darkcyan->foreground},
};
Text text_info_line_1{{14 * CHARACTER_WIDTH, 5 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
@@ -109,6 +110,7 @@ class DfuMenu2 : public View {
Text text_info_line_9{{14 * CHARACTER_WIDTH, 13 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_10{{14 * CHARACTER_WIDTH, 14 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_11{{14 * CHARACTER_WIDTH, 15 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_12{{(14 + 1) * CHARACTER_WIDTH, 16 * LINE_HEIGHT, (10 - 1) * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
};
} /* namespace ui */
+11 -4
View File
@@ -43,6 +43,7 @@ namespace ui {
static const fs::path txt_ext{u".TXT"};
static const fs::path ppl_ext{u".PPL"};
static const fs::path c8_ext{u".C8"};
static const fs::path cu8_ext{u".CU8"};
static const fs::path c16_ext{u".C16"};
static const fs::path cxx_ext{u".C*"};
static const fs::path png_ext{u".PNG"};
@@ -88,8 +89,11 @@ fs::path get_partner_file(fs::path path) {
path.replace_extension(txt_ext);
else if (path_iequal(ext, txt_ext)) {
path.replace_extension(c8_ext);
if (!fs::file_exists(path))
path.replace_extension(cu8_ext);
if (!fs::file_exists(path))
path.replace_extension(c16_ext);
} else
return {};
@@ -386,6 +390,7 @@ std::string get_stem(std::string t) {
return t.substr(0, index);
}
}
std::string get_filename(std::string _s) {
const auto index = _s.find_last_of("/");
if (index == _s.npos) {
@@ -394,6 +399,7 @@ std::string get_filename(std::string _s) {
return _s.substr(index + 1);
}
}
void FileManBaseView::refresh_list() {
if (on_refresh_widgets)
on_refresh_widgets(false);
@@ -404,8 +410,6 @@ void FileManBaseView::refresh_list() {
menu_view.clear();
for (const auto& entry : entry_list) {
auto entry_name = std::string{entry.path.length() <= max_filename_length ? entry.path : entry.path.substr(0, max_filename_length)};
if (entry.is_directory) {
std::string size_str{};
if (entry.path == str_next || entry.path == str_back) {
@@ -414,8 +418,10 @@ void FileManBaseView::refresh_list() {
size_str = (entry.path == parent_dir_path.string()) ? "" : to_string_dec_uint(file_count(current_path / entry.path));
}
// CRITICAL: Use "\t" (Tab) to separate the path and the size!
// Do not use spaces or padding here. The menu will handle the alignment.
menu_view.add_item(
{entry_name.substr(0, max_filename_length) + std::string((max_filename_length + 1) - entry_name.length(), ' ') + size_str,
{entry.path + "\t" + size_str,
Theme::getInstance()->fg_yellow->foreground,
&bitmap_icon_dir,
[this](KeyEvent key) {
@@ -427,8 +433,9 @@ void FileManBaseView::refresh_list() {
const auto& assoc = get_assoc(get_extension(entry.path));
auto size_str = to_string_file_size(entry.size);
// CRITICAL: Use "\t" (Tab) to separate the path and the size!
menu_view.add_item(
{entry_name.substr(0, max_filename_length) + std::string((max_filename_length + 1) - entry_name.length(), ' ') + size_str,
{entry.path + "\t" + size_str,
assoc.color,
assoc.icon,
[this](KeyEvent key) {
+1
View File
@@ -80,6 +80,7 @@ class FileManBaseView : public View {
{u".PNG", &bitmap_icon_file_image, ui::Color::green()},
{u".BMP", &bitmap_icon_file_image, ui::Color::green()},
{u".C8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".CU8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".C16", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".WAV", &bitmap_icon_file_wav, ui::Color::dark_magenta()},
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // Playlist/Replay
@@ -32,7 +32,7 @@ static const char* hackrf_magic = "HACKRFFW";
#define FIRST_CHECKSUM_NIGHTLY 240125
Thread* FlashUtilityView::thread{nullptr};
static constexpr size_t max_filename_length = 26;
static constexpr size_t max_filename_length = 60; // max length of filename
bool valid_firmware_file(std::filesystem::path::string_type path) {
File firmware_file;
+1
View File
@@ -172,6 +172,7 @@ void IQTrimView::update_range_controls(iq::TrimRange trim_range) {
void IQTrimView::profile_capture() {
power_buckets_.clear();
power_buckets_.resize(screen_width);
iq::PowerBuckets buckets{
.p = power_buckets_.data(),
.size = power_buckets_.size()};
@@ -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},
+8
View File
@@ -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");
+2
View File
@@ -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();
+44 -14
View File
@@ -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 -> XTALPLLB -> 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
-8
View File
@@ -268,17 +268,11 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
// Save interrupt states before mass disable
uint32_t saved_iser0 = NVIC->ISER[0];
uint32_t saved_iser1 = NVIC->ISER[1];
uint32_t saved_iser2 = NVIC->ISER[2];
// Disable and clear all pending interrupts
NVIC->ICER[0] = 0xFFFFFFFF;
NVIC->ICER[1] = 0xFFFFFFFF;
NVIC->ICER[2] = 0xFFFFFFFF;
NVIC->ICPR[0] = 0xFFFFFFFF;
NVIC->ICPR[1] = 0xFFFFFFFF;
NVIC->ICPR[2] = 0xFFFFFFFF;
// Re-enable only necessary wakeup sources
NVIC_EnableIRQ(RTC_IRQn);
@@ -326,8 +320,6 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
// Restore original interrupt enable states
NVIC->ISER[0] = saved_iser0;
NVIC->ISER[1] = saved_iser1;
NVIC->ISER[2] = saved_iser2;
} // End of while(1) deep sleep loop
} else {
+89 -105
View File
@@ -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"
+59
View File
@@ -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
View File
@@ -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__
+95
View File
@@ -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
+56
View File
@@ -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
View File
@@ -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
View File
@@ -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
+76
View File
@@ -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
View File
@@ -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,
+100
View File
@@ -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__
+143
View File
@@ -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__*/
File diff suppressed because it is too large Load Diff
+323 -296
View File
@@ -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
View File
@@ -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__*/
File diff suppressed because it is too large Load Diff
+417 -404
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+87
View File
@@ -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__
@@ -193,15 +193,12 @@ void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile
auto reader = FileLineReader(playlist_file);
for (const auto& line : reader) {
// remove empty lines
if (line == "\n" || line == "\r\n" || line == "\r") continue;
profile_levels.push_back(line);
}
for (auto& line : profile_levels) {
// remove empty lines
if (line == "\n" || line == "\r\n" || line == "\r") {
profile_levels.erase(std::remove(profile_levels.begin(), profile_levels.end(), line), profile_levels.end());
}
// remove line end \n etc
if (line.length() > 0 && (line[line.length() - 1] == '\n' || line[line.length() - 1] == '\r')) {
line = line.substr(0, line.length() - 1);
+4 -1
View File
@@ -30,6 +30,7 @@
namespace fs = std::filesystem;
static const fs::path c8_ext{u".C8"};
static const fs::path cu8_ext{u".CU8"};
static const fs::path c16_ext{u".C16"};
Optional<File::Error> File::open_fatfs(const std::filesystem::path& filename, BYTE mode) {
@@ -543,12 +544,14 @@ bool path_iequal(
bool is_cxx_capture_file(const path& filename) {
auto ext = filename.extension();
return path_iequal(c8_ext, ext) || path_iequal(c16_ext, ext);
return path_iequal(c8_ext, ext) || path_iequal(cu8_ext, ext) || path_iequal(c16_ext, ext);
}
uint8_t capture_file_sample_size(const path& filename) {
if (path_iequal(filename.extension(), c8_ext))
return sizeof(complex8_t);
if (path_iequal(filename.extension(), cu8_ext))
return sizeof(complexu8_t);
if (path_iequal(filename.extension(), c16_ext))
return sizeof(complex16_t);
return 0;
+18 -10
View File
@@ -28,13 +28,11 @@ namespace fs = std::filesystem;
const std::filesystem::path default_gradient_file = u"waterfall.txt";
Gradient::Gradient() {
prev_index = 0;
prev_r = 0;
prev_g = 0;
prev_b = 0;
step(0, 0, 0, 0);
}
void Gradient::set_default() {
step(0, 0, 0, 0);
step(86, 0, 0, 255);
step(171, 0, 255, 0);
step(255, 255, 0, 0);
@@ -47,6 +45,9 @@ bool Gradient::load_file(const std::filesystem::path& file_path) {
if (error)
return false;
// make sure the table starts at 0
step(0, 0, 0, 0);
auto reader = FileLineReader(gradient_file);
for (const auto& line : reader) {
if (line.length() == 0 || line[0] == '#')
@@ -73,17 +74,24 @@ bool Gradient::load_file(const std::filesystem::path& file_path) {
}
}
// extend the gradient from the current index to the end of the table
step(255, prev_r, prev_g, prev_b);
return true;
}
void Gradient::step(int16_t index, int16_t r, int16_t g, int16_t b) {
for (int16_t i = prev_index; i <= index; i++) {
float x = (float)(i - prev_index) / (index - prev_index);
float y = 1.0f - x;
// prevent division by 0 if index == prev_index
int16_t range = index - prev_index;
if (range == 0) range = 1;
for (int16_t i = prev_index; i <= index; i++) {
int16_t t = i - prev_index;
int16_t new_r = prev_r + (t * (r - prev_r) + range / 2) / range;
int16_t new_g = prev_g + (t * (g - prev_g) + range / 2) / range;
int16_t new_b = prev_b + (t * (b - prev_b) + range / 2) / range;
int16_t new_r = prev_r * y + r * x;
int16_t new_g = prev_g * y + g * x;
int16_t new_b = prev_b * y + b * x;
if (i <= 255) lut[i] = ui::Color(new_r, new_g, new_b);
}
+23 -1
View File
@@ -24,6 +24,7 @@
namespace fs = std::filesystem;
static const fs::path c8_ext = u".C8";
static const fs::path cu8_ext = u".CU8";
namespace file_convert {
@@ -59,21 +60,42 @@ void c8_to_c16(const void* buffer, File::Size bytes) {
}
}
// Convert cu8 buffer to c16 buffer.
// Same buffer used for input & output; input size is bytes; output size is 2*bytes.
void cu8_to_c16(const void* buffer, File::Size bytes) {
complexu8_t* src = (complexu8_t*)buffer;
complex16_t* dest = (complex16_t*)buffer;
uint32_t i = bytes / sizeof(complexu8_t);
if (i != 0) {
do {
i--;
int16_t re_out = ((int16_t)src[i].real() - 128) * 256;
int16_t im_out = ((int16_t)src[i].imag() - 128) * 256;
dest[i] = {(int16_t)re_out, (int16_t)im_out};
} while (i != 0);
}
}
} /* namespace file_convert */
// Automatically enables C8/C16 conversion based on file extension
Optional<File::Error> FileConvertReader::open(const std::filesystem::path& filename) {
convert_c8_to_c16 = path_iequal(filename.extension(), c8_ext);
convert_cu8_to_c16 = path_iequal(filename.extension(), cu8_ext);
return file_.open(filename);
}
// If C8 conversion enabled, half the number of bytes are read from the file & expanded to fill the whole buffer.
File::Result<File::Size> FileConvertReader::read(void* const buffer, const File::Size bytes) {
auto read_result = file_.read(buffer, convert_c8_to_c16 ? bytes / 2 : bytes);
auto read_result = file_.read(buffer, (convert_c8_to_c16 || convert_cu8_to_c16) ? bytes / 2 : bytes);
if (read_result.is_ok()) {
if (convert_c8_to_c16) {
file_convert::c8_to_c16(buffer, read_result.value());
read_result = read_result.value() * 2;
} else if (convert_cu8_to_c16) {
file_convert::cu8_to_c16(buffer, read_result.value());
read_result = read_result.value() * 2;
}
bytes_read_ += read_result.value();
}
+1
View File
@@ -52,6 +52,7 @@ class FileConvertReader : public stream::Reader {
const File& file() const& { return file_; }
bool convert_c8_to_c16{};
bool convert_cu8_to_c16{};
protected:
File file_{};
+22 -1
View File
@@ -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>
@@ -404,6 +404,14 @@ void set_antenna_bias(const bool on) {
#endif
}
bool get_mixer_invert() {
return mixer_invert;
}
bool get_baseband_invert() {
return baseband_invert;
}
void set_tx_max283x_iq_phase_calibration(const size_t v) {
second_if->set_tx_LO_iq_phase_calibration(v);
}
@@ -413,6 +421,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
+2
View File
@@ -54,6 +54,8 @@ void set_baseband_filter_bandwidth_rx(const uint32_t bandwidth_minimum);
void set_baseband_filter_bandwidth_tx(const uint32_t bandwidth_minimum);
void set_baseband_rate(const uint32_t rate);
void set_antenna_bias(const bool on);
bool get_mixer_invert();
bool get_baseband_invert();
void set_tx_max283x_iq_phase_calibration(const size_t v);
void set_rx_max283x_iq_phase_calibration(const size_t v);
+14 -2
View File
@@ -72,19 +72,31 @@ void TransmitterModel::set_channel_bandwidth(uint32_t v) {
}
uint8_t TransmitterModel::tx_gain() const {
if (persistent_memory::config_tx_disabled()) {
return 0;
}
return std::min(settings_.tx_gain_db, portapack::persistent_memory::config_tx_gain_max_db());
}
void TransmitterModel::set_tx_gain(uint8_t v_db) {
settings_.tx_gain_db = v_db;
uint8_t newgain = v_db;
if (persistent_memory::config_tx_disabled()) {
newgain = 0;
} else if (v_db > portapack::persistent_memory::config_tx_gain_max_db()) {
newgain = portapack::persistent_memory::config_tx_gain_max_db();
}
settings_.tx_gain_db = newgain;
update_tx_gain();
}
bool TransmitterModel::rf_amp() const {
return settings_.rf_amp && !portapack::persistent_memory::config_tx_amp_disabled();
return settings_.rf_amp && !portapack::persistent_memory::config_tx_amp_disabled() && !portapack::persistent_memory::config_tx_disabled();
}
void TransmitterModel::set_rf_amp(bool enabled) {
if (enabled && (portapack::persistent_memory::config_tx_amp_disabled() || portapack::persistent_memory::config_tx_disabled())) {
return;
}
settings_.rf_amp = enabled;
update_rf_amp();
}
+2 -7
View File
@@ -28,7 +28,6 @@
#include "rtc_time.hpp"
#include "sd_card.hpp"
#include <algorithm>
#include "ui_external_items_menu_loader.hpp"
namespace ui {
@@ -61,7 +60,6 @@ BtnGridView::BtnGridView(
}
BtnGridView::~BtnGridView() {
ExternalItemsMenuLoader::unload_external_items();
}
void BtnGridView::set_max_rows(int rows) {
@@ -89,7 +87,6 @@ void BtnGridView::set_parent_rect(const Rect new_parent_rect) {
remove_child(item.get());
menu_item_views.clear();
menu_item_views.shrink_to_fit();
}
button_w = screen_width / rows_;
@@ -138,17 +135,15 @@ void BtnGridView::set_arrow_down_enabled(bool enabled) {
};
void BtnGridView::clear() {
// clear vector and release memory, not using swap since it's causing capture to glitch/fault
// clear vector, not using swap since it's causing capture to glitch/fault
menu_items.clear();
menu_items.shrink_to_fit();
// TODO(u-foka): Clean up my mess, move this somewhere to clear memory when the view is not visible, but not to be confused with clearing the menu items...
for (auto& item : menu_item_views)
remove_child(item.get());
// clear vector and release memory, not using swap since it's causing capture to glitch/fault
// clear vector, not using swap since it's causing capture to glitch/fault
menu_item_views.clear();
menu_item_views.shrink_to_fit();
}
void BtnGridView::add_items(std::initializer_list<GridItem> new_items, bool inhibit_update) {
+97 -24
View File
@@ -22,6 +22,7 @@
#include "ui_menu.hpp"
#include "rtc_time.hpp"
#include <string_view>
namespace ui {
@@ -29,57 +30,112 @@ namespace ui {
void MenuItemView::set_item(MenuItem* item_) {
item = item_;
scroll_offset = 0;
can_scroll = false;
}
void MenuItemView::highlight() {
set_highlighted(true);
scroll_offset = 0;
set_dirty();
}
void MenuItemView::unhighlight() {
set_highlighted(false);
scroll_offset = 0;
set_dirty();
}
void MenuItemView::paint(Painter& painter) {
Coord offset_x{};
void MenuItemView::set_scroll_offset(size_t offset) {
if (can_scroll && scroll_offset != offset) {
scroll_offset = offset;
set_dirty();
}
}
void MenuItemView::paint(Painter& painter) {
if (!item) return;
const auto r = screen_rect();
const auto rect = screen_rect();
const auto paint_style = (highlighted() && (parent()->has_focus() || keep_highlight)) ? style().invert() : style();
const auto font_height = paint_style.font.line_height();
const int char_width = paint_style.font.char_width();
const int line_height = paint_style.font.line_height();
if (char_width == 0 || line_height == 0) return;
const int margin_x = char_width / 2;
ui::Color final_item_color = (highlighted() && (parent()->has_focus() || keep_highlight)) ? paint_style.foreground : item->color;
ui::Color final_bg_color = (highlighted() && (parent()->has_focus() || keep_highlight)) ? item->color : paint_style.background;
if (final_item_color.v == final_bg_color.v) final_item_color = paint_style.foreground;
painter.fill_rectangle(
r,
final_bg_color);
painter.fill_rectangle(rect, final_bg_color);
Coord offset_x = 0;
if (item->bitmap) {
painter.draw_bitmap(
{r.location().x() + 4, r.location().y() + 4},
{rect.location().x() + 4, rect.location().y() + 4},
*item->bitmap,
final_item_color,
final_bg_color);
offset_x = 26;
} else
} else {
offset_x = 0;
}
Style text_style{
.font = paint_style.font,
.background = final_bg_color,
.foreground = final_item_color};
Style text_style{.font = paint_style.font, .background = final_bg_color, .foreground = final_item_color};
painter.draw_string(
{r.location().x() + offset_x, r.location().y() + (r.size().height() - font_height) / 2},
text_style,
item->text);
std::string_view full_text = item->text;
std::string_view file_name = full_text;
std::string_view file_size_text = "";
auto tab_pos = full_text.find('\t');
if (tab_pos != std::string_view::npos) {
file_size_text = full_text.substr(tab_pos + 1);
file_name = full_text.substr(0, tab_pos);
}
int available_width_px = rect.width() - offset_x;
if (available_width_px <= 0) return;
size_t max_name_chars = available_width_px / char_width;
if (max_name_chars == 0) return;
if (!file_size_text.empty() && max_name_chars > file_size_text.length()) {
max_name_chars = max_name_chars - file_size_text.length() - 1;
}
if (max_name_chars == 0) return;
can_scroll = (file_name.length() > max_name_chars);
std::string_view display_name = file_name;
if (file_name.length() > max_name_chars) {
if (highlighted()) {
size_t max_scroll = file_name.length() - max_name_chars;
size_t actual_offset = scroll_offset % (max_scroll + 1);
display_name = file_name.substr(actual_offset, max_name_chars);
} else {
display_name = file_name.substr(0, max_name_chars);
}
}
Coord text_y = rect.location().y() + (rect.height() - line_height) / 2;
painter.draw_string({rect.location().x() + offset_x, text_y}, text_style, display_name);
if (!file_size_text.empty()) {
int file_size_width = static_cast<int>(file_size_text.length()) * char_width;
int file_size_x = rect.width() - file_size_width - margin_x;
// Csak akkor rajzoljuk ki, ha pozitív koordinátára esik és nem takarja el az ikont/nevet
if (file_size_x > offset_x) {
painter.draw_string({rect.location().x() + static_cast<Coord>(file_size_x), text_y}, text_style, file_size_text);
}
}
}
/* MenuView **************************************************************/
@@ -134,6 +190,20 @@ void MenuView::set_parent_rect(const Rect new_parent_rect) {
update_items();
}
void MenuView::increment_scroll() {
if (menu_items.empty()) return;
const size_t view_index = highlighted_item - offset;
if (view_index >= menu_item_views.size()) {
return;
}
// The MenuView checks if the currently highlighted item needs scrolling
scroll_offset++;
auto* view = item_view(view_index);
if (view) {
view->set_scroll_offset(scroll_offset);
}
}
void MenuView::on_tick_second() {
if (more && blink)
arrow_more.set_foreground(Theme::getInstance()->bg_darkest->foreground);
@@ -141,8 +211,9 @@ void MenuView::on_tick_second() {
arrow_more.set_foreground(Theme::getInstance()->bg_darkest->background);
blink = !blink;
arrow_more.set_dirty();
increment_scroll();
}
void MenuView::clear() {
@@ -153,22 +224,22 @@ void MenuView::clear() {
menu_items.shrink_to_fit();
highlighted_item = 0;
offset = 0;
reset_scroll();
}
size_t MenuView::item_count() const {
return menu_items.size();
}
void MenuView::add_item(MenuItem new_item) {
menu_items.push_back(new_item);
void MenuView::add_item(MenuItem&& new_item) {
menu_items.push_back(std::move(new_item));
update_items();
}
void MenuView::add_items(std::initializer_list<MenuItem> new_items) {
for (auto item : new_items) {
add_item(item);
}
menu_items.insert(menu_items.end(), new_items);
update_items();
}
void MenuView::update_items() {
@@ -226,6 +297,8 @@ bool MenuView::set_highlighted(int32_t new_value) {
item_view(highlighted_item - offset)->highlight();
}
reset_scroll();
if (on_highlight)
on_highlight();
+14 -1
View File
@@ -67,9 +67,13 @@ class MenuItemView : public Widget {
void highlight();
void unhighlight();
void set_scroll_offset(size_t offset);
private:
MenuItem* item{nullptr};
bool keep_highlight = false;
size_t scroll_offset{0};
bool can_scroll{false};
};
class MenuView : public View {
@@ -83,7 +87,10 @@ class MenuView : public View {
~MenuView();
void add_item(MenuItem new_item);
// This function takes a temporary to avoid copies and heap allocations.
// To pass a variable 'item', use add_item(std::move(item)),
// or construct the MenuItem in the call, add_item(MenuItem{...})
void add_item(MenuItem&& new_item);
void add_items(std::initializer_list<MenuItem> new_items);
void clear();
size_t item_count() const;
@@ -105,6 +112,10 @@ class MenuView : public View {
void update_items();
void on_tick_second();
// SCROLLING
void increment_scroll();
inline void reset_scroll() { scroll_offset = 0; }
bool keep_highlight{false};
SignalToken signal_token_tick_second{};
@@ -123,6 +134,8 @@ class MenuView : public View {
size_t displayed_max{0};
size_t highlighted_item{0};
size_t offset{0};
size_t scroll_offset{0};
};
} /* namespace ui */
+63 -7
View File
@@ -39,7 +39,13 @@ using namespace portapack;
namespace ui {
/* Gets a style indicating total TX gain level. */
static const Style* get_style_for_gain(uint8_t tot_gain) {
static const Style* get_style_for_gain(uint8_t tot_gain, uint8_t gain) {
if (persistent_memory::config_tx_disabled()) {
return Theme::getInstance()->fg_dark; // disabled color
}
if (gain == persistent_memory::config_tx_gain_max_db() && gain != max2837::tx::gain_db_range.maximum) { // only if limited
return Theme::getInstance()->fg_cyan;
}
if (tot_gain > POWER_THRESHOLD_HIGH) return Theme::getInstance()->fg_red;
if (tot_gain > POWER_THRESHOLD_MED)
@@ -50,7 +56,21 @@ static const Style* get_style_for_gain(uint8_t tot_gain) {
return nullptr; // Uses default.
}
static const Style* get_style_for_amp(uint8_t tot_gain) {
if (persistent_memory::config_tx_disabled() || persistent_memory::config_tx_amp_disabled()) {
return Theme::getInstance()->fg_dark; // disabled color
}
if (tot_gain > POWER_THRESHOLD_HIGH) return Theme::getInstance()->fg_red;
if (tot_gain > POWER_THRESHOLD_MED)
return Theme::getInstance()->fg_orange;
if (tot_gain > POWER_THRESHOLD_LOW)
return Theme::getInstance()->fg_yellow;
return nullptr; // Uses default.
}
/* TransmitterView *******************************************************/
void TransmitterView::paint(Painter& painter) {
@@ -95,12 +115,12 @@ void TransmitterView::on_tx_amp_changed(bool rf_amp) {
void TransmitterView::update_gainlevel_styles() {
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
auto style = get_style_for_gain(tot_gain);
auto style = get_style_for_gain(tot_gain, transmitter_model.tx_gain());
auto styleamp = get_style_for_amp(tot_gain);
field_gain.set_style(style);
text_gain.set_style(style);
field_amp.set_style(style);
text_amp.set_style(style);
field_amp.set_style(styleamp);
text_amp.set_style(styleamp);
}
void TransmitterView::set_transmitting(const bool transmitting) {
@@ -199,6 +219,24 @@ TransmitterView::TransmitterView(
on_start();
}
};
if (persistent_memory::config_tx_amp_disabled() || persistent_memory::config_tx_disabled()) { // amp disabled / tx disabled
field_amp.set_value(0);
field_amp.set_focusable(false);
}
if (persistent_memory::config_tx_disabled()) { // tx disabled
field_gain.set_value(0);
field_gain.set_focusable(false);
} else {
if (persistent_memory::config_tx_gain_max_db() < max2837::tx::gain_db_range.maximum) {
field_gain.set_range(max2837::tx::gain_db_range.minimum, persistent_memory::config_tx_gain_max_db());
if (field_gain.value() > persistent_memory::config_tx_gain_max_db()) {
field_gain.set_value(persistent_memory::config_tx_gain_max_db());
}
}
}
update_gainlevel_styles();
}
TransmitterView::~TransmitterView() {
@@ -252,16 +290,34 @@ TransmitterView2::TransmitterView2(Point pos, bool short_ui) {
update_gainlevel_styles();
};
if (persistent_memory::config_tx_amp_disabled() || persistent_memory::config_tx_disabled()) { // amp disabled / tx disabled
field_amp.set_value(0);
field_amp.set_focusable(false);
}
if (persistent_memory::config_tx_disabled()) { // tx disabled
field_gain.set_value(0);
field_gain.set_focusable(false);
} else {
if (persistent_memory::config_tx_gain_max_db() < max2837::tx::gain_db_range.maximum) {
field_gain.set_range(max2837::tx::gain_db_range.minimum, persistent_memory::config_tx_gain_max_db());
if (field_gain.value() > persistent_memory::config_tx_gain_max_db()) {
field_gain.set_value(persistent_memory::config_tx_gain_max_db());
}
}
}
update_gainlevel_styles();
}
void TransmitterView2::update_gainlevel_styles() {
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
auto style = get_style_for_gain(tot_gain);
auto style = get_style_for_gain(tot_gain, transmitter_model.tx_gain());
auto styleamp = get_style_for_amp(tot_gain);
text_labels.set_style(style);
field_gain.set_style(style);
field_amp.set_style(style);
field_amp.set_style(styleamp);
}
} /* namespace ui */
@@ -9,13 +9,7 @@
namespace ui {
/* static */ std::vector<std::unique_ptr<DynamicBitmap<16, 16>>> ExternalItemsMenuLoader::bitmaps;
// to save ram when entering an app
void ExternalItemsMenuLoader::unload_external_items() {
bitmaps.clear();
bitmaps.shrink_to_fit();
}
/* static */ std::vector<DynamicBitmap<16, 16>> ExternalItemsMenuLoader::bitmaps;
// iterates over all possible ext apps-s, and if it is runnable on the current system, it'll call the callback, and pass minimal info. used to print to console, and for autostart setting's app list. where the minimal info is enough
// please keep in sync with load_external_items
@@ -103,9 +97,8 @@ void ExternalItemsMenuLoader::unload_external_items() {
}
}
std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_external_items(app_location_t app_location, NavigationView& nav) {
/* static */ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_external_items(app_location_t app_location, NavigationView& nav) {
bitmaps.clear();
bitmaps.shrink_to_fit();
std::vector<GridItemEx> external_apps;
@@ -133,8 +126,8 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
gridItem.color = Color((uint16_t)appInfo->icon_color);
auto dyn_bmp = std::make_unique<DynamicBitmap<16, 16>>(appInfo->bitmap_data);
gridItem.bitmap = dyn_bmp->bitmap();
auto dyn_bmp = DynamicBitmap<16, 16>{appInfo->bitmap_data};
gridItem.bitmap = dyn_bmp.bitmap();
bitmaps.push_back(std::move(dyn_bmp));
gridItem.on_select = [&nav, appInfo, i]() {
@@ -211,8 +204,8 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
if (versionMatches) {
gridItem.color = Color((uint16_t)application_information.icon_color);
auto dyn_bmp = std::make_unique<DynamicBitmap<16, 16>>(application_information.bitmap_data);
gridItem.bitmap = dyn_bmp->bitmap();
auto dyn_bmp = DynamicBitmap<16, 16>{application_information.bitmap_data};
gridItem.bitmap = dyn_bmp.bitmap();
bitmaps.push_back(std::move(dyn_bmp));
gridItem.on_select = [&nav, app_location, filePath]() {
@@ -262,8 +255,8 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
gridItem.color = Color((uint16_t)application_information.icon_color);
auto dyn_bmp = std::make_unique<DynamicBitmap<16, 16>>(application_information.bitmap_data);
gridItem.bitmap = dyn_bmp->bitmap();
auto dyn_bmp = DynamicBitmap<16, 16>{application_information.bitmap_data};
gridItem.bitmap = dyn_bmp.bitmap();
bitmaps.push_back(std::move(dyn_bmp));
gridItem.on_select = [&nav, app_location, filePath]() {
@@ -357,8 +350,6 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
if (checksum != EXT_APP_EXPECTED_CHECKSUM)
return false;
nav.pop();
nav.set_last_menu_went_deeper(true);
application_information.externalAppEntry(nav);
return true;
}
@@ -395,8 +386,7 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
}
}
nav.set_last_menu_went_deeper(true);
nav.replace<StandaloneView>(app_image);
nav.push<StandaloneView>(app_image);
return true;
}
@@ -410,8 +400,7 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
}
}
nav.set_last_menu_went_deeper(true);
nav.replace<StandaloneView>(app_image);
nav.push<StandaloneView>(app_image);
return true;
}
@@ -33,38 +33,24 @@
#define EXT_APP_EXPECTED_CHECKSUM 0x00000000
namespace ui {
template <size_t Width, size_t Height>
class DynamicBitmap {
public:
static constexpr size_t buffer_size = Width * Height / (sizeof(uint8_t) * 8);
static constexpr size_t buffer_size = Width * Height / (sizeof(uint8_t) * 8); // one bit per pixel
// Main constructor
DynamicBitmap(const uint8_t data[buffer_size])
: _bitmap{{Width, Height}, _buffer.data()} {
std::memcpy(_buffer.data(), data, buffer_size);
: _buffer(buffer_size, 0),
_bitmap{new Bitmap{{Width, Height}, &_buffer[0]}} {
memcpy(&_buffer[0], data, buffer_size);
}
DynamicBitmap(const DynamicBitmap& other)
: _buffer(other._buffer),
_bitmap{{Width, Height}, _buffer.data()} {}
DynamicBitmap(DynamicBitmap&& other) noexcept
: _buffer(std::move(other._buffer)),
_bitmap{{Width, Height}, _buffer.data()} {}
DynamicBitmap& operator=(const DynamicBitmap& other) = delete;
DynamicBitmap& operator=(DynamicBitmap&& other) noexcept = delete;
// Destructor (Default is fine, no heap memory to free manually)
~DynamicBitmap() = default;
const Bitmap* bitmap() const { return &_bitmap; }
const Bitmap* bitmap() { return _bitmap.get(); }
private:
// Order matters: _buffer must be declared before _bitmap
// so it is initialized first and its .data() pointer is valid.
std::array<uint8_t, buffer_size> _buffer{};
Bitmap _bitmap{};
// Allocating both members so the class is movable without invalidation.
std::vector<uint8_t> _buffer;
std::unique_ptr<Bitmap> _bitmap;
};
class ExternalItemsMenuLoader {
@@ -79,10 +65,9 @@ class ExternalItemsMenuLoader {
static bool run_standalone_app(ui::NavigationView&, std::filesystem::path);
static bool run_module_app(ui::NavigationView&, uint8_t*, size_t);
static void load_all_external_items_callback(std::function<void(AppInfoConsole&)> callback, bool module_included = false);
static void unload_external_items();
private:
static std::vector<std::unique_ptr<DynamicBitmap<16, 16>>> bitmaps;
static std::vector<DynamicBitmap<16, 16>> bitmaps;
};
} // namespace ui
+40 -77
View File
@@ -85,47 +85,44 @@ namespace ui {
// TODO(u-foka): Check consistency of command names (where we add rx/tx postfix)
const NavigationView::AppList NavigationView::appList = {
/* HOME ******************************************************************/
{nullptr, "Receive", HOME, Color::cyan(), &bitmap_icon_receivers, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<ReceiversMenuView>(nav); }},
{nullptr, "Transmit", HOME, Color::cyan(), &bitmap_icon_transmit, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<TransmittersMenuView>(nav); }},
{nullptr, "Transceiver", HOME, Color::cyan(), &bitmap_icon_transceivers, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<TransceiversMenuView>(nav); }},
{"recon", "Recon", HOME, Color::green(), &bitmap_icon_scanner, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<ReconView>(nav); }},
{"capture", "Capture", HOME, Color::red(), &bitmap_icon_capture, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<CaptureAppView>(nav); }},
{"replay", "Replay", HOME, Color::green(), &bitmap_icon_replay, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<PlaylistView>(nav); }},
{"lookingglass", "Looking Glass", HOME, Color::green(), &bitmap_icon_looking, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<GlassView>(nav); }},
{nullptr, "Utilities", HOME, Color::cyan(), &bitmap_icon_utilities, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<UtilitiesMenuView>(nav); }},
{nullptr, "Games", HOME, Color::cyan(), &bitmap_icon_games, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<GamesMenuView>(nav); }},
{nullptr, "Settings", HOME, Color::cyan(), &bitmap_icon_setup, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<SettingsMenuView>(nav); }},
{nullptr, "Receive", HOME, Color::cyan(), &bitmap_icon_receivers, new ViewFactory<ReceiversMenuView>()},
{nullptr, "Transmit", HOME, Color::cyan(), &bitmap_icon_transmit, new ViewFactory<TransmittersMenuView>()},
{nullptr, "Transceiver", HOME, Color::cyan(), &bitmap_icon_transceivers, new ViewFactory<TransceiversMenuView>()},
{"recon", "Recon", HOME, Color::green(), &bitmap_icon_scanner, new ViewFactory<ReconView>()},
{"capture", "Capture", HOME, Color::red(), &bitmap_icon_capture, new ViewFactory<CaptureAppView>()},
{"replay", "Replay", HOME, Color::green(), &bitmap_icon_replay, new ViewFactory<PlaylistView>()},
{"lookingglass", "Looking Glass", HOME, Color::green(), &bitmap_icon_looking, new ViewFactory<GlassView>()},
{nullptr, "Utilities", HOME, Color::cyan(), &bitmap_icon_utilities, new ViewFactory<UtilitiesMenuView>()},
{nullptr, "Games", HOME, Color::cyan(), &bitmap_icon_games, new ViewFactory<GamesMenuView>()},
{nullptr, "Settings", HOME, Color::cyan(), &bitmap_icon_setup, new ViewFactory<SettingsMenuView>()},
/* RX ********************************************************************/
{"adsbrx", "ADS-B", RX, Color::green(), &bitmap_icon_adsb, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<ADSBRxView>(nav); }},
{"ais", "AIS Boats", RX, Color::green(), &bitmap_icon_ais, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<AISAppView>(nav); }},
{"aprsrx", "APRS", RX, Color::green(), &bitmap_icon_aprs, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<APRSRXView>(nav); }},
{"audio", "Audio", RX, Color::green(), &bitmap_icon_speaker, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<AnalogAudioView>(nav); }},
{"blerx", "BLE Rx", RX, Color::green(), &bitmap_icon_btle, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<BLERxView>(nav); }},
{"pocsag", "POCSAG", RX, Color::green(), &bitmap_icon_pocsag, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<POCSAGAppView>(nav); }},
{"radiosonde", "Radiosnde", RX, Color::green(), &bitmap_icon_sonde, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<SondeView>(nav); }},
{"search", "Search", RX, Color::yellow(), &bitmap_icon_search, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<SearchView>(nav); }},
{"subghzd", "SubGhzD", RX, Color::yellow(), &bitmap_icon_remote, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<SubGhzDView>(nav); }},
{"weather", "Weather", RX, Color::green(), &bitmap_icon_thermometer, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<WeatherView>(nav); }},
{"adsbrx", "ADS-B", RX, Color::green(), &bitmap_icon_adsb, new ViewFactory<ADSBRxView>()},
{"ais", "AIS Boats", RX, Color::green(), &bitmap_icon_ais, new ViewFactory<AISAppView>()},
{"aprsrx", "APRS", RX, Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSRXView>()},
{"audio", "Audio", RX, Color::green(), &bitmap_icon_speaker, new ViewFactory<AnalogAudioView>()},
{"blerx", "BLE Rx", RX, Color::green(), &bitmap_icon_btle, new ViewFactory<BLERxView>()},
{"pocsag", "POCSAG", RX, Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGAppView>()},
{"radiosonde", "Radiosnde", RX, Color::green(), &bitmap_icon_sonde, new ViewFactory<SondeView>()},
{"search", "Search", RX, Color::yellow(), &bitmap_icon_search, new ViewFactory<SearchView>()},
{"subghzd", "SubGhzD", RX, Color::yellow(), &bitmap_icon_remote, new ViewFactory<SubGhzDView>()},
{"weather", "Weather", RX, Color::green(), &bitmap_icon_thermometer, new ViewFactory<WeatherView>()},
/* TX ********************************************************************/
{"aprstx", "APRS TX", TX, ui::Color::green(), &bitmap_icon_aprs, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<APRSTXView>(nav); }},
{"bletx", "BLE Tx", TX, ui::Color::green(), &bitmap_icon_btle, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<BLETxView>(nav); }},
{"ooktx", "OOK", TX, ui::Color::yellow(), &bitmap_icon_remote, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<EncodersView>(nav); }},
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<RDSView>(nav); }},
{"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<TouchTunesView>(nav); }},
{"aprstx", "APRS TX", TX, ui::Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSTXView>()},
{"bletx", "BLE Tx", TX, ui::Color::green(), &bitmap_icon_btle, new ViewFactory<BLETxView>()},
{"ooktx", "OOK", TX, ui::Color::yellow(), &bitmap_icon_remote, new ViewFactory<EncodersView>()},
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory<RDSView>()},
{"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, new ViewFactory<TouchTunesView>()},
/* TRX ********************************************************************/
{"microphone", "Mic", TRX, Color::green(), &bitmap_icon_microphone, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<MicTXView>(nav); }},
{"microphone", "Mic", TRX, Color::green(), &bitmap_icon_microphone, new ViewFactory<MicTXView>()},
/* UTILITIES *************************************************************/
{"filemanager", "File Manager", UTILITIES, Color::green(), &bitmap_icon_dir, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<FileManagerView>(nav); }},
{"freqman", "Freq. Manager", UTILITIES, Color::green(), &bitmap_icon_freqman, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<FrequencyManagerView>(nav); }},
{"iqtrim", "IQ Trim", UTILITIES, Color::orange(), &bitmap_icon_trim, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<IQTrimView>(nav); }},
{"notepad", "Notepad", UTILITIES, Color::dark_cyan(), &bitmap_icon_notepad, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<TextEditorView>(nav); }},
{nullptr, "Debug", UTILITIES, Color::light_grey(), &bitmap_icon_debug, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<DebugMenuView>(nav); }},
{"filemanager", "File Manager", UTILITIES, Color::green(), &bitmap_icon_dir, new ViewFactory<FileManagerView>()},
{"freqman", "Freq. Manager", UTILITIES, Color::green(), &bitmap_icon_freqman, new ViewFactory<FrequencyManagerView>()},
{"iqtrim", "IQ Trim", UTILITIES, Color::orange(), &bitmap_icon_trim, new ViewFactory<IQTrimView>()},
{"notepad", "Notepad", UTILITIES, Color::dark_cyan(), &bitmap_icon_notepad, new ViewFactory<TextEditorView>()},
{nullptr, "Debug", UTILITIES, Color::light_grey(), &bitmap_icon_debug, new ViewFactory<DebugMenuView>()},
//{"testapp", "Test App", UTILITIES, Color::dark_grey(), nullptr, new ViewFactory<TestView>()},
// Dangerous apps.
{nullptr, "Flash Utility", UTILITIES, Color::red(), &bitmap_icon_peripherals_details, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<FlashUtilityView>(nav); }},
{nullptr, "Flash Utility", UTILITIES, Color::red(), &bitmap_icon_peripherals_details, new ViewFactory<FlashUtilityView>()},
};
bool NavigationView::StartAppByName(const char* name) {
@@ -133,7 +130,7 @@ bool NavigationView::StartAppByName(const char* name) {
for (const auto& app : appList) {
if (app.id != nullptr && strcmp(app.id, name) == 0) {
push_view(app.producer(*this));
push_view(app.viewFactory->produce(*this));
return true;
}
}
@@ -633,7 +630,6 @@ void NavigationView::pop(bool trigger_update) {
free_view();
view_stack.pop_back();
view_stack.shrink_to_fit();
// NB: These are executed _after_ the view has been
// destroyed. The old view MUST NOT be referenced in
@@ -757,22 +753,10 @@ void NavigationView::handle_autostart() {
void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc) {
for (auto& app : NavigationView::appList) {
if (app.menuLocation == loc) {
const AppInfo* p_app = &app;
grid.add_item({app.displayName, app.iconColor, app.icon,
[&nav, p_app]() {
NavigationView& local_nav = nav;
if (p_app->menuLocation == HOME) {
local_nav.store_last_menu_name(p_app->displayName);
local_nav.set_last_menu_went_deeper(false);
auto new_view = p_app->producer(local_nav);
local_nav.push_view(std::move(new_view));
} else {
auto new_view = p_app->producer(local_nav);
local_nav.pop();
local_nav.set_last_menu_went_deeper(true);
local_nav.push_view(std::move(new_view));
}
}},
[&nav, &app]() {
i2cdev::I2CDevManager::set_autoscan_interval(0); //if i navigate away from any menu, turn off autoscan
nav.push_view(app.viewFactory->produce(nav)); }},
true);
}
};
@@ -799,7 +783,7 @@ void add_external_items(NavigationView& nav, app_location_t location, BtnGridVie
return a.desired_position < b.desired_position;
});
for (auto & gridItem : externalItems) {
for (auto const& gridItem : externalItems) {
if (gridItem.desired_position < 0) {
grid.add_item(std::move(gridItem), true);
} else {
@@ -931,27 +915,6 @@ SystemView::SystemView(
{{0, 0},
{parent_rect.width(), status_view_height}});
status_view.on_back = [this]() {
if (this->navigation_view.view_stack_size() == 2) {
const AppInfo* lastmenu = nullptr;
const auto last_menu_name = this->navigation_view.get_last_menu_name();
this->navigation_view.store_last_menu_name("");
if (!last_menu_name.empty()) {
for (const auto& app : NavigationView::appList) {
if (app.displayName == last_menu_name) {
lastmenu = &app;
break;
}
}
if (lastmenu && this->navigation_view.get_last_menu_went_deeper()) {
this->navigation_view.pop();
this->navigation_view.push_view(lastmenu->producer(this->navigation_view));
this->navigation_view.store_last_menu_name(lastmenu->displayName); // since we opened it again, store it again
this->navigation_view.set_last_menu_went_deeper(false); // not yet
return;
}
}
}
this->navigation_view.pop();
};
@@ -968,7 +931,6 @@ SystemView::SystemView(
navigation_view.on_view_changed = [this](const View& new_view) {
if (!this->navigation_view.is_top()) {
remove_child(&info_view);
i2cdev::I2CDevManager::set_autoscan_interval(0);
} else {
add_child(&info_view);
info_view.refresh();
@@ -1029,15 +991,16 @@ void SystemView::toggle_overlay() {
this->set_dirty();
shared_memory.request_m4_performance_counter = last_perf_counter_status;
overlay_active = 0;
break;
}
}
void SystemView::paint_overlay() {
static bool last_paint_state = false;
if (overlay_active) {
// paint background only every other second
if ((((chTimeNow() >> 10) & 0x01) == 0x01) == last_paint_state)
return;
last_paint_state = !last_paint_state;
if (overlay_active == 1 && overlay)
overlay->set_dirty();
+3 -14
View File
@@ -50,6 +50,7 @@
#include "lfsr_random.hpp"
#include "sd_card.hpp"
#include "external_app.hpp"
#include "view_factory.hpp"
#include "battery.hpp"
// for incrementing fake date when RTC battery is dead
@@ -59,9 +60,6 @@ using namespace sd_card;
namespace ui {
class NavigationView;
using ViewProducer = std::unique_ptr<View> (*)(NavigationView&);
void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc);
void add_external_items(NavigationView& nav, app_location_t location, BtnGridView& grid, uint8_t error_tile_pos, bool show_error_tile = true);
@@ -71,7 +69,7 @@ enum modal_t {
ABORT
};
// Should only be used as part of the appList in NavigationView.
// Should only be used as part of the appList in NavigationView, the viewFactory will never be destroyed.
class AppInfo {
public:
const char* id; // MUST be unique! Used by serial command to start the app so it also has to make sense
@@ -79,7 +77,7 @@ class AppInfo {
app_location_t menuLocation;
Color iconColor;
const Bitmap* icon;
ViewProducer producer;
ViewFactoryBase* viewFactory; // Never destroyed, and I believe it's ok ;) Having a unique_ptr here breaks the initializer list of appList
};
struct AppInfoConsole {
@@ -140,13 +138,6 @@ class NavigationView : public View {
bool StartAppByName(const char* name); // Starts a View (app) by name stored in appListFC. This is to start apps from console
void handle_autostart();
void store_last_menu_name(const std::string& name) { last_menu_name_ = name; }
std::string get_last_menu_name() const { return last_menu_name_; }
size_t view_stack_size() const { return view_stack.size(); }
bool get_last_menu_went_deeper() { return last_menu_went_deeper; }
void set_last_menu_went_deeper(bool went_deeper) { last_menu_went_deeper = went_deeper; }
private:
struct ViewState {
std::unique_ptr<View> view;
@@ -159,8 +150,6 @@ class NavigationView : public View {
void free_view();
void update_view();
std::string last_menu_name_{}; // this stores the last menu name, when we replace the menu with the app, we'll know, where to navigate back
bool last_menu_went_deeper = false;
};
/* Holds widgets and grows dynamically toward the left.
+28 -1
View File
@@ -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);
}
+1 -1
View File
@@ -157,7 +157,7 @@ static void cmd_flash(BaseSequentialStream* chp, int argc, char* argv[]) {
// call nav with flash
auto open_view = nav->push<ui::FlashUtilityView>();
chprintf(chp, "Flashing started\r\n");
chThdSleepMilliseconds(50); // to give display some time to paint the screen
chThdSleepMilliseconds(150); // to give display some time to paint the screen
open_view->wait_till_loaded(); // also wait for first frame sync
if (!open_view->flash_firmware(path.native())) {
chprintf(chp, "error\r\n");
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright 2024 Tamas Eisenberger <e.tamas@iwstudio.hu>
*
* 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 __VIEW_FACTORY_HPP__
#define __VIEW_FACTORY_HPP__
#include "view_factory_base.hpp"
namespace ui {
template <typename T>
class ViewFactory : public ViewFactoryBase {
public:
virtual std::unique_ptr<View> produce(NavigationView& nav) const override {
return std::unique_ptr<View>(new T(nav));
}
};
} // namespace ui
#endif //__VIEW_FACTORY_HPP__
@@ -0,0 +1,28 @@
/*
* Copyright 2024 Tamas Eisenberger <e.tamas@iwstudio.hu>
*
* 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 "view_factory_base.hpp"
namespace ui {
ViewFactoryBase::~ViewFactoryBase() {}
} // namespace ui
@@ -0,0 +1,40 @@
/*
* Copyright 2024 Tamas Eisenberger <e.tamas@iwstudio.hu>
*
* 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 __VIEW_FACTORY_BASE_HPP__
#define __VIEW_FACTORY_BASE_HPP__
#include <memory>
#include "ui_widget.hpp"
namespace ui {
class NavigationView;
class ViewFactoryBase {
public:
virtual ~ViewFactoryBase();
virtual std::unique_ptr<View> produce(NavigationView& nav) const = 0;
};
} // namespace ui
#endif //__VIEW_FACTORY_BASE_HPP__
+19 -3
View File
@@ -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
+12 -3
View File
@@ -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();
+272 -96
View File
@@ -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
View File
@@ -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__*/
+83 -84
View File
@@ -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_fast_clock are defined in
* hackrf/firmware/common/i2c_bus.c and i2c_lpc.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,
@@ -617,7 +553,7 @@ void cpu_clock_init(void) {
/* use IRC as clock source for APB3 */
CGU_BASE_APB3_CLK = CGU_BASE_APB3_CLK_CLK_SEL(CGU_SRC_IRC);
i2c_bus_start(clock_gen.bus, &i2c_config_si5351c_fast_clock);
i2c_bus_start(clock_gen.bus, &i2c_config_fast_clock);
si5351c_init(&clock_gen);
si5351c_disable_all_outputs(&clock_gen);
@@ -625,8 +561,7 @@ void cpu_clock_init(void) {
si5351c_power_down_all_clocks(&clock_gen);
si5351c_set_crystal_configuration(&clock_gen);
si5351c_enable_xo_and_ms_fanout(&clock_gen);
si5351c_configure_pll_sources(&clock_gen, PLL_SOURCE_XTAL);
si5351c_configure_pll_multisynth(&clock_gen, PLL_SOURCE_XTAL);
si5351c_configure_pll_multisynth(&clock_gen, SI5351C_INPUT_XTAL);
/*
* Clocks on HackRF One r9:
@@ -679,14 +614,15 @@ void cpu_clock_init(void) {
/* Set to 10 MHz, the common rate between Jawbreaker and HackRF One. */
sample_rate_set(10000000 * (fp_40_24_t)FP_ONE_HZ, true);
si5351c_set_clock_source(&clock_gen, PLL_SOURCE_XTAL);
si5351c_configure_clock_control(&clock_gen);
si5351c_change_input(&clock_gen, SI5351C_INPUT_XTAL);
// soft reset
si5351c_reset_pll(&clock_gen);
si5351c_reset_plls(&clock_gen, SI5351C_PLL_MASK_BOTH);
si5351c_enable_clock_outputs(&clock_gen);
// FIXME disable I2C
/* Kick I2C0 down to 400kHz when we switch over to APB1 clock = 204MHz */
i2c_bus_start(clock_gen.bus, &i2c_config_si5351c_fast_clock);
i2c_bus_start(clock_gen.bus, &i2c_config_fast_clock);
/*
* 12MHz clock is entering LPC XTAL1/OSC input now.
@@ -848,9 +784,9 @@ clock_source_t activate_best_clock_source(void) {
/* No external or PortaPack clock was found. Use HackRF Si5351C crystal. */
}
si5351c_set_clock_source(
si5351c_change_input(
&clock_gen,
(source == CLOCK_SOURCE_HACKRF) ? PLL_SOURCE_XTAL : PLL_SOURCE_CLKIN);
(source == CLOCK_SOURCE_HACKRF) ? SI5351C_INPUT_XTAL : SI5351C_INPUT_CLKIN);
hackrf_ui()->set_clock_source(source);
return source;
}
@@ -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);
}
+1
View File
@@ -27,6 +27,7 @@
#include <libopencm3/lpc43xx/rgu.h>
#include <libopencm3/lpc43xx/wwdt.h>
#include "delay.h"
#include "string.h"
volatile bool usb_bulk_block_done = false;
+28 -1
View File
@@ -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);
+2
View File
@@ -106,10 +106,12 @@ struct complex<int16_t> {
} /* namespace std */
using complex8_t = std::complex<int8_t>;
using complexu8_t = std::complex<uint8_t>;
using complex16_t = std::complex<int16_t>;
using complex32_t = std::complex<int32_t>;
static_assert(sizeof(complex8_t) == 2, "complex8_t size wrong");
static_assert(sizeof(complexu8_t) == 2, "complexu8_t size wrong");
static_assert(sizeof(complex16_t) == 4, "complex16_t size wrong");
static_assert(sizeof(complex32_t) == 8, "complex32_t size wrong");
+9
View File
@@ -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);
}
+11
View File
@@ -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(
@@ -47,7 +47,7 @@
// to check battery mins and maxes in pmem, and validity
#define BATT_18650_MIN_MAH 1000
#define BATT_18650_MAX_MAH 5000
#define BATT_18650_MAX_MAH 20000
using namespace modems;
using namespace serializer;
+1 -1
View File
@@ -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
+36 -7
View File
@@ -393,27 +393,56 @@ void Text::getAccessibilityText(std::string& result) {
void Text::getWidgetName(std::string& result) {
result = "Text";
}
void Text::paint(Painter& painter) {
const auto rect = screen_rect();
auto s = has_focus() ? style().invert() : style();
painter.fill_rectangle(rect, s.background);
const int char_width = s.font.char_width();
const int line_height = s.font.line_height();
auto default_style = has_focus() ? style().invert() : style();
painter.fill_rectangle(rect, default_style.background);
const int char_width = default_style.font.char_width();
const int line_height = default_style.font.line_height();
if (char_width == 0 || line_height == 0) return;
const size_t chars_per_line = rect.width() / char_width;
if (chars_per_line == 0) return;
size_t lines_capacity = rect.height() / line_height;
if (lines_capacity == 0) lines_capacity = 1; // at least one line, even if it overflows vertically
if (lines_capacity == 0) lines_capacity = 1;
auto text_view = std::string_view{text};
size_t current_offset = 0;
// Track the active text color across line breaks
Color active_color = default_style.foreground;
for (size_t line_idx = 0; line_idx < lines_capacity; ++line_idx) {
if (current_offset >= text_view.length()) break;
size_t chunk_len = std::min(chars_per_line, text_view.length() - current_offset);
size_t scan_offset = current_offset;
size_t printable_count = 0;
Color next_color = active_color; // This will hold the color state as we scan the current chunk
// Scan forward to find chunk length AND track color changes
while (scan_offset < text_view.length() && printable_count < chars_per_line) {
if (text_view[scan_offset] == '\x1B') {
scan_offset++;
if (scan_offset < text_view.length()) {
uint8_t color_idx = text_view[scan_offset];
// Mirror the logic from Painter::draw_string
if (color_idx < std::size(term_colors)) {
next_color = term_colors[color_idx];
} else {
next_color = active_color;
}
scan_offset++;
}
} else {
scan_offset++;
printable_count++;
}
}
size_t chunk_len = scan_offset - current_offset;
// Create a temporary style for this line, initialized with the carried-over color
Style line_style = {.font = default_style.font, .background = default_style.background, .foreground = active_color};
painter.draw_string(
rect.location() + Point(0, line_idx * line_height),
s,
line_style,
text_view.substr(current_offset, chunk_len));
current_offset += chunk_len;
// Carry the final color state of this line into the start of the next line
active_color = next_color;
}
}
+1 -1
Submodule hackrf updated: 442d95e23d...216949a844
+11 -2
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
406025000
406028000
406037000
406040000
406049000
406052000
406061000
406064000
433025000
144875000
406022000
+59
View File
@@ -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
+292
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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
+33
View File
@@ -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