mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-07-26 10:38:52 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b363b79aa | |||
| 0d51787099 | |||
| 8f5b25b10d | |||
| 4e56d0f420 | |||
| 2bb3b415d1 | |||
| 6e0394fb6b | |||
| da21ea7866 | |||
| 2c434946cd | |||
| f3aee44e3b | |||
| 165c190d92 | |||
| 8f9246c1ea | |||
| f1d52ae78f | |||
| 865bdee9b3 | |||
| 314e910cbc | |||
| ec6c7eff86 | |||
| 95b2e9fbc3 | |||
| 52ff442fcc | |||
| 95d246a020 | |||
| c34a6940ca | |||
| 33330e4c34 | |||
| 66f1835232 | |||
| 4dfbf666d0 |
@@ -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)
|
||||
|
||||
@@ -17,12 +17,12 @@ This is a fork of the [Havoc](https://github.com/furrtek/portapack-havoc/) firmw
|
||||
|
||||
If you are new to *HackRF+PortaPack+Mayhem*, check these:
|
||||
|
||||
[<img alt="PortaPack H4M Crash Course" src="https://img.youtube.com/vi/CjI-nUzdpas/maxresdefault.jpg" width="701">](https://share.hackrf.app/SPFOZO)
|
||||
[<img alt="HackRF Pro Quick Look" src="https://img.youtube.com/vi/IoURGzIMhzo/maxresdefault.jpg" width="701">](https://share.hackrf.app/VNBTP6)
|
||||
|
||||
[<img alt="PortaPack H4M Crash Course" src="https://img.youtube.com/vi/CjI-nUzdpas/maxresdefault.jpg" width="172">](https://share.hackrf.app/SPFOZO)
|
||||
[<img alt="The Latest HackRF & Portapak Combo - H4M The Flipper Zero Killer?" src="https://img.youtube.com/vi/Ew2qDgm2hf0/maxresdefault.jpg" width="172">](https://share.hackrf.app/6HKX9A)
|
||||
[<img alt="It’s TOO Easy to Accidentally Do Illegal Stuff with This" src="https://img.youtube.com/vi/OPckpjBSAOw/maxresdefault.jpg" width="172">](https://share.hackrf.app/X4D5TF)
|
||||
[<img alt="HackRF Portapack H4M - Getting Started Guide" src="https://img.youtube.com/vi/wzP0zWi85SI/maxresdefault.jpg" width="172">](https://share.hackrf.app/F9MPOO)
|
||||
[<img alt="The new HackRF Portapack H4M" src="https://img.youtube.com/vi/onQRdCITmuk/maxresdefault.jpg" width="172">](https://share.hackrf.app/0JUHZ6)
|
||||
|
||||
|
||||
# Frequently Asked Questions
|
||||
@@ -32,11 +32,11 @@ This repository expands upon the previous work by many people and aims to consta
|
||||
## What to buy?
|
||||
|
||||
<!---not direct to h4m but to opensourcesdrlab https://share.hackrf.app/TUOLYI--->
|
||||
:heavy_check_mark:  The fabulous H4M [complete](https://share.hackrf.app/DF2BZS) or [upgrade](https://share.hackrf.app/OCSJYI), featuring numerous improvements and accessories. Sold by our friends at [OpenSourceSDRLab](https://share.hackrf.app/99SAMT). Join their giveaways on discord (check the badge on top). _EU customers_ can purchase via [Lab401](https://share.hackrf.app/0CI2CR).
|
||||
:heavy_check_mark:  The fabulous H4M [complete](https://share.hackrf.app/O4OC90) or [upgrade](https://share.hackrf.app/OCSJYI), featuring numerous improvements and accessories. Sold by our friends at [OpenSourceSDRLab](https://share.hackrf.app/99SAMT). Join their giveaways on discord (check the badge on top). _EU customers_ can purchase via [Lab401](https://share.hackrf.app/0CI2CR).
|
||||
|
||||
:heavy_check_mark: A recommended one is this [PortaPack H2](https://share.hackrf.app/QT5JMB), that includes everything you need with the plastic case "inspired" on [this](https://github.com/portapack-mayhem/mayhem-firmware/wiki/3d-printed-enclosure).
|
||||
:heavy_check_mark: A recommended one is this [PortaPack H2](https://share.hackrf.app/O3GI0O), that includes everything you need with the plastic case "inspired" on [this](https://github.com/portapack-mayhem/mayhem-firmware/wiki/3d-printed-enclosure).
|
||||
|
||||
:heavy_check_mark: Some individuals lean towards the [H2 with a metal enclosure](https://share.hackrf.app/24T3TO), but its advantages remain debated. Share your insights on our [wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Hardware-overview).
|
||||
:heavy_check_mark: Some individuals lean towards the [H2 with a metal enclosure](https://share.hackrf.app/H7OR74), but its advantages remain debated. Share your insights on our [wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Hardware-overview).
|
||||
|
||||
:warning: Be cautious , *ask* the seller about compatibility with the latest releases. Look out for the description of the item, if they provide the firmware files for an older version or they have custom setup instructions, this means it might be **NOT compatible**, for example:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include <variant>
|
||||
|
||||
#include "file.hpp"
|
||||
#include "hackrf_hal.hpp"
|
||||
#include "max283x.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
@@ -134,7 +135,7 @@ struct AppSettings {
|
||||
uint8_t vga = 32;
|
||||
uint8_t rx_amp = 0;
|
||||
uint8_t tx_amp = 0;
|
||||
uint8_t tx_gain = 35;
|
||||
uint8_t tx_gain = hackrf::one::default_tx_gain_db;
|
||||
uint32_t channel_bandwidth = 1;
|
||||
uint32_t rx_frequency;
|
||||
uint32_t tx_frequency;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -238,6 +238,7 @@ APRSTXView::APRSTXView(NavigationView& nav) {
|
||||
process_coordinates(lat, lon);
|
||||
});
|
||||
};
|
||||
|
||||
// process_coordinates(last_lat, last_lon); //don't load last, so won't confuse users
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -390,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) {
|
||||
@@ -398,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);
|
||||
@@ -408,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) {
|
||||
@@ -418,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) {
|
||||
@@ -431,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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,17 +156,21 @@ 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
|
||||
if (show_rssi_guides) {
|
||||
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));
|
||||
freq_stats.set("MAX: " + to_string_short_freq(max_freq_hold));
|
||||
}
|
||||
plot_marker(marker_pixel_index);
|
||||
} else {
|
||||
@@ -392,6 +396,7 @@ GlassView::GlassView(
|
||||
&button_marker_plus,
|
||||
&button_jump,
|
||||
&button_rst,
|
||||
&button_rssi,
|
||||
&freq_stats});
|
||||
|
||||
load_presets(); // Load available presets from TXT files (or default).
|
||||
@@ -446,6 +451,7 @@ GlassView::GlassView(
|
||||
freq_stats.hidden(true);
|
||||
button_jump.hidden(true);
|
||||
button_rst.hidden(true);
|
||||
button_rssi.hidden(true);
|
||||
display.scroll_set_area(109, screen_height - 1); // Restart scroll on the correct coordinates.
|
||||
break;
|
||||
|
||||
@@ -456,6 +462,7 @@ GlassView::GlassView(
|
||||
freq_stats.hidden(false);
|
||||
button_jump.hidden(false);
|
||||
button_rst.hidden(false);
|
||||
button_rssi.hidden(false);
|
||||
break;
|
||||
|
||||
case 2: // PEAK
|
||||
@@ -466,6 +473,7 @@ GlassView::GlassView(
|
||||
freq_stats.hidden(false);
|
||||
button_jump.hidden(false);
|
||||
button_rst.hidden(false);
|
||||
button_rssi.hidden(false);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -535,6 +543,16 @@ GlassView::GlassView(
|
||||
reset_live_view();
|
||||
};
|
||||
|
||||
button_rssi.on_select = [this](Button& button) {
|
||||
show_rssi_guides = !show_rssi_guides;
|
||||
if (show_rssi_guides) {
|
||||
button.set_style(Theme::getInstance()->fg_green);
|
||||
} else {
|
||||
button.set_style(Theme::getInstance()->bg_darkest);
|
||||
}
|
||||
reset_live_view();
|
||||
};
|
||||
|
||||
display.scroll_set_area(109, screen_height - 1); // Restart scroll on the correct coordinates
|
||||
|
||||
// trigger:
|
||||
|
||||
@@ -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();
|
||||
@@ -163,6 +164,10 @@ class GlassView : public View {
|
||||
uint8_t bin_length = screen_width;
|
||||
uint8_t offset = 0;
|
||||
uint8_t ignore_dc = 0;
|
||||
bool show_rssi_guides = false;
|
||||
|
||||
// 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},
|
||||
@@ -284,15 +289,19 @@ class GlassView : public View {
|
||||
}};
|
||||
|
||||
Button button_jump{
|
||||
{screen_width - 4 * 8, 5 * 16, 4 * 8, 16},
|
||||
{UI_POS_X_RIGHT(4), UI_POS_Y(5), UI_POS_WIDTH(4), UI_POS_HEIGHT(1)},
|
||||
"JMP"};
|
||||
|
||||
Button button_rst{
|
||||
{screen_width - 9 * 8, 5 * 16, 4 * 8, 16},
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(5), UI_POS_WIDTH(4), UI_POS_HEIGHT(1)},
|
||||
"RST"};
|
||||
|
||||
Button button_rssi{
|
||||
{UI_POS_X_RIGHT(15), UI_POS_Y(5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)},
|
||||
"RSSI"};
|
||||
|
||||
Text freq_stats{
|
||||
{UI_POS_X(0), 5 * 16, screen_width - 10 * 8, 8},
|
||||
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(14), UI_POS_HEIGHT(1)},
|
||||
""};
|
||||
|
||||
MessageHandlerRegistration message_handler_spectrum_config{
|
||||
|
||||
@@ -186,26 +186,26 @@ void set_dirty() {
|
||||
}
|
||||
|
||||
standalone_application_api_t api = {
|
||||
/* .malloc = */ &alloc,
|
||||
/* .calloc = */ &calloc,
|
||||
/* .realloc = */ &realloc,
|
||||
/* .free = */ &chHeapFree,
|
||||
/* .create_thread = */ &create_thread,
|
||||
/* .fill_rectangle = */ &fill_rectangle,
|
||||
/* .swizzled_switches = */ &swizzled_switches,
|
||||
/* .get_switches_state = */ &get_switches_state_ulong,
|
||||
/* .fixed_5x8_glyph_data = */ ui::font::fixed_5x8.get_data(),
|
||||
/* .fixed_8x16_glyph_data = */ ui::font::fixed_8x16.get_data(),
|
||||
/* .fill_rectangle_unrolled8 = */ &fill_rectangle_unrolled8,
|
||||
/* .draw_bitmap = */ &draw_bitmap,
|
||||
/* .scroll_area_y = */ &scroll_area_y,
|
||||
/* .scroll_set_area = */ &scroll_set_area,
|
||||
/* .scroll_disable = */ &scroll_disable,
|
||||
/* .scroll_set_position = */ &scroll_set_position,
|
||||
/* .scroll = */ &scroll,
|
||||
/* .i2c_read = */ &i2c_read,
|
||||
/* .panic = */ &chDbgPanic,
|
||||
/* .set_dirty = */ &set_dirty,
|
||||
.malloc = &alloc,
|
||||
.calloc = &calloc,
|
||||
.realloc = &realloc,
|
||||
.free = &chHeapFree,
|
||||
.create_thread = &create_thread,
|
||||
.fill_rectangle = &fill_rectangle,
|
||||
.swizzled_switches = &swizzled_switches,
|
||||
.get_switches_state = &get_switches_state_ulong,
|
||||
.fixed_5x8_glyph_data = ui::font::fixed_5x8.get_data(),
|
||||
.fixed_8x16_glyph_data = ui::font::fixed_8x16.get_data(),
|
||||
.fill_rectangle_unrolled8 = &fill_rectangle_unrolled8,
|
||||
.draw_bitmap = &draw_bitmap,
|
||||
.scroll_area_y = &scroll_area_y,
|
||||
.scroll_set_area = &scroll_set_area,
|
||||
.scroll_disable = &scroll_disable,
|
||||
.scroll_set_position = &scroll_set_position,
|
||||
.scroll = &scroll,
|
||||
.i2c_read = &i2c_read,
|
||||
.panic = &chDbgPanic,
|
||||
.set_dirty = &set_dirty,
|
||||
// Version 3
|
||||
.f_open = &ext_f_open,
|
||||
.f_close = &ext_f_close,
|
||||
|
||||
@@ -432,6 +432,10 @@ void set_epirb_tx_config(EPIRBTXDataMessage& message) {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_epirb_rx_config(EPIRBRXConfig& message) {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length) {
|
||||
const size_t max_len = sizeof(shared_memory.bb_data.data);
|
||||
if (frame_length > max_len) frame_length = max_len;
|
||||
@@ -443,6 +447,10 @@ void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length) {
|
||||
|
||||
static bool baseband_image_running = false;
|
||||
|
||||
bool is_image_running() {
|
||||
return baseband_image_running;
|
||||
}
|
||||
|
||||
void run_image(const spi_flash::image_tag_t image_tag) {
|
||||
if (baseband_image_running) {
|
||||
chDbgPanic("BBRunning");
|
||||
|
||||
@@ -118,6 +118,7 @@ void set_bitstream_config(uint32_t deviation, uint8_t mode);
|
||||
void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload = nullptr, uint16_t payload_length = 0); // baud*100
|
||||
void set_rtty_config(RTTYDataMessage& message);
|
||||
void set_epirb_tx_config(EPIRBTXDataMessage& message);
|
||||
void set_epirb_rx_config(EPIRBRXConfig& message);
|
||||
void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length);
|
||||
|
||||
void request_roger_beep();
|
||||
@@ -125,6 +126,7 @@ void request_rssi_beep();
|
||||
void request_beep_stop();
|
||||
void request_audio_beep(uint32_t freq, uint32_t sample_rate, uint32_t duration_ms);
|
||||
|
||||
bool is_image_running();
|
||||
void run_image(const portapack::spi_flash::image_tag_t image_tag);
|
||||
void run_prepared_image(const uint32_t m4_code);
|
||||
void shutdown();
|
||||
|
||||
@@ -325,6 +325,12 @@ static constexpr ClockControl::MultiSynthSource get_si5351a_reference_clock_gene
|
||||
? ClockControl::MultiSynthSource::PLLA
|
||||
: ClockControl::MultiSynthSource::PLLB;
|
||||
}
|
||||
|
||||
static constexpr ClockControl::MultiSynthSource get_si5351c_reference_clock_generator_pll(const ClockManager::ReferenceSource reference_source) {
|
||||
return (reference_source == ClockManager::ReferenceSource::Xtal)
|
||||
? ClockControl::MultiSynthSource::PLLA
|
||||
: ClockControl::MultiSynthSource::PLLB;
|
||||
}
|
||||
#else
|
||||
static constexpr ClockControl::MultiSynthSource get_si5351c_reference_clock_generator_pll(const ClockManager::ReferenceSource reference_source) {
|
||||
return (reference_source == ClockManager::ReferenceSource::Xtal)
|
||||
@@ -424,17 +430,24 @@ 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
|
||||
/* Route the PRALINE P22 10MHz reference to the Si5351 CLKIN pin. */
|
||||
LPC_SCU->SFSP[2][10] = 0xF0;
|
||||
LPC_SCU->SFSP[6][8] = 0xF4;
|
||||
LPC_SCU->SFSP[6][9] = 0xF0;
|
||||
LPC_SCU->SFSP[1][20] = 0xF0;
|
||||
|
||||
gpio_p1_ctrl0.output();
|
||||
gpio_p1_ctrl1.output();
|
||||
gpio_p1_ctrl2.output();
|
||||
gpio_clkin_ctrl.output();
|
||||
|
||||
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) {
|
||||
@@ -448,22 +461,45 @@ void ClockManager::init_clock_generator() {
|
||||
clock_generator.enable_fanout();
|
||||
|
||||
#ifdef PRALINE
|
||||
/* PRALINE has Si5351A (NOT Si5351C like HackRF One OG).
|
||||
* Must use Si5351A configuration: PLLA only, no CLKIN support.
|
||||
*
|
||||
* IMPORTANT: Follow HackRF reference sequence:
|
||||
* 1. Set PLL input sources
|
||||
* 2. Configure PLL and multisynths
|
||||
* 3. Set clock control registers (AFTER multisynths!)
|
||||
* 4. Reset PLLs
|
||||
* 5. Enable outputs
|
||||
constexpr size_t clock_generator_output_pro_mcu_clkin = 2;
|
||||
auto si5351_clock_control_common = si5351a_clock_control_common;
|
||||
|
||||
/*
|
||||
* Match the HackRF One init flow:
|
||||
* 1. PLLA stays on the local 25MHz crystal.
|
||||
* 2. PLLB is configured from the Si5351 CLKIN pin.
|
||||
* 3. CLK2 temporarily drives GP_CLKIN directly from CLKIN so we can
|
||||
* detect whether P22 is carrying a valid 10MHz reference.
|
||||
* 4. Once the source is chosen, all runtime clocks are switched to the
|
||||
* selected PLL and the working multisynths are programmed.
|
||||
*/
|
||||
clock_generator.set_pll_input_sources(si5351a_pll_input_sources);
|
||||
clock_generator.set_pll_input_sources(si5351c_pll_input_sources);
|
||||
|
||||
/* Skip MCU CLKIN setup and reference detection for PRALINE - not applicable */
|
||||
reference = Reference{ReferenceSource::Xtal, 0}; // PLLA
|
||||
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);
|
||||
|
||||
/* Clock control will be set AFTER multisynth configuration - see below */
|
||||
chThdSleepMilliseconds(20);
|
||||
reference = choose_reference();
|
||||
clock_generator.disable_output(clock_generator_output_pro_mcu_clkin);
|
||||
|
||||
const auto ref_pll =
|
||||
get_si5351c_reference_clock_generator_pll(reference.source);
|
||||
|
||||
const ClockControls si5351_clock_control = ClockControls{{
|
||||
si5351_clock_control_common[0].ms_src(ref_pll),
|
||||
si5351_clock_control_common[1].ms_src(ref_pll),
|
||||
si5351_clock_control_common[2].ms_src(ref_pll),
|
||||
si5351_clock_control_common[3].ms_src(ref_pll),
|
||||
si5351_clock_control_common[4].ms_src(ref_pll),
|
||||
si5351_clock_control_common[5].ms_src(ref_pll),
|
||||
si5351_clock_control_common[6].ms_src(ref_pll),
|
||||
si5351_clock_control_common[7].ms_src(ref_pll),
|
||||
}};
|
||||
clock_generator.set_clock_control_single_byte(si5351_clock_control);
|
||||
#else
|
||||
clock_generator.set_pll_input_sources(hackrf_r9
|
||||
? si5351a_pll_input_sources
|
||||
@@ -508,86 +544,50 @@ void ClockManager::init_clock_generator() {
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
clock_generator.write_pll_single_byte(0, si5351_pll_a_afe_800m);
|
||||
|
||||
/* * Praline HackRF Pro Clock Assignments (800 MHz VCO Configuration)
|
||||
* VCO Frequency: 800,000,000 Hz (Master Reference)
|
||||
* * CLK0: AFE_CLK (MAX5864 Codec & FPGA ADC Interface)
|
||||
* - Note: Defines hardware sample rate. Essential for WFM purity.
|
||||
* * CLK1: SCT_CLK (iCE40 FPGA System/Timing Clock)
|
||||
* - Note: Timing for SGPIO data bus; scales to 2x SR in wideband modes.
|
||||
* * CLK2: MCU_CLKIN (LPC43xx MCU External Clock Input)
|
||||
* - Note: Synchronizes MCU processing to the RF clock tree.
|
||||
* * CLK3: SG_CLK (Switching Regulator/Internal Logic Sync) SMA Port 1
|
||||
* - Note: Used for internal FPGA logic/gateware synchronization.
|
||||
* * CLK4: P_CLK (MAX2831 Peripheral/Expansion Clock)
|
||||
* - Note: Routed to expansion headers for external hardware sync.
|
||||
* * CLK5: AUX_CLK (RFFC5371 Auxiliary reference for secondary logic)
|
||||
* - Note: Provides additional timing flexibility for the iCE40 FPGA.
|
||||
* * CLK6: SG_CLK (Switching Regulator/Internal Logic Sync) SMA Port 2
|
||||
* - Note: Used for internal FPGA logic/gateware synchronization.
|
||||
* * CLK7: Unused / Power-Down
|
||||
* - State: Disabled (si5351a_ms6_7_off_reg)
|
||||
* - Note: Kept OFF to reduce EMI/RFI near the RF front-end.
|
||||
* * CLKOUT: Optional external clock output on the header.
|
||||
*/
|
||||
|
||||
/* Write PLL A (800 MHz based on 25 MHz xtal for RF) and
|
||||
* PLL B (800 MHZ based on 25 MHz xtalfor Digital)
|
||||
* Use single-byte writes to debug I2C issues
|
||||
*/
|
||||
{
|
||||
// Write PLLA (Registers 26-33)
|
||||
/* Write PLL A configuration (Base 26) */
|
||||
const auto& pll_a = si5351_pll_a_800_reg;
|
||||
for (size_t i = 1; i < pll_a.size(); i++) {
|
||||
clock_generator.write_register(pll_a[0] + i - 1, pll_a[i]);
|
||||
}
|
||||
|
||||
// Write PLLB (Registers 34-41)
|
||||
/* Write PLL B configuration (Base 34) */
|
||||
const auto& pll_b = si5351_pll_b_800_reg;
|
||||
const auto& pll_b = si5351c_pll_b_clkin_reg;
|
||||
for (size_t i = 1; i < pll_b.size(); i++) {
|
||||
clock_generator.write_register(pll_b[0] + i - 1, pll_b[i]);
|
||||
}
|
||||
}
|
||||
// CLK0: DAFE_CLK initial 4 MHz
|
||||
clock_generator.write_ms_single_byte(
|
||||
0,
|
||||
si5351_ms_afe_4m);
|
||||
|
||||
/* Write multisynth configurations using single-byte writes */
|
||||
// These cover all active channels on the Praline board
|
||||
clock_generator.write_ms_single_byte(0, si5351_ms_afe_4m); // CLK0: PLL A AFE Codec (4 MHz)
|
||||
clock_generator.write_ms_single_byte(1, si5351_ms_10m); // CLK1: PLL B SGPIO/FPGA Timing (10 MHz)
|
||||
clock_generator.write_ms_single_byte(2, si5351_ms_afe_40m); // CLK2: PLL A Audio and MCU Input (40 MHz)
|
||||
clock_generator.write_ms_single_byte(3, si5351_ms_0_4m); // CLK3: PLL B SMA Port 1 Logic Sync (4 MHz or 10 MHz)
|
||||
clock_generator.write_ms_single_byte(4, si5351_ms_afe_40m); // CLK4: PLL A MAX2831 Second IF (40 MHz)
|
||||
clock_generator.write_ms_single_byte(5, si5351_ms_afe_40m); // CLK5: PLL A RFFC5071First IF (40 MHz)
|
||||
clock_generator.write_ms_single_byte(6, si5351_ms_0_4m); // CLK6: PLL B SMA Port 2 Logic Sync (4 MHz or 10 MHz)
|
||||
clock_generator.write_ms_single_byte(7, si5351_ms_0_4m); // CLK7: PLL B Unused (4 MHz)
|
||||
// CLK1: DSCT_CLK initial 10 MHz
|
||||
clock_generator.write_ms_single_byte(
|
||||
1,
|
||||
si5351_ms_10m);
|
||||
|
||||
/* NOW set clock control registers (AFTER multisynths per HackRF reference) */
|
||||
const auto ref_pll_a = ClockControl::MultiSynthSource::PLLA;
|
||||
const auto ref_pll_b = ClockControl::MultiSynthSource::PLLB;
|
||||
const ClockControls si5351_clock_control = ClockControls{{
|
||||
si5351a_clock_control_common[0].ms_src(ref_pll_a),
|
||||
si5351a_clock_control_common[1].ms_src(ref_pll_b),
|
||||
si5351a_clock_control_common[2].ms_src(ref_pll_a),
|
||||
si5351a_clock_control_common[3].ms_src(ref_pll_b),
|
||||
si5351a_clock_control_common[4].ms_src(ref_pll_a),
|
||||
si5351a_clock_control_common[5].ms_src(ref_pll_a),
|
||||
si5351a_clock_control_common[6].ms_src(ref_pll_b),
|
||||
si5351a_clock_control_common[7].ms_src(ref_pll_b),
|
||||
}};
|
||||
// clock_generator.set_clock_control(si5351_clock_control);
|
||||
// Use single-byte writes instead of multi-byte
|
||||
clock_generator.set_clock_control_single_byte(si5351_clock_control);
|
||||
// CLK2: MCU_CLK 40 MHz
|
||||
clock_generator.write_ms_single_byte(
|
||||
2,
|
||||
si5351_ms_afe_40m);
|
||||
|
||||
// Don't write CLKS 3, 6, and 7 multisynth
|
||||
// Ensure CLK3 clock control has Power_Off
|
||||
// Verify output is disabled
|
||||
clock_generator.disable_output(3);
|
||||
clock_generator.disable_clock(3);
|
||||
clock_generator.disable_output(6);
|
||||
clock_generator.disable_clock(6);
|
||||
clock_generator.disable_output(7);
|
||||
clock_generator.disable_clock(7);
|
||||
// CLK3: P2 clock mux, disabled below
|
||||
clock_generator.write_ms_single_byte(
|
||||
3,
|
||||
si5351_ms_10m);
|
||||
|
||||
// CLK4: DXCVR_CLK 40 MHz
|
||||
clock_generator.write_ms_single_byte(
|
||||
4,
|
||||
si5351_ms_afe_40m);
|
||||
|
||||
// CLK5: DMIX_CLK 40 MHz
|
||||
clock_generator.write_ms_single_byte(
|
||||
5,
|
||||
si5351_ms_afe_40m);
|
||||
|
||||
{
|
||||
const auto& ms6_7 = si5351a_ms6_7_off_reg;
|
||||
for (size_t i = 1; i < ms6_7.size(); i++) {
|
||||
clock_generator.write_register(ms6_7[0] + i - 1, ms6_7[i]);
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
const PLLReg pll_reg = (reference.source == ReferenceSource::Xtal)
|
||||
@@ -615,15 +615,14 @@ void ClockManager::init_clock_generator() {
|
||||
|
||||
// Wait for PLL(s) to lock.
|
||||
#ifdef PRALINE
|
||||
// PRALINE: Wait for 0x60 (0x20 | 0x40), PLLA and PLLB to lock (0x20 = LOL_A bit, 0x40 = LOL_B bit)
|
||||
uint8_t device_status_mask = 0x60;
|
||||
uint8_t device_status_mask =
|
||||
(ref_pll == ClockControl::MultiSynthSource::PLLB) ? 0x40 : 0x20;
|
||||
uint32_t pll_timeout = 100000;
|
||||
while ((clock_generator.device_status() & device_status_mask) != 0 && pll_timeout > 0) {
|
||||
pll_timeout--;
|
||||
}
|
||||
// Store PLL lock status for debugging
|
||||
static volatile uint32_t pll_lock_timeout = pll_timeout;
|
||||
(void)pll_lock_timeout;
|
||||
|
||||
clock_generator.enable_output(clock_generator_output_pro_mcu_clkin);
|
||||
|
||||
#else
|
||||
// Wait for PLL(s) to lock - with timeout to prevent hang
|
||||
@@ -723,13 +722,13 @@ void ClockManager::shutdown() {
|
||||
|
||||
void ClockManager::enable_codec_clocks() {
|
||||
#ifdef PRALINE
|
||||
/* PRALINE: CLK0 (AFE_CLK) for codec/FPGA, CLK1 (SCT_CLK) for FPGA timing.
|
||||
* Reference hackrf_core.c shows PRALINE needs both CLK0 and CLK1. */
|
||||
/* PRALINE: only gate the clocks owned by the RF datapath here.
|
||||
* CLK2 is MCU_CLKIN and must stay independent of runtime RX/TX clock
|
||||
* management or the SGPIO/GPIO subsystem can glitch mid-stream. */
|
||||
clock_generator.enable_clock(clock_generator_output_og_codec); /* CLK0 MAX5864*/
|
||||
clock_generator.enable_clock(clock_generator_output_og_cpld); /* CLK1 iCE40 FPGA*/
|
||||
clock_generator.enable_clock(clock_generator_output_og_sgpio); /* CLK2 LPC43xx*/
|
||||
clock_generator.enable_output_mask(
|
||||
(1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld) | (1U << clock_generator_output_og_sgpio));
|
||||
(1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld));
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
clock_generator.enable_clock(clock_generator_output_r9_sgpio);
|
||||
@@ -757,12 +756,11 @@ void ClockManager::disable_codec_clocks() {
|
||||
* CLKx_DISABLE_STATE.
|
||||
*/
|
||||
#ifdef PRALINE
|
||||
/* PRALINE: CLK0 (AFE_CLK), CLK1 (SCT_CLK), and CLK2 MCU used for codec/FPGA */
|
||||
/* PRALINE: leave CLK2/MCU_CLKIN alone; only disable datapath-owned clocks. */
|
||||
clock_generator.disable_output_mask(
|
||||
(1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld) | (1U << clock_generator_output_og_sgpio));
|
||||
(1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld));
|
||||
clock_generator.disable_clock(clock_generator_output_og_codec);
|
||||
clock_generator.disable_clock(clock_generator_output_og_cpld);
|
||||
clock_generator.disable_clock(clock_generator_output_og_sgpio);
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
clock_generator.disable_output_mask(1U << clock_generator_output_r9_sgpio);
|
||||
@@ -821,7 +819,7 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
|
||||
/*
|
||||
* PRALINE sample rate strategy:
|
||||
* 1. Maximize AFE rate to push Nyquist above MAX2831's 11.6 MHz LPF minimum
|
||||
* 2. Use FPGA decimation to achieve desired output rate
|
||||
* 2. Use FPGA decimation/interpolation to achieve desired output rate
|
||||
* 3. Ensure AFE rate is achievable by Si5351 (clean division from 800 MHz VCO)
|
||||
*/
|
||||
|
||||
@@ -842,50 +840,96 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
|
||||
n++;
|
||||
}
|
||||
|
||||
_resampling_n = n;
|
||||
|
||||
// === Stop FPGA processing and flush filters ===
|
||||
fpga_debug_register_write(1, 0x00); // Disable FPGA filters (resets CIC accumulators)
|
||||
|
||||
// Verify we're in RX mode before writing RX registers
|
||||
if (fpga_get_mode() != FPGA_MODE_RX) {
|
||||
// Either set mode or return error
|
||||
fpga_set_mode(FPGA_MODE_RX);
|
||||
/* The TX gateware only implements interpolation factors x1..x16
|
||||
* (tx_intrp 0..4). The rate search above can select n == 5 (x32) for very
|
||||
* low TX sample rates. If we kept afe_rate/CLK0/CLK1 at the x32 rate while
|
||||
* the FPGA only interpolated by x16, the TX datapath would be clocked at
|
||||
* twice the gateware's output rate, doubling the waveform speed. Cap the
|
||||
* AFE rate and _resampling_n to the x16 limit in TX so the clocks and the
|
||||
* interpolation setting stay aligned. */
|
||||
if (radio::debug::get_cached_direction() == rf::Direction::Transmit) {
|
||||
constexpr uint8_t MAX_TX_N = 4; // x16, matches max tx_intrp
|
||||
if (n > MAX_TX_N) {
|
||||
n = MAX_TX_N;
|
||||
afe_rate = frequency << n;
|
||||
}
|
||||
}
|
||||
|
||||
// Set FPGA RX decimation register
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, n);
|
||||
|
||||
/* RX Mode: Register 3 is FPGA_REG_RX_DIGITAL_GAIN.
|
||||
* We shift up by (3 * n) to compensate for CIC bit-growth.
|
||||
* Relationship: ds = (Stages * n) - Offset
|
||||
* For a 3-stage filter, every increment of n grows the signal by 3 bits.
|
||||
* We subtract a baseline offset to keep the signal within 8-bit bounds.
|
||||
* Add a baseline shift to ensure the signal isn't too quiet
|
||||
*/
|
||||
uint8_t ds = (3 * n);
|
||||
ds += 2;
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds);
|
||||
_resampling_n = n;
|
||||
|
||||
radio::invalidate_spi_config();
|
||||
|
||||
// Configure Si5351 clocks
|
||||
// CLK0: AFE_CLK (with r_div=1 for ÷2)
|
||||
// CLK1: SCT_CLK (with r_div=0 for ÷1, runs at 2× AFE for FPGA timing)
|
||||
// Configure Si5351 clocks using the correct AFE VCO
|
||||
// CLK0 always drives the AFE/MAX5864 clock.
|
||||
clock_generator.set_ms_frequency(0, afe_rate * 2, si5351_vco_afe_f, 1);
|
||||
clock_generator.set_ms_frequency(1, afe_rate * 2, si5351_vco_afe_f, 0);
|
||||
|
||||
// === Reset PLL A for phase alignment ===
|
||||
clock_generator.write_register(si5351::Register::PLLReset, 0x20);
|
||||
/* Do not reset PLLA here. On PRALINE, CLK2/MCU_CLKIN is sourced from PLLA,
|
||||
* so a runtime PLLA reset can glitch the LPC43xx peripheral clock tree and
|
||||
* break SGPIO-driven RX apps such as ADSB/APRS. Updating the multisynths is
|
||||
* sufficient for sample-rate changes. */
|
||||
|
||||
// Brief delay for PLL lock and clock stability ===
|
||||
// ~1ms at 96MHz = ~96000 cycles, use 10ms for safety
|
||||
volatile uint32_t delay = 240000; // ~2.5ms
|
||||
while (delay--);
|
||||
if (radio::debug::get_cached_direction() == rf::Direction::Transmit) {
|
||||
/*
|
||||
* TX path:
|
||||
* Baseband generators such as AFSK still synthesize samples at their
|
||||
* legacy logical sample rates (for APRS this is 1.536MHz). On PRALINE,
|
||||
* the FPGA must interpolate those samples up to the active AFE rate.
|
||||
*
|
||||
* The TX datapath is clocked from CLK1/fpgaclk. Therefore CLK1 must
|
||||
* equal the desired complex sample rate at the DAC side. Driving CLK1
|
||||
* at 2x the intended TX sample rate causes the entire APRS waveform to
|
||||
* run twice as fast, which matches the "energy present, undecodable"
|
||||
* symptom seen on HackRF One receivers.
|
||||
*
|
||||
* Gateware mapping from standard.py:
|
||||
* tx_intrp = 0 -> x1
|
||||
* tx_intrp = 1 -> x2
|
||||
* tx_intrp = 2 -> x4
|
||||
* tx_intrp = 3 -> x8
|
||||
* tx_intrp = 4 -> x16
|
||||
*/
|
||||
if (fpga_get_mode() != FPGA_MODE_TX) {
|
||||
fpga_set_mode(FPGA_MODE_TX);
|
||||
}
|
||||
|
||||
// Re-enable FPGA processing with clean state ===
|
||||
fpga_debug_register_write(1, 0x01);
|
||||
// TX gateware consumes complex samples at the rate presented on CLK1.
|
||||
clock_generator.set_ms_frequency(1, afe_rate, si5351_vco_afe_f, 0);
|
||||
|
||||
uint8_t tx_interp = 0;
|
||||
uint32_t tx_rate = frequency;
|
||||
while ((tx_rate < afe_rate) && (tx_interp < 4)) {
|
||||
tx_rate <<= 1;
|
||||
tx_interp++;
|
||||
}
|
||||
fpga_tx_set_interpolation(tx_interp);
|
||||
fpga_tx_set_nco_enable(false);
|
||||
fpga_tx_set_phase_step(0);
|
||||
}
|
||||
// for RX mode
|
||||
else {
|
||||
// RX path keeps CLK1 at 2x AFE for receive timing / SGPIO alignment.
|
||||
clock_generator.set_ms_frequency(1, afe_rate * 2, si5351_vco_afe_f, 0);
|
||||
|
||||
// === Stop FPGA processing and flush filters ===
|
||||
fpga_debug_register_write(1, 0x00); // Disable FPGA filters (resets CIC accumulators)
|
||||
|
||||
if (fpga_get_mode() != FPGA_MODE_RX) {
|
||||
fpga_set_mode(FPGA_MODE_RX);
|
||||
}
|
||||
|
||||
// Set FPGA RX decimation register
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, n);
|
||||
|
||||
/* RX Mode: Register 3 is FPGA_REG_RX_DIGITAL_GAIN.
|
||||
* We shift up by (3 * n) to compensate for CIC bit-growth.
|
||||
*/
|
||||
uint8_t ds = (3 * n);
|
||||
ds += 2;
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds);
|
||||
|
||||
// Re-enable FPGA processing with clean state ===
|
||||
fpga_debug_register_write(1, 0x01);
|
||||
}
|
||||
|
||||
#else
|
||||
/* Codec clock is at sampling frequency, CPLD and SGPIO clocks are at
|
||||
|
||||
+89
-105
@@ -6,11 +6,9 @@
|
||||
* ------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize("Os")
|
||||
#include "ui_battleship.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "modems.hpp"
|
||||
#include "bch_code.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
@@ -28,7 +26,6 @@ BattleshipView::BattleshipView(NavigationView& nav)
|
||||
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
|
||||
|
||||
add_children({&text_title, &text_subtitle,
|
||||
//&rect_radio_settings, &rect_audio_settings, &rect_team_selection,
|
||||
&label_radio, &button_frequency,
|
||||
&label_rf_amp, &checkbox_rf_amp,
|
||||
&label_lna, &field_lna,
|
||||
@@ -151,8 +148,6 @@ BattleshipView::BattleshipView(NavigationView& nav)
|
||||
field_tx_gain.set_parent_rect({185, 118, 32, 16});
|
||||
checkbox_sound.set_parent_rect({17, 187, 80, 24});
|
||||
field_volume.set_parent_rect({165, 187, 32, 16});
|
||||
// button_red_team.set_parent_rect({25, 242, 85, 45});
|
||||
// button_blue_team.set_parent_rect({130, 242, 85, 45});
|
||||
|
||||
// Make menu elements focusable
|
||||
button_frequency.set_focusable(true);
|
||||
@@ -196,34 +191,22 @@ void BattleshipView::init_game() {
|
||||
}
|
||||
|
||||
void BattleshipView::show_hide_menu(bool menu_vis) {
|
||||
text_title.hidden(!menu_vis);
|
||||
text_subtitle.hidden(!menu_vis);
|
||||
// rect_radio_settings.hidden(!menu_vis); rect_audio_settings.hidden(!menu_vis); rect_team_selection.hidden(!menu_vis);
|
||||
label_radio.hidden(!menu_vis);
|
||||
button_frequency.hidden(!menu_vis);
|
||||
label_rf_amp.hidden(!menu_vis);
|
||||
checkbox_rf_amp.hidden(!menu_vis);
|
||||
label_lna.hidden(!menu_vis);
|
||||
field_lna.hidden(!menu_vis);
|
||||
label_vga.hidden(!menu_vis);
|
||||
field_vga.hidden(!menu_vis);
|
||||
label_tx_gain.hidden(!menu_vis);
|
||||
field_tx_gain.hidden(!menu_vis);
|
||||
label_audio.hidden(!menu_vis);
|
||||
checkbox_sound.hidden(!menu_vis);
|
||||
label_volume.hidden(!menu_vis);
|
||||
field_volume.hidden(!menu_vis);
|
||||
label_team.hidden(!menu_vis);
|
||||
button_red_team.hidden(!menu_vis);
|
||||
button_blue_team.hidden(!menu_vis);
|
||||
rssi.hidden(menu_vis);
|
||||
button_rotate.hidden(menu_vis);
|
||||
button_place.hidden(menu_vis);
|
||||
button_menu.hidden(menu_vis);
|
||||
Widget* const menu_widgets[] = {
|
||||
&text_title, &text_subtitle, &label_radio, &button_frequency,
|
||||
&label_rf_amp, &checkbox_rf_amp, &label_lna, &field_lna,
|
||||
&label_vga, &field_vga, &label_tx_gain, &field_tx_gain,
|
||||
&label_audio, &checkbox_sound, &label_volume, &field_volume,
|
||||
&label_team, &button_red_team, &button_blue_team};
|
||||
for (auto* w : menu_widgets) {
|
||||
w->hidden(!menu_vis);
|
||||
}
|
||||
|
||||
Widget* const game_widgets[] = {
|
||||
&rssi, &button_rotate, &button_place, &button_menu};
|
||||
for (auto* w : game_widgets) {
|
||||
w->hidden(menu_vis);
|
||||
}
|
||||
|
||||
// button_rotate.set_focusable(false); //no need, since can't focus on hidden
|
||||
// button_place.set_focusable(false);
|
||||
// button_menu.set_focusable(false);
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
@@ -340,40 +323,23 @@ void BattleshipView::paint(Painter& painter) {
|
||||
if (game_state == GameState::MENU) {
|
||||
draw_menu_screen(painter);
|
||||
|
||||
// Custom paint team buttons
|
||||
if (!button_red_team.hidden()) {
|
||||
Rect r = button_red_team.screen_rect();
|
||||
painter.fill_rectangle(r, Color::dark_red());
|
||||
painter.draw_rectangle(r, Color::red());
|
||||
auto draw_team_btn = [&painter](Button& btn, Color bg, Color border, const char* name) {
|
||||
if (btn.hidden()) return;
|
||||
Rect r = btn.screen_rect();
|
||||
painter.fill_rectangle(r, bg);
|
||||
painter.draw_rectangle(r, border);
|
||||
|
||||
if (button_red_team.has_focus()) {
|
||||
if (btn.has_focus()) {
|
||||
painter.draw_rectangle({r.location().x() - 1, r.location().y() - 1, r.size().width() + 2, r.size().height() + 2}, Color::yellow());
|
||||
}
|
||||
|
||||
auto style_white = Style{
|
||||
.font = ui::font::fixed_8x16,
|
||||
.background = Color::dark_red(),
|
||||
.foreground = Color::white()};
|
||||
painter.draw_string(r.center() - Point(24, 16), style_white, "RED");
|
||||
painter.draw_string(r.center() - Point(24, 0), style_white, "TEAM");
|
||||
}
|
||||
auto style = Style{.font = ui::font::fixed_8x16, .background = bg, .foreground = Color::white()};
|
||||
painter.draw_string(r.center() - Point(24, 16), style, name);
|
||||
painter.draw_string(r.center() - Point(24, 0), style, "TEAM");
|
||||
};
|
||||
|
||||
if (!button_blue_team.hidden()) {
|
||||
Rect r = button_blue_team.screen_rect();
|
||||
painter.fill_rectangle(r, Color::dark_blue());
|
||||
painter.draw_rectangle(r, Color::blue());
|
||||
|
||||
if (button_blue_team.has_focus()) {
|
||||
painter.draw_rectangle({r.location().x() - 1, r.location().y() - 1, r.size().width() + 2, r.size().height() + 2}, Color::yellow());
|
||||
}
|
||||
|
||||
auto style_white = Style{
|
||||
.font = ui::font::fixed_8x16,
|
||||
.background = Color::dark_blue(),
|
||||
.foreground = Color::white()};
|
||||
painter.draw_string(r.center() - Point(24, 16), style_white, "BLUE");
|
||||
painter.draw_string(r.center() - Point(24, 0), style_white, "TEAM");
|
||||
}
|
||||
draw_team_btn(button_red_team, Color::dark_red(), Color::red(), "RED");
|
||||
draw_team_btn(button_blue_team, Color::dark_blue(), Color::blue(), "BLUE");
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -417,27 +383,25 @@ void BattleshipView::paint(Painter& painter) {
|
||||
void BattleshipView::draw_menu_screen(Painter& painter) {
|
||||
painter.draw_hline({12, 20 + 16}, screen_width - 24, Color::dark_cyan());
|
||||
|
||||
painter.fill_rectangle({13, 59, screen_width - 26, 116}, Color::dark_grey());
|
||||
painter.draw_hline({12, 58}, screen_width - 24, Color::cyan());
|
||||
painter.draw_hline({12, 157}, screen_width - 24, Color::cyan());
|
||||
const struct {
|
||||
Rect rect;
|
||||
Point line1_pos;
|
||||
Point line2_pos;
|
||||
} boxes[] = {
|
||||
{{13, 59, screen_width - 26, 116}, {12, 58}, {12, 157}},
|
||||
{{13, 165, screen_width - 24, 57}, {12, 164}, {12, 223}},
|
||||
{{13, 232, screen_width - 26, 67}, {12, 232}, {12, 300}}};
|
||||
|
||||
painter.fill_rectangle({13, 165, screen_width - 24, 57}, Color::dark_grey());
|
||||
painter.draw_hline({12, 164}, screen_width - 24, Color::cyan());
|
||||
painter.draw_hline({12, 223}, screen_width - 24, Color::cyan());
|
||||
|
||||
painter.fill_rectangle({13, 232, screen_width - 26, 67}, Color::dark_grey());
|
||||
painter.draw_hline({12, 232}, screen_width - 24, Color::cyan());
|
||||
painter.draw_hline({12, 300}, screen_width - 24, Color::cyan());
|
||||
for (const auto& box : boxes) {
|
||||
painter.fill_rectangle(box.rect, Color::dark_grey());
|
||||
painter.draw_hline(box.line1_pos, screen_width - 24, Color::cyan());
|
||||
painter.draw_hline(box.line2_pos, screen_width - 24, Color::cyan());
|
||||
}
|
||||
|
||||
// Radio status indicator
|
||||
Point indicator_pos{UI_POS_MAXWIDTH - 20, 59};
|
||||
if (is_transmitting) {
|
||||
painter.fill_rectangle({indicator_pos, {6, 6}}, Color::red());
|
||||
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
|
||||
} else {
|
||||
painter.fill_rectangle({indicator_pos, {6, 6}}, Color::green());
|
||||
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
|
||||
}
|
||||
painter.fill_rectangle({indicator_pos, {6, 6}}, is_transmitting ? Color::red() : Color::green());
|
||||
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
|
||||
}
|
||||
|
||||
void BattleshipView::draw_grid(Painter& painter, uint16_t grid_x, uint16_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid) {
|
||||
@@ -464,11 +428,14 @@ void BattleshipView::draw_cell(Painter& painter, uint16_t cell_x, uint16_t cell_
|
||||
|
||||
if (game_state == GameState::PLACING_SHIPS && !is_offense_grid && current_ship_index < 5) {
|
||||
uint8_t ship_size = my_ships[current_ship_index].size();
|
||||
uint8_t dx = placing_horizontal ? 1 : 0;
|
||||
uint8_t dy = placing_horizontal ? 0 : 1;
|
||||
uint16_t grid_x = (cell_x - 1) / CELL_SIZE;
|
||||
uint16_t grid_y = (cell_y - GRID_OFFSET_Y - 6) / CELL_SIZE;
|
||||
|
||||
for (uint8_t i = 0; i < ship_size; i++) {
|
||||
uint16_t preview_x = placing_horizontal ? cursor_x + i : cursor_x;
|
||||
uint16_t preview_y = placing_horizontal ? cursor_y : cursor_y + i;
|
||||
uint16_t grid_x = (cell_x - 1) / CELL_SIZE;
|
||||
uint16_t grid_y = (cell_y - GRID_OFFSET_Y - 6) / CELL_SIZE;
|
||||
uint16_t preview_x = cursor_x + (i * dx);
|
||||
uint16_t preview_y = cursor_y + (i * dy);
|
||||
if (grid_x == preview_x && grid_y == preview_y && preview_x < GRID_SIZE && preview_y < GRID_SIZE) {
|
||||
return;
|
||||
}
|
||||
@@ -540,9 +507,12 @@ void BattleshipView::draw_ship_preview(Painter& painter) {
|
||||
uint8_t size = ship.size();
|
||||
bool can_place = can_place_ship(cursor_x, cursor_y, size, placing_horizontal);
|
||||
|
||||
uint8_t dx = placing_horizontal ? 1 : 0;
|
||||
uint8_t dy = placing_horizontal ? 0 : 1;
|
||||
|
||||
for (uint8_t i = 0; i < size; i++) {
|
||||
uint8_t x = placing_horizontal ? cursor_x + i : cursor_x;
|
||||
uint8_t y = placing_horizontal ? cursor_y : cursor_y + i;
|
||||
uint8_t x = cursor_x + (i * dx);
|
||||
uint8_t y = cursor_y + (i * dy);
|
||||
|
||||
if (x < GRID_SIZE && y < GRID_SIZE) {
|
||||
uint16_t cell_x = GRID_OFFSET_X + x * CELL_SIZE + 1;
|
||||
@@ -561,18 +531,21 @@ bool BattleshipView::can_place_ship(uint8_t x, uint8_t y, uint8_t size, bool hor
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t dx = horizontal ? 1 : 0;
|
||||
uint8_t dy = horizontal ? 0 : 1;
|
||||
|
||||
for (uint8_t i = 0; i < size; i++) {
|
||||
uint8_t check_x = horizontal ? x + i : x;
|
||||
uint8_t check_y = horizontal ? y : y + i;
|
||||
uint8_t check_x = x + (i * dx);
|
||||
uint8_t check_y = y + (i * dy);
|
||||
|
||||
if (my_grid[check_y][check_x] != CellState::EMPTY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int dy = -1; dy <= 1; dy++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
int adj_x = check_x + dx;
|
||||
int adj_y = check_y + dy;
|
||||
for (int ddy = -1; ddy <= 1; ddy++) {
|
||||
for (int ddx = -1; ddx <= 1; ddx++) {
|
||||
int adj_x = check_x + ddx;
|
||||
int adj_y = check_y + ddy;
|
||||
|
||||
if (adj_x >= 0 && adj_x < GRID_SIZE &&
|
||||
adj_y >= 0 && adj_y < GRID_SIZE) {
|
||||
@@ -604,10 +577,11 @@ void BattleshipView::place_ship() {
|
||||
ship.horizontal = placing_horizontal;
|
||||
ship.placed = true;
|
||||
|
||||
uint8_t dx = placing_horizontal ? 1 : 0;
|
||||
uint8_t dy = placing_horizontal ? 0 : 1;
|
||||
|
||||
for (uint8_t i = 0; i < size; i++) {
|
||||
uint8_t x = placing_horizontal ? cursor_x + i : cursor_x;
|
||||
uint8_t y = placing_horizontal ? cursor_y : cursor_y + i;
|
||||
my_grid[y][x] = CellState::SHIP;
|
||||
my_grid[cursor_y + (i * dy)][cursor_x + (i * dx)] = CellState::SHIP;
|
||||
}
|
||||
|
||||
current_ship_index++;
|
||||
@@ -644,9 +618,14 @@ void BattleshipView::place_ship() {
|
||||
void BattleshipView::send_message(const GameMessage& msg) {
|
||||
static const char* msg_strings[] = {"READY", "SHOT:", "HIT:", "MISS:", "SUNK:", "WIN"};
|
||||
|
||||
std::string message = msg_strings[static_cast<int>(msg.type)];
|
||||
std::string message;
|
||||
message.reserve(16);
|
||||
message.append(msg_strings[static_cast<int>(msg.type)]);
|
||||
|
||||
if (msg.type != MessageType::READY && msg.type != MessageType::WIN) {
|
||||
message += to_string_dec_uint(msg.x) + "," + to_string_dec_uint(msg.y);
|
||||
message.append(to_string_dec_uint(msg.x));
|
||||
message.append(",");
|
||||
message.append(to_string_dec_uint(msg.y));
|
||||
}
|
||||
|
||||
configure_radio_tx();
|
||||
@@ -836,11 +815,12 @@ void BattleshipView::process_shot(uint8_t x, uint8_t y) {
|
||||
if (!ship.placed) continue;
|
||||
|
||||
bool hit_this_ship = false;
|
||||
for (uint8_t i = 0; i < ship.size(); i++) {
|
||||
uint8_t check_x = ship.horizontal ? ship.x + i : ship.x;
|
||||
uint8_t check_y = ship.horizontal ? ship.y : ship.y + i;
|
||||
|
||||
if (check_x == x && check_y == y) {
|
||||
uint8_t dx = ship.horizontal ? 1 : 0;
|
||||
uint8_t dy = ship.horizontal ? 0 : 1;
|
||||
|
||||
for (uint8_t i = 0; i < ship.size(); i++) {
|
||||
if ((ship.x + i * dx) == x && (ship.y + i * dy) == y) {
|
||||
hit_this_ship = true;
|
||||
ship.hits++;
|
||||
break;
|
||||
@@ -852,9 +832,7 @@ void BattleshipView::process_shot(uint8_t x, uint8_t y) {
|
||||
ships_remaining--;
|
||||
|
||||
for (uint8_t i = 0; i < ship.size(); i++) {
|
||||
uint8_t sunk_x = ship.horizontal ? ship.x + i : ship.x;
|
||||
uint8_t sunk_y = ship.horizontal ? ship.y : ship.y + i;
|
||||
my_grid[sunk_y][sunk_x] = CellState::SUNK;
|
||||
my_grid[ship.y + (i * dy)][ship.x + (i * dx)] = CellState::SUNK;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -887,7 +865,12 @@ void BattleshipView::process_shot(uint8_t x, uint8_t y) {
|
||||
}
|
||||
|
||||
void BattleshipView::update_score() {
|
||||
current_score = "W:" + to_string_dec_uint(wins) + " L:" + to_string_dec_uint(losses);
|
||||
current_score.clear();
|
||||
current_score.reserve(16);
|
||||
current_score.append("W:");
|
||||
current_score.append(to_string_dec_uint(wins));
|
||||
current_score.append(" L:");
|
||||
current_score.append(to_string_dec_uint(losses));
|
||||
}
|
||||
|
||||
bool BattleshipView::on_touch(const TouchEvent event) {
|
||||
@@ -980,3 +963,4 @@ bool BattleshipView::on_key(const KeyEvent key) {
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::battleship
|
||||
#pragma GCC pop_options
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
#ifndef __UI_BATTLESHIP_H__
|
||||
#define __UI_BATTLESHIP_H__
|
||||
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "modems.hpp"
|
||||
#include "bch_code.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "beacon.hpp"
|
||||
#include <cstdio>
|
||||
#include "string_format.hpp"
|
||||
|
||||
// Some but not all methods of beacon class have been externalized in .cpp file
|
||||
// This has been done to try and optimize application size by preventing the compiler to inline methods
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
size_t Beacon::formatSummary(char* buffer, bool with_time) {
|
||||
size_t result = 0;
|
||||
if (with_time) {
|
||||
result += formatTime(buffer);
|
||||
buffer[result++] = '-';
|
||||
}
|
||||
result += sprintf((buffer + result), "%4s-%5s-", shortId().c_str(), getType());
|
||||
if (location.isUnknown()) {
|
||||
result += sprintf((buffer + result), " ");
|
||||
} else {
|
||||
result += location.toString((buffer + result), Location::LocationFormat::MAIDENHEAD_LOCATOR);
|
||||
}
|
||||
result += sprintf((buffer + result), "[%s%s%s]", isFrameValid() ? (bch1Corrected || bch2Corrected) ? STR_COLOR_YELLOW : STR_COLOR_GREEN : STR_COLOR_RED, getSatus().c_str(), STR_COLOR_WHITE);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Beacon::isFrameValid() {
|
||||
return isBch1Valid() && ((!hasBch2) || isBch2Valid()) && (!isEmpty);
|
||||
}
|
||||
|
||||
std::string Beacon::shortId() {
|
||||
std::string result = std::string(hexId);
|
||||
return (result.size() >= 4) ? result.substr(0, 4) : result;
|
||||
}
|
||||
|
||||
size_t Beacon::formatTime(char* buffer) {
|
||||
return sprintf(buffer, "%02d:%02d:%02d", date.hour(), date.minute(), date.second());
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
+856
@@ -0,0 +1,856 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __BEACON_RX_H__
|
||||
#define __BEACON_RX_H__
|
||||
|
||||
#include "country.hpp"
|
||||
#include "location.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "resources.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
// Size for beacon frame buffer
|
||||
#define BEACON_DATA_SIZE 18 // Max 144 bits => 18 bytes
|
||||
|
||||
// Constants for BCH control code calculation
|
||||
#define BCH_21_POLYNOMIAL 0b1001101101100111100011UL
|
||||
#define BCH_21_POLY_LENGTH 22
|
||||
#define BCH_12_POLYNOMIAL 0b1010100111001UL
|
||||
#define BCH_12_POLY_LENGTH 13
|
||||
|
||||
// See content of sdcard/EPIRB/RES/BEACON.RES
|
||||
#define ADDITIONAL_DATA_RESOURCE_START 25
|
||||
#define LONG_ADDITIONAL_DATA_RESOURCE_START 33
|
||||
#define EMERGENCY_RESOURCE_START 39
|
||||
#define EMERGENCY_OTHER_RESOURCE_START 49
|
||||
#define AUX_DEVICE_RESOURCE_START 53
|
||||
|
||||
/**
|
||||
* This is the main class for beaon handling
|
||||
*/
|
||||
class Beacon {
|
||||
public:
|
||||
// Real beacon or test beacon ?
|
||||
enum class FrameMode { NORMAL,
|
||||
SELF_TEST,
|
||||
UNKNOWN };
|
||||
// Type of main location device
|
||||
enum class MainLocatingDevice { UNDEFINED,
|
||||
INTERNAL_NAV,
|
||||
EXTERNAL_NAV };
|
||||
// Type of aux location device
|
||||
enum class AuxLocatingDevice { UNDEFINED,
|
||||
NONE,
|
||||
NONE_OR_OTHER,
|
||||
OTHER,
|
||||
MHZ121_5,
|
||||
SART };
|
||||
|
||||
// Protcol type
|
||||
enum class ProtocolType {
|
||||
UNKNOWN,
|
||||
USER,
|
||||
STANDARD_LOCATION,
|
||||
NATIONAL_LOCATION,
|
||||
RLS_LOCATION,
|
||||
ELT_DT_LOCATION,
|
||||
SPARE
|
||||
};
|
||||
|
||||
// Protocol
|
||||
enum class Protocol {
|
||||
USER_EPIRB_MARITIME,
|
||||
USER_EPIRB_RADIO,
|
||||
USER_ELT,
|
||||
USER_SERIAL,
|
||||
USER_TEST,
|
||||
USER_ORB,
|
||||
USER_NAT,
|
||||
USER_2G,
|
||||
STD_EPIRB,
|
||||
STD_ELT_24,
|
||||
STD_ELT_SERIAL,
|
||||
STD_ELT_AIRCRAFT,
|
||||
STD_EPIRB_SERIAL,
|
||||
STD_PLB_SERIAL,
|
||||
STD_SHIP,
|
||||
STD_TEST,
|
||||
NAT_ELT,
|
||||
NAT_EPIRB,
|
||||
NAT_PLB,
|
||||
NAT_TEST,
|
||||
RLS,
|
||||
ELT_DT,
|
||||
SPARE,
|
||||
UNKNOWN
|
||||
};
|
||||
|
||||
#define UNKNOWN_LABEL "Unk."
|
||||
|
||||
// Returns true for User protocol beacons
|
||||
bool protocolIsUser() { return (protocolType == ProtocolType::USER); };
|
||||
// Returns true for National Location protocol beacons
|
||||
bool protocolIsNational() { return (protocolType == ProtocolType::NATIONAL_LOCATION); };
|
||||
// Returns true for Standard Location protocol beacons
|
||||
bool protocolIsStandard() { return (protocolType == ProtocolType::STANDARD_LOCATION); };
|
||||
// Returns true for RLS location protocol beacons
|
||||
bool protocolIsRls() { return (protocolType == ProtocolType::RLS_LOCATION); };
|
||||
// Returns true for RLS or ELT Location protocol beacons
|
||||
bool protocolIsRlsOrElt() { return (protocolType == ProtocolType::RLS_LOCATION || protocolType == ProtocolType::ELT_DT_LOCATION); };
|
||||
// Returns true for unknown protocol beacons
|
||||
bool protocolIsUnknown() { return (protocolType == ProtocolType::UNKNOWN); };
|
||||
|
||||
// Returns the protocol name
|
||||
const char* getProtocolName() {
|
||||
switch (protocolType) {
|
||||
case ProtocolType::STANDARD_LOCATION:
|
||||
return "Standard";
|
||||
case ProtocolType::NATIONAL_LOCATION:
|
||||
return "National";
|
||||
case ProtocolType::RLS_LOCATION:
|
||||
return "RLS";
|
||||
case ProtocolType::ELT_DT_LOCATION:
|
||||
return "ELT(DT)";
|
||||
case ProtocolType::USER:
|
||||
return longFrame ? "User Location" : "User";
|
||||
case ProtocolType::SPARE:
|
||||
return "Spare";
|
||||
default:
|
||||
return UNKNOWN_LABEL;
|
||||
}
|
||||
}
|
||||
|
||||
// Get protocol type for this beacon
|
||||
ProtocolType getProtocolType() {
|
||||
switch (protocol) {
|
||||
case Protocol::USER_EPIRB_MARITIME:
|
||||
case Protocol::USER_EPIRB_RADIO:
|
||||
case Protocol::USER_ELT:
|
||||
case Protocol::USER_SERIAL:
|
||||
case Protocol::USER_TEST:
|
||||
case Protocol::USER_ORB:
|
||||
case Protocol::USER_NAT:
|
||||
case Protocol::USER_2G:
|
||||
return ProtocolType::USER;
|
||||
case Protocol::STD_EPIRB:
|
||||
case Protocol::STD_ELT_24:
|
||||
case Protocol::STD_ELT_SERIAL:
|
||||
case Protocol::STD_ELT_AIRCRAFT:
|
||||
case Protocol::STD_PLB_SERIAL:
|
||||
case Protocol::STD_SHIP:
|
||||
case Protocol::STD_TEST:
|
||||
return ProtocolType::STANDARD_LOCATION;
|
||||
case Protocol::NAT_ELT:
|
||||
case Protocol::NAT_EPIRB:
|
||||
case Protocol::NAT_PLB:
|
||||
case Protocol::NAT_TEST:
|
||||
return ProtocolType::NATIONAL_LOCATION;
|
||||
case Protocol::RLS:
|
||||
return ProtocolType::RLS_LOCATION;
|
||||
case Protocol::ELT_DT:
|
||||
return ProtocolType::ELT_DT_LOCATION;
|
||||
case Protocol::SPARE:
|
||||
return ProtocolType::SPARE;
|
||||
case Protocol::UNKNOWN:
|
||||
default:
|
||||
return ProtocolType::UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// True for long frames, fals for short grames
|
||||
bool longFrame{true};
|
||||
// True if protocol flag is set for this frame
|
||||
bool protocolFlag{false};
|
||||
// Frame mode (Test or Real)
|
||||
FrameMode frameMode{FrameMode::UNKNOWN};
|
||||
// Main location device
|
||||
MainLocatingDevice mainLocatingDevice{MainLocatingDevice::UNDEFINED};
|
||||
// Axiliary location device
|
||||
AuxLocatingDevice auxLocatingDevice{AuxLocatingDevice::UNDEFINED};
|
||||
// Protocol code for this beacon
|
||||
long protocolCode{0};
|
||||
// Protocol of this beacon
|
||||
Protocol protocol{Protocol::UNKNOWN};
|
||||
// Protocol type of this beacon
|
||||
ProtocolType protocolType{ProtocolType::UNKNOWN};
|
||||
// Country of this beacon
|
||||
Country country{};
|
||||
// Frame data
|
||||
uint8_t frame[BEACON_DATA_SIZE];
|
||||
// Location of this beacon
|
||||
Location location{};
|
||||
// HexId of this beacon
|
||||
uint64_t identifier{0};
|
||||
// Date of reception of this beacon
|
||||
rtc::RTC date{};
|
||||
// Trus if this beacon has additional data
|
||||
bool hasAdditionalData{false};
|
||||
// The additional data tring
|
||||
char additionalData[48]{0};
|
||||
// True if this beacon has a serial number
|
||||
bool hasSerialNumber{false};
|
||||
// The serial number string
|
||||
char serialNumber[32]{0};
|
||||
// Beacon's Hex ID string
|
||||
char hexId[32]{0};
|
||||
// BCH1 code value
|
||||
uint32_t bch1{};
|
||||
// BCH1 calculated value
|
||||
uint32_t computedBch1{};
|
||||
// True if data has been corrected to match BCH1 code
|
||||
bool bch1Corrected{false};
|
||||
// True if this beacon has a BCH2 correction code
|
||||
bool hasBch2{false};
|
||||
// BCH2 code value
|
||||
uint32_t bch2{};
|
||||
// BCH2 calculated value
|
||||
uint32_t computedBch2{};
|
||||
// True if data has been corrected to match BCH2 code
|
||||
bool bch2Corrected{false};
|
||||
// True if this beacon is empty (not initialized)
|
||||
bool isEmpty{true};
|
||||
// True if thsi beacon contains emergency data
|
||||
bool hasEmergency{false};
|
||||
// True if emenrgency data is automatic for this beacon
|
||||
bool isAutoamticEmergency{false};
|
||||
// True if this beacon is a Maritime beacon
|
||||
bool isMaritime{false};
|
||||
// Emergency type string
|
||||
char emergencyType[32]{0};
|
||||
|
||||
/**
|
||||
* Returns the requested bits in this beacon's frame
|
||||
*/
|
||||
uint64_t getBits(int startBit, int endBit) {
|
||||
uint64_t result = 0;
|
||||
startBit--;
|
||||
int numBits = endBit - startBit;
|
||||
const uint8_t* pData = &(frame[startBit / 8]);
|
||||
uint8_t b = *pData;
|
||||
int bitOffset = 7 - (startBit % 8);
|
||||
for (int i = 0; i < numBits; ++i) {
|
||||
result <<= 1;
|
||||
result |= ((b >> bitOffset) & 0x01);
|
||||
if (--bitOffset < 0) {
|
||||
b = *(++pData);
|
||||
bitOffset = 7;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a BCH control code for the given bits and poly
|
||||
*/
|
||||
uint64_t computeBCH(int startBit, int endBit, unsigned long poly, int polyLength) {
|
||||
int dataLength = endBit - startBit + 1;
|
||||
int totalLength = dataLength + polyLength - 1;
|
||||
uint64_t result = getBits(startBit, startBit + polyLength - 1);
|
||||
for (int i = polyLength; i <= totalLength; i++) {
|
||||
bool firstBit = result >> (polyLength - 1);
|
||||
if (firstBit) result = result ^ poly;
|
||||
if (i < totalLength) {
|
||||
result = result << 1;
|
||||
if (i < dataLength) result |= getBits(startBit + i, startBit + i);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute BCH1 code
|
||||
*/
|
||||
uint64_t computeBCH1() { return computeBCH(25, 85, BCH_21_POLYNOMIAL, BCH_21_POLY_LENGTH); }
|
||||
/**
|
||||
* Compute BCH2 code
|
||||
*/
|
||||
uint64_t computeBCH2() { return computeBCH(107, 132, BCH_12_POLYNOMIAL, BCH_12_POLY_LENGTH); }
|
||||
|
||||
/**
|
||||
* Convert this frame's data to an hex string
|
||||
*/
|
||||
static size_t toHexString(char* buffer, uint8_t* frame, bool withSpace, int start, int end) {
|
||||
size_t result = 0;
|
||||
for (uint8_t i = start; i < end; i++) {
|
||||
if (withSpace && i > start) buffer[result++] = ' ';
|
||||
result += sprintf((buffer + result), "%02X", frame[i]);
|
||||
}
|
||||
buffer[result] = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
Beacon() {}
|
||||
Beacon(const Beacon& other) = delete;
|
||||
Beacon& operator=(const Beacon& other) = delete;
|
||||
|
||||
/**
|
||||
* Set the data for this Beacon and parse it
|
||||
*/
|
||||
void setFrame(const uint8_t* frameBuffer) {
|
||||
frameMode = FrameMode::UNKNOWN;
|
||||
mainLocatingDevice = MainLocatingDevice::UNDEFINED;
|
||||
auxLocatingDevice = AuxLocatingDevice::UNDEFINED;
|
||||
protocolCode = 0;
|
||||
protocol = Protocol::UNKNOWN;
|
||||
protocolType = ProtocolType::UNKNOWN;
|
||||
country.code = 0;
|
||||
country.alphaCode[0] = 0;
|
||||
country.shortName[0] = 0;
|
||||
location.clear();
|
||||
identifier = 0;
|
||||
hasAdditionalData = false;
|
||||
additionalData[0] = 0;
|
||||
hasSerialNumber = false;
|
||||
serialNumber[0] = 0;
|
||||
hexId[0] = 0;
|
||||
bch1 = 0;
|
||||
computedBch1 = 0;
|
||||
hasBch2 = false;
|
||||
bch1Corrected = false;
|
||||
bch2 = 0;
|
||||
computedBch2 = 0;
|
||||
bch2Corrected = false;
|
||||
isEmpty = true;
|
||||
hasEmergency = false;
|
||||
isAutoamticEmergency = false;
|
||||
isMaritime = false;
|
||||
emergencyType[0] = 0;
|
||||
std::memcpy(frame, (const void*)frameBuffer, BEACON_DATA_SIZE);
|
||||
parseFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flip the specified bit in this beacon's data
|
||||
*/
|
||||
void flipBit(int bit) {
|
||||
int byteIdx = (bit - 1) / 8;
|
||||
int bitIdx = 7 - ((bit - 1) % 8);
|
||||
frame[byteIdx] ^= (1 << bitIdx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Single bit error correction to match BCH1 or BCH2 code
|
||||
*/
|
||||
void simpleCorrection(bool isBCH1) {
|
||||
for (int i = (isBCH1 ? 25 : 107); i <= (isBCH1 ? 85 : 132); i++) {
|
||||
flipBit(i);
|
||||
if ((isBCH1 ? (computeBCH1() == bch1) : (computeBCH2() == bch2))) {
|
||||
if (isBCH1) {
|
||||
computedBch1 = bch1;
|
||||
bch1Corrected = true;
|
||||
} else {
|
||||
computedBch2 = bch2;
|
||||
bch2Corrected = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
flipBit(i); // Restore bit value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the string representation of this beacon's type
|
||||
*/
|
||||
const char* getType() {
|
||||
if ((protocol == Protocol::USER_EPIRB_MARITIME) || (protocol == Protocol::USER_EPIRB_RADIO) || (protocol == Protocol::STD_EPIRB) || (protocol == Protocol::STD_EPIRB_SERIAL) || (protocol == Protocol::NAT_EPIRB)) return "EPIRB";
|
||||
if ((protocol == Protocol::USER_ELT) || (protocol == Protocol::STD_ELT_24) || (protocol == Protocol::STD_ELT_SERIAL) || (protocol == Protocol::STD_ELT_AIRCRAFT) || (protocol == Protocol::NAT_ELT) || (protocol == Protocol::ELT_DT)) return "ELT";
|
||||
if ((protocol == Protocol::STD_PLB_SERIAL) || (protocol == Protocol::NAT_PLB)) return "PLB";
|
||||
if ((protocol == Protocol::USER_TEST) || (protocol == Protocol::STD_TEST) || (protocol == Protocol::NAT_TEST)) return "TEST";
|
||||
if (protocol == Protocol::USER_SERIAL) return "SRIAL";
|
||||
if (protocol == Protocol::USER_ORB) return "ORB";
|
||||
if (protocol == Protocol::USER_NAT) return "NAT";
|
||||
if (protocol == Protocol::USER_2G) return "2G";
|
||||
if (protocol == Protocol::STD_SHIP) return "SHIP";
|
||||
if (protocol == Protocol::RLS) return "RLS";
|
||||
if (protocol == Protocol::SPARE) return "SPARE";
|
||||
return UNKNOWN_LABEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this beacon has a main location device
|
||||
*/
|
||||
bool hasMainLocatingDevice() { return (mainLocatingDevice != MainLocatingDevice::UNDEFINED); }
|
||||
|
||||
/**
|
||||
* Returns this beacon's main location device string representation
|
||||
*/
|
||||
const char* getMainLocatingDeviceName() {
|
||||
if (mainLocatingDevice == MainLocatingDevice::EXTERNAL_NAV) return "Exernal";
|
||||
if (mainLocatingDevice == MainLocatingDevice::INTERNAL_NAV) return "Internal";
|
||||
return UNKNOWN_LABEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this beacon has a main location device
|
||||
*/
|
||||
bool hasAuxLocatingDevice() { return (auxLocatingDevice != AuxLocatingDevice::UNDEFINED); }
|
||||
|
||||
/**
|
||||
* Returns true if this beacon has a main location device
|
||||
*/
|
||||
const char* getAuxLocatingDeviceName() {
|
||||
return ResourceManager::get_beacon_resource(AUX_DEVICE_RESOURCE_START + ((int)auxLocatingDevice));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the serial number for this beacon
|
||||
*/
|
||||
void setSerialNumber(uint32_t serial) {
|
||||
sprintf(serialNumber, "%ld (0x%08lX)", serial, serial);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if BCH1 code is valid
|
||||
*/
|
||||
bool isBch1Valid() { return (bch1 == computedBch1); }
|
||||
|
||||
/**
|
||||
* Returns true if BCH2 code is valid
|
||||
*/
|
||||
|
||||
bool isBch2Valid() { return (bch2 == computedBch2); }
|
||||
|
||||
/**
|
||||
* Returns true if frame is valid
|
||||
*/
|
||||
bool isFrameValid();
|
||||
|
||||
/**
|
||||
* Returns true if frame is orbito
|
||||
*/
|
||||
bool isOrbito() { return (protocol == Protocol::USER_ORB); }
|
||||
|
||||
/**
|
||||
* Write the hex preresentation of this frame in the provided buffer
|
||||
*/
|
||||
size_t hexString(char* buffer, bool withHeader) {
|
||||
return toHexString(buffer, frame, false, (withHeader ? 0 : 3), (longFrame ? 18 : 14));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the short ID for this beacon
|
||||
*/
|
||||
std::string shortId();
|
||||
|
||||
/**
|
||||
* Returns the string representation of the status of this frame
|
||||
*/
|
||||
std::string getSatus() {
|
||||
if (isFrameValid())
|
||||
return "OK";
|
||||
else
|
||||
return "KO";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format this beacon's time
|
||||
*/
|
||||
size_t formatTime(char* buffer);
|
||||
|
||||
/**
|
||||
* Format this beacon's summary
|
||||
*/
|
||||
size_t formatSummary(char* buffer, bool with_time);
|
||||
|
||||
private:
|
||||
/* Baudot code matrix */
|
||||
static constexpr char BAUDOT_CODE[64] = {' ', '5', ' ', '9', ' ', ' ', ' ', ' ', ' ', ' ', '4', ' ', '8', '0', ' ', ' ',
|
||||
'3', ' ', ' ', ' ', ' ', '6', ' ', '/', '-', '2', ' ', ' ', '7', '1', ' ', ' ',
|
||||
' ', 'T', ' ', 'O', ' ', 'H', 'N', 'M', ' ', 'L', 'R', 'G', 'I', 'P', 'C', 'V',
|
||||
'E', 'Z', 'D', 'B', 'S', 'Y', 'F', 'X', 'A', 'W', 'J', ' ', 'U', 'Q', 'K', '\0'};
|
||||
|
||||
/**
|
||||
* Parse this beacon's protocol
|
||||
*/
|
||||
void parseProtocol() {
|
||||
protocolFlag = getBits(26, 26);
|
||||
if (protocolFlag)
|
||||
protocolCode = getBits(37, 39);
|
||||
else
|
||||
protocolCode = getBits(37, 40);
|
||||
|
||||
if (!longFrame || protocolFlag == 1) {
|
||||
switch (protocolCode) {
|
||||
case 0b000:
|
||||
protocol = Protocol::USER_ORB;
|
||||
break;
|
||||
case 0b001:
|
||||
protocol = Protocol::USER_ELT;
|
||||
break;
|
||||
case 0b010:
|
||||
protocol = Protocol::USER_EPIRB_MARITIME;
|
||||
isMaritime = true;
|
||||
break;
|
||||
case 0b011:
|
||||
protocol = Protocol::USER_SERIAL;
|
||||
break;
|
||||
case 0b100:
|
||||
protocol = Protocol::USER_NAT;
|
||||
break;
|
||||
case 0b101:
|
||||
protocol = Protocol::USER_2G;
|
||||
break;
|
||||
case 0b110:
|
||||
protocol = Protocol::USER_EPIRB_RADIO;
|
||||
isMaritime = true;
|
||||
break;
|
||||
case 0b111:
|
||||
protocol = Protocol::USER_TEST;
|
||||
break;
|
||||
default:
|
||||
protocol = Protocol::UNKNOWN;
|
||||
}
|
||||
} else {
|
||||
switch (protocolCode) {
|
||||
case 0b0010:
|
||||
protocol = Protocol::STD_EPIRB;
|
||||
break;
|
||||
case 0b0011:
|
||||
protocol = Protocol::STD_ELT_24;
|
||||
break;
|
||||
case 0b0100:
|
||||
protocol = Protocol::STD_ELT_SERIAL;
|
||||
break;
|
||||
case 0b0101:
|
||||
protocol = Protocol::STD_ELT_AIRCRAFT;
|
||||
break;
|
||||
case 0b0110:
|
||||
protocol = Protocol::STD_EPIRB_SERIAL;
|
||||
break;
|
||||
case 0b0111:
|
||||
protocol = Protocol::STD_PLB_SERIAL;
|
||||
break;
|
||||
case 0b1000:
|
||||
protocol = Protocol::NAT_ELT;
|
||||
break;
|
||||
case 0b1001:
|
||||
protocol = Protocol::ELT_DT;
|
||||
break;
|
||||
case 0b1010:
|
||||
protocol = Protocol::NAT_EPIRB;
|
||||
break;
|
||||
case 0b1011:
|
||||
protocol = Protocol::NAT_PLB;
|
||||
break;
|
||||
case 0b1100:
|
||||
protocol = Protocol::STD_SHIP;
|
||||
break;
|
||||
case 0b1101:
|
||||
protocol = Protocol::RLS;
|
||||
break;
|
||||
case 0b1110:
|
||||
protocol = Protocol::STD_TEST;
|
||||
break;
|
||||
case 0b1111:
|
||||
protocol = Protocol::NAT_TEST;
|
||||
break;
|
||||
default:
|
||||
protocol = Protocol::UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse this beacon's additional data
|
||||
*/
|
||||
void parseAdditionalData() {
|
||||
hasAdditionalData = false;
|
||||
hasSerialNumber = false;
|
||||
if (protocolFlag) {
|
||||
if (protocolCode == 0b011) {
|
||||
uint8_t serialUserProtocol = getBits(40, 42);
|
||||
hasAdditionalData = true;
|
||||
switch (serialUserProtocol) {
|
||||
case 0b100:
|
||||
case 0b010:
|
||||
isMaritime = true;
|
||||
// fallthrough
|
||||
case 0b000:
|
||||
case 0b001:
|
||||
case 0b011:
|
||||
case 0b110:
|
||||
strcpy(additionalData, ResourceManager::get_beacon_resource(ADDITIONAL_DATA_RESOURCE_START + serialUserProtocol));
|
||||
break;
|
||||
default:
|
||||
hasAdditionalData = false;
|
||||
}
|
||||
hasSerialNumber = true;
|
||||
setSerialNumber((serialUserProtocol == 0b011) ? getBits(44, 67) : (serialUserProtocol == 0b001) ? getBits(62, 73)
|
||||
: getBits(44, 63));
|
||||
}
|
||||
if (!longFrame) {
|
||||
hasEmergency = getBits(107, 107);
|
||||
if (hasEmergency) {
|
||||
isAutoamticEmergency = getBits(108, 108);
|
||||
if (isMaritime) {
|
||||
uint8_t emmergency = getBits(109, 112);
|
||||
switch (emmergency) {
|
||||
case 0b0001:
|
||||
case 0b0010:
|
||||
case 0b0011:
|
||||
case 0b0100:
|
||||
case 0b0101:
|
||||
case 0b0110:
|
||||
case 0b0111:
|
||||
case 0b1000:
|
||||
strcpy(emergencyType, ResourceManager::get_beacon_resource(EMERGENCY_RESOURCE_START + emmergency));
|
||||
break;
|
||||
default:
|
||||
strcpy(emergencyType, ResourceManager::get_beacon_resource(EMERGENCY_RESOURCE_START));
|
||||
}
|
||||
} else {
|
||||
bool fire = getBits(109, 109);
|
||||
bool med = getBits(110, 110);
|
||||
bool disabled = getBits(111, 111);
|
||||
char* emmergency_pointer = emergencyType;
|
||||
if (fire) emmergency_pointer += sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START));
|
||||
if (med) {
|
||||
if (fire) emmergency_pointer += sprintf(emmergency_pointer, "%c", '+');
|
||||
emmergency_pointer += sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START + 1));
|
||||
}
|
||||
if (disabled) {
|
||||
if (fire || med) emmergency_pointer += sprintf(emmergency_pointer, "%c", '+');
|
||||
sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START + 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (longFrame) {
|
||||
switch (protocolCode) {
|
||||
case 0b0010:
|
||||
hasSerialNumber = true;
|
||||
setSerialNumber(getBits(61, 64));
|
||||
break;
|
||||
case 0b1100: {
|
||||
hasAdditionalData = true;
|
||||
uint32_t mmsi = getBits(41, 60);
|
||||
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START), mmsi, mmsi);
|
||||
} break;
|
||||
case 0b0011: {
|
||||
hasAdditionalData = true;
|
||||
hasSerialNumber = true;
|
||||
setSerialNumber(getBits(41, 64));
|
||||
strcpy(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 1));
|
||||
} break;
|
||||
case 0b0100:
|
||||
case 0b0110:
|
||||
case 0b0111: {
|
||||
hasAdditionalData = true;
|
||||
hasSerialNumber = true;
|
||||
uint32_t csTaNumber = getBits(41, 50);
|
||||
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 2), csTaNumber);
|
||||
setSerialNumber(getBits(51, 64));
|
||||
} break;
|
||||
case 0b0101: {
|
||||
hasAdditionalData = true;
|
||||
hasSerialNumber = true;
|
||||
uint32_t data = getBits(41, 45);
|
||||
char char1 = BAUDOT_CODE[data + 32];
|
||||
data = getBits(46, 50);
|
||||
char char2 = BAUDOT_CODE[data + 32];
|
||||
data = getBits(51, 55);
|
||||
char char3 = BAUDOT_CODE[data + 32];
|
||||
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 3), char1, char2, char3);
|
||||
setSerialNumber(getBits(56, 64));
|
||||
} break;
|
||||
case 0b1000:
|
||||
case 0b1010:
|
||||
case 0b1011:
|
||||
case 0b1111: {
|
||||
hasSerialNumber = true;
|
||||
hasAdditionalData = true;
|
||||
setSerialNumber(getBits(41, 58));
|
||||
uint32_t natNum = getBits(127, 132);
|
||||
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 4), natNum);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse this beacon's locating device
|
||||
*/
|
||||
void parseLocatingDevices() {
|
||||
bool mainLoc;
|
||||
if (protocolIsStandard() || protocolIsNational()) {
|
||||
mainLoc = getBits(111, 111);
|
||||
mainLocatingDevice = mainLoc ? MainLocatingDevice::INTERNAL_NAV : MainLocatingDevice::EXTERNAL_NAV;
|
||||
} else if (protocolIsUser() || protocolIsRls()) {
|
||||
mainLoc = getBits(107, 107);
|
||||
mainLocatingDevice = mainLoc ? MainLocatingDevice::INTERNAL_NAV : MainLocatingDevice::EXTERNAL_NAV;
|
||||
}
|
||||
if (protocolIsStandard() || protocolIsNational()) {
|
||||
auxLocatingDevice = getBits(112, 112) ? AuxLocatingDevice::MHZ121_5 : AuxLocatingDevice::NONE_OR_OTHER;
|
||||
} else if (protocolIsRls()) {
|
||||
auxLocatingDevice = getBits(108, 108) ? AuxLocatingDevice::MHZ121_5 : AuxLocatingDevice::NONE_OR_OTHER;
|
||||
} else if (protocolIsUser() && protocolCode != 0b100) {
|
||||
uint8_t aux = getBits(84, 85);
|
||||
if (aux == 0b00)
|
||||
auxLocatingDevice = AuxLocatingDevice::NONE;
|
||||
else if (aux == 0b01)
|
||||
auxLocatingDevice = AuxLocatingDevice::MHZ121_5;
|
||||
else if (aux == 0b10)
|
||||
auxLocatingDevice = AuxLocatingDevice::SART;
|
||||
else
|
||||
auxLocatingDevice = AuxLocatingDevice::OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse this beacon's frame data
|
||||
*/
|
||||
void parseFrame() {
|
||||
long latofmin, latofsec, lonofmin, lonofsec;
|
||||
bool latoffset, lonoffset;
|
||||
|
||||
longFrame = getBits(25, 25);
|
||||
parseProtocol();
|
||||
protocolType = getProtocolType();
|
||||
CountryManager::get_country(getBits(27, 36), country);
|
||||
|
||||
if (frame[2] == 0xD0)
|
||||
frameMode = FrameMode::SELF_TEST;
|
||||
else if (frame[2] == 0x2F)
|
||||
frameMode = FrameMode::NORMAL;
|
||||
else
|
||||
frameMode = FrameMode::UNKNOWN;
|
||||
|
||||
if (longFrame) {
|
||||
if (protocolIsUser() && !isOrbito()) {
|
||||
location.latitude.orientation = (frame[13] & 0x10) >> 4;
|
||||
location.latitude.degrees = ((frame[13] & 0x0F) << 3 | (frame[14] & 0xE0) >> 5);
|
||||
location.latitude.minutes = ((frame[14] & 0x1E) >> 1) * 4;
|
||||
location.longitude.orientation = (frame[14] & 0x01);
|
||||
location.longitude.degrees = (frame[15]);
|
||||
location.longitude.minutes = ((frame[16] & 0xF0) >> 4) * 4;
|
||||
} else if (protocolIsNational()) {
|
||||
latoffset = (frame[14] & 0x80) >> 7;
|
||||
location.latitude.orientation = (frame[7] & 0x20) >> 5;
|
||||
location.latitude.degrees = ((frame[7] & 0x1F) << 2 | (frame[8] & 0xC0) >> 6);
|
||||
location.latitude.minutes = ((frame[8] & 0x3E) >> 1) * 2;
|
||||
latofmin = (frame[14] & 0x60) >> 5;
|
||||
latofsec = ((frame[14] & 0x1E) >> 1) * 4;
|
||||
location.latitude.apply_offset(latoffset, latofmin, latofsec);
|
||||
lonoffset = (frame[14] & 0x01);
|
||||
location.longitude.orientation = (frame[8] & 0x01);
|
||||
location.longitude.degrees = (frame[9]);
|
||||
location.longitude.minutes = ((frame[10] & 0xF8) >> 3) * 2;
|
||||
lonofmin = (frame[15] & 0xC0) >> 6;
|
||||
lonofsec = ((frame[15] & 0x3C) >> 2) * 4;
|
||||
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
|
||||
} else if (protocolIsStandard()) {
|
||||
latoffset = (frame[14] & 0x80) >> 7;
|
||||
location.latitude.orientation = (frame[8] & 0x80) >> 7;
|
||||
location.latitude.degrees = (frame[8] & 0x7F);
|
||||
location.latitude.minutes = ((frame[9] & 0xC0) >> 6) * 15;
|
||||
latofmin = (frame[14] & 0x7C) >> 2;
|
||||
latofsec = ((frame[14] & 0x03) << 2 | (frame[15] & 0xC0) >> 6) * 4;
|
||||
location.latitude.apply_offset(latoffset, latofmin, latofsec);
|
||||
lonoffset = (frame[15] & 0x20) >> 5;
|
||||
location.longitude.orientation = (frame[9] & 0x20) >> 5;
|
||||
location.longitude.degrees = ((frame[9] & 0x1F) << 3 | (frame[10] & 0xE0) >> 5);
|
||||
location.longitude.minutes = ((frame[10] & 0x18) >> 3) * 15;
|
||||
lonofmin = (frame[15] & 0x1F);
|
||||
lonofsec = ((frame[16] & 0xF0) >> 4) * 4;
|
||||
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
|
||||
} else if (protocolIsRlsOrElt()) {
|
||||
latoffset = (frame[14] & 0x20) >> 5;
|
||||
location.latitude.orientation = (frame[8] & 0x20) >> 5;
|
||||
location.latitude.degrees = ((frame[8] & 0x1F) << 2) | ((frame[9] & 0xC0) >> 6);
|
||||
location.latitude.minutes = ((frame[9] & 0x20) >> 5) * 30;
|
||||
latofmin = (frame[14] & 0x1E) >> 1;
|
||||
latofsec = ((frame[14] & 0x01) << 3 | (frame[15] & 0xE0) >> 5) * 4;
|
||||
location.latitude.apply_offset(latoffset, latofmin, latofsec);
|
||||
lonoffset = (frame[15] & 0x10) >> 4;
|
||||
location.longitude.orientation = (frame[9] & 0x10) >> 4;
|
||||
location.longitude.degrees = ((frame[9] & 0x0F) << 4 | (frame[10] & 0xF0) >> 4);
|
||||
location.longitude.minutes = ((frame[10] & 0x08) >> 3) * 30;
|
||||
lonofmin = (frame[15] & 0x0F);
|
||||
lonofsec = ((frame[16] & 0xF0) >> 4) * 4;
|
||||
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
|
||||
}
|
||||
}
|
||||
|
||||
parseAdditionalData();
|
||||
parseLocatingDevices();
|
||||
|
||||
identifier = getBits(26, 85);
|
||||
|
||||
uint32_t hexIdHigh = (uint32_t)(identifier >> 32);
|
||||
uint32_t hexIdLow = (uint32_t)identifier;
|
||||
|
||||
if (protocolIsStandard()) {
|
||||
// default value of bits 65 to 74 = 0 111111111 / default value of bits 75 to 85 = 0 1111111111
|
||||
hexIdLow &= 0b11111111'11100000'00000000'00000000;
|
||||
hexIdLow |= 0b00000000'00001111'11111011'11111111;
|
||||
} else if (protocolIsNational()) {
|
||||
// default value of bits 59 to 71 = 0 1111111 00000 / default value of bits 72 to 85 = 0 11111111 00000
|
||||
hexIdLow &= 0b11111000'00000000'00000000'00000000;
|
||||
hexIdLow |= 0b00000011'11111000'00011111'11100000;
|
||||
} else if (protocolIsRlsOrElt()) {
|
||||
// default value of bits 67 to 75 = 0 11111111 / default value of bits 76 to 85 = 0 111111111
|
||||
hexIdLow &= 0b11111111'11111000'00000000'00000000;
|
||||
hexIdLow |= 0b00000000'00000011'11111101'11111111;
|
||||
}
|
||||
std::sprintf(hexId, "%07lX%08lX", hexIdHigh, hexIdLow);
|
||||
|
||||
if (isOrbito() && !longFrame) {
|
||||
isEmpty = true;
|
||||
for (size_t i = 3; i < BEACON_DATA_SIZE; i++) {
|
||||
if (frame[i] != 0) {
|
||||
isEmpty = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else
|
||||
isEmpty = false;
|
||||
|
||||
bch1 = getBits(86, 106);
|
||||
computedBch1 = computeBCH1();
|
||||
// Try and correct single bit error on BCH1 (bits 25-85)
|
||||
if (computedBch1 != bch1) {
|
||||
simpleCorrection(true);
|
||||
}
|
||||
|
||||
hasBch2 = longFrame && !isOrbito();
|
||||
if (hasBch2) {
|
||||
bch2 = getBits(133, 144);
|
||||
computedBch2 = computeBCH2();
|
||||
// Try and correct single bit error on BCH2 (bits 107 - 132)
|
||||
if (computedBch2 != bch2) {
|
||||
simpleCorrection(false);
|
||||
}
|
||||
}
|
||||
static bool isCorrecting = false;
|
||||
// If BCH1 or BCH2 has been corrected, we need to parse frame again to update beacon properties
|
||||
if (!isCorrecting && (bch1Corrected || bch2Corrected)) {
|
||||
// Prevent recursion
|
||||
isCorrecting = true;
|
||||
parseFrame();
|
||||
isCorrecting = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif // __BEACON_RX_H__
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "beacon_db.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
/**
|
||||
* Add a beacon to the database.
|
||||
* @return the added beacon reference.
|
||||
*/
|
||||
Beacon& BeaconDB::add_beacon() {
|
||||
Beacon& result = recent_beacons[recent_beacon_pos];
|
||||
recent_beacon_pos = (recent_beacon_pos + 1) % BEACON_HISTORY_SIZE;
|
||||
if (recent_beacon_pos == 0) recent_beacon_full = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the beacon database
|
||||
* @return the size of the beacon database
|
||||
*/
|
||||
size_t BeaconDB::size() {
|
||||
return recent_beacon_full ? BEACON_HISTORY_SIZE : recent_beacon_pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the database is empty
|
||||
* @return true if the database is empty
|
||||
*/
|
||||
bool BeaconDB::empty() {
|
||||
return (!recent_beacon_full && (recent_beacon_pos == 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the beacon at the provided index
|
||||
*/
|
||||
Beacon& BeaconDB::get_beacon(size_t index) {
|
||||
// Start from last beacon et go backward
|
||||
int16_t pos = (int16_t)recent_beacon_pos - 1 - index;
|
||||
while (pos < 0) pos += BEACON_HISTORY_SIZE;
|
||||
return recent_beacons[pos % BEACON_HISTORY_SIZE];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set currently selected beacon
|
||||
* @param index the selected index
|
||||
*/
|
||||
void BeaconDB::set_current_beacon(size_t index) {
|
||||
current_beacon_index = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently selected index
|
||||
* @return the currently selected index
|
||||
*/
|
||||
size_t BeaconDB::get_current_beacon_index() {
|
||||
return current_beacon_index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected beacon
|
||||
* @return the currently selected beacon
|
||||
*/
|
||||
Beacon& BeaconDB::get_current_beacon() {
|
||||
return get_beacon(current_beacon_index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear beacon database
|
||||
*/
|
||||
void BeaconDB::clear() {
|
||||
recent_beacon_pos = 0;
|
||||
recent_beacon_full = false;
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __BEACON_DB_H__
|
||||
#define __BEACON_DB_H__
|
||||
|
||||
#include "beacon.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
// Size of beacon database calculated to match the beacon list component size
|
||||
#define BEACON_HISTORY_SIZE 13
|
||||
|
||||
class BeaconDB {
|
||||
public:
|
||||
Beacon& add_beacon();
|
||||
Beacon& get_beacon(size_t index);
|
||||
Beacon& get_current_beacon();
|
||||
size_t get_current_beacon_index();
|
||||
void set_current_beacon(size_t index);
|
||||
size_t size();
|
||||
bool empty();
|
||||
void clear();
|
||||
|
||||
private:
|
||||
// Actual beacon database array
|
||||
Beacon recent_beacons[BEACON_HISTORY_SIZE];
|
||||
// Position of the insertion pointer
|
||||
int8_t recent_beacon_pos{0};
|
||||
// Beacon selection handling
|
||||
size_t current_beacon_index{0};
|
||||
// True when database is full
|
||||
bool recent_beacon_full{false};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif /* __BEACON_DB_H__ */
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __COUNTRY_RX_H__
|
||||
#define __COUNTRY_RX_H__
|
||||
|
||||
#include "string_format.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
#define DISABLE_COUNTRY_CACHE
|
||||
|
||||
// Country struct
|
||||
struct Country {
|
||||
int16_t code;
|
||||
char alphaCode[4]; // 3 chars + null
|
||||
char shortName[11]; // 10 chars + null
|
||||
};
|
||||
|
||||
class CountryManager {
|
||||
public:
|
||||
static void get_country(int code, Country& out) {
|
||||
#ifndef DISABLE_COUNTRY_CACHE
|
||||
// Check cache
|
||||
for (int i = 0; i < cache_count; i++) {
|
||||
if (cache[i].code == code) {
|
||||
out = cache[i];
|
||||
if (i > 0) { // Promotion
|
||||
Country tmp = cache[i];
|
||||
for (int j = i; j > 0; j--) cache[j] = cache[j - 1];
|
||||
cache[0] = tmp;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Cache miss => load from file
|
||||
if (load_from_file(code, out)) {
|
||||
#ifndef DISABLE_COUNTRY_CACHE
|
||||
// Insert in cache
|
||||
int limit = (cache_count < 16) ? cache_count : 15;
|
||||
for (int j = limit; j > 0; j--) cache[j] = cache[j - 1];
|
||||
cache[0] = out;
|
||||
if (cache_count < 16) cache_count++;
|
||||
#endif
|
||||
} else {
|
||||
out.code = code;
|
||||
out.alphaCode[0] = '\0';
|
||||
out.shortName[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
#ifndef DISABLE_COUNTRY_CACHE
|
||||
static Country cache[16];
|
||||
static int cache_count;
|
||||
#endif
|
||||
|
||||
static bool load_from_file(int target_code, Country& out) {
|
||||
FIL file;
|
||||
if (f_open(&file, (epirb_dir / u"RES/COUNTRY.RES").tchar(), FA_READ) != FR_OK) return false;
|
||||
|
||||
TCHAR line[48];
|
||||
bool found = false;
|
||||
|
||||
while (f_gets(line, sizeof(line) / sizeof(TCHAR), &file)) {
|
||||
// Light parsing method
|
||||
TCHAR* p = line;
|
||||
|
||||
// Parse CODE
|
||||
int c = 0;
|
||||
while (*p >= '0' && *p <= '9') c = c * 10 + (*p++ - '0');
|
||||
if (c != target_code || *p != ':') continue;
|
||||
p++; // Skip ':'
|
||||
|
||||
out.code = (int16_t)c;
|
||||
|
||||
// Parse ALPHA CODE (3 chars)
|
||||
for (int i = 0; i < 3 && *p != ':'; i++) out.alphaCode[i] = (char)*p++;
|
||||
out.alphaCode[3] = '\0';
|
||||
if (*p != ':') continue;
|
||||
p++; // Skip ':'
|
||||
|
||||
// Parse SHORT NAME
|
||||
int i = 0;
|
||||
while (i < static_cast<int>(sizeof(out.shortName) - 1) && *p != '\r' && *p != '\n' && *p != '\0') {
|
||||
out.shortName[i++] = (char)*p++;
|
||||
}
|
||||
out.shortName[i] = '\0';
|
||||
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
f_close(&file);
|
||||
return found;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif // __COUNTRY_RX_H__
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "location.hpp"
|
||||
#include <cstdio>
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
void Location::Angle::clear() {
|
||||
degrees = 255;
|
||||
minutes = 0;
|
||||
seconds = 0;
|
||||
orientation = false;
|
||||
floatValue = 255;
|
||||
}
|
||||
|
||||
float Location::Angle::getFloatValue() {
|
||||
if (floatValue >= 255) {
|
||||
floatValue = (float)degrees;
|
||||
floatValue += ((float)minutes / 60.0f);
|
||||
floatValue += ((float)seconds / 3600.0f);
|
||||
if (orientation) {
|
||||
floatValue = -floatValue;
|
||||
}
|
||||
}
|
||||
return floatValue;
|
||||
}
|
||||
|
||||
void Location::Angle::apply_offset(bool positive, long ofmin, long ofsec) {
|
||||
if (positive) {
|
||||
minutes += ofmin;
|
||||
seconds += ofsec;
|
||||
} else {
|
||||
minutes -= ofmin;
|
||||
if (minutes < 0) {
|
||||
minutes += 60;
|
||||
degrees -= 1;
|
||||
}
|
||||
seconds -= ofsec;
|
||||
if (seconds < 0) {
|
||||
seconds += 60;
|
||||
minutes -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Location::clear() {
|
||||
latitude.clear();
|
||||
longitude.clear();
|
||||
}
|
||||
|
||||
bool Location::isUnknown() {
|
||||
return (latitude.degrees >= 255 || longitude.degrees >= 255);
|
||||
}
|
||||
|
||||
char Location::gps_letterize(int x) {
|
||||
return (char)x + 65;
|
||||
}
|
||||
|
||||
size_t Location::gps_compute_locator(char* buffer, float lat, float lon, int precision) {
|
||||
size_t result = 0;
|
||||
|
||||
lon += 180.0f;
|
||||
lat += 90.0f;
|
||||
|
||||
int A = lon / 20;
|
||||
int B = lat / 10;
|
||||
|
||||
lon -= A * 20;
|
||||
lat -= B * 10;
|
||||
|
||||
int C = lon / 2;
|
||||
int D = lat / 1;
|
||||
|
||||
lon -= C * 2;
|
||||
lat -= D * 1;
|
||||
|
||||
int E = lon / (5.0f / 60.0f);
|
||||
int F = lat / (2.5f / 60.0f);
|
||||
|
||||
lon -= E * (5.0f / 60.0f);
|
||||
lat -= F * (2.5f / 60.0f);
|
||||
|
||||
int G = lon / (5.0f / 600.0f);
|
||||
int H = lat / (2.5f / 600.0f);
|
||||
|
||||
buffer[result++] = char('A' + A);
|
||||
buffer[result++] = char('A' + B);
|
||||
|
||||
if (precision >= 4) {
|
||||
buffer[result++] = char('0' + C);
|
||||
buffer[result++] = char('0' + D);
|
||||
}
|
||||
|
||||
if (precision >= 6) {
|
||||
buffer[result++] = char('a' + E);
|
||||
buffer[result++] = char('a' + F);
|
||||
}
|
||||
|
||||
if (precision >= 8) {
|
||||
buffer[result++] = char('0' + G);
|
||||
buffer[result++] = char('0' + H);
|
||||
}
|
||||
buffer[result] = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t Location::toString(char* buffer, LocationFormat format, int precision) {
|
||||
if (isUnknown()) {
|
||||
return sprintf(buffer, "%s", "GPS not synchronized");
|
||||
}
|
||||
switch (format) {
|
||||
case LocationFormat::SEXAGESIMAL: {
|
||||
return sprintf(buffer, "%ld\xB0%02ld'%02ld\"%c, %ld\xB0%02ld'%02ld\"%c", // 0xB0 is degree ° symbol in our 8x16 font
|
||||
latitude.degrees, latitude.minutes, latitude.seconds, latitude.orientation ? 'S' : 'N',
|
||||
longitude.degrees, longitude.minutes, longitude.seconds, longitude.orientation ? 'W' : 'E');
|
||||
}
|
||||
case LocationFormat::MAIDENHEAD_LOCATOR:
|
||||
return gps_compute_locator(buffer, latitude.getFloatValue(), longitude.getFloatValue(), precision);
|
||||
default:
|
||||
case LocationFormat::DECIMAL:
|
||||
return sprintf(buffer, "%s, %s", to_string_decimal(latitude.getFloatValue(), 5).c_str(), to_string_decimal(longitude.getFloatValue(), 5).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Location::formatFloatLocation(char* buffer, const char* format) {
|
||||
sprintf(buffer, format, to_string_decimal(latitude.getFloatValue(), 6).c_str(), to_string_decimal(longitude.getFloatValue(), 6).c_str());
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
#ifndef __LOCATION_RX_H__
|
||||
#define __LOCATION_RX_H__
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
class Location {
|
||||
public:
|
||||
class Angle {
|
||||
public:
|
||||
long degrees = 0;
|
||||
long minutes = 0;
|
||||
long seconds = 0;
|
||||
bool orientation = false; // false = N/E, true = S/W
|
||||
|
||||
Angle() {}
|
||||
Angle(long degrees)
|
||||
: degrees(degrees) {}
|
||||
|
||||
void clear();
|
||||
|
||||
float getFloatValue();
|
||||
|
||||
// Apply an offset (minutes / seconds) with sign-based borrow into minutes/degrees.
|
||||
// Out-of-line to keep parseFrame from inlining the same arithmetic 6 times.
|
||||
void apply_offset(bool positive, long ofmin, long ofsec);
|
||||
|
||||
private:
|
||||
float floatValue = 255.0f;
|
||||
};
|
||||
|
||||
enum class LocationFormat { DECIMAL,
|
||||
SEXAGESIMAL,
|
||||
MAIDENHEAD_LOCATOR };
|
||||
|
||||
Angle latitude = Angle(127);
|
||||
Angle longitude = Angle(255);
|
||||
|
||||
void clear();
|
||||
|
||||
bool isUnknown();
|
||||
|
||||
static char gps_letterize(int x);
|
||||
|
||||
// For code size optimization reasons, string manipulation is performed with char buffers
|
||||
// with no char buffer size check. Caller methods take care of providing large enough buffers
|
||||
static size_t gps_compute_locator(char* buffer, float lat, float lon, int precision = 6);
|
||||
size_t toString(char* buffer, LocationFormat format, int precision = 6);
|
||||
|
||||
void formatFloatLocation(char* buffer, const char* format);
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif // __LOCATION_RX_H__
|
||||
+1
-1
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_epirb_rx.application_information"), us
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __RESOUCES_H__
|
||||
#define __RESOUCES_H__
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include "file_reader.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
#define UNKNOWN_LABEL "Unk."
|
||||
|
||||
extern std::vector<std::string> beacon_res;
|
||||
extern std::vector<std::string> freq;
|
||||
|
||||
/**
|
||||
* This class is used to load string resources from SD card.
|
||||
* This is done mostly to reduce application size and keep it under the 32kb limit.
|
||||
* It's also used to load frequency values, allowing users to customize frequency list by editing FREQ.TXT file on SD card.
|
||||
*/
|
||||
class ResourceManager {
|
||||
public:
|
||||
// Singleton getter
|
||||
static ResourceManager* getInstance();
|
||||
|
||||
// Singleton instance
|
||||
static ResourceManager* current;
|
||||
// Used from standalone app, to prevent memleak
|
||||
static void destroy();
|
||||
|
||||
// Get a string from BEACON.RES file
|
||||
static const char* get_beacon_resource(uint8_t line) {
|
||||
ResourceManager* rm = getInstance();
|
||||
if (rm->beacon_res.empty()) {
|
||||
load_file((epirb_dir / u"RES/BEACON.RES"), rm->beacon_res);
|
||||
}
|
||||
if (line < rm->beacon_res.size()) {
|
||||
return rm->beacon_res[line].c_str();
|
||||
}
|
||||
return UNKNOWN_LABEL;
|
||||
}
|
||||
|
||||
// Get frequency values
|
||||
static const std::vector<std::string>& get_frequencies() {
|
||||
ResourceManager* rm = getInstance();
|
||||
if (rm->freq.empty()) {
|
||||
load_file((epirb_dir / u"FREQ.TXT"), rm->freq);
|
||||
}
|
||||
return rm->freq;
|
||||
}
|
||||
|
||||
private:
|
||||
// BEACON.RES content
|
||||
std::vector<std::string> beacon_res{};
|
||||
// FREQ.TXT content
|
||||
std::vector<std::string> freq{};
|
||||
|
||||
static void load_file(const std::filesystem::path& path, std::vector<std::string>& vect) {
|
||||
FIL file;
|
||||
if (f_open(&file, path.tchar(), FA_READ) == FR_OK) {
|
||||
TCHAR line_buffer[32];
|
||||
// Read the file line by line
|
||||
while (f_gets(line_buffer, sizeof(line_buffer) / sizeof(TCHAR), &file)) {
|
||||
std::string s;
|
||||
for (int i = 0; line_buffer[i] != '\0' && line_buffer[i] != '\n' && line_buffer[i] != '\r'; i++) {
|
||||
// Convert each TCHAR in char
|
||||
s += (char)line_buffer[i];
|
||||
}
|
||||
vect.push_back(std::move(s));
|
||||
}
|
||||
f_close(&file);
|
||||
}
|
||||
if (vect.empty()) vect.push_back(UNKNOWN_LABEL);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif // __RESOUCES_H__
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_beaconlist.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
BeaconUIList::BeaconUIList(Rect parent_rect)
|
||||
: View{parent_rect} {
|
||||
this->set_focusable(true);
|
||||
}
|
||||
|
||||
void BeaconUIList::paint(Painter& painter) {
|
||||
auto rect = screen_rect();
|
||||
// Clear view
|
||||
painter.fill_rectangle(rect, Theme::getInstance()->bg_darkest->background);
|
||||
|
||||
if (!db_ || (db_->empty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto base_style = Theme::getInstance()->bg_darkest;
|
||||
|
||||
for (auto offset = 0u; offset < BEACON_HISTORY_SIZE; ++offset) {
|
||||
// The whole frame needs to be cleared so every line 'slot'
|
||||
// is redrawn even when `text` just left empty.
|
||||
auto text = std::string{};
|
||||
auto index = start_index_ + offset;
|
||||
auto line_position = rect.location() + Point{0, 1 + (int)offset * char_height};
|
||||
auto is_selected = offset == selected_index_;
|
||||
auto style = base_style;
|
||||
|
||||
if (index < db_->size()) {
|
||||
// Get beacon entry and format it's summary
|
||||
auto& entry = db_->get_beacon(index);
|
||||
char buffer[64];
|
||||
entry.formatSummary(buffer, true);
|
||||
text = std::string(buffer);
|
||||
}
|
||||
|
||||
if (index == db_->get_current_beacon_index())
|
||||
// If this is the currently displayed beacon change color
|
||||
style = Theme::getInstance()->bg_medium;
|
||||
|
||||
// Draw entry line
|
||||
painter.draw_string(
|
||||
line_position, (is_selected ? style->invert() : *style), text);
|
||||
}
|
||||
|
||||
// Draw a bounding rectangle when focused.
|
||||
painter.draw_rectangle(rect, (has_focus() ? Theme::getInstance()->bg_darkest->foreground : Theme::getInstance()->bg_darkest->background));
|
||||
}
|
||||
|
||||
void BeaconUIList::on_show() {
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
bool BeaconUIList::on_key(const KeyEvent key) {
|
||||
if (!db_ || db_->empty())
|
||||
return false;
|
||||
|
||||
if (key == KeyEvent::Select && on_select) {
|
||||
// Select this beacon
|
||||
on_select(get_index());
|
||||
set_dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
auto delta = 0;
|
||||
// Change position in the list
|
||||
if (key == KeyEvent::Up && get_index() > 0)
|
||||
delta = -1;
|
||||
else if (key == KeyEvent::Down && get_index() < db_->size() - 1)
|
||||
delta = 1;
|
||||
else
|
||||
return false;
|
||||
|
||||
adjust_selected_index(delta);
|
||||
set_dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BeaconUIList::on_encoder(EncoderEvent delta) {
|
||||
if (!db_ || db_->empty())
|
||||
return false;
|
||||
// Change position in the list according to encoder
|
||||
adjust_selected_index(delta);
|
||||
set_dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t BeaconUIList::get_index() const {
|
||||
return start_index_ + selected_index_;
|
||||
}
|
||||
|
||||
void BeaconUIList::set_db(BeaconDB& db) {
|
||||
db_ = &db;
|
||||
start_index_ = 0;
|
||||
selected_index_ = 0;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void BeaconUIList::adjust_selected_index(int delta) {
|
||||
int32_t new_index = selected_index_ + delta;
|
||||
|
||||
// The selection went off the top of the screen, move up.
|
||||
if (new_index < 0) {
|
||||
start_index_ = std::max<int32_t>(start_index_ + new_index, 0);
|
||||
selected_index_ = 0;
|
||||
}
|
||||
|
||||
// Selection is off the bottom of the screen, move down.
|
||||
else if (new_index >= (int32_t)BEACON_HISTORY_SIZE) {
|
||||
start_index_ = std::min<int32_t>(start_index_ + delta, db_->size() - BEACON_HISTORY_SIZE);
|
||||
selected_index_ = BEACON_HISTORY_SIZE - 1;
|
||||
}
|
||||
|
||||
// Otherwise, scroll within the screen, but not past the end.
|
||||
else {
|
||||
selected_index_ = std::min<int32_t>(new_index, db_->size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __UI_BEACONLIST_H__
|
||||
#define __UI_BEACONLIST_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "beacon_db.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
/**
|
||||
* Beacon list component
|
||||
*/
|
||||
class BeaconUIList : public View {
|
||||
public:
|
||||
std::function<void(size_t)> on_select{};
|
||||
|
||||
BeaconUIList(Rect parent_rect);
|
||||
BeaconUIList(const BeaconUIList& other) = delete;
|
||||
BeaconUIList& operator=(const BeaconUIList& other) = delete;
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void on_show() override;
|
||||
bool on_key(const KeyEvent key) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
|
||||
size_t get_index() const;
|
||||
void set_db(BeaconDB& db);
|
||||
|
||||
private:
|
||||
void adjust_selected_index(int index);
|
||||
|
||||
BeaconDB* db_{nullptr};
|
||||
size_t start_index_{0};
|
||||
size_t selected_index_{0};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif /*__UI_BEACONLIST_H__*/
|
||||
+600
-707
File diff suppressed because it is too large
Load Diff
+323
-296
@@ -1,297 +1,324 @@
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Decoder Implementation
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __UI_EPIRB_RX_H__
|
||||
#define __UI_EPIRB_RX_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
|
||||
#include "event_m0.hpp"
|
||||
#include "signal.hpp"
|
||||
#include "message.hpp"
|
||||
#include "log_file.hpp"
|
||||
|
||||
#include "baseband_packet.hpp"
|
||||
|
||||
/* #include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <array> */
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
// EPIRB 406 MHz beacon types
|
||||
enum class BeaconType : uint8_t {
|
||||
OrbitingLocationBeacon = 0,
|
||||
PersonalLocatorBeacon = 1,
|
||||
EmergencyLocatorTransmitter = 2,
|
||||
SerialELT = 3,
|
||||
NationalELT = 4,
|
||||
Other = 15
|
||||
};
|
||||
|
||||
// EPIRB distress and emergency types
|
||||
enum class EmergencyType : uint8_t {
|
||||
Fire = 0,
|
||||
Flooding = 1,
|
||||
Collision = 2,
|
||||
Grounding = 3,
|
||||
Sinking = 4,
|
||||
Disabled = 5,
|
||||
Abandoning = 6,
|
||||
Piracy = 7,
|
||||
Man_Overboard = 8,
|
||||
Other = 15
|
||||
};
|
||||
|
||||
struct EPIRBLocation {
|
||||
float latitude; // degrees, -90 to +90
|
||||
float longitude; // degrees, -180 to +180
|
||||
bool valid;
|
||||
|
||||
EPIRBLocation()
|
||||
: latitude(0.0f), longitude(0.0f), valid(false) {}
|
||||
EPIRBLocation(float lat, float lon)
|
||||
: latitude(lat), longitude(lon), valid(true) {}
|
||||
};
|
||||
|
||||
enum class PacketStatus : uint8_t {
|
||||
Valid = 0,
|
||||
Corrected = 1,
|
||||
Error = 2
|
||||
};
|
||||
|
||||
struct EPIRBBeacon {
|
||||
uint32_t beacon_id;
|
||||
BeaconType beacon_type;
|
||||
EmergencyType emergency_type;
|
||||
EPIRBLocation location;
|
||||
uint32_t country_code;
|
||||
std::string vessel_name;
|
||||
rtc::RTC timestamp;
|
||||
uint32_t sequence_number;
|
||||
PacketStatus packet_status;
|
||||
uint8_t error_count;
|
||||
|
||||
EPIRBBeacon()
|
||||
: beacon_id(0), beacon_type(BeaconType::Other), emergency_type(EmergencyType::Other), location(), country_code(0), vessel_name(), timestamp(), sequence_number(0), packet_status(PacketStatus::Error), error_count(0) {}
|
||||
};
|
||||
|
||||
class EPIRBDecoder {
|
||||
public:
|
||||
static EPIRBBeacon decode_packet(const baseband::Packet& packet);
|
||||
|
||||
private:
|
||||
static EPIRBLocation decode_location(const std::array<uint8_t, 16>& data);
|
||||
static BeaconType decode_beacon_type(uint8_t type_bits);
|
||||
static EmergencyType decode_emergency_type(uint8_t emergency_bits);
|
||||
static uint32_t decode_country_code(const std::array<uint8_t, 16>& data);
|
||||
static std::string decode_vessel_name(const std::array<uint8_t, 16>& data);
|
||||
|
||||
// BCH error correction methods
|
||||
static PacketStatus perform_bch_check(std::array<uint8_t, 16>& data, uint8_t& error_count);
|
||||
static uint32_t calculate_bch_syndrome(const std::array<uint8_t, 16>& data);
|
||||
static bool correct_single_error(std::array<uint8_t, 16>& data, uint32_t syndrome);
|
||||
static uint8_t count_bit_errors(const std::array<uint8_t, 16>& original, const std::array<uint8_t, 16>& corrected);
|
||||
};
|
||||
|
||||
class EPIRBLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void on_packet(const EPIRBBeacon& beacon);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
// Forward declarations of formatting functions
|
||||
std::string format_beacon_type(BeaconType type);
|
||||
std::string format_emergency_type(EmergencyType type);
|
||||
std::string format_packet_status(PacketStatus status);
|
||||
ui::Color get_packet_status_color(PacketStatus status);
|
||||
|
||||
class EPIRBBeaconDetailView : public ui::View {
|
||||
public:
|
||||
std::function<void(void)> on_close{};
|
||||
|
||||
EPIRBBeaconDetailView(ui::NavigationView& nav);
|
||||
EPIRBBeaconDetailView(const EPIRBBeaconDetailView&) = delete;
|
||||
EPIRBBeaconDetailView& operator=(const EPIRBBeaconDetailView&) = delete;
|
||||
|
||||
void set_beacon(const EPIRBBeacon& beacon);
|
||||
const EPIRBBeacon& beacon() const { return beacon_; }
|
||||
|
||||
void focus() override;
|
||||
void paint(ui::Painter&) override;
|
||||
|
||||
ui::GeoMapView* get_geomap_view() { return geomap_view; }
|
||||
|
||||
private:
|
||||
EPIRBBeacon beacon_{};
|
||||
|
||||
ui::Button button_done{
|
||||
{125, 224, 96, 24},
|
||||
"Done"};
|
||||
ui::Button button_see_map{
|
||||
{19, 224, 96, 24},
|
||||
"See on map"};
|
||||
|
||||
ui::GeoMapView* geomap_view{nullptr};
|
||||
|
||||
ui::Rect draw_field(
|
||||
ui::Painter& painter,
|
||||
const ui::Rect& draw_rect,
|
||||
const ui::Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value);
|
||||
};
|
||||
|
||||
class EPIRBAppView : public ui::View {
|
||||
public:
|
||||
EPIRBAppView(ui::NavigationView& nav);
|
||||
~EPIRBAppView();
|
||||
|
||||
void set_parent_rect(const ui::Rect new_parent_rect) override;
|
||||
void paint(ui::Painter&) override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "EPIRB RX"; }
|
||||
|
||||
private:
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_epirb", app_settings::Mode::RX};
|
||||
|
||||
ui::NavigationView& nav_;
|
||||
|
||||
std::vector<EPIRBBeacon> recent_beacons{};
|
||||
std::unique_ptr<EPIRBLogger> logger{};
|
||||
|
||||
EPIRBBeaconDetailView beacon_detail_view{nav_};
|
||||
|
||||
static constexpr auto header_height = 4 * 16;
|
||||
|
||||
ui::Text label_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0), 4 * 8, 1 * 16},
|
||||
"Freq"};
|
||||
|
||||
ui::OptionsField options_frequency{
|
||||
{5 * 8, UI_POS_Y(0)},
|
||||
7,
|
||||
{
|
||||
{"406.028", 406028000},
|
||||
{"406.025", 406025000},
|
||||
{"406.037", 406037000},
|
||||
{"433.025", 433025000},
|
||||
{"144.875", 144875000},
|
||||
}};
|
||||
|
||||
ui::RFAmpField field_rf_amp{
|
||||
{13 * 8, UI_POS_Y(0)}};
|
||||
|
||||
ui::LNAGainField field_lna{
|
||||
{15 * 8, UI_POS_Y(0)}};
|
||||
|
||||
ui::VGAGainField field_vga{
|
||||
{18 * 8, UI_POS_Y(0)}};
|
||||
|
||||
ui::RSSI rssi{
|
||||
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
|
||||
ui::Channel channel{
|
||||
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
|
||||
ui::AudioVolumeField field_volume{
|
||||
{screen_width - 2 * 8, UI_POS_Y(0)}};
|
||||
|
||||
// Status display
|
||||
ui::Text label_status{
|
||||
{UI_POS_X(0), 1 * 16, 15 * 8, 1 * 16},
|
||||
"Listening..."};
|
||||
|
||||
ui::Text label_beacons_count{
|
||||
{16 * 8, 1 * 16, 14 * 8, 1 * 16},
|
||||
"Beacons: 0"};
|
||||
|
||||
ui::Text label_packet_stats{
|
||||
{UI_POS_X(0), 3 * 16, 29 * 8, 1 * 16},
|
||||
""};
|
||||
|
||||
// Latest beacon info display
|
||||
ui::Text label_latest{
|
||||
{UI_POS_X(0), 2 * 16, 8 * 8, 1 * 16},
|
||||
"Latest:"};
|
||||
|
||||
ui::Text text_latest_info{
|
||||
{8 * 8, 2 * 16, 22 * 8, 1 * 16},
|
||||
""};
|
||||
|
||||
// Beacon list
|
||||
ui::Console console{
|
||||
{0, 4 * 16, 240, 152}};
|
||||
|
||||
ui::Button button_map{
|
||||
{0, 224, 60, 24},
|
||||
"Map"};
|
||||
|
||||
ui::Button button_clear{
|
||||
{64, 224, 60, 24},
|
||||
"Clear"};
|
||||
|
||||
ui::Button button_log{
|
||||
{128, 224, 60, 24},
|
||||
"Log"};
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
uint32_t beacons_received = 0;
|
||||
uint32_t packets_valid = 0;
|
||||
uint32_t packets_corrected = 0;
|
||||
uint32_t packets_error = 0;
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::EPIRBPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const EPIRBPacketMessage*>(p);
|
||||
this->on_packet(message->packet);
|
||||
}};
|
||||
|
||||
void on_packet(const baseband::Packet& packet);
|
||||
void on_beacon_decoded(const EPIRBBeacon& beacon);
|
||||
void on_show_map();
|
||||
void on_clear_beacons();
|
||||
void on_toggle_log();
|
||||
void on_tick_second();
|
||||
|
||||
void update_display();
|
||||
std::string format_beacon_summary(const EPIRBBeacon& beacon);
|
||||
std::string format_location(const EPIRBLocation& location);
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Decoder Implementation
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __UI_EPIRB_RX_H__
|
||||
#define __UI_EPIRB_RX_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
|
||||
// Specan is disable to keep application size below the 32k limit
|
||||
// #define SPECAN
|
||||
|
||||
// Comment to disable timout reset on select and save approx 200 bytes of flash
|
||||
#ifndef PRALINE
|
||||
// Application does not fit on Praline with RESET_TIMER enabled
|
||||
#define RESET_TIMER
|
||||
#endif
|
||||
// Comment to disable squelch control
|
||||
#define SQUELCH
|
||||
// Comment to disable beacon selection by encoder on detail tab
|
||||
#define DETAIL_TAB_BEACON_SEL
|
||||
// #define LOGGER
|
||||
|
||||
#ifdef SPECAN
|
||||
#include "ui_spectrum.hpp"
|
||||
#endif
|
||||
|
||||
#include "ui_tabview.hpp"
|
||||
|
||||
#include "ui_qrcode.hpp"
|
||||
|
||||
#include "event_m0.hpp"
|
||||
#include "message.hpp"
|
||||
#include "log_file.hpp"
|
||||
|
||||
#include "baseband_packet.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
|
||||
#include "beacon.hpp"
|
||||
#include "beacon_db.hpp"
|
||||
#include "ui_beaconlist.hpp"
|
||||
#include "resources.hpp"
|
||||
|
||||
namespace ui::external_app::epirb_rx {
|
||||
|
||||
/**
|
||||
* Status of a packet
|
||||
*/
|
||||
enum class PacketStatus : uint8_t {
|
||||
Valid = 0,
|
||||
Corrected = 1,
|
||||
Error = 2
|
||||
};
|
||||
|
||||
// Position of tabs in tab view
|
||||
#define EPIRB_TAB_POS_Y (UI_POS_Y(4) + 3 * 8)
|
||||
// Height of tabs in tab view
|
||||
#define EPIRB_TAB_HEIGHT (screen_height - EPIRB_TAB_POS_Y - UI_POS_HEIGHT(1))
|
||||
|
||||
#ifdef LOGGER
|
||||
class EPIRBLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void on_packet(Beacon& beacon);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Dedicated TextArea component used to optimize application code size
|
||||
*/
|
||||
class TextArea : public Widget {
|
||||
public:
|
||||
TextArea(Rect parent_rect);
|
||||
|
||||
#ifdef RESET_TIMER
|
||||
std::function<void(TextArea&)> on_select{};
|
||||
bool on_key(const KeyEvent key) override;
|
||||
#endif
|
||||
|
||||
void set_content(std::string_view value);
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
std::string content{};
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
class EPIRBAppView;
|
||||
|
||||
/**
|
||||
* View for beacon detail tab
|
||||
*/
|
||||
class EPIRBDetailView : public View {
|
||||
public:
|
||||
EPIRBDetailView(Rect parent_rect, EPIRBAppView& parent);
|
||||
void set_beacon(Beacon& beacon);
|
||||
#ifdef DETAIL_TAB_BEACON_SEL
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
#endif
|
||||
|
||||
private:
|
||||
TextArea text_beacon{{UI_POS_X(0), UI_POS_Y(0), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
|
||||
EPIRBAppView& parent_app;
|
||||
};
|
||||
|
||||
#define EPIRB_RX_DEFAULT_LATITUDE 43.604f
|
||||
#define EPIRB_RX_DEFAULT_LONGITUDE 1.458f
|
||||
|
||||
/**
|
||||
* View for beacon map tab
|
||||
*/
|
||||
class EPIRBMapView : public View {
|
||||
public:
|
||||
EPIRBMapView(Rect parent_rect);
|
||||
void paint(Painter& painter) override;
|
||||
void on_show() override;
|
||||
void set_main_marker(const std::string& label, float lat, float lon);
|
||||
void clear_markers();
|
||||
void add_marker(GeoMarker& marker);
|
||||
void hide_map(bool hide);
|
||||
void repaint();
|
||||
|
||||
private:
|
||||
GeoMap geomap{{0, 0, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
|
||||
float lat_{EPIRB_RX_DEFAULT_LATITUDE};
|
||||
float lon_{EPIRB_RX_DEFAULT_LONGITUDE};
|
||||
bool map_hidden{true};
|
||||
};
|
||||
|
||||
#define QR_WIDTH 126
|
||||
#define QR_HEIGHT 127
|
||||
|
||||
/**
|
||||
* Vieaw for Beacon QR tab
|
||||
*/
|
||||
class EPRIBQRView : public View {
|
||||
public:
|
||||
EPRIBQRView(Rect parent_rect);
|
||||
EPRIBQRView(const EPRIBQRView&) = delete;
|
||||
EPRIBQRView& operator=(const EPRIBQRView&) = delete;
|
||||
|
||||
void set_beacon(Beacon* beacon);
|
||||
void update_qr();
|
||||
void update_display();
|
||||
|
||||
private:
|
||||
bool show_map{true};
|
||||
Beacon* current_beacon{nullptr};
|
||||
char qr_url[128];
|
||||
|
||||
OptionsField options_qr{
|
||||
{UI_POS_X(5), UI_POS_Y(1)},
|
||||
6,
|
||||
{{"Map", 0},
|
||||
{"Detail", 1}}};
|
||||
|
||||
QRCodeImage qr_code{
|
||||
{UI_POS_MAXWIDTH - QR_WIDTH - UI_POS_X(1), UI_POS_Y(1), QR_WIDTH, QR_HEIGHT}};
|
||||
|
||||
TextArea text_data{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT - UI_POS_Y(1)}};
|
||||
};
|
||||
|
||||
#ifdef SPECAN
|
||||
class EPIRBRxView : public spectrum::WaterfallView {
|
||||
public:
|
||||
EPIRBRxView(EPIRBAppView& parent, Rect parent_rect);
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
|
||||
private:
|
||||
EPIRBAppView& app_view;
|
||||
};
|
||||
#endif
|
||||
|
||||
class EPIRBAppView final : public ui::View {
|
||||
public:
|
||||
EPIRBAppView(ui::NavigationView& nav);
|
||||
~EPIRBAppView();
|
||||
|
||||
void focus() override;
|
||||
void refresh();
|
||||
|
||||
// Message to configure rx baseband
|
||||
EPIRBRXConfig epirb_rx_config_message{};
|
||||
void send_config();
|
||||
// Beacons database
|
||||
BeaconDB beacon_db{};
|
||||
// Update display when beacon selection changed0
|
||||
void on_beacon_change();
|
||||
|
||||
std::string title() const override { return "EPIRB RX"; }
|
||||
|
||||
private:
|
||||
uint8_t squelch{50};
|
||||
// The delay between each frame
|
||||
uint32_t countdown{50};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_epirb",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"epirb_squelch"sv, &squelch},
|
||||
{"countdown"sv, &countdown},
|
||||
}};
|
||||
|
||||
ui::NavigationView& nav_;
|
||||
|
||||
#ifdef LOGGER
|
||||
std::unique_ptr<EPIRBLogger> logger{};
|
||||
#endif
|
||||
|
||||
OptionsField options_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
7,
|
||||
{}};
|
||||
|
||||
ui::RFAmpField field_rf_amp{
|
||||
{UI_POS_X(8), UI_POS_Y(0)}};
|
||||
|
||||
ui::LNAGainField field_lna{
|
||||
{UI_POS_X(10), UI_POS_Y(0)}};
|
||||
|
||||
ui::VGAGainField field_vga{
|
||||
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||
|
||||
ui::RSSI rssi{
|
||||
{UI_POS_X(16), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(22), 4}};
|
||||
|
||||
ui::Channel channel{
|
||||
{UI_POS_X(16), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(22), 4}};
|
||||
|
||||
// ui::Audio audio{
|
||||
// {UI_POS_X(16), UI_POS_Y(0) + 10, UI_POS_WIDTH_REMAINING(22), 4}};
|
||||
|
||||
ui::AudioVolumeField field_volume{
|
||||
{UI_POS_WIDTH_REMAINING(2), UI_POS_Y(0)}};
|
||||
|
||||
#ifdef SQUELCH
|
||||
NumberField field_squelch{
|
||||
{UI_POS_WIDTH_REMAINING(5), UI_POS_Y(0)},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' '};
|
||||
#endif
|
||||
|
||||
// Status display
|
||||
TextArea text_status{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(3)}};
|
||||
TextArea text_timeout{
|
||||
{UI_POS_X(13), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}};
|
||||
SignalToken signal_token_tick_second{};
|
||||
// Timeout string
|
||||
int16_t timeout{0};
|
||||
|
||||
// Tab View
|
||||
Rect view_rect = {0, EPIRB_TAB_POS_Y, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT};
|
||||
|
||||
BeaconUIList view_list{view_rect};
|
||||
EPIRBDetailView view_detail{view_rect, (*this)};
|
||||
EPIRBMapView view_map{view_rect};
|
||||
#ifdef SPECAN
|
||||
EPIRBRxView view_rx{*this, view_rect};
|
||||
#endif
|
||||
|
||||
EPRIBQRView view_qr{view_rect};
|
||||
|
||||
TabView tab_view{
|
||||
{"List", Theme::getInstance()->fg_cyan->foreground, &view_list},
|
||||
{"Detail", Theme::getInstance()->fg_green->foreground, &view_detail},
|
||||
{"Map", Theme::getInstance()->fg_yellow->foreground, &view_map},
|
||||
#ifdef SPECAN
|
||||
{"RX", Theme::getInstance()->fg_orange->foreground, &view_rx},
|
||||
#endif
|
||||
{"QR", Theme::getInstance()->fg_orange->foreground, &view_qr}};
|
||||
|
||||
uint16_t beacons_received = 0;
|
||||
uint16_t packets_valid = 0;
|
||||
uint16_t packets_corrected = 0;
|
||||
uint16_t packets_error = 0;
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::EPIRBPacket,
|
||||
[this](Message* const p) { on_packet(p); }};
|
||||
|
||||
static void decode_packet(const baseband::Packet& packet, Beacon& beacon);
|
||||
void on_packet(Message* const p);
|
||||
void update_map();
|
||||
void on_tick_second();
|
||||
|
||||
void update_display();
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_rx
|
||||
|
||||
#endif // __UI_EPIRB_RX_H__
|
||||
+61
-57
@@ -24,7 +24,6 @@
|
||||
#include "portapack.hpp"
|
||||
#include "ui_epirb_tx.hpp"
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
|
||||
namespace ui::external_app::epirb_tx {
|
||||
@@ -37,17 +36,17 @@ namespace ui::external_app::epirb_tx {
|
||||
* @param min output minutes value
|
||||
* @param sec output seconds value
|
||||
*/
|
||||
static void decimal_to_dms(double value, bool& negative, uint16_t& deg, uint8_t& min, uint8_t& sec) {
|
||||
static void decimal_to_dms(float value, bool& negative, int16_t& deg, int8_t& min, int8_t& sec) {
|
||||
negative = value < 0;
|
||||
value = std::fabs(value);
|
||||
|
||||
deg = (uint16_t)value;
|
||||
deg = (int16_t)value;
|
||||
|
||||
double m = (value - deg) * 60.0;
|
||||
min = (uint8_t)m;
|
||||
float m = (value - deg) * 60.0f;
|
||||
min = (int8_t)m;
|
||||
|
||||
double s = (m - min) * 60.0;
|
||||
sec = (uint8_t)(s + 0.5);
|
||||
float s = (m - min) * 60.0f;
|
||||
sec = (int8_t)(s + 0.5f);
|
||||
|
||||
if (sec == 60) {
|
||||
sec = 0;
|
||||
@@ -68,8 +67,8 @@ static void decimal_to_dms(double value, bool& negative, uint16_t& deg, uint8_t&
|
||||
* @param sec seconds value
|
||||
* @return value the decimal value
|
||||
*/
|
||||
static double dms_to_decimal(bool negative, uint16_t deg, uint8_t min, uint8_t sec) {
|
||||
double v = deg + min / 60.0 + sec / 3600.0;
|
||||
static float dms_to_decimal(bool negative, int16_t deg, int8_t min, int8_t sec) {
|
||||
float v = deg + min / 60.0f + sec / 3600.0f;
|
||||
return negative ? -v : v;
|
||||
}
|
||||
|
||||
@@ -80,55 +79,60 @@ static double dms_to_decimal(bool negative, uint16_t deg, uint8_t min, uint8_t s
|
||||
* @param lon output longitude
|
||||
*/
|
||||
static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon) {
|
||||
static const double lon_step[] =
|
||||
static const float lon_step[] =
|
||||
{
|
||||
20.0,
|
||||
2.0,
|
||||
1.0 / 12.0,
|
||||
1.0 / 120.0,
|
||||
1.0 / 2880.0};
|
||||
20.0f,
|
||||
2.0f,
|
||||
1.0f / 12.0f,
|
||||
1.0f / 120.0f,
|
||||
1.0f / 2880.0f};
|
||||
|
||||
static const double lat_step[] =
|
||||
static const float lat_step[] =
|
||||
{
|
||||
10.0,
|
||||
1.0,
|
||||
1.0 / 24.0,
|
||||
1.0 / 240.0,
|
||||
1.0 / 5760.0};
|
||||
10.0f,
|
||||
1.0f,
|
||||
1.0f / 24.0f,
|
||||
1.0f / 240.0f,
|
||||
1.0f / 5760.0f};
|
||||
|
||||
lon = -180.0;
|
||||
lat = -90.0;
|
||||
lon = -180.0f;
|
||||
lat = -90.0f;
|
||||
|
||||
int pairs = loc.size() / 2;
|
||||
|
||||
if (pairs > 5)
|
||||
pairs = 5;
|
||||
|
||||
for (int i = 0; i < pairs; i++) {
|
||||
char c1 = std::toupper(loc[i * 2]);
|
||||
char c2 = std::toupper(loc[i * 2 + 1]);
|
||||
if (pairs > 0) {
|
||||
for (int i = 0; i < pairs; i++) {
|
||||
char c1 = loc[i * 2];
|
||||
char c2 = loc[i * 2 + 1];
|
||||
if (c1 >= 'a' && c1 <= 'z') c1 -= ('a' - 'A');
|
||||
if (c2 >= 'a' && c2 <= 'z') c2 -= ('a' - 'A');
|
||||
|
||||
double lon_size = lon_step[i];
|
||||
double lat_size = lat_step[i];
|
||||
float lon_size = lon_step[i];
|
||||
float lat_size = lat_step[i];
|
||||
|
||||
int v1, v2;
|
||||
int v1, v2;
|
||||
|
||||
if (i % 2 == 0) // letters
|
||||
{
|
||||
v1 = c1 - 'A';
|
||||
v2 = c2 - 'A';
|
||||
} else // digits
|
||||
{
|
||||
v1 = c1 - '0';
|
||||
v2 = c2 - '0';
|
||||
if (i % 2 == 0) // letters
|
||||
{
|
||||
v1 = c1 - 'A';
|
||||
v2 = c2 - 'A';
|
||||
} else // digits
|
||||
{
|
||||
v1 = c1 - '0';
|
||||
v2 = c2 - '0';
|
||||
}
|
||||
|
||||
lon += v1 * lon_size;
|
||||
lat += v2 * lat_size;
|
||||
}
|
||||
|
||||
lon += v1 * lon_size;
|
||||
lat += v2 * lat_size;
|
||||
// Cell center
|
||||
lon += lon_step[pairs - 1] / 2.0f;
|
||||
lat += lat_step[pairs - 1] / 2.0f;
|
||||
}
|
||||
|
||||
// Cell center
|
||||
lon += lon_step[pairs - 1] / 2.0;
|
||||
lat += lat_step[pairs - 1] / 2.0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,9 +142,9 @@ static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon
|
||||
* @param precision (optional, defaults to 8) the number of charater for the maidenhead locator
|
||||
* @return the locator
|
||||
*/
|
||||
static std::string decimal_to_maidenhead(double lat, double lon, int precision = 8) {
|
||||
lon += 180.0;
|
||||
lat += 90.0;
|
||||
static std::string decimal_to_maidenhead(float lat, float lon, int precision = 8) {
|
||||
lon += 180.0f;
|
||||
lat += 90.0f;
|
||||
|
||||
int A = lon / 20;
|
||||
int B = lat / 10;
|
||||
@@ -154,14 +158,14 @@ static std::string decimal_to_maidenhead(double lat, double lon, int precision =
|
||||
lon -= C * 2;
|
||||
lat -= D * 1;
|
||||
|
||||
int E = lon / (5.0 / 60.0);
|
||||
int F = lat / (2.5 / 60.0);
|
||||
int E = lon / (5.0f / 60.0f);
|
||||
int F = lat / (2.5f / 60.0f);
|
||||
|
||||
lon -= E * (5.0 / 60.0);
|
||||
lat -= F * (2.5 / 60.0);
|
||||
lon -= E * (5.0f / 60.0f);
|
||||
lat -= F * (2.5f / 60.0f);
|
||||
|
||||
int G = lon / (5.0 / 600.0);
|
||||
int H = lat / (2.5 / 600.0);
|
||||
int G = lon / (5.0f / 600.0f);
|
||||
int H = lat / (2.5f / 600.0f);
|
||||
|
||||
std::string locator;
|
||||
|
||||
@@ -247,9 +251,9 @@ void init_from_dms(Location& loc) {
|
||||
* Convert the provided Loaction to it's latitude string (format <xxx°yy'zz"N>)
|
||||
*/
|
||||
std::string to_latitude_string(const Location& location) {
|
||||
char buffer[16];
|
||||
char buffer[20];
|
||||
// Format : <xxx°yy'zz"N>
|
||||
snprintf(buffer, sizeof(buffer), "%3d\260%02d'%02d\"%c", location.lat_deg, location.lat_min, location.lat_sec, location.south ? 'S' : 'N');
|
||||
sprintf(buffer, "%3d\260%02d'%02d\"%c", location.lat_deg, location.lat_min, location.lat_sec, location.south ? 'S' : 'N');
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
@@ -257,12 +261,12 @@ std::string to_latitude_string(const Location& location) {
|
||||
* Convert the provided Loaction to it's longitude string (format <xxx°yy'zz"W>)
|
||||
*/
|
||||
std::string to_longitude_string(const Location& location) {
|
||||
char buffer[16];
|
||||
char buffer[20];
|
||||
// Format : <xxx°yy'zz"W>
|
||||
snprintf(buffer, sizeof(buffer), "%3d\260%02d'%02d\"%c", location.long_deg, location.long_min, location.long_sec, location.west ? 'W' : 'E');
|
||||
sprintf(buffer, "%3d\260%02d'%02d\"%c", location.long_deg, location.long_min, location.long_sec, location.west ? 'W' : 'E');
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_tx
|
||||
|
||||
#endif /*__LOCATION_H__*/
|
||||
#endif /*__LOCATION_H__*/
|
||||
|
||||
+659
-631
File diff suppressed because it is too large
Load Diff
+417
-404
@@ -1,404 +1,417 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __EPIRB_TX_H__
|
||||
#define __EPIRB_TX_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "tonesets.hpp"
|
||||
|
||||
#define BEACON_HEXA_SIZE 36
|
||||
#define BEACON_HEXA_HALF_SIZE 18
|
||||
#define BEACON_SIZE 18
|
||||
|
||||
#define AM_TEST_FREQUENCY 121375000
|
||||
#define AM_REAL_FREQUENCY 121500000
|
||||
|
||||
#define BPSK_FREQUENCY_HAM 433025000
|
||||
#define BPSK_FREQUENCY_B 406025000
|
||||
#define BPSK_FREQUENCY_C 406028000
|
||||
#define BPSK_FREQUENCY_F 406037000
|
||||
#define BPSK_FREQUENCY_G 406040000
|
||||
#define BPSK_FREQUENCY_J 406049000
|
||||
#define BPSK_FREQUENCY_K 406052000
|
||||
#define BPSK_FREQUENCY_N 406061000
|
||||
#define BPSK_FREQUENCY_O 406064000
|
||||
|
||||
namespace ui::external_app::epirb_tx {
|
||||
|
||||
enum class BeaconMode {
|
||||
FILE = 0,
|
||||
MODE_MANUAL = 1
|
||||
};
|
||||
|
||||
enum class BeaconType {
|
||||
EPIRB = 0,
|
||||
ELT = 1,
|
||||
PLB = 2
|
||||
};
|
||||
|
||||
enum class BeaconProtocol {
|
||||
USER = 0,
|
||||
STANDARD = 1,
|
||||
NATIONAL = 2
|
||||
};
|
||||
|
||||
enum class AmChannel {
|
||||
TEST = 0,
|
||||
REAL = 1,
|
||||
MANUAL = 2
|
||||
};
|
||||
|
||||
enum class BpskChannel {
|
||||
HAM = 0,
|
||||
B = 1,
|
||||
C = 2,
|
||||
F = 3,
|
||||
G = 4,
|
||||
J = 5,
|
||||
K = 6,
|
||||
N = 7,
|
||||
O = 8,
|
||||
MANUAL = 10
|
||||
};
|
||||
|
||||
struct Location {
|
||||
std::string locator;
|
||||
bool south;
|
||||
uint16_t lat_deg;
|
||||
uint8_t lat_min;
|
||||
uint8_t lat_sec;
|
||||
float latitude;
|
||||
bool west;
|
||||
uint16_t long_deg;
|
||||
uint8_t long_min;
|
||||
uint8_t long_sec;
|
||||
float longitude;
|
||||
};
|
||||
|
||||
struct BeaconParams {
|
||||
BeaconType type;
|
||||
BeaconProtocol protocol;
|
||||
uint32_t country;
|
||||
bool is_test;
|
||||
bool is_internal;
|
||||
bool has_121_5;
|
||||
Location location;
|
||||
};
|
||||
|
||||
class EPIRBTXAppView : public View {
|
||||
public:
|
||||
EPIRBTXAppView(NavigationView& nav);
|
||||
~EPIRBTXAppView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "EPIRB TX"; };
|
||||
|
||||
private:
|
||||
void start_tx();
|
||||
void stop_tx();
|
||||
void update_config();
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void on_timer();
|
||||
void load_beacons();
|
||||
void set_tx_button_state(bool active);
|
||||
std::string frame_to_hex_string(bool start);
|
||||
void generate_frame(BeaconParams params);
|
||||
void update_frame(bool updateConfig = true);
|
||||
void update_bpsk_frequency();
|
||||
void update_am_transmission();
|
||||
void update_mode();
|
||||
void update_location(bool updateLocatorField = true);
|
||||
|
||||
struct Beacon {
|
||||
std::string title{};
|
||||
std::string description{};
|
||||
std::string frame{};
|
||||
};
|
||||
std::vector<Beacon> beacons{};
|
||||
Beacon default_beacon{"Self test", "Serial User Location Protocol", "FFFED0D6E6202820000C29FF51041775302D"};
|
||||
|
||||
BeaconParams beacon_params{BeaconType::ELT, BeaconProtocol::STANDARD, 227, true, true, true, {"JN03RO", false, 0, 0, 0, 0, false, 0, 0, 0, 0}};
|
||||
|
||||
// Currently selected beacon index (from BEACONS.TXT file / options_frame combo)
|
||||
uint32_t selected_beacon{0};
|
||||
|
||||
// Frequency of the transmitter before starting the app (used to restore frequency when leaving)
|
||||
rf::Frequency original_frequency{0};
|
||||
// Frequency of the AM emergency signal
|
||||
rf::Frequency am_frequency{AM_TEST_FREQUENCY};
|
||||
// Frequency of the 406 MHz BPSK signal
|
||||
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||
// Selected am channel
|
||||
uint8_t am_channel{(uint8_t)AmChannel::TEST};
|
||||
// Selected bpsk channel
|
||||
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
|
||||
// Manual AM frequency value
|
||||
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
|
||||
// Manual BPSK frequency value
|
||||
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||
|
||||
// True when using a beacon from the BEACONS.TXT file
|
||||
bool mode_file{false};
|
||||
// True when looping on sending beacons is enabled
|
||||
bool loop_enabled{true};
|
||||
// True if AM emergency signal transmission is enabled
|
||||
bool am_enabled{true};
|
||||
// True if we want to send a new frame each time the user changes the current beacon
|
||||
bool send_on_change{true};
|
||||
// The current locator string
|
||||
std::string locator{"JN03RO"};
|
||||
// The delay between each frame when on loop mode
|
||||
uint32_t delay{50};
|
||||
uint8_t beacon_type{(uint8_t)BeaconType::EPIRB};
|
||||
// Currently selected beacon protocol
|
||||
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
|
||||
// Currently selected beacon country
|
||||
uint32_t beacon_country{227};
|
||||
// Current beacon's internal state (true for internal location system)
|
||||
bool beacon_internal{true};
|
||||
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
TONES_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_epirb",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"sbeacon"sv, &selected_beacon},
|
||||
{"amfreq"sv, &am_frequency},
|
||||
{"bpskfreq"sv, &bpsk_frequency},
|
||||
{"amchan"sv, &am_channel},
|
||||
{"bpskchan"sv, &bpsk_channel},
|
||||
{"loop"sv, &loop_enabled},
|
||||
{"delay"sv, &delay},
|
||||
{"file"sv, &mode_file},
|
||||
{"am"sv, &am_enabled},
|
||||
{"soc"sv, &send_on_change},
|
||||
{"type"sv, &beacon_type},
|
||||
{"proto"sv, &beacon_protocol},
|
||||
{"country"sv, &beacon_country},
|
||||
{"internal"sv, &beacon_internal},
|
||||
{"locator"sv, &locator},
|
||||
}};
|
||||
|
||||
// Time of the last sent frame
|
||||
uint32_t last_frame_time{0};
|
||||
// True when transmission is enabled
|
||||
bool transmitting{false};
|
||||
// True when transmitting a BPSK frame
|
||||
bool transmitting_bpsk{false};
|
||||
// True when currently looping on sending beacons
|
||||
bool loop{false};
|
||||
|
||||
// Current EPIRBTXDataMessage for baseband
|
||||
EPIRBTXDataMessage epirb_tx_message{};
|
||||
|
||||
const size_t max_text_width = UI_POS_WIDTH_REMAINING(6) / UI_POS_DEFAULT_WIDTH;
|
||||
const size_t max_text_width_ext = UI_POS_WIDTH_REMAINING(0) / UI_POS_DEFAULT_WIDTH;
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency: MHz", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
// For file mode
|
||||
Text text_beacon{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_DEFAULT_HEIGHT},
|
||||
"Beacon:"};
|
||||
// Beacon selection from BEACONS.TXT
|
||||
OptionsField options_frame{
|
||||
{UI_POS_X(7), UI_POS_Y(1)},
|
||||
30,
|
||||
{}};
|
||||
Text text_description_label{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
"Description:"};
|
||||
Text text_description{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_description_end{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
// For manual mode
|
||||
Text text_beacon_type{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Type:"};
|
||||
Text text_beacon_country{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Country:"};
|
||||
Checkbox checkbox_beacon_internal{
|
||||
{UI_POS_X_RIGHT(12), UI_POS_Y(2)},
|
||||
12,
|
||||
"Internal",
|
||||
true};
|
||||
Text text_beacon_locator{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Locator:"};
|
||||
Text text_beacon_latitude{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Lat.:"};
|
||||
Text text_beacon_longitude{
|
||||
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Long.:"};
|
||||
Text text_beacon_latitude_value{
|
||||
{UI_POS_X(7), UI_POS_Y(4), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_beacon_longitude_value{
|
||||
{UI_POS_X(7), UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
OptionsField options_beacon_type{
|
||||
{UI_POS_X(9), UI_POS_Y(1)},
|
||||
7,
|
||||
{{"EPIRB", (uint8_t)BeaconType::EPIRB},
|
||||
{"ELT", (uint8_t)BeaconType::ELT},
|
||||
{"PLB", (uint8_t)BeaconType::PLB}}};
|
||||
OptionsField options_beacon_protocol{
|
||||
{UI_POS_X(9 + 7), UI_POS_Y(1)},
|
||||
30,
|
||||
{{"User", (uint8_t)BeaconProtocol::USER},
|
||||
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
|
||||
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
|
||||
OptionsField options_beacon_country{
|
||||
{UI_POS_X(9), UI_POS_Y(2)},
|
||||
7,
|
||||
{{"France", 227},
|
||||
{"USA", 366},
|
||||
{"Germany", 211},
|
||||
{"Russia", 273},
|
||||
{"Spain", 224},
|
||||
{"Japan", 431},
|
||||
{"UK", 232}}};
|
||||
TextField text_field_beacon_locator{
|
||||
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_DEFAULT_HEIGHT},
|
||||
"JN03RO"};
|
||||
Button button_mangps{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||
"Set pos."};
|
||||
|
||||
// Mode selection (file/manual)
|
||||
OptionsField options_mode{
|
||||
{UI_POS_X(7), UI_POS_Y(0)},
|
||||
30,
|
||||
{{"File (BEACONS.TXT)", (uint8_t)BeaconMode::FILE},
|
||||
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
|
||||
|
||||
// Frame content
|
||||
Text text_frame{
|
||||
{UI_POS_X(6), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_frame_end{
|
||||
{UI_POS_X(6), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
Text text_timeout{
|
||||
{UI_POS_X(14), UI_POS_Y(10), UI_POS_WIDTH(2), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
// Transmission settings
|
||||
Checkbox checkbox_loop{
|
||||
{UI_POS_X(0), UI_POS_Y(9)},
|
||||
10,
|
||||
"Resend every",
|
||||
true};
|
||||
NumberField field_delay{
|
||||
{UI_POS_X(15), UI_POS_Y(9)},
|
||||
2,
|
||||
{1, 99},
|
||||
1,
|
||||
' '};
|
||||
Checkbox checkbox_am{
|
||||
{UI_POS_X(0), UI_POS_Y(11)},
|
||||
10,
|
||||
"AM signal",
|
||||
true};
|
||||
FrequencyField field_am_frequency{
|
||||
{UI_POS_X(13), UI_POS_Y(12)}};
|
||||
Checkbox checkbox_send_on_change{
|
||||
{UI_POS_X(0), UI_POS_Y(13)},
|
||||
14,
|
||||
"Send on change",
|
||||
true};
|
||||
Button button_tx{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(9), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||
"START"};
|
||||
const Style& style_tx_start = *Theme::getInstance()->fg_green;
|
||||
const Style& style_tx_stop = *Theme::getInstance()->fg_red;
|
||||
OptionsField options_am_channel{
|
||||
{UI_POS_X(11), UI_POS_Y(14)},
|
||||
20,
|
||||
{{"121.375 MHz (Test)", 0},
|
||||
{"121.500 MHz /!\\Real", 1},
|
||||
{"Manual", 2}}};
|
||||
OptionsField options_bpsk_channel{
|
||||
{UI_POS_X(11), UI_POS_Y(15)},
|
||||
20,
|
||||
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
|
||||
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
|
||||
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
|
||||
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
|
||||
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
|
||||
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
|
||||
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
|
||||
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
|
||||
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
|
||||
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
|
||||
|
||||
// Transmitter view
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
10000,
|
||||
12};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
// Use frame sync for our applcation timer callback
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_tx
|
||||
|
||||
#endif /*__EPIRB_TX_H__*/
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __EPIRB_TX_H__
|
||||
#define __EPIRB_TX_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "tonesets.hpp"
|
||||
|
||||
#define BEACON_HEXA_SIZE 36
|
||||
#define BEACON_HEXA_HALF_SIZE 18
|
||||
#define BEACON_SIZE 18
|
||||
|
||||
#define AM_TEST_FREQUENCY 121375000
|
||||
#define AM_REAL_FREQUENCY 121500000
|
||||
|
||||
#define BPSK_FREQUENCY_HAM 433025000
|
||||
#define BPSK_FREQUENCY_B 406025000
|
||||
#define BPSK_FREQUENCY_C 406028000
|
||||
#define BPSK_FREQUENCY_F 406037000
|
||||
#define BPSK_FREQUENCY_G 406040000
|
||||
#define BPSK_FREQUENCY_J 406049000
|
||||
#define BPSK_FREQUENCY_K 406052000
|
||||
#define BPSK_FREQUENCY_N 406061000
|
||||
#define BPSK_FREQUENCY_O 406064000
|
||||
|
||||
namespace ui::external_app::epirb_tx {
|
||||
|
||||
enum class BeaconMode {
|
||||
FILE = 0,
|
||||
MODE_MANUAL = 1
|
||||
};
|
||||
|
||||
enum class BeaconType {
|
||||
EPIRB = 0,
|
||||
ELT = 1,
|
||||
PLB = 2
|
||||
};
|
||||
|
||||
enum class BeaconProtocol {
|
||||
USER = 0,
|
||||
STANDARD = 1,
|
||||
NATIONAL = 2
|
||||
};
|
||||
|
||||
enum class AmChannel {
|
||||
TEST = 0,
|
||||
REAL = 1,
|
||||
MANUAL = 2
|
||||
};
|
||||
|
||||
enum class BpskChannel {
|
||||
HAM = 0,
|
||||
B = 1,
|
||||
C = 2,
|
||||
F = 3,
|
||||
G = 4,
|
||||
J = 5,
|
||||
K = 6,
|
||||
N = 7,
|
||||
O = 8,
|
||||
MANUAL = 10
|
||||
};
|
||||
|
||||
struct Location {
|
||||
std::string locator;
|
||||
bool south;
|
||||
int16_t lat_deg;
|
||||
int8_t lat_min;
|
||||
int8_t lat_sec;
|
||||
float latitude;
|
||||
bool west;
|
||||
int16_t long_deg;
|
||||
int8_t long_min;
|
||||
int8_t long_sec;
|
||||
float longitude;
|
||||
};
|
||||
|
||||
struct BeaconParams {
|
||||
BeaconType type;
|
||||
BeaconProtocol protocol;
|
||||
uint32_t country;
|
||||
bool is_test;
|
||||
bool is_internal;
|
||||
bool has_121_5;
|
||||
Location location;
|
||||
};
|
||||
|
||||
class EPIRBTXAppView : public View {
|
||||
public:
|
||||
EPIRBTXAppView(NavigationView& nav);
|
||||
~EPIRBTXAppView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "EPIRB TX"; };
|
||||
|
||||
private:
|
||||
void start_tx();
|
||||
void stop_tx();
|
||||
void update_config();
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void on_timer();
|
||||
void load_beacons();
|
||||
void set_tx_button_state(bool active);
|
||||
std::string frame_to_hex_string(bool start);
|
||||
void generate_frame(BeaconParams params);
|
||||
void update_frame(bool updateConfig = true);
|
||||
void update_bpsk_frequency();
|
||||
void update_am_transmission();
|
||||
void update_mode();
|
||||
void update_location(bool updateLocatorField = true);
|
||||
|
||||
struct Beacon {
|
||||
std::string title{};
|
||||
std::string description{};
|
||||
std::string frame{};
|
||||
};
|
||||
NavigationView& nav_;
|
||||
|
||||
std::vector<Beacon> beacons{};
|
||||
Beacon default_beacon{"Self test", "Serial User Location Protocol", "FFFED0D6E6202820000C29FF51041775302D"};
|
||||
|
||||
BeaconParams beacon_params{BeaconType::ELT, BeaconProtocol::STANDARD, 227, true, true, true, {"JN03RO", false, 0, 0, 0, 0, false, 0, 0, 0, 0}};
|
||||
|
||||
// Currently selected beacon index (from BEACONS.TXT file / options_frame combo)
|
||||
uint32_t selected_beacon{0};
|
||||
|
||||
// Frequency of the transmitter before starting the app (used to restore frequency when leaving)
|
||||
rf::Frequency original_frequency{0};
|
||||
// Frequency of the AM emergency signal
|
||||
rf::Frequency am_frequency{AM_TEST_FREQUENCY};
|
||||
// Frequency of the 406 MHz BPSK signal
|
||||
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||
// Selected am channel
|
||||
uint8_t am_channel{(uint8_t)AmChannel::TEST};
|
||||
// Selected bpsk channel
|
||||
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
|
||||
// Manual AM frequency value
|
||||
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
|
||||
// Manual BPSK frequency value
|
||||
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||
|
||||
// True when using a beacon from the BEACONS.TXT file
|
||||
bool mode_file{false};
|
||||
// True when slideshow is enabled for file mode (change beacon after every transmission)
|
||||
bool slideshow_enabled{false};
|
||||
// True when looping on sending beacons is enabled
|
||||
bool loop_enabled{true};
|
||||
// True if AM emergency signal transmission is enabled
|
||||
bool am_enabled{true};
|
||||
// True if we want to send a new frame each time the user changes the current beacon
|
||||
bool send_on_change{true};
|
||||
// The current locator string
|
||||
std::string locator{"JN03RO"};
|
||||
// The delay between each frame when on loop mode
|
||||
uint32_t delay{50};
|
||||
uint8_t beacon_type{(uint8_t)BeaconType::EPIRB};
|
||||
// Currently selected beacon protocol
|
||||
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
|
||||
// Currently selected beacon country
|
||||
uint32_t beacon_country{227};
|
||||
// Current beacon's internal state (true for internal location system)
|
||||
bool beacon_internal{true};
|
||||
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
TONES_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_epirb",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"sbeacon"sv, &selected_beacon},
|
||||
{"amfreq"sv, &am_frequency},
|
||||
{"bpskfreq"sv, &bpsk_frequency},
|
||||
{"amchan"sv, &am_channel},
|
||||
{"bpskchan"sv, &bpsk_channel},
|
||||
{"loop"sv, &loop_enabled},
|
||||
{"delay"sv, &delay},
|
||||
{"file"sv, &mode_file},
|
||||
{"slideshow"sv, &slideshow_enabled},
|
||||
{"am"sv, &am_enabled},
|
||||
{"soc"sv, &send_on_change},
|
||||
{"type"sv, &beacon_type},
|
||||
{"proto"sv, &beacon_protocol},
|
||||
{"country"sv, &beacon_country},
|
||||
{"internal"sv, &beacon_internal},
|
||||
{"locator"sv, &locator},
|
||||
}};
|
||||
|
||||
// Time of the last sent frame
|
||||
uint32_t last_frame_time{0};
|
||||
// True when transmission is enabled
|
||||
bool transmitting{false};
|
||||
// True when transmitting a BPSK frame
|
||||
bool transmitting_bpsk{false};
|
||||
// True when currently looping on sending beacons
|
||||
bool loop{false};
|
||||
|
||||
// Current EPIRBTXDataMessage for baseband
|
||||
EPIRBTXDataMessage epirb_tx_message{};
|
||||
|
||||
const size_t max_text_width = UI_POS_WIDTH_REMAINING(6) / UI_POS_DEFAULT_WIDTH;
|
||||
const size_t max_text_width_ext = UI_POS_WIDTH_REMAINING(0) / UI_POS_DEFAULT_WIDTH;
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency: MHz", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
// For file mode
|
||||
struct FileModeWidgets {
|
||||
Text text_beacon{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_DEFAULT_HEIGHT},
|
||||
"Beacon:"};
|
||||
// Beacon selection from BEACONS.TXT
|
||||
OptionsField options_frame{
|
||||
{UI_POS_X(7), UI_POS_Y(1)},
|
||||
30,
|
||||
{}};
|
||||
Text text_description_label{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
"Description:"};
|
||||
Text text_description{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_description_end{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Checkbox checkbox_slideshow{
|
||||
{UI_POS_X(0), UI_POS_Y(5)},
|
||||
9,
|
||||
"Slideshow",
|
||||
true};
|
||||
};
|
||||
std::unique_ptr<FileModeWidgets> file_mode_ui{};
|
||||
|
||||
// For manual mode
|
||||
Text text_beacon_type{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Type:"};
|
||||
Text text_beacon_country{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Country:"};
|
||||
Checkbox checkbox_beacon_internal{
|
||||
{UI_POS_X_RIGHT(12), UI_POS_Y(2)},
|
||||
12,
|
||||
"Internal",
|
||||
true};
|
||||
Text text_beacon_locator{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Locator:"};
|
||||
Text text_beacon_latitude{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Lat.:"};
|
||||
Text text_beacon_longitude{
|
||||
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Long.:"};
|
||||
Text text_beacon_latitude_value{
|
||||
{UI_POS_X(7), UI_POS_Y(4), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_beacon_longitude_value{
|
||||
{UI_POS_X(7), UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
OptionsField options_beacon_type{
|
||||
{UI_POS_X(9), UI_POS_Y(1)},
|
||||
7,
|
||||
{{"EPIRB", (uint8_t)BeaconType::EPIRB},
|
||||
{"ELT", (uint8_t)BeaconType::ELT},
|
||||
{"PLB", (uint8_t)BeaconType::PLB}}};
|
||||
OptionsField options_beacon_protocol{
|
||||
{UI_POS_X(9 + 7), UI_POS_Y(1)},
|
||||
30,
|
||||
{{"User", (uint8_t)BeaconProtocol::USER},
|
||||
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
|
||||
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
|
||||
OptionsField options_beacon_country{
|
||||
{UI_POS_X(9), UI_POS_Y(2)},
|
||||
7,
|
||||
{{"France", 227},
|
||||
{"USA", 366},
|
||||
{"Germany", 211},
|
||||
{"Russia", 273},
|
||||
{"Spain", 224},
|
||||
{"Japan", 431},
|
||||
{"UK", 232}}};
|
||||
TextField text_field_beacon_locator{
|
||||
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_DEFAULT_HEIGHT},
|
||||
"JN03RO"};
|
||||
Button button_mangps{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||
"Set pos."};
|
||||
|
||||
// Mode selection (file/manual)
|
||||
OptionsField options_mode{
|
||||
{UI_POS_X(7), UI_POS_Y(0)},
|
||||
30,
|
||||
{{"File (BEACONS.TXT)", (uint8_t)BeaconMode::FILE},
|
||||
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
|
||||
|
||||
// Frame content
|
||||
Text text_frame{
|
||||
{UI_POS_X(6), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_frame_end{
|
||||
{UI_POS_X(6), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
Text text_timeout{
|
||||
{UI_POS_X(14), UI_POS_Y(10), UI_POS_WIDTH(2), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
// Transmission settings
|
||||
Checkbox checkbox_loop{
|
||||
{UI_POS_X(0), UI_POS_Y(9)},
|
||||
10,
|
||||
"Resend every",
|
||||
true};
|
||||
NumberField field_delay{
|
||||
{UI_POS_X(15), UI_POS_Y(9)},
|
||||
2,
|
||||
{1, 99},
|
||||
1,
|
||||
' '};
|
||||
Checkbox checkbox_am{
|
||||
{UI_POS_X(0), UI_POS_Y(11)},
|
||||
10,
|
||||
"AM signal",
|
||||
true};
|
||||
FrequencyField field_am_frequency{
|
||||
{UI_POS_X(13), UI_POS_Y(12)}};
|
||||
Checkbox checkbox_send_on_change{
|
||||
{UI_POS_X(0), UI_POS_Y(13)},
|
||||
14,
|
||||
"Send on change",
|
||||
true};
|
||||
Button button_tx{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(9), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||
"START"};
|
||||
const Style& style_tx_start = *Theme::getInstance()->fg_green;
|
||||
const Style& style_tx_stop = *Theme::getInstance()->fg_red;
|
||||
OptionsField options_am_channel{
|
||||
{UI_POS_X(11), UI_POS_Y(14)},
|
||||
20,
|
||||
{{"121.375 MHz (Test)", 0},
|
||||
{"121.500 MHz /!\\Real", 1},
|
||||
{"Manual", 2}}};
|
||||
OptionsField options_bpsk_channel{
|
||||
{UI_POS_X(11), UI_POS_Y(15)},
|
||||
20,
|
||||
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
|
||||
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
|
||||
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
|
||||
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
|
||||
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
|
||||
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
|
||||
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
|
||||
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
|
||||
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
|
||||
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
|
||||
|
||||
// Transmitter view
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
10000,
|
||||
12};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
// Use frame sync for our applcation timer callback
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::epirb_tx
|
||||
|
||||
#endif /*__EPIRB_TX_H__*/
|
||||
|
||||
+10
-1
@@ -263,6 +263,10 @@ set(EXTCPPSRC
|
||||
#epirb_rx 168 byte flash
|
||||
external/epirb_rx/main.cpp
|
||||
external/epirb_rx/ui_epirb_rx.cpp
|
||||
external/epirb_rx/ui_beaconlist.cpp
|
||||
external/epirb_rx/beacon_db.cpp
|
||||
external/epirb_rx/beacon.cpp
|
||||
external/epirb_rx/location.cpp
|
||||
|
||||
#epirb_tx
|
||||
external/epirb_tx/main.cpp
|
||||
@@ -353,7 +357,11 @@ set(EXTCPPSRC
|
||||
|
||||
#two_tone_rx
|
||||
external/two_tone_rx/main.cpp
|
||||
external/two_tone_rx/ui_two_tone_rx.cpp
|
||||
external/two_tone_rx/ui_two_tone_rx.cpp
|
||||
|
||||
#hard_reset
|
||||
external/hard_reset/main.cpp
|
||||
external/hard_reset/ui_hard_reset.cpp
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -442,6 +450,7 @@ set(EXTAPPLIST
|
||||
p25_tx
|
||||
two_tone_pager
|
||||
two_tone_rx
|
||||
hard_reset
|
||||
)
|
||||
|
||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
||||
|
||||
+7
@@ -109,6 +109,7 @@ MEMORY
|
||||
ram_external_app_two_tone_pager (rwx) : org = 0xAE040000, len = 32k
|
||||
ram_external_app_two_tone_rx (rwx) : org = 0xAE050000, len = 32k
|
||||
ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k
|
||||
ram_external_app_hard_reset (rwx) : org = 0xAE070000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -628,4 +629,10 @@ SECTIONS
|
||||
KEEP(*(.external_app.app_flex_tx.application_information));
|
||||
*(*ui*external_app*flex_tx*);
|
||||
} > ram_external_app_flex_tx
|
||||
|
||||
.external_app_hard_reset : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_hard_reset.application_information));
|
||||
*(*ui*external_app*hard_reset*);
|
||||
} > ram_external_app_hard_reset
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_hard_reset.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::hard_reset {
|
||||
|
||||
void initialize_app(NavigationView& nav) {
|
||||
nav.push<HardResetView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::hard_reset
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_hard_reset.application_information"), used))
|
||||
application_information_t _application_information_hard_reset = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::hard_reset::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "Hard Reset",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xC0,
|
||||
0x01,
|
||||
0xFF,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0x7F,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0x56,
|
||||
0x35,
|
||||
0xFE,
|
||||
0x3F,
|
||||
0xFE,
|
||||
0x3F,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::red().v,
|
||||
/*.menu_location = */ app_location_t::SETTINGS,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {0, 0, 0, 0},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_hard_reset.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "file.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "ui_touch_calibration.hpp"
|
||||
#include "ui_external_items_menu_loader.hpp"
|
||||
#include "app_settings.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
namespace ui::external_app::hard_reset {
|
||||
HardResetView::HardResetView(ui::NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
add_children({&chk_settings_file,
|
||||
&chk_bad_apps,
|
||||
&chk_pmem,
|
||||
&txt_settings_file_count,
|
||||
&txt_bad_apps_count,
|
||||
&btn_yes,
|
||||
&btn_no,
|
||||
&text,
|
||||
&txt_wait});
|
||||
|
||||
txt_wait.set_style(Theme::getInstance()->warning_dark);
|
||||
|
||||
btn_yes.on_select = [this](Button&) {
|
||||
{ // to free painter after draw
|
||||
Painter painter;
|
||||
txt_wait.hidden(false);
|
||||
text.hidden(true);
|
||||
painter.fill_rectangle(text.screen_rect(), this->style().background);
|
||||
txt_wait.paint(painter);
|
||||
}
|
||||
if (chk_settings_file.value()) {
|
||||
clear_settings_folder();
|
||||
}
|
||||
if (chk_bad_apps.value()) {
|
||||
delete_bad_apps(apps_dir);
|
||||
}
|
||||
if (chk_pmem.value()) {
|
||||
pmem::cache::defaults();
|
||||
StatusRefreshMessage message{};
|
||||
EventDispatcher::send_message(message);
|
||||
nav_.replace<TouchCalibrationView>();
|
||||
} else {
|
||||
nav_.pop();
|
||||
}
|
||||
};
|
||||
|
||||
btn_no.on_select = [this](Button&) {
|
||||
nav_.pop();
|
||||
};
|
||||
txt_settings_file_count.set_style(Theme::getInstance()->fg_magenta);
|
||||
txt_bad_apps_count.set_style(Theme::getInstance()->fg_magenta);
|
||||
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
this->calculate();
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
};
|
||||
}
|
||||
|
||||
HardResetView::~HardResetView() {
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
}
|
||||
|
||||
void HardResetView::calculate() {
|
||||
settings_file_count = count_ini_files_in_directory(settings_dir);
|
||||
txt_settings_file_count.set(to_string_dec_uint(settings_file_count));
|
||||
|
||||
bad_apps_count = count_bad_apps(apps_dir);
|
||||
txt_bad_apps_count.set(to_string_dec_uint(bad_apps_count));
|
||||
|
||||
if (settings_file_count > 0) {
|
||||
chk_settings_file.set_value(true);
|
||||
}
|
||||
if (bad_apps_count > 0) {
|
||||
chk_bad_apps.set_value(true);
|
||||
}
|
||||
chk_pmem.set_value(true);
|
||||
txt_wait.hidden(true);
|
||||
text.set("Warning! Checked items will beerased (bad apps, P.Mem, settings). Uncheck to keep. Touch calibration is required only if P.Mem is reset.");
|
||||
}
|
||||
|
||||
void HardResetView::focus() {
|
||||
btn_no.focus();
|
||||
}
|
||||
|
||||
uint16_t HardResetView::count_ini_files_in_directory(const std::filesystem::path& dir_path) {
|
||||
int count = 0;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir_path, u"*.ini")) {
|
||||
if (std::filesystem::is_regular_file(entry.status())) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void HardResetView::delete_ini_files_in_directory(const std::filesystem::path& dir_path) {
|
||||
bool found_and_deleted;
|
||||
do {
|
||||
found_and_deleted = false;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir_path, u"*.ini")) {
|
||||
if (std::filesystem::is_regular_file(entry.status())) {
|
||||
auto full_path = dir_path / entry.path().filename();
|
||||
delete_file(full_path);
|
||||
found_and_deleted = true;
|
||||
break; // iterator can become invalid if the dir is modified, so break and restart the loop after deletion
|
||||
}
|
||||
}
|
||||
} while (found_and_deleted);
|
||||
}
|
||||
|
||||
void HardResetView::clear_settings_folder() {
|
||||
delete_ini_files_in_directory(settings_dir);
|
||||
}
|
||||
|
||||
bool HardResetView::is_bad_app(const std::filesystem::path& file_path, bool is_ppma) {
|
||||
File app;
|
||||
if (app.open(file_path)) {
|
||||
return true;
|
||||
}
|
||||
bool is_bad = false;
|
||||
if (is_ppma) {
|
||||
application_information_t app_info = {};
|
||||
auto readResult = app.read(&app_info, sizeof(application_information_t));
|
||||
if (!readResult || readResult.value() != sizeof(application_information_t)) {
|
||||
is_bad = true;
|
||||
} else if (app_info.header_version != CURRENT_HEADER_VERSION) {
|
||||
is_bad = true;
|
||||
} else if (VERSION_MD5 != app_info.app_version) {
|
||||
is_bad = true;
|
||||
} else {
|
||||
// --- CHECKSUM VALIDATION ---
|
||||
// Similar to run_external_app: read through the file and calculate the checksum
|
||||
app.seek(0);
|
||||
uint32_t checksum = 0;
|
||||
uint8_t buffer[512]; // 512-byte buffer to conserve RAM
|
||||
|
||||
while (true) {
|
||||
auto res = app.read(buffer, sizeof(buffer));
|
||||
if (!res) break;
|
||||
checksum += simple_checksum((uint32_t)buffer, res.value());
|
||||
if (res.value() < sizeof(buffer)) break; // End of file
|
||||
}
|
||||
|
||||
if (checksum != EXT_APP_EXPECTED_CHECKSUM) {
|
||||
is_bad = true; // The file body is corrupted / truncated!
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// PPMP validation
|
||||
standalone_application_information_t app_info = {};
|
||||
auto readResult = app.read(&app_info, sizeof(standalone_application_information_t));
|
||||
if (!readResult || readResult.value() != sizeof(standalone_application_information_t)) {
|
||||
is_bad = true;
|
||||
} else if (app_info.header_version > CURRENT_STANDALONE_APPLICATION_API_VERSION) {
|
||||
is_bad = true;
|
||||
}
|
||||
}
|
||||
|
||||
app.close();
|
||||
return is_bad;
|
||||
}
|
||||
|
||||
uint16_t HardResetView::count_bad_apps(const std::filesystem::path& apps_dir) {
|
||||
int bad_count = 0;
|
||||
|
||||
// Check .ppma files
|
||||
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppma")) {
|
||||
auto file_path = apps_dir / entry.path();
|
||||
if (is_bad_app(file_path, true)) {
|
||||
bad_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check .ppmp files
|
||||
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppmp")) {
|
||||
auto file_path = apps_dir / entry.path();
|
||||
if (is_bad_app(file_path, false)) {
|
||||
bad_count++;
|
||||
}
|
||||
}
|
||||
|
||||
return bad_count;
|
||||
}
|
||||
|
||||
void HardResetView::delete_bad_apps(const std::filesystem::path& apps_dir) {
|
||||
// Delete bad .ppma files
|
||||
bool deleted_something;
|
||||
do {
|
||||
deleted_something = false;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppma")) {
|
||||
auto file_path = apps_dir / entry.path();
|
||||
if (is_bad_app(file_path, true)) {
|
||||
delete_file(file_path);
|
||||
deleted_something = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (deleted_something);
|
||||
|
||||
// Delete bad .ppmp files
|
||||
do {
|
||||
deleted_something = false;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppmp")) {
|
||||
auto file_path = apps_dir / entry.path();
|
||||
if (is_bad_app(file_path, false)) {
|
||||
delete_file(file_path);
|
||||
deleted_something = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (deleted_something);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::hard_reset
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __UI_HARD_RESET_H__
|
||||
#define __UI_HARD_RESET_H__
|
||||
|
||||
#include "ui_navigation.hpp"
|
||||
#include "signal.hpp"
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::hard_reset {
|
||||
|
||||
class HardResetView : public ui::View {
|
||||
public:
|
||||
HardResetView(ui::NavigationView& nav);
|
||||
~HardResetView();
|
||||
|
||||
std::string title() const override { return "Hard Reset"; }
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
ui::NavigationView& nav_;
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
ui::Button btn_yes{{UI_POS_X(1), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(2)}, "Erase sel."};
|
||||
ui::Button btn_no{{UI_POS_X_RIGHT(11), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)}, "No"};
|
||||
ui::Text text{{UI_POS_X(0), UI_POS_Y(6), UI_POS_MAXWIDTH, UI_POS_HEIGHT(5)}, ""};
|
||||
Checkbox chk_settings_file{{UI_POS_X(1), UI_POS_Y(1)}, 14, "Settings file:", true};
|
||||
Checkbox chk_bad_apps{{UI_POS_X(1), UI_POS_Y(2.5)}, 9, "Bad apps:", true};
|
||||
Checkbox chk_pmem{{UI_POS_X(1), UI_POS_Y(4)}, 11, "P.mem reset", true};
|
||||
|
||||
Text txt_settings_file_count{{UI_POS_X(19), UI_POS_Y(1), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "?"};
|
||||
Text txt_bad_apps_count{{UI_POS_X(14), UI_POS_Y(2.5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "?"};
|
||||
|
||||
Text txt_wait{{UI_POS_X_CENTER(14), UI_POS_Y(8), UI_POS_WIDTH(14), UI_POS_HEIGHT(1)}, "Please wait..."};
|
||||
|
||||
uint16_t count_ini_files_in_directory(const std::filesystem::path& dir_path);
|
||||
void delete_ini_files_in_directory(const std::filesystem::path& dir_path);
|
||||
void clear_settings_folder();
|
||||
void calculate();
|
||||
bool is_bad_app(const std::filesystem::path& file_path, bool is_ppma);
|
||||
uint16_t count_bad_apps(const std::filesystem::path& apps_dir);
|
||||
void delete_bad_apps(const std::filesystem::path& apps_dir);
|
||||
|
||||
uint16_t settings_file_count = 0;
|
||||
uint16_t bad_apps_count = 0;
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::hard_reset
|
||||
|
||||
#endif // __UI_HARD_RESET_H__
|
||||
@@ -360,19 +360,12 @@ static void set_cpu_clock_speed() {
|
||||
|
||||
/* Step into the 90-110MHz M4 clock range */
|
||||
#ifdef PRALINE
|
||||
/* PRALINE: Enable and use 12MHz XTAL directly (no GP_CLKIN from Si5351) */
|
||||
|
||||
/* Step 1: Enable the crystal oscillator */
|
||||
LPC_CGU->XTAL_OSC_CTRL.ENABLE = 0; // 0 = enable (active low)
|
||||
LPC_CGU->XTAL_OSC_CTRL.HF = 0; // 0 = low frequency mode (1-20MHz)
|
||||
|
||||
/* Step 2: Wait for oscillator to stabilize (~250us at IRC speed) */
|
||||
volatile uint32_t delay = 3000; // ~250us at 12MHz IRC
|
||||
while (delay--);
|
||||
|
||||
/* Step 3: Configure PLL1 from XTAL
|
||||
* Fclkin = 12M, /N=1 = 12M, Fcco = 12M * 17 = 204M
|
||||
* Fclk = Fcco / (2*(P=1)) = 102M
|
||||
/* PRALINE: source PLL1 from GP_CLKIN, which is driven by the Si5351
|
||||
* CLK2/MCU_CLK output at 40MHz (see ClockManager Si5351 setup). The
|
||||
* on-chip 12MHz XTAL bootstrap path is intentionally not used here.
|
||||
*
|
||||
* Fclkin = 40M (GP_CLKIN), /N=2 = 20M, Fcco = 20M * 10 = 200M
|
||||
* Fclk = Fcco / (2*(P=1)) = 100M
|
||||
*/
|
||||
cgu::pll1::ctrl({
|
||||
.pd = 1,
|
||||
@@ -381,10 +374,11 @@ static void set_cpu_clock_speed() {
|
||||
.direct = 0,
|
||||
.psel = 0,
|
||||
.autoblock = 1,
|
||||
.nsel = 0, // N = 1
|
||||
.msel = 16, // M = 17, so 12MHz * 17 = 204MHz
|
||||
.clk_sel = cgu::CLK_SEL::XTAL,
|
||||
.nsel = 1UL, // N = 2
|
||||
.msel = 9UL, // M = 10
|
||||
.clk_sel = cgu::CLK_SEL::GP_CLKIN,
|
||||
});
|
||||
|
||||
#else
|
||||
/* OG:
|
||||
* Fclkin = 40M, /N=2 = 20M, Fcco = 20M * 10 = 200M
|
||||
@@ -613,6 +607,7 @@ init_status_t init() {
|
||||
cgu::pll1::disable();
|
||||
|
||||
set_cpu_clock_speed();
|
||||
|
||||
/* sample max: 1023 sample_t AKA uint16_t
|
||||
* touch_sensitivity: range: 1 to 128
|
||||
* threshold range: 1023/1 to 1023/128 = 1023 to 8
|
||||
|
||||
@@ -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>
|
||||
@@ -390,11 +390,13 @@ void set_baseband_rate(const uint32_t rate) {
|
||||
}
|
||||
|
||||
void set_antenna_bias(const bool on) {
|
||||
/* Pull MOSFET gate low to turn on antenna bias. */
|
||||
#ifdef PRALINE
|
||||
// Praline: P2_12 = GPIO1[12], ANT_BIAS_EN_N (active LOW)
|
||||
LPC_GPIO->CLR[1] = on ? (1 << 12) : 0;
|
||||
LPC_GPIO->SET[1] = on ? 0 : (1 << 12);
|
||||
/* Keep bias control inside the PRALINE RF path state machine so later
|
||||
* band/direction/amp updates don't silently force ANT_BIAS_EN_N off.
|
||||
* Reassert output mode here because this pin is safety-critical and
|
||||
* direct GPIO writes on an input-configured pad have no effect. */
|
||||
gpio_ant_bias_disable.output();
|
||||
rf_path.set_antenna_bias(on);
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
gpio_r9_not_ant_pwr.write(on ? 0 : 1);
|
||||
@@ -404,6 +406,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 +423,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
|
||||
@@ -606,4 +629,4 @@ uint32_t register_read(const size_t register_number) {
|
||||
|
||||
} /* namespace debug */
|
||||
|
||||
} /* namespace radio */
|
||||
} /* namespace radio */
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -263,6 +263,11 @@ void Path::set_rf_amp(const bool new_rf_amp) {
|
||||
update();
|
||||
}
|
||||
|
||||
void Path::set_antenna_bias(const bool new_antenna_bias) {
|
||||
antenna_bias = new_antenna_bias;
|
||||
update();
|
||||
}
|
||||
|
||||
void Path::update() {
|
||||
/* 0 ^ 0 => 0 & 0 = 0 ^ 0 = 0 (no change)
|
||||
* 0 ^ 1 => 1 & 0 = 0 ^ 0 = 0 (ignore change to 1)
|
||||
@@ -301,8 +306,8 @@ void Path::update() {
|
||||
/* RF amp when amplification requested */
|
||||
config.rf_amp_en = rf_amp;
|
||||
|
||||
/* Antenna bias off by default */
|
||||
config.ant_bias_en_n = true;
|
||||
/* Preserve requested antenna bias state across band/direction/amp updates. */
|
||||
config.ant_bias_en_n = !antenna_bias;
|
||||
|
||||
config.apply();
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ class Path {
|
||||
void set_direction(const Direction direction);
|
||||
void set_band(const Band band);
|
||||
void set_rf_amp(const bool rf_amp);
|
||||
void set_antenna_bias(const bool antenna_bias);
|
||||
|
||||
Band get_band() const { return _band; } //_band is used solely for debugging purposes.
|
||||
|
||||
@@ -72,6 +73,7 @@ class Path {
|
||||
Direction direction{Direction::Receive};
|
||||
Band band{Band::Mid};
|
||||
bool rf_amp{false};
|
||||
bool antenna_bias{false};
|
||||
|
||||
void update();
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
#include "lpc43xx_cpp.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include <ctime>
|
||||
|
||||
namespace rtc_time {
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <cstddef>
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "hackrf_hal.hpp"
|
||||
#include "max2837.hpp"
|
||||
#include "message.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
@@ -40,8 +41,9 @@ class TransmitterModel {
|
||||
uint32_t baseband_bandwidth = max283x::filter::bandwidth_minimum;
|
||||
uint32_t sampling_rate = 3'072'000;
|
||||
uint32_t channel_bandwidth = 1;
|
||||
/* 35 should give approx 1m transmission range. */
|
||||
uint8_t tx_gain_db = 35;
|
||||
/* Platform default chosen to avoid PRALINE MAX2831 overdrive while
|
||||
* preserving legacy HackRF One behavior. */
|
||||
uint8_t tx_gain_db = hackrf::one::default_tx_gain_db;
|
||||
bool rf_amp = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,6 +224,7 @@ void MenuView::clear() {
|
||||
menu_items.shrink_to_fit();
|
||||
highlighted_item = 0;
|
||||
offset = 0;
|
||||
reset_scroll();
|
||||
}
|
||||
|
||||
size_t MenuView::item_count() const {
|
||||
@@ -225,6 +297,8 @@ bool MenuView::set_highlighted(int32_t new_value) {
|
||||
item_view(highlighted_item - offset)->highlight();
|
||||
}
|
||||
|
||||
reset_scroll();
|
||||
|
||||
if (on_highlight)
|
||||
on_highlight();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -108,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{};
|
||||
@@ -126,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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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__
|
||||
@@ -217,7 +217,25 @@ set(CPPWARN "-Wall -Wextra")
|
||||
if(NOT DEFINED BOARD)
|
||||
set(BOARD "HACKRF_ONE")
|
||||
endif()
|
||||
set(DDEFS "-DLPC43XX -DLPC43XX_M4 -D__NEWLIB__ -D${BOARD} -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"'")
|
||||
|
||||
# Upstream hackrf headers now gate on IS_* macros normally populated by
|
||||
# hackrf/firmware/platform-detect.cmake. We don't include that here, so derive
|
||||
# the IS_* set from BOARD ourselves. IS_H1_R9 is forced to 0 (compile-time
|
||||
# constant) so `if (IS_H1_R9)` branches resolve without needing the runtime
|
||||
# detected_platform() symbol, which mayhem doesn't link in.
|
||||
if(BOARD STREQUAL "HACKRF_ONE")
|
||||
set(BOARD_IS_DEFS "-DIS_HACKRF_ONE=1 -DIS_NOT_PRALINE=1 -DIS_H1_R9=0 -DIS_NOT_H1_R9=1 -DIS_NOT_RAD1O=1 -DIS_NOT_JAWBREAKER=1 -DIS_H1_OR_PRALINE=1 -DIS_H1_OR_RAD1O=1 -DIS_H1_OR_JAWBREAKER=1 -DIS_EXPANSION_COMPATIBLE=1")
|
||||
elseif(BOARD STREQUAL "PRALINE")
|
||||
set(BOARD_IS_DEFS "-DIS_PRALINE=1 -DIS_NOT_HACKRF_ONE=1 -DIS_NOT_H1_R9=1 -DIS_NOT_RAD1O=1 -DIS_NOT_JAWBREAKER=1 -DIS_H1_OR_PRALINE=1 -DIS_FOUR_LEDS=1 -DIS_EXPANSION_COMPATIBLE=1")
|
||||
elseif(BOARD STREQUAL "RAD1O")
|
||||
set(BOARD_IS_DEFS "-DIS_RAD1O=1 -DIS_NOT_PRALINE=1 -DIS_NOT_HACKRF_ONE=1 -DIS_NOT_H1_R9=1 -DIS_NOT_JAWBREAKER=1 -DIS_H1_OR_RAD1O=1 -DIS_FOUR_LEDS=1")
|
||||
elseif(BOARD STREQUAL "JAWBREAKER")
|
||||
set(BOARD_IS_DEFS "-DIS_JAWBREAKER=1 -DIS_NOT_PRALINE=1 -DIS_NOT_HACKRF_ONE=1 -DIS_NOT_H1_R9=1 -DIS_NOT_RAD1O=1 -DIS_H1_OR_JAWBREAKER=1")
|
||||
else()
|
||||
set(BOARD_IS_DEFS "")
|
||||
endif()
|
||||
|
||||
set(DDEFS "-DLPC43XX -DLPC43XX_M4 -D__NEWLIB__ -D${BOARD} ${BOARD_IS_DEFS} -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"'")
|
||||
|
||||
# List all default ASM defines here, like -D_DEBUG=1
|
||||
set(DADEFS)
|
||||
@@ -765,7 +783,6 @@ set(MODE_CPPSRC
|
||||
${HACKRF_PATH}/firmware/common/si5351c.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_bus.c
|
||||
${HACKRF_PATH}/firmware/common/mixer.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/spi_bus.c
|
||||
${HACKRF_PATH}/firmware/common/sgpio.c
|
||||
${HACKRF_PATH}/firmware/common/rf_path.c
|
||||
@@ -773,7 +790,6 @@ set(MODE_CPPSRC
|
||||
${HACKRF_PATH}/firmware/common/spi_ssp.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071_spi.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/gpdma.c
|
||||
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c
|
||||
|
||||
@@ -90,7 +90,7 @@ void BasebandThread::run() {
|
||||
#ifdef PRALINE
|
||||
shared_memory.m4_streaming_marker = 0xAA; // Phase 0 instrumentation
|
||||
#endif
|
||||
|
||||
uint8_t buffer_drained = 0; // to count how many dma buffers we already emptied
|
||||
while (!chThdShouldTerminate()) {
|
||||
#ifdef PRALINE
|
||||
shared_memory.m4_baseband_loops++; // Phase 0 instrumentation
|
||||
@@ -117,11 +117,20 @@ void BasebandThread::run() {
|
||||
}
|
||||
|
||||
if (baseband_processor_) {
|
||||
baseband_processor_->execute(buffer);
|
||||
if (shared_memory.radio_tx_drain == 0) { // only generate if not draining.
|
||||
baseband_processor_->execute(buffer);
|
||||
}
|
||||
}
|
||||
if (shared_memory.radio_tx_drain == 1) {
|
||||
buffer_drained++;
|
||||
if (buffer_drained >= 4) { // We have 4 buffers, so after draining 4 we should be safe to disable.
|
||||
shared_memory.radio_tx_drain = 0; // Clear the drain request to allow normal operation to resume.
|
||||
buffer_drained = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_memory.radio_tx_drain = 0;
|
||||
i2s::i2s0::tx_mute();
|
||||
baseband::dma::disable();
|
||||
baseband_sgpio.streaming_disable();
|
||||
|
||||
@@ -1,96 +1,272 @@
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "proc_epirb.hpp"
|
||||
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "dsp_fir_taps.hpp"
|
||||
|
||||
#include "event_m4.hpp"
|
||||
#include <ch.h>
|
||||
|
||||
EPIRBProcessor::EPIRBProcessor() {
|
||||
// Configure the decimation filters for narrowband EPIRB signal
|
||||
// Target: Reduce 2.457600 MHz to ~38.4 kHz for 400 bps processing
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
||||
/* 2.4576MHz, 2048 samples */
|
||||
|
||||
// First decimation stage: 2.4576 MHz -> 307.2 kHz
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
|
||||
// Second decimation stage: 307.2 kHz -> 38.4 kHz
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
const auto decimator_out = decim_1_out;
|
||||
|
||||
/* 38.4kHz, 32 samples (approximately) */
|
||||
feed_channel_stats(decimator_out);
|
||||
|
||||
// Process each decimated sample through the matched filter
|
||||
for (size_t i = 0; i < decimator_out.count; i++) {
|
||||
// Apply matched filter for BPSK demodulation
|
||||
if (mf.execute_once(decimator_out.p[i])) {
|
||||
// Feed symbol to clock recovery when matched filter triggers
|
||||
clock_recovery(mf.get_output());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBProcessor::consume_symbol(const float raw_symbol) {
|
||||
// BPSK demodulation: positive = 1, negative = 0
|
||||
const uint_fast8_t sliced_symbol = (raw_symbol >= 0.0f) ? 1 : 0;
|
||||
|
||||
// Decode bi-phase L encoding manually
|
||||
// In bi-phase L: 0 = no transition, 1 = transition
|
||||
// This is a simple edge detector
|
||||
const auto decoded_symbol = sliced_symbol ^ last_symbol;
|
||||
last_symbol = sliced_symbol;
|
||||
|
||||
// Build packet from decoded symbols
|
||||
packet_builder.execute(decoded_symbol);
|
||||
}
|
||||
|
||||
void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
|
||||
// EPIRB packet received - validate and process
|
||||
if (packet.size() >= 112) { // Minimum EPIRB data payload size (112 bits)
|
||||
packets_received++;
|
||||
last_packet_timestamp = Timestamp::now();
|
||||
|
||||
// Create and send EPIRB packet message to application layer
|
||||
const EPIRBPacketMessage message{packet};
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBProcessor::on_message(const Message* const message) {
|
||||
(void)message; // Unused in this processor
|
||||
}
|
||||
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<EPIRBProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "proc_epirb.hpp"
|
||||
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "dsp_fir_taps.hpp"
|
||||
|
||||
#include "audio_dma.hpp"
|
||||
|
||||
#include "event_m4.hpp"
|
||||
#include <ch.h>
|
||||
|
||||
EPIRBProcessor::EPIRBProcessor() {
|
||||
// Configure the decimation filters for narrowband EPIRB signal
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
// Configure channel filter for audio filtering
|
||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||
// Configure demodulation for audio output
|
||||
demod.configure(SAMPLE_RATE, 5000);
|
||||
// Configure audio output (+squelch level)
|
||||
configure_audio();
|
||||
#ifdef SPECAN
|
||||
channel_spectrum.set_decimation_factor(1);
|
||||
#endif
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
void EPIRBProcessor::configure_audio() {
|
||||
// UI sends an squelch value ranging from 0 to 99, 0 disables squelch, dividing UI value by 40 gives a valid UI threashold around 50
|
||||
audio_output.configure(audio_24k_hpf_300hz_config, audio_24k_deemph_300_6_config, ((float)squelch_level) / 40.0f);
|
||||
}
|
||||
|
||||
float EPIRBProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
|
||||
// Calculate the phase difference between two samples
|
||||
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
|
||||
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
|
||||
float phase_diff = atan2f(dQ, dI);
|
||||
// Prevent phase diff from wrapping around
|
||||
if (phase_diff > M_PI) phase_diff -= 2.0f * M_PI;
|
||||
if (phase_diff < -M_PI) phase_diff += 2.0f * M_PI;
|
||||
return phase_diff;
|
||||
}
|
||||
|
||||
bool EPIRBProcessor::filtered_rise_detect(bool condition) {
|
||||
bool result = false;
|
||||
if (condition) {
|
||||
// If rise condition is matched, filter peaks that last less than 3 samples
|
||||
rise_detection_count++;
|
||||
if (rise_detection_count >= RISE_FILTER_SAMPLES) {
|
||||
result = true;
|
||||
rise_detection_count = 0;
|
||||
}
|
||||
} else {
|
||||
rise_detection_count = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
||||
// First decimation stage: 3.072000 MHz / 8 -> 384 kHz
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
|
||||
// Second decimation stage: 384 kHz / 8 -> 48 kHz
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
// We use decim1 output as decimator output
|
||||
const auto decimator_out = decim_1_out;
|
||||
|
||||
#ifdef SPECAN
|
||||
// Feed IQ data into spectrum collector for the RF waterfall.
|
||||
if (spectrum_on) channel_spectrum.feed(decim_1_out, -5500, 5500, 3400);
|
||||
#endif
|
||||
|
||||
feed_channel_stats(decimator_out);
|
||||
|
||||
// if (audio_on) {
|
||||
// Channel filter for audio out
|
||||
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||
auto audio = demod.execute(channel_out, audio_buffer);
|
||||
audio_output.write(audio);
|
||||
//}
|
||||
|
||||
// Process each decimated sample through state machine
|
||||
for (size_t i = 0; i < decimator_out.count; i++) {
|
||||
// Track sample count since last symbol and since begining of the frame
|
||||
sample_count++;
|
||||
frame_sample_count++;
|
||||
// Compute phase delta since last sample
|
||||
float phase_delta = get_phase_diff(last_sample, decimator_out.p[i]);
|
||||
last_sample = decimator_out.p[i];
|
||||
|
||||
// Let's sum phase delta over a 12 sample window to get the full phase jump
|
||||
phase_delta_acc -= phase_delta_buffer[pahse_delta_index];
|
||||
phase_delta_buffer[pahse_delta_index] = phase_delta;
|
||||
phase_delta_acc += phase_delta_buffer[pahse_delta_index];
|
||||
pahse_delta_index = (pahse_delta_index + 1) % PHASE_DELTA_ACC_SIZE;
|
||||
|
||||
// Use accumulated delta
|
||||
phase_delta = phase_delta_acc;
|
||||
|
||||
// State machine for COSPAS frame detection
|
||||
switch (current_state) {
|
||||
case IDLE:
|
||||
// We are waiting for a 160ms empty carrier => phase shouls be stable during this period
|
||||
// We accept a 0.6 phase shift since phase may drift durring carrier if carrier frequency is not alligned with tuner frequency
|
||||
if (filtered_rise_detect(phase_delta >= 0.6f)) {
|
||||
stability_counter = 0;
|
||||
} else {
|
||||
stability_counter++;
|
||||
if (stability_counter > CARRIER_SAMPLES_THRESHOLD) {
|
||||
// Carrier has been stable long enought, go to locked state
|
||||
current_state = CARRIER_LOCKED;
|
||||
frame_sample_count = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CARRIER_LOCKED:
|
||||
// Carrier is locked, we now wait for a phase 1.1 rad phase jump corresponding to the befining of the frame
|
||||
// Let's use a 0.7 phase jump threshold
|
||||
if (filtered_rise_detect(phase_delta >= 0.7f)) {
|
||||
// Jump detected, frame starts now
|
||||
frame_sample_count = 0;
|
||||
// Go to data sync state
|
||||
current_state = DATA_SYNC;
|
||||
// Frame should always start with a positive phase shift
|
||||
last_phase_positive = true;
|
||||
// And a 1 value
|
||||
last_bit = true;
|
||||
} else if (frame_sample_count > CARRIER_MAX_SAMPLES) {
|
||||
// We missed sync pattern
|
||||
frame_end();
|
||||
}
|
||||
break;
|
||||
|
||||
case DATA_SYNC: {
|
||||
float abs_phase_delta = fabsf(phase_delta);
|
||||
|
||||
if (abs_phase_delta >= 1.6f) {
|
||||
// Phase should jump from 1.1 rad to -1.1 rad or the other way around
|
||||
// Absolute phase jump is expected to be 2.2 rad
|
||||
// Phase jump is either positive or negative
|
||||
bool phase_positive = (phase_delta >= 0.0f);
|
||||
|
||||
if (phase_positive != last_phase_positive) {
|
||||
// Phase jumped to the opposit direction of last jump
|
||||
last_phase_positive = phase_positive;
|
||||
bool cur_bit;
|
||||
// Phase change => how long since last change ?
|
||||
if ((frame_sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (frame_sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
||||
// Frame start
|
||||
if (!phase_positive) {
|
||||
// Symbol detection is made on falling edge
|
||||
cur_bit = true;
|
||||
} else {
|
||||
// Ignore rising edge
|
||||
continue;
|
||||
}
|
||||
} else if (sample_count > (SAMPLES_PER_SYMBOL * 2 + SAMPLES_MARGIN)) {
|
||||
// We missed something...
|
||||
// Let's keep same value for current bit
|
||||
cur_bit = last_bit;
|
||||
} else if (sample_count >= (SAMPLES_PER_SYMBOL * 2 - SAMPLES_MARGIN)) {
|
||||
// 2 symbols since last change => bit value changes
|
||||
cur_bit = !last_bit;
|
||||
} else if ((sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
||||
// Phase change occured in first half bit => we keep the same value
|
||||
if ((phase_positive && last_bit) || (!phase_positive && !last_bit)) {
|
||||
sample_count = 0;
|
||||
// Ignore rising edge if current value is 1 and falling edge if current value is 0 and move to next symbol
|
||||
continue;
|
||||
}
|
||||
// Same value on falling/rising edge
|
||||
cur_bit = last_bit;
|
||||
} else {
|
||||
// Filter the rest
|
||||
continue;
|
||||
}
|
||||
// Store new bit and move to next symbol
|
||||
sample_count = 0;
|
||||
packet_builder.execute(cur_bit);
|
||||
last_bit = cur_bit;
|
||||
}
|
||||
}
|
||||
if (frame_sample_count > FRAME_MAX_SAMPLES) {
|
||||
// End of frame
|
||||
current_state = POST_FRAME;
|
||||
packet_builder.flush();
|
||||
}
|
||||
} break;
|
||||
case POST_FRAME:
|
||||
if (frame_sample_count > CARRIER_MAX_SAMPLES) {
|
||||
// End of carrier
|
||||
frame_end();
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBProcessor::frame_end() {
|
||||
sample_count = 0;
|
||||
frame_sample_count = 0;
|
||||
stability_counter = 0;
|
||||
last_phase_positive = false;
|
||||
last_bit = false;
|
||||
current_state = IDLE;
|
||||
packet_builder.reset_state();
|
||||
}
|
||||
|
||||
void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
|
||||
// EPIRB packet received: create and send EPIRB packet message to application layer
|
||||
const EPIRBPacketMessage message{packet};
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
|
||||
void EPIRBProcessor::on_message(const Message* const msg) {
|
||||
// Configure the processor
|
||||
switch (msg->id) {
|
||||
#ifdef SPECAN
|
||||
case Message::ID::UpdateSpectrum:
|
||||
case Message::ID::SpectrumStreamingConfig:
|
||||
channel_spectrum.on_message(msg);
|
||||
break;
|
||||
#endif
|
||||
case Message::ID::EPIRBRXConfig: {
|
||||
const EPIRBRXConfig message = *reinterpret_cast<const EPIRBRXConfig*>(msg);
|
||||
// audio_on = message.audio_on;
|
||||
#ifdef SPECAN
|
||||
spectrum_on = message.spectrum_on;
|
||||
#endif
|
||||
if (message.squelch != squelch_level) {
|
||||
// Update squelch config
|
||||
squelch_level = message.squelch;
|
||||
configure_audio();
|
||||
}
|
||||
} break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
audio::dma::init_audio_out();
|
||||
|
||||
EventDispatcher event_dispatcher{std::make_unique<EPIRBProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
+267
-134
@@ -1,135 +1,268 @@
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __PROC_EPIRB_H__
|
||||
#define __PROC_EPIRB_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <complex>
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
#include "channel_decimator.hpp"
|
||||
#include "matched_filter.hpp"
|
||||
#include "clock_recovery.hpp"
|
||||
#include "symbol_coding.hpp"
|
||||
#include "packet_builder.hpp"
|
||||
#include "baseband_packet.hpp"
|
||||
#include "message.hpp"
|
||||
#include "buffer.hpp"
|
||||
|
||||
// Forward declarations for types only used as pointers/references
|
||||
class Message;
|
||||
namespace baseband {
|
||||
class Packet;
|
||||
}
|
||||
|
||||
// EPIRB 406 MHz Emergency Position Indicating Radio Beacon
|
||||
// Signal characteristics:
|
||||
// - Frequency: 406.025 - 406.028 MHz (typically 406.028 MHz)
|
||||
// - Modulation: BPSK (Binary Phase Shift Keying)
|
||||
// - Data rate: 400 bps
|
||||
// - Encoding: Bi-phase L (Manchester)
|
||||
// - Transmission: Every 50 seconds ± 2.5 seconds
|
||||
// - Power: 5W ± 2dB
|
||||
// - Message length: 144 bits (including sync pattern)
|
||||
|
||||
// Matched filter for BPSK demodulation at 400 bps
|
||||
// Using raised cosine filter taps optimized for 400 bps BPSK
|
||||
static constexpr std::array<std::complex<float>, 64> bpsk_taps = {{// Raised cosine filter coefficients for BPSK 400 bps
|
||||
-5, -8, -12, -15, -17, -17, -15, -11,
|
||||
-5, 2, 11, 20, 29, 37, 43, 47,
|
||||
48, 46, 42, 35, 26, 16, 4, -8,
|
||||
-21, -33, -44, -53, -59, -62, -62, -58,
|
||||
-51, -41, -28, -13, 3, 19, 36, 51,
|
||||
64, 74, 80, 82, 80, 74, 64, 51,
|
||||
36, 19, 3, -13, -28, -41, -51, -58,
|
||||
-62, -62, -59, -53, -44, -33, -21, -8}};
|
||||
|
||||
class EPIRBProcessor : public BasebandProcessor {
|
||||
public:
|
||||
EPIRBProcessor();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
// EPIRB operates at 406 MHz with narrow bandwidth
|
||||
static constexpr size_t baseband_fs = 2457600;
|
||||
static constexpr uint32_t epirb_center_freq = 406028000; // 406.028 MHz
|
||||
static constexpr uint32_t symbol_rate = 400; // 400 bps
|
||||
static constexpr size_t decimation_factor = 64; // Decimate to ~38.4kHz
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
// Decimation chain for 406 MHz EPIRB signal processing
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
|
||||
dsp::matched_filter::MatchedFilter mf{bpsk_taps, 2};
|
||||
|
||||
// Clock recovery for 400 bps symbol rate
|
||||
// Sampling rate after decimation: ~38.4kHz
|
||||
// Symbols per sample: 38400 / 400 = 96 samples per symbol
|
||||
clock_recovery::ClockRecovery<clock_recovery::FixedErrorFilter> clock_recovery{
|
||||
38400, // sampling_rate
|
||||
400, // symbol_rate (400 bps)
|
||||
{0.0555f}, // error_filter coefficient
|
||||
[this](const float symbol) { this->consume_symbol(symbol); }};
|
||||
|
||||
// Simple bi-phase L decoder state
|
||||
uint_fast8_t last_symbol = 0;
|
||||
|
||||
// EPIRB packet structure:
|
||||
// - Sync pattern: 000101010101... (15 bits)
|
||||
// - Frame sync: 0111110 (7 bits)
|
||||
// - Data: 112 bits
|
||||
// - BCH error correction: 10 bits
|
||||
// Total: 144 bits
|
||||
PacketBuilder<BitPattern, BitPattern, BitPattern> packet_builder{
|
||||
{0b010101010101010, 15, 1}, // Preamble pattern
|
||||
{0b0111110, 7}, // Frame sync pattern
|
||||
{0b0111110, 7}, // End pattern (same as sync for simplicity)
|
||||
[this](const baseband::Packet& packet) {
|
||||
this->payload_handler(packet);
|
||||
}};
|
||||
|
||||
void consume_symbol(const float symbol);
|
||||
void payload_handler(const baseband::Packet& packet);
|
||||
|
||||
// Statistics
|
||||
uint32_t packets_received = 0;
|
||||
Timestamp last_packet_timestamp{};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __PROC_EPIRB_H__
|
||||
#define __PROC_EPIRB_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <complex>
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
#include "channel_decimator.hpp"
|
||||
#include "matched_filter.hpp"
|
||||
#include "packet_builder.hpp"
|
||||
#include "baseband_packet.hpp"
|
||||
#include "message.hpp"
|
||||
#include "buffer.hpp"
|
||||
|
||||
// Specan is disable to keep application size below the 32k limit
|
||||
// #define SPECAN
|
||||
|
||||
#ifdef SPECAN
|
||||
#include "spectrum_collector.hpp"
|
||||
#endif
|
||||
|
||||
#include "audio_output.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
|
||||
// Forward declarations for types only used as pointers/references
|
||||
class Message;
|
||||
namespace baseband {
|
||||
class Packet;
|
||||
}
|
||||
|
||||
// COSPAS / SARSAT 406 frame constants
|
||||
// Size of preamble (bits)
|
||||
#define COSPAS_PREAMBLE_SIZE 24
|
||||
// Size of long frame (bits)
|
||||
#define COSPAS_LONG_FRAME_SIZE 144
|
||||
// Siz of short frame (bits)
|
||||
#define COSPAS_SHORT_FRAME_SIZE 112
|
||||
// Preamble for real frames
|
||||
#define COSPAS_REAL_PREAMBLE 0b1111'1111'1111'1110'0010'1111
|
||||
// Preable for test frames
|
||||
#define COSPAS_TEST_PREAMBLE 0b1111'1111'1111'1110'1101'0000
|
||||
|
||||
// Dedicated EPIRB PacketBuilder
|
||||
// Usees diedicated preamble detection logic to find both real and test frames
|
||||
// Also as a dedicated packet size detection based on frame's size bit
|
||||
class EPIRBPacketBuilder {
|
||||
public:
|
||||
using EPIRBHandler = void (*)(void* context, const baseband::Packet& packet);
|
||||
|
||||
EPIRBPacketBuilder(
|
||||
void* context,
|
||||
EPIRBHandler handler)
|
||||
: context(context),
|
||||
handler(handler) {
|
||||
}
|
||||
|
||||
void execute(
|
||||
const uint_fast8_t symbol) {
|
||||
bit_history.add(symbol);
|
||||
|
||||
switch (state) {
|
||||
case State::Preamble: {
|
||||
// Detect both real and test fram preambles
|
||||
bool is_real = real_sync_matcher(bit_history, packet.size());
|
||||
bool is_test = test_sync_matcher(bit_history, packet.size());
|
||||
if (is_real || is_test) {
|
||||
// Append preamble to the begining of the packet
|
||||
uint64_t preamble = is_real ? COSPAS_REAL_PREAMBLE : COSPAS_TEST_PREAMBLE;
|
||||
for (int8_t i = (COSPAS_PREAMBLE_SIZE - 1); i >= 0; i--) {
|
||||
packet.add((preamble >> i) & 0x1);
|
||||
}
|
||||
state = State::Format;
|
||||
}
|
||||
} break;
|
||||
case State::Format:
|
||||
packet.add(symbol);
|
||||
// 144 bits for long frames and 112 for short frames
|
||||
size = symbol ? COSPAS_LONG_FRAME_SIZE : COSPAS_SHORT_FRAME_SIZE;
|
||||
state = State::Payload;
|
||||
|
||||
break;
|
||||
case State::Payload:
|
||||
packet.add(symbol);
|
||||
|
||||
if (packet.size() >= size) {
|
||||
flush();
|
||||
} else {
|
||||
if (packet.size() >= packet.capacity()) {
|
||||
reset_state();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
reset_state();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void flush() {
|
||||
// Timestamp is not set here to save some app space (not used on ui side)
|
||||
// packet.set_timestamp(Timestamp::now());
|
||||
if (handler) handler(context, packet);
|
||||
reset_state();
|
||||
}
|
||||
|
||||
void reset_state() {
|
||||
packet.clear();
|
||||
bit_history = BitHistory();
|
||||
state = State::Preamble;
|
||||
}
|
||||
|
||||
private:
|
||||
enum State {
|
||||
Preamble,
|
||||
Format,
|
||||
Payload,
|
||||
};
|
||||
|
||||
BitHistory bit_history{};
|
||||
BitPattern real_sync_matcher{COSPAS_REAL_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||
BitPattern test_sync_matcher{COSPAS_TEST_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||
void* context;
|
||||
EPIRBHandler handler;
|
||||
uint8_t size{0};
|
||||
|
||||
State state{State::Preamble};
|
||||
baseband::Packet packet{};
|
||||
};
|
||||
|
||||
class EPIRBProcessor : public BasebandProcessor {
|
||||
public:
|
||||
EPIRBProcessor();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
// Baseband frequency is set to 3,072,000 samples / sec
|
||||
static constexpr uint32_t BASEBAND_SAMPLE_RATE = 3072000;
|
||||
static constexpr uint32_t SAMPLE_RATE = BASEBAND_SAMPLE_RATE / 8 / 8; // We use to decimators with factor 8 each
|
||||
static constexpr uint32_t SYMBOL_RATE = 800; // 400 bps + Manchester (2 1/2 bits per symbol) => 800
|
||||
static constexpr size_t SAMPLES_PER_SYMBOL = SAMPLE_RATE / SYMBOL_RATE; // = 60 samples per symbol
|
||||
static constexpr size_t SAMPLES_PER_BIT = SAMPLES_PER_SYMBOL * 2; // = 120 samples per bit
|
||||
static constexpr size_t SAMPLES_MARGIN = SAMPLES_PER_SYMBOL / 3; // = Allow 20 sample drift
|
||||
static constexpr size_t SAMPLES_ACCUMUMLATOR = SAMPLES_PER_SYMBOL / 5; // Accumulate phase change across 12 samples
|
||||
static constexpr size_t RISE_FILTER_SAMPLES = SAMPLES_PER_SYMBOL / 20; // Filter peaks of less than 3 samples
|
||||
|
||||
static constexpr size_t CARRIER_SAMPLES_THRESHOLD = 0.080f * SAMPLE_RATE; // Carrier before frame lasts 160ms, require at least 80ms
|
||||
static constexpr size_t CARRIER_MAX_SAMPLES = 0.900f * SAMPLE_RATE; // Carrier + frame lasts 160ms + 520ms + 100ms post carrier = 880ms
|
||||
static constexpr size_t FRAME_MAX_SAMPLES = SAMPLES_PER_BIT * (144 * 1.1f); // Frame max length (add 1% error margin)
|
||||
|
||||
AudioOutput audio_output{};
|
||||
|
||||
// Config
|
||||
uint8_t squelch_level{50};
|
||||
// Audio on/off logic is disabled to save app space
|
||||
// bool audio_on{true};
|
||||
#ifdef SPECAN
|
||||
bool spectrum_on{false};
|
||||
#endif
|
||||
|
||||
std::array<float, 32> audio{};
|
||||
const buffer_f32_t audio_buffer{
|
||||
audio.data(),
|
||||
audio.size()};
|
||||
|
||||
// Last received bit (for manchester deconding)
|
||||
bool last_bit = false;
|
||||
// Sample count since last symbol
|
||||
uint16_t sample_count{0};
|
||||
// Sample count since frame start
|
||||
uint16_t frame_sample_count{0};
|
||||
// True if last phase shift was positive, false otherwise
|
||||
bool last_phase_positive = false;
|
||||
// Counter used for peak filtering
|
||||
uint16_t rise_detection_count{0};
|
||||
|
||||
// Frame detection state machine states
|
||||
enum State { IDLE,
|
||||
CARRIER_LOCKED,
|
||||
DATA_SYNC,
|
||||
POST_FRAME };
|
||||
// Current state for frame detection state machine
|
||||
State current_state = IDLE;
|
||||
// Carrier detection counter
|
||||
uint32_t stability_counter = 0;
|
||||
|
||||
// Phase delta accumulator (6 samples)
|
||||
static constexpr size_t PHASE_DELTA_ACC_SIZE = SAMPLES_ACCUMUMLATOR;
|
||||
float phase_delta_buffer[PHASE_DELTA_ACC_SIZE] = {0.0f};
|
||||
size_t pahse_delta_index = 0;
|
||||
float phase_delta_acc = 0.0f;
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
// Decimation chain for 406 MHz EPIRB signal processing
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
// Audio filtering
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
// Audio demodulation
|
||||
dsp::demodulate::FM demod{};
|
||||
#ifdef SPECAN
|
||||
SpectrumCollector channel_spectrum{};
|
||||
#endif
|
||||
// Store last stample for phase delta calculation
|
||||
complex16_t last_sample{};
|
||||
|
||||
// EPIRB packet structure:
|
||||
// - Sync pattern: 111111111111111 (15 bits)
|
||||
// - Frame sync: 000101111(real) / 011010000(test) (9 bits)
|
||||
// - Data: 120 bits (long frame) / // bits (short frame)
|
||||
// - BCH error correction: 10 bits
|
||||
// Total: 144 bits (long frame) / 112 bits (short frame)
|
||||
EPIRBPacketBuilder packet_builder{
|
||||
this,
|
||||
[](void* ctx, const baseband::Packet& p) {
|
||||
static_cast<EPIRBProcessor*>(ctx)->payload_handler(p);
|
||||
}};
|
||||
|
||||
void payload_handler(const baseband::Packet& packet);
|
||||
// Compute phase diff between two samples
|
||||
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
|
||||
// End current frame
|
||||
void frame_end();
|
||||
// Rise detection with peak filtering
|
||||
bool filtered_rise_detect(bool condition);
|
||||
// Configure audio processing
|
||||
void configure_audio();
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
BASEBAND_SAMPLE_RATE, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
#endif /*__PROC_EPIRB_H__*/
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "platform_scu.h"
|
||||
#include "fixed_point.h"
|
||||
#include "clkin.h"
|
||||
#include "usb_type.h"
|
||||
#include <libopencm3/lpc43xx/cgu.h>
|
||||
#include <libopencm3/lpc43xx/ccu.h>
|
||||
#include <libopencm3/lpc43xx/scu.h>
|
||||
@@ -147,34 +148,16 @@ static struct gpio gpio_h1r9_hw_sync_enable = GPIO(5, 5);
|
||||
#endif
|
||||
// clang-format on
|
||||
|
||||
i2c_bus_t i2c0 = {
|
||||
.obj = (void*)I2C0_BASE,
|
||||
.start = i2c_lpc_start,
|
||||
.stop = i2c_lpc_stop,
|
||||
.transfer = i2c_lpc_transfer,
|
||||
};
|
||||
|
||||
i2c_bus_t i2c1 = {
|
||||
.obj = (void*)I2C1_BASE,
|
||||
.start = i2c_lpc_start,
|
||||
.stop = i2c_lpc_stop,
|
||||
.transfer = i2c_lpc_transfer,
|
||||
};
|
||||
|
||||
// const i2c_lpc_config_t i2c_config_si5351c_slow_clock = {
|
||||
// .duty_cycle_count = 15,
|
||||
// };
|
||||
|
||||
const i2c_lpc_config_t i2c_config_si5351c_fast_clock = {
|
||||
.duty_cycle_count = 255,
|
||||
};
|
||||
/* i2c0, i2c1, and i2c_config_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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
// ============================================================================
|
||||
static fpga_mode_t current_mode = FPGA_MODE_OFF;
|
||||
// Cached register values for debug reads (since reads may require mode switch)
|
||||
static uint8_t fpga_reg_cache[6] = {0, 0x01, 0x00, 0x00, 0x00, 0x00};
|
||||
static uint8_t fpga_reg_cache[7] = {0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
// Context structure for SPIFI-based reading
|
||||
struct spifi_fpga_read_ctx {
|
||||
@@ -315,7 +315,7 @@ static void fpga_spi_write(uint8_t reg, uint8_t value) {
|
||||
// Public function to read FPGA register (callable from C++ application code)
|
||||
// Switches SPI mode, reads register, switches back
|
||||
uint8_t fpga_debug_register_read(uint8_t reg) {
|
||||
if (reg == 0 || reg > 5) return 0xFF;
|
||||
if (reg == 0 || reg > 6) return 0xFF;
|
||||
|
||||
uint8_t value;
|
||||
ssp1_set_mode_ice40();
|
||||
@@ -328,7 +328,7 @@ uint8_t fpga_debug_register_read(uint8_t reg) {
|
||||
|
||||
// Public function to write FPGA register (callable from C++ application code)
|
||||
void fpga_debug_register_write(uint8_t reg, uint8_t value) {
|
||||
if (reg == 0 || reg > 5) return;
|
||||
if (reg == 0 || reg > 6) return;
|
||||
|
||||
ssp1_set_mode_ice40();
|
||||
fpga_spi_write(reg, value);
|
||||
@@ -342,7 +342,7 @@ void fpga_debug_register_write(uint8_t reg, uint8_t value) {
|
||||
// ============================================================================
|
||||
|
||||
uint8_t fpga_register_read(uint8_t reg) {
|
||||
if (reg == 0 || reg > 5) return 0xFF;
|
||||
if (reg == 0 || reg > 6) return 0xFF;
|
||||
|
||||
ssp1_set_mode_ice40();
|
||||
uint8_t val = fpga_spi_read(reg);
|
||||
@@ -353,7 +353,7 @@ uint8_t fpga_register_read(uint8_t reg) {
|
||||
}
|
||||
|
||||
void fpga_register_write(uint8_t reg, uint8_t value) {
|
||||
if (reg == 0 || reg > 5) return;
|
||||
if (reg == 0 || reg > 6) return;
|
||||
|
||||
ssp1_set_mode_ice40();
|
||||
fpga_spi_write(reg, value);
|
||||
@@ -422,22 +422,22 @@ void fpga_rx_set_dc_adapt_rate(uint8_t rate) {
|
||||
/* TX Functions with mode assertion */
|
||||
void fpga_tx_set_nco_enable(bool enable) {
|
||||
if (current_mode != FPGA_MODE_TX) return;
|
||||
uint8_t val = fpga_register_read(FPGA_REG_SHARED_3);
|
||||
uint8_t val = fpga_register_read(FPGA_REG3_TX_NCO_CTRL);
|
||||
if (enable)
|
||||
val |= FPGA_TX_NCO_EN;
|
||||
else
|
||||
val &= ~FPGA_TX_NCO_EN;
|
||||
fpga_register_write(FPGA_REG_SHARED_3, val);
|
||||
fpga_register_write(FPGA_REG3_TX_NCO_CTRL, val);
|
||||
}
|
||||
|
||||
void fpga_tx_set_interpolation(uint8_t ratio) {
|
||||
if (current_mode != FPGA_MODE_TX) return;
|
||||
fpga_register_write(FPGA_REG_SHARED_4, ratio & FPGA_TX_INTERP_MASK);
|
||||
fpga_register_write(FPGA_REG_TX_INTERP, ratio & FPGA_TX_INTERP_MASK);
|
||||
}
|
||||
|
||||
void fpga_tx_set_phase_step(uint8_t step) {
|
||||
if (current_mode != FPGA_MODE_TX) return;
|
||||
fpga_register_write(FPGA_REG_SHARED_5, step);
|
||||
fpga_register_write(FPGA_REG_TX_PHASE_STEP, step);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -16,17 +16,21 @@ extern "C" {
|
||||
|
||||
#ifdef PRALINE
|
||||
|
||||
/* FPGA Register Map Address 0x03 (Dual Purpose) */
|
||||
/* RX path (legacy PRALINE software map kept for compatibility) */
|
||||
#define FPGA_REG_RX_DIGITAL_GAIN 0x03 /* Digital Shift / scaling (RX Mode) */
|
||||
#define FPGA_REG_TX_CONTROL 0x03 /* NCO_EN and TX flags (TX Mode) */
|
||||
|
||||
/* FPGA Register Map Address 0x04 (Shared) */
|
||||
#define FPGA_REG_RX_DC_BLOCK_WIDTH 0x04 /* Notch filter cutoff (RX Mode) */
|
||||
#define FPGA_REG_TX_INTERP 0x04 /* Interpolation ratio (TX Mode) */
|
||||
|
||||
/* FPGA Register Map Address 0x05 (Shared) */
|
||||
#define FPGA_REG_RX_DC_ADAPT_RATE 0x05 /* Settle time/Integration (RX Mode) */
|
||||
#define FPGA_REG_TX_PHASE_STEP 0x05 /* NCO frequency step (TX Mode) */
|
||||
|
||||
/*
|
||||
* TX path must match the currently built PRALINE standard gateware in
|
||||
* hackrf/firmware/fpga/top/standard.py:
|
||||
* 0x04 tx_ctrl
|
||||
* 0x05 tx_intrp
|
||||
* 0x06 tx_pstep
|
||||
*/
|
||||
#define FPGA_REG_TX_CONTROL 0x04
|
||||
#define FPGA_REG_TX_INTERP 0x05
|
||||
#define FPGA_REG_TX_PHASE_STEP 0x06
|
||||
|
||||
/*
|
||||
* FPGA Operating Mode
|
||||
@@ -39,13 +43,21 @@ typedef enum {
|
||||
|
||||
/*
|
||||
* FPGA Register Addresses
|
||||
* Note: Registers 3-5 are dual-purpose (meaning depends on RX/TX mode)
|
||||
* NOTE:
|
||||
* The currently loaded PRALINE standard gateware uses:
|
||||
* 0x01 CTRL
|
||||
* 0x02 RX_DECIM
|
||||
* 0x03 RX_DIGITAL_GAIN (RX) / TX_NCO_CTRL (TX)
|
||||
* 0x04 TX_CTRL
|
||||
* 0x05 TX_INTRP
|
||||
* 0x06 TX_PSTEP
|
||||
*/
|
||||
#define FPGA_REG_CTRL 0x01 /* Control register */
|
||||
#define FPGA_REG_DECIM 0x02 /* Decimation (RX) / unused (TX) */
|
||||
#define FPGA_REG_SHARED_3 0x03 /* Dual-purpose register */
|
||||
#define FPGA_REG_SHARED_4 0x04 /* Dual-purpose register */
|
||||
#define FPGA_REG_SHARED_5 0x05 /* Dual-purpose register */
|
||||
#define FPGA_REG_DECIM 0x02 /* RX decimation */
|
||||
#define FPGA_REG_SHARED_3 0x03 /* Legacy shared register */
|
||||
#define FPGA_REG_SHARED_4 0x04 /* Legacy shared register */
|
||||
#define FPGA_REG_SHARED_5 0x05 /* Legacy shared register */
|
||||
#define FPGA_REG_SHARED_6 0x06 /* TX phase step */
|
||||
|
||||
/*
|
||||
* Register 1 (CTRL) Bit Definitions
|
||||
@@ -77,7 +89,7 @@ typedef enum {
|
||||
#define FPGA_RX_DC_WIDTH_MASK 0x07 /* Bits [2:0] */
|
||||
|
||||
/* TX Mode: Interpolation ratio */
|
||||
#define FPGA_REG4_TX_INTERP 0x04
|
||||
#define FPGA_REG4_TX_INTERP 0x05
|
||||
#define FPGA_TX_INTERP_MASK 0x07 /* Bits [2:0] */
|
||||
|
||||
/*
|
||||
@@ -88,7 +100,7 @@ typedef enum {
|
||||
#define FPGA_RX_DC_RATE_MASK 0xFF /* Bits [7:0] */
|
||||
|
||||
/* TX Mode: NCO phase step (frequency) */
|
||||
#define FPGA_REG5_TX_PHASE_STEP 0x05
|
||||
#define FPGA_REG5_TX_PHASE_STEP 0x06
|
||||
#define FPGA_TX_PHASE_STEP_MASK 0xFF /* Bits [7:0] */
|
||||
|
||||
/* Export default values so other methods can use them */
|
||||
|
||||
@@ -54,6 +54,12 @@ constexpr ClockFrequency max283x_reference_f = 40000000U;
|
||||
constexpr ClockFrequency mcu_clkin_og_f = 40000000U;
|
||||
constexpr ClockFrequency mcu_clkin_r9_f = 10000000U;
|
||||
|
||||
#ifdef PRALINE
|
||||
constexpr int8_t default_tx_gain_db = 16;
|
||||
#else
|
||||
constexpr int8_t default_tx_gain_db = 35;
|
||||
#endif
|
||||
|
||||
constexpr uint8_t si5351_i2c_address = 0x60;
|
||||
|
||||
/* Clock Generator */
|
||||
|
||||
@@ -551,6 +551,15 @@ bool I2cDev_MAX17055::setModelCfg(const uint8_t _Model_ID) {
|
||||
}
|
||||
|
||||
bool I2cDev_MAX17055::setHibCFG(const uint16_t _Config) {
|
||||
uint16_t config1_reg = read_register(0x1D);
|
||||
|
||||
// (SHDN: 0x80) and (COMMSH: 0x40) Otherwise it will go into sleep mode after 45 seconds.
|
||||
config1_reg &= ~(0x0080 | 0x0040);
|
||||
|
||||
if (!write_register(0x1D, config1_reg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return write_register(0xBA, _Config);
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ class Message {
|
||||
ToneDetectData = 103,
|
||||
ToneDetectConfig = 104,
|
||||
FlexTosend = 105,
|
||||
EPIRBRXConfig = 106,
|
||||
MAX
|
||||
};
|
||||
|
||||
@@ -390,6 +391,16 @@ class EPIRBPacketMessage : public Message {
|
||||
baseband::Packet packet;
|
||||
};
|
||||
|
||||
class EPIRBRXConfig : public Message {
|
||||
public:
|
||||
constexpr EPIRBRXConfig()
|
||||
: Message{ID::EPIRBRXConfig} {
|
||||
}
|
||||
bool spectrum_on = false;
|
||||
bool audio_on = true;
|
||||
uint8_t squelch{50};
|
||||
};
|
||||
|
||||
class TPMSPacketMessage : public Message {
|
||||
public:
|
||||
constexpr TPMSPacketMessage(
|
||||
@@ -1203,7 +1214,7 @@ class SSTVRXConfigureMessage : public Message {
|
||||
public:
|
||||
constexpr SSTVRXConfigureMessage(
|
||||
const uint8_t code)
|
||||
: Message{id : ID::SSTVRXConfigure},
|
||||
: Message{ID::SSTVRXConfigure},
|
||||
code(code) {
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ struct SharedMemory {
|
||||
uint16_t volatile m4_stack_usage{0};
|
||||
uint32_t volatile m4_heap_usage{0};
|
||||
uint16_t volatile m4_buffer_missed{0};
|
||||
|
||||
uint8_t volatile radio_tx_drain{0}; // to indicate the baseband thread to drain the tx buffer, and wait for it, before radio::disable()
|
||||
#ifdef PRALINE
|
||||
// Phase 0 instrumentation counters for PRALINE radio debugging
|
||||
uint32_t volatile m4_dma_xfr_count{0}; // DMA transfer_complete() calls
|
||||
|
||||
+1
-1
Submodule hackrf updated: 442d95e23d...216949a844
@@ -13,7 +13,7 @@ EPIRB; Standard Location Protocol EPIRB-MMSI; FFFED0A0D205F260850F3DC9E0B70B6E4F
|
||||
PLB; Standard Location Protocol - PLB (Serial); FFFED0A3E7B10016150D364D8B3689C09437
|
||||
SAAS; Std. Location ship security protocol (SSAS); FFFED0A5DC19E1A07FDFFE5C803483E0FCCA
|
||||
ELT 24; Serial user Location (ELT with Aircraft 24-bit Address); FFFED0CE46E76EF8C00C2BAA31CFE0FF0F61
|
||||
MMSO; Maritime User Protocol MMSI - EPIRB; FFFED0D9D4EB28140AA6893F16A2C67282EC
|
||||
MMSI; Maritime User Protocol MMSI - EPIRB; FFFED0D9D4EB28140AA6893F16A2C67282EC
|
||||
Float Free EPIRB; Float Free EPIRB with Serial Identification Number; FFFED0DF76A9A9C800174DE27BE1F0277E45
|
||||
Non Float Free EPIRB; Non Float Free EPIRB with Serial Identification; FFFED0DF77200000001168C610AFE0FF0146
|
||||
Radio Call Sign; Radio Call Sign - EPIRB; FFFED0E0DDAE599508268E4C05E054651307
|
||||
@@ -22,4 +22,13 @@ ELT Aircraft; ELT with Aircraft Operator Designator & Serial Number; FFFED0EDF67
|
||||
PLB Serial; Standard Location Protocol - PLB (Serial); FFFED0A157B081437FDFF8B4833783E0F66C
|
||||
RLS Location; RLS Location Protocol; FFFED096ED09900149D4D467EE0851A3B2E8
|
||||
Orbitography; Orbitography Protocol (from Toulouse FMCC's LUT); FFFE2FCE3000000000000DBD0E4022417500
|
||||
ELT 24 2; ELT 24 bits; FFFED08DB345B146202DDF3C71F59BAB7072
|
||||
ELT 24 2; ELT 24 bits; FFFED08DB345B146202DDF3C71F59BAB7072
|
||||
Selftest BCH1 ERR; Serial user Location Protocol; FFFED0D7E6202820000C29FF51041775302D
|
||||
Selftest BCH2 ERR; Serial user Location Protocol; FFFED0D6E6202820000C29FF51041765302D
|
||||
Selftest BCH12 ERR; Serial user Location Protocol; FFFED0D7E6202820000C29FF51041765302D
|
||||
Emergency Cap;EPIRB Emergency Capsizing;FFFED056E68040022021A4CDC675
|
||||
Emergency No Mar.;Emergency non maritime;FFFED056EE8040022021A5623CFE
|
||||
Emergency PLB;Emergency PLB no fire+med+dis;FFFED056E7804002202123F39E36
|
||||
Emergency PLB fire;Emergency PLB fire;FFFED056E7804002202123F39E39
|
||||
EPIRB Sinking;Emergency EPIRB Sinking;FFFED056E58040022021342FAD76
|
||||
EPIRB Call Sign Col.;EPIRB Radio Call Sign Emergency collision;FFFED056ED80400220217185E933
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
406025000
|
||||
406028000
|
||||
406037000
|
||||
406040000
|
||||
406049000
|
||||
406052000
|
||||
406061000
|
||||
406064000
|
||||
433025000
|
||||
144875000
|
||||
406022000
|
||||
@@ -0,0 +1,59 @@
|
||||
EPIRB - Maritime
|
||||
EPIRB - Radio Call Sign
|
||||
ELT - Aviation
|
||||
Serial User Protocol
|
||||
Test User Protocol
|
||||
Orbitography Protocol
|
||||
National User Protocol
|
||||
2nd Generation Beacons
|
||||
EPIRB - MMSI / Location
|
||||
ELT-24-bit Address / Location
|
||||
ELT Serial Location
|
||||
ELT Serial Aircraft Location
|
||||
EPIRB Serial Location
|
||||
PLB Serial Location
|
||||
Ship Security Location
|
||||
Test Standard Location
|
||||
ELT National Location
|
||||
EPIRB National Location
|
||||
PLB National Location
|
||||
Test National Location
|
||||
RLS Location Protocol
|
||||
ELT(DT) Location Protocol
|
||||
Spare
|
||||
Unknown
|
||||
# L. 25: additional data
|
||||
ELTs/SN
|
||||
ELTs/AC op & SN
|
||||
FF EPIRBs/SN
|
||||
ELTs+AC 24-bit ad.
|
||||
NFF EPIRBs/SN
|
||||
Not Used
|
||||
PLBs/SN
|
||||
# L. 33: long additinal data
|
||||
MMSI=0x%08lX MID=%ld
|
||||
24 bits AC ad.
|
||||
C/S TA #=%ld
|
||||
Op Design.=%c%c%c
|
||||
Nati. data=%ld
|
||||
# L. 39: emmergency
|
||||
Other
|
||||
Fire
|
||||
Flooding
|
||||
Collision
|
||||
Grounding
|
||||
Capsizing
|
||||
Sinking
|
||||
Disabled
|
||||
Abandoning
|
||||
# L. 49: other emmergency
|
||||
Fire
|
||||
Med.
|
||||
Dis.
|
||||
# L. 53:
|
||||
Undefined
|
||||
No device
|
||||
Other/no device
|
||||
Other device
|
||||
121.5 MHz
|
||||
SART
|
||||
@@ -0,0 +1,292 @@
|
||||
501:ADE:AdelieLand
|
||||
201:ALB:Albania
|
||||
202:AND:Andorra
|
||||
203:AUT:Austria
|
||||
204:AZC:Azores
|
||||
205:BEL:Belgium
|
||||
206:BLR:Belarus
|
||||
207:BUL:Bulgaria
|
||||
208:VAT:Vatican
|
||||
209:CYP:Cyprus
|
||||
210:CYP:Cyprus
|
||||
211:GER:Germany
|
||||
212:CYP:Cyprus
|
||||
213:GOG:Georgia
|
||||
214:MOL:Moldova
|
||||
215:MAL:MALTA
|
||||
216:ARM:Armenia
|
||||
218:GER:Germany
|
||||
219:DEN:Denmark
|
||||
220:DEN:Denmark
|
||||
224:SPA:Spain
|
||||
225:SPA:Spain
|
||||
226:FRA:France
|
||||
227:FRA:France
|
||||
228:FRA:France
|
||||
229:MAL:Malta
|
||||
230:FIN:Finland
|
||||
231:FAR:Faro Isle
|
||||
232:UKM:G Britain
|
||||
233:UKM:G Britain
|
||||
234:UKM:G Britain
|
||||
235:UKM:G Britain
|
||||
236:GIB:Gibraltar
|
||||
237:GRE:Greece
|
||||
238:CRT:Croatia
|
||||
239:GRE:Greece
|
||||
240:GRE:Greece
|
||||
241:GRE:Greece
|
||||
242:MOR:Morocco
|
||||
243:HUN:Hungary
|
||||
244:NET:Netherland
|
||||
245:NET:Netherland
|
||||
246:NET:Netherland
|
||||
247:ITA:Italy
|
||||
248:MAL:Malta
|
||||
249:MAL:Malta
|
||||
250:IRE:Ireland
|
||||
251:ICE:Iceland
|
||||
252:LIE:Liechten
|
||||
253:LUX:Luxembourg
|
||||
254:MON:Monaco
|
||||
255:MAE:Madeira
|
||||
256:MAL:Malta
|
||||
257:NOR:Norway
|
||||
258:NOR:Norway
|
||||
259:NOR:Norway
|
||||
261:POL:Poland
|
||||
262:MNT:Montenegro
|
||||
263:POR:Portugal
|
||||
264:ROM:Romania
|
||||
265:SWE:Sweden
|
||||
266:SWE:Sweden
|
||||
267:SLV:Slovakia
|
||||
268:SAN:San Marino
|
||||
269:SWT:Swiss
|
||||
270:CZH:Czech Rep
|
||||
271:TUR:Turkiye
|
||||
272:UKR:Ukraine
|
||||
273:RUS:Russia
|
||||
274:MKD:North Mac
|
||||
275:LAT:Latvia
|
||||
276:EST:Estonia
|
||||
277:LIT:Lithuania
|
||||
278:SVN:Slovenia
|
||||
279:SER:Serbia
|
||||
301:ANA:Anguilla
|
||||
303:ALA:Alaska
|
||||
304:ANT:Antigua
|
||||
305:ANT:Antigua
|
||||
306:NEA:N Antilles
|
||||
307:ARU:Aruba
|
||||
308:BAA:Bahamas
|
||||
309:BAA:Bahamas
|
||||
310:BER:Bermuda
|
||||
311:BAA:Bahamas
|
||||
312:BEZ:Belize
|
||||
314:BAR:Barbados
|
||||
316:CAN:Canada
|
||||
319:CAY:Cayman Is
|
||||
321:COS:Costa Rica
|
||||
323:CUB:Cuba
|
||||
325:DOM:Dominica
|
||||
327:DOR:Dominican
|
||||
329:GUA:Guadeloupe
|
||||
330:GRA:Grenada
|
||||
331:GRN:Greenland
|
||||
332:GUT:Guatemala
|
||||
334:HON:Honduras
|
||||
336:HAI:Haiti
|
||||
338:USA:USA
|
||||
339:JAM:Jamaica
|
||||
341:SKN:St Kitts
|
||||
343:SLU:St Lucia
|
||||
345:MEX:Mexico
|
||||
347:MTQ:Martinique
|
||||
348:MOT:Montserrat
|
||||
350:NIC:Nicaragua
|
||||
351:PAN:Panama
|
||||
352:PAN:Panama
|
||||
353:PAN:Panama
|
||||
354:PAN:Panama
|
||||
355:PAN:Panama
|
||||
356:PAN:Panama
|
||||
357:PAN:Panama
|
||||
358:PUE:PuertoRico
|
||||
359:ELS:ElSalvador
|
||||
361:SPI:St Pierre
|
||||
362:TAT:Trinidad
|
||||
364:TUK:Caicos Is
|
||||
366:USA:USA
|
||||
367:USA:USA
|
||||
368:USA:USA
|
||||
369:USA:USA
|
||||
370:PAN:Panama
|
||||
371:PAN:Panama
|
||||
372:PAN:Panama
|
||||
373:PAN:Panama
|
||||
374:PAN:Panama
|
||||
375:SVG:St Vincent
|
||||
376:SVG:St Vincent
|
||||
377:SVG:St Vincent
|
||||
378:BVI:Virgin GB
|
||||
379:USV:Virgin US
|
||||
401:AFG:Afghan
|
||||
403:SAU:Saudi
|
||||
405:BAN:Bangladesh
|
||||
408:BAH:Bahrain
|
||||
410:BHU:Bhutan
|
||||
412:CHN:China
|
||||
413:CHN:China
|
||||
414:CHN:CHINA
|
||||
416:TAI:Taipei
|
||||
417:SRI:Sri Lanka
|
||||
419:IND:India
|
||||
422:IRN:Iran
|
||||
423:AZR:Azerbaijan
|
||||
425:IRQ:Iraq
|
||||
428:ISR:Israel
|
||||
431:JPN:Japan
|
||||
432:JPN:Japan
|
||||
434:TKM:Turkmenist
|
||||
436:KAZ:Kazakhstan
|
||||
437:UZB:Uzbekistan
|
||||
438:JOR:Jordan
|
||||
440:KOR:Korea Sou
|
||||
441:KOR:Korea Sou
|
||||
443:PAA:Palestine
|
||||
445:KDR:Korea Nor
|
||||
447:KUW:Kuwait
|
||||
450:LEB:Lebanon
|
||||
451:KYR:Kyrgyzia
|
||||
453:MAC:Macao
|
||||
455:MAV:Maldives
|
||||
457:MNG:Mongolia
|
||||
459:NEP:Nepal
|
||||
461:OMN:Oman
|
||||
463:PAK:Pakistan
|
||||
466:QAT:Qatar
|
||||
468:SYR:Syria
|
||||
470:UAE:UAE
|
||||
471:UAE:UAE
|
||||
472:TJK:TAJIKISTAN
|
||||
473:YEM:Yemen
|
||||
475:YEM:Yemen
|
||||
477:HKG:Hong Kong
|
||||
478:BOS:Bosniaherz
|
||||
503:AUS:Australia
|
||||
506:BUR:Burma
|
||||
508:BRU:Brunei
|
||||
510:MIC:Micronesia
|
||||
511:PAL:Palau
|
||||
512:NZL:NewZealand
|
||||
514:CMB:Cambodia
|
||||
515:CMB:Cambodia
|
||||
516:CHR:Christmas
|
||||
518:COO:Cook Isles
|
||||
520:FIJ:Fiji
|
||||
523:COC:Cocos Isle
|
||||
525:INO:Indonesia
|
||||
529:KIR:Kiribati
|
||||
531:LAO:Lao
|
||||
533:MLY:Malaysia
|
||||
536:MAI:Mariana Is
|
||||
538:MAR:Marshall I
|
||||
540:NCA:Caledonia
|
||||
542:NIU:Niue Isle
|
||||
544:NAU:Nauru
|
||||
546:PLY:Polynesia
|
||||
548:PHI:Philippine
|
||||
550:TIM:TimorLeste
|
||||
553:PAP:Papua NG
|
||||
555:PIT:Pitcairn I
|
||||
557:SOL:Solomon Is
|
||||
559:ASA:Samoa USA
|
||||
561:WSA:West Samoa
|
||||
563:SIN:Singapore
|
||||
564:SIN:Singapore
|
||||
565:SIN:Singapore
|
||||
566:SIN:SINGAPORE
|
||||
567:THA:Thailand
|
||||
570:TON:Tonga
|
||||
572:TUV:Tuvalu Is
|
||||
574:VIE:Vietnam
|
||||
576:VAN:Vanuatu
|
||||
577:VAN:Vanuatu
|
||||
578:WAL:Wallis Is
|
||||
601:SAF:So Africa
|
||||
603:ANG:Angola
|
||||
605:ALG:Algeria
|
||||
607:SPL:St Paul
|
||||
608:ASC:Ascension
|
||||
609:BUI:Burundi
|
||||
610:BEN:Benin
|
||||
611:BOT:Botswana
|
||||
612:CAR:CenAfr Rep
|
||||
613:CAM:Cameroon
|
||||
615:CON:Congo
|
||||
616:COM:Comoros
|
||||
617:CAP:Cape Verde
|
||||
618:CRP:Crozet
|
||||
619:IVO:IvoryCoast
|
||||
620:COM:Comoros
|
||||
621:DJI:Djibouti
|
||||
622:EGY:Egypt
|
||||
624:ETH:Ethiopia
|
||||
625:ERT:Eritrea
|
||||
626:GAB:Gabon Rep
|
||||
627:GHA:Ghana
|
||||
629:GAM:Gambia
|
||||
630:GUB:Guinea Bis
|
||||
631:EQG:Eq Guinea
|
||||
632:GUN:Guinea Rep
|
||||
633:BUF:Burkina FS
|
||||
634:KEN:Kenya
|
||||
635:KER:Kerguelen
|
||||
636:LIB:Liberia
|
||||
637:LIB:Liberia
|
||||
638:SSD:Southsudan
|
||||
642:LBY:Libya
|
||||
644:LES:Lesotho
|
||||
645:MAU:Mauritius
|
||||
647:MAD:Madagascar
|
||||
649:MLI:Mali
|
||||
650:MOZ:Mozambique
|
||||
654:MAA:Mauritania
|
||||
655:MAW:Malawi
|
||||
656:NIG:Niger
|
||||
657:NIA:Nigeria
|
||||
659:NAM:Namibia
|
||||
660:REU:Reunion
|
||||
661:RWA:Rwanda
|
||||
662:SUD:Sudan
|
||||
663:SEN:Senegal
|
||||
664:SEY:Seychelle
|
||||
665:SHE:St Helena
|
||||
666:SOM:Somali
|
||||
667:SIL:Sierra Leo
|
||||
668:SAO:Sao Tome
|
||||
669:SWZ:Eswatini
|
||||
670:CHA:Chad
|
||||
671:TOG:Togo
|
||||
672:TUN:Tunisia
|
||||
674:TAN:Tanzania
|
||||
675:UGA:Uganda
|
||||
676:ZAI:Zaire
|
||||
677:TAN:Tanzania
|
||||
678:ZAM:Zambia
|
||||
679:ZIM:Zimbabwe
|
||||
701:ARG:Argentina
|
||||
710:BRA:Brazil
|
||||
720:BOL:Bolivia
|
||||
725:CHI:Chile
|
||||
730:COL:Colombia
|
||||
735:ECU:Ecuador
|
||||
740:FAL:Falkland I
|
||||
745:GUI:Guiana
|
||||
750:GUY:Guyana
|
||||
755:PAR:Paraguay
|
||||
760:PER:Peru
|
||||
765:SUR:Suriname
|
||||
770:URU:Uruguay
|
||||
775:VEN:Venezuela
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'apricity' colormap from cmyt
|
||||
0,89,105,175
|
||||
8,90,111,171
|
||||
16,91,116,169
|
||||
25,93,121,167
|
||||
33,94,127,166
|
||||
41,96,132,165
|
||||
49,97,137,164
|
||||
58,99,141,164
|
||||
66,101,147,164
|
||||
74,103,151,163
|
||||
82,106,156,163
|
||||
90,108,161,163
|
||||
99,111,166,162
|
||||
107,114,171,161
|
||||
115,118,175,160
|
||||
123,121,180,159
|
||||
132,125,185,157
|
||||
140,128,190,155
|
||||
148,132,195,152
|
||||
156,137,199,149
|
||||
165,142,205,144
|
||||
173,147,209,139
|
||||
181,153,214,134
|
||||
189,160,218,129
|
||||
197,170,223,122
|
||||
206,179,227,115
|
||||
214,188,230,108
|
||||
222,199,234,100
|
||||
230,211,237,91
|
||||
239,223,240,81
|
||||
247,234,243,70
|
||||
255,246,246,55
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'arbre' colormap from cmyt
|
||||
0,113,14,13
|
||||
8,117,22,34
|
||||
16,121,29,54
|
||||
25,125,35,73
|
||||
33,129,41,95
|
||||
41,133,45,115
|
||||
49,136,49,136
|
||||
58,139,52,158
|
||||
66,141,56,184
|
||||
74,141,60,208
|
||||
82,138,69,228
|
||||
90,130,82,241
|
||||
99,118,100,245
|
||||
107,105,116,241
|
||||
115,92,129,234
|
||||
123,82,141,225
|
||||
132,72,153,217
|
||||
140,65,162,209
|
||||
148,60,171,203
|
||||
156,54,179,196
|
||||
165,48,189,189
|
||||
173,43,197,182
|
||||
181,40,206,173
|
||||
189,45,214,162
|
||||
197,62,222,147
|
||||
206,84,228,131
|
||||
214,110,234,113
|
||||
222,139,238,93
|
||||
230,173,240,69
|
||||
239,203,241,50
|
||||
247,231,241,44
|
||||
255,254,242,69
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'cividis' colormap from matplotlib
|
||||
0,0,34,78
|
||||
8,0,40,91
|
||||
16,0,46,106
|
||||
25,5,51,113
|
||||
33,28,57,111
|
||||
41,41,63,110
|
||||
49,51,68,109
|
||||
58,60,74,108
|
||||
66,69,80,108
|
||||
74,77,85,108
|
||||
82,85,91,109
|
||||
90,92,97,110
|
||||
99,100,103,112
|
||||
107,107,109,114
|
||||
115,114,114,116
|
||||
123,120,120,119
|
||||
132,128,127,120
|
||||
140,136,133,120
|
||||
148,144,139,120
|
||||
156,151,145,119
|
||||
165,160,152,117
|
||||
173,168,158,115
|
||||
181,176,165,113
|
||||
189,185,171,109
|
||||
197,194,179,105
|
||||
206,203,185,101
|
||||
214,211,192,95
|
||||
222,220,200,89
|
||||
230,230,208,81
|
||||
239,239,215,72
|
||||
247,248,223,60
|
||||
255,254,232,56
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'dusk' colormap from cmyt
|
||||
0,6,3,5
|
||||
8,11,10,15
|
||||
16,15,18,27
|
||||
25,18,27,38
|
||||
33,16,36,52
|
||||
41,7,45,62
|
||||
49,17,53,64
|
||||
58,35,59,64
|
||||
66,50,65,66
|
||||
74,63,70,69
|
||||
82,74,75,72
|
||||
90,86,81,73
|
||||
99,101,85,73
|
||||
107,115,89,73
|
||||
115,130,92,72
|
||||
123,145,95,70
|
||||
132,163,97,68
|
||||
140,179,99,64
|
||||
148,195,100,60
|
||||
156,213,100,54
|
||||
165,231,101,42
|
||||
173,239,111,31
|
||||
181,240,126,49
|
||||
189,241,140,73
|
||||
197,241,155,98
|
||||
206,242,168,119
|
||||
214,242,180,140
|
||||
222,243,192,161
|
||||
230,244,206,184
|
||||
239,245,218,204
|
||||
247,246,230,223
|
||||
255,249,241,241
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'inferno' colormap from matplotlib
|
||||
0,0,0,4
|
||||
8,4,3,18
|
||||
16,11,7,36
|
||||
25,21,11,55
|
||||
33,35,12,76
|
||||
41,49,10,92
|
||||
49,62,9,102
|
||||
58,76,12,107
|
||||
66,90,17,110
|
||||
74,103,22,110
|
||||
82,116,26,110
|
||||
90,128,31,108
|
||||
99,143,36,105
|
||||
107,155,41,100
|
||||
115,168,46,95
|
||||
123,180,51,89
|
||||
132,193,58,80
|
||||
140,204,66,72
|
||||
148,215,75,63
|
||||
156,224,85,54
|
||||
165,233,97,43
|
||||
173,239,110,33
|
||||
181,245,123,23
|
||||
189,248,137,12
|
||||
197,251,153,6
|
||||
206,252,168,13
|
||||
214,251,184,29
|
||||
222,249,199,47
|
||||
230,245,217,73
|
||||
239,242,232,101
|
||||
247,243,245,134
|
||||
255,252,255,164
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'kelp' colormap from cmyt
|
||||
0,20,6,117
|
||||
8,32,14,122
|
||||
16,43,20,126
|
||||
25,54,26,130
|
||||
33,66,32,133
|
||||
41,76,38,135
|
||||
49,85,44,135
|
||||
58,94,50,134
|
||||
66,103,57,133
|
||||
74,111,64,132
|
||||
82,117,71,130
|
||||
90,124,78,129
|
||||
99,130,87,128
|
||||
107,135,95,128
|
||||
115,139,103,128
|
||||
123,143,111,129
|
||||
132,146,120,132
|
||||
140,150,128,135
|
||||
148,153,137,139
|
||||
156,156,145,142
|
||||
165,158,154,147
|
||||
173,161,163,152
|
||||
181,163,172,156
|
||||
189,165,181,159
|
||||
197,168,191,161
|
||||
206,174,199,159
|
||||
214,182,207,157
|
||||
222,193,214,153
|
||||
230,207,222,149
|
||||
239,220,229,144
|
||||
247,235,235,139
|
||||
255,250,242,133
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'magma' colormap from matplotlib
|
||||
0,0,0,4
|
||||
8,3,3,18
|
||||
16,10,8,34
|
||||
25,19,13,52
|
||||
33,30,17,73
|
||||
41,42,17,92
|
||||
49,56,16,108
|
||||
58,69,16,119
|
||||
66,84,19,125
|
||||
74,96,24,128
|
||||
82,109,29,129
|
||||
90,121,34,130
|
||||
99,136,39,129
|
||||
107,148,44,128
|
||||
115,161,48,126
|
||||
123,174,52,123
|
||||
132,189,57,119
|
||||
140,202,62,114
|
||||
148,214,69,108
|
||||
156,226,77,102
|
||||
165,236,88,96
|
||||
173,243,101,92
|
||||
181,248,116,92
|
||||
189,251,131,95
|
||||
197,253,148,103
|
||||
206,254,163,111
|
||||
214,254,178,122
|
||||
222,254,193,133
|
||||
230,254,209,148
|
||||
239,253,224,161
|
||||
247,252,238,176
|
||||
255,252,253,191
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'mako' colormap from seaborn
|
||||
0,11,4,5
|
||||
8,20,9,16
|
||||
16,28,16,28
|
||||
25,36,22,40
|
||||
33,44,28,54
|
||||
41,50,34,67
|
||||
49,55,40,81
|
||||
58,59,47,95
|
||||
66,63,54,111
|
||||
74,65,62,125
|
||||
82,64,70,138
|
||||
90,62,79,148
|
||||
99,58,89,154
|
||||
107,56,99,157
|
||||
115,54,108,160
|
||||
123,53,117,161
|
||||
132,52,127,164
|
||||
140,52,136,166
|
||||
148,52,145,168
|
||||
156,52,154,170
|
||||
165,54,164,171
|
||||
173,58,173,172
|
||||
181,63,182,173
|
||||
189,71,191,173
|
||||
197,83,201,173
|
||||
206,101,208,173
|
||||
214,124,214,175
|
||||
222,148,220,181
|
||||
230,171,226,190
|
||||
239,190,232,202
|
||||
247,206,238,215
|
||||
255,222,245,229
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'octarine' colormap from cmyt
|
||||
0,5,37,81
|
||||
8,8,45,75
|
||||
16,15,51,70
|
||||
25,20,57,67
|
||||
33,25,63,65
|
||||
41,29,69,63
|
||||
49,32,75,60
|
||||
58,38,80,55
|
||||
66,48,85,47
|
||||
74,61,89,40
|
||||
82,76,91,33
|
||||
90,92,93,27
|
||||
99,108,94,23
|
||||
107,123,94,21
|
||||
115,137,94,21
|
||||
123,152,93,21
|
||||
132,168,91,24
|
||||
140,182,89,27
|
||||
148,196,85,31
|
||||
156,211,81,37
|
||||
165,227,75,47
|
||||
173,238,71,63
|
||||
181,244,77,85
|
||||
189,244,89,108
|
||||
197,243,103,131
|
||||
206,241,115,151
|
||||
214,239,127,169
|
||||
222,236,138,186
|
||||
230,233,149,204
|
||||
239,232,159,220
|
||||
247,231,167,236
|
||||
255,238,172,250
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'plasma' colormap from matplotlib
|
||||
0,13,8,135
|
||||
8,34,6,144
|
||||
16,49,5,151
|
||||
25,63,4,156
|
||||
33,78,2,162
|
||||
41,91,1,165
|
||||
49,103,0,168
|
||||
58,116,1,168
|
||||
66,129,4,167
|
||||
74,141,11,165
|
||||
82,152,20,160
|
||||
90,162,29,154
|
||||
99,173,39,147
|
||||
107,182,48,139
|
||||
115,191,57,132
|
||||
123,199,66,124
|
||||
132,207,76,116
|
||||
140,214,85,109
|
||||
148,221,94,102
|
||||
156,227,104,95
|
||||
165,233,114,87
|
||||
173,239,124,81
|
||||
181,243,135,74
|
||||
189,247,145,67
|
||||
197,250,158,59
|
||||
206,252,169,52
|
||||
214,253,181,46
|
||||
222,253,194,41
|
||||
230,252,208,37
|
||||
239,249,221,37
|
||||
247,245,235,39
|
||||
255,240,249,33
|
||||
@@ -0,0 +1,33 @@
|
||||
# 'viridis' colormap from matplotlib
|
||||
0,68,1,84
|
||||
8,71,13,96
|
||||
16,72,24,106
|
||||
25,72,35,116
|
||||
33,71,46,124
|
||||
41,69,56,130
|
||||
49,66,65,134
|
||||
58,62,74,137
|
||||
66,58,84,140
|
||||
74,54,93,141
|
||||
82,50,101,142
|
||||
90,46,109,142
|
||||
99,43,117,142
|
||||
107,40,125,142
|
||||
115,37,132,142
|
||||
123,34,140,141
|
||||
132,31,148,140
|
||||
140,30,156,137
|
||||
148,32,163,134
|
||||
156,37,171,130
|
||||
165,46,179,124
|
||||
173,58,186,118
|
||||
181,72,193,110
|
||||
189,88,199,101
|
||||
197,108,205,90
|
||||
206,127,211,78
|
||||
214,147,215,65
|
||||
222,168,219,52
|
||||
230,192,223,37
|
||||
239,213,226,26
|
||||
247,234,229,26
|
||||
255,253,231,37
|
||||
Reference in New Issue
Block a user