Compare commits

...

19 Commits

Author SHA1 Message Date
Totoo f852900cd0 Epirbrx sizefix (#3221) 2026-06-11 17:18:05 +02:00
Pezsma 9ab367f2e0 Add praline gpio (#3218)
* Implement antenna bias control for Praline and update GPIO configurations
* Add GPIO power management functions and update board initialization for Praline
* Refactor power management functions for Praline and update GPIO configurations
* Update GPIO configurations for Praline and enable power control pins. AA_EN enabled
* Refactor GPIO configuration for Praline: update array size definitions and clean up comments
* Add P1 and P2 control functions for PRALINE
* Introduced P1_Function and P2_Function enums in ClockManager to manage multiplexer control.
* mplemented set_p1_control and set_p2_control methods for configuring P1 and P2 control pins based on specified functions.
* Updated clock_manager.hpp and clock_manager.cpp to include new functionality.
* Modified MAX2837 and MAX2839 initialization to conditionally configure GPIO based on PRALINE.
* Adjusted RFFC507x and RFFC507x_SPI initialization to include GPIO setup for PRALINE.
- Refactored RF path initialization to remove unnecessary GPIO setup.
- Updated board configuration for PRALINE to reflect new GPIO settings and pin configurations.
- Cleaned up FPGA bridge code by removing unused pin configuration functions.
- Adjusted SCU array size in pal_lld.h for PRALINE.
- Enhanced hackrf_gpio.hpp to define multiplexer control pins for PRALINE.
- Modified LED setup to accommodate active-low configuration for PRALINE.

* Refactor GPIO and power control functions

- Removed unused power control function declarations from board.h.
- Enhanced gpio.hpp with new pin setup functions and GPIO control structures for PRALINE.
- Consolidated multiplexer control pin definitions into gpio_control namespace.
- Moved power control function implementations to gpio.cpp, including detailed VAA power management logic.
- Updated power control functions to handle both PRALINE and HackRF R9 configurations.
- Cleaned up hackrf_gpio.hpp by removing redundant multiplexer control definitions.

* Refactor GPIO control for PRALINE: update MAX2831 and RFFC507x configurations, add new GPIO mappings

* Refactor GPIO configurations for PRALINE: update anti-aliasing filter pin mapping, enhance LED setup logic, and add placeholder GPIO entries.

* Refactor GPIO configurations for PRALINE: update LED mappings and remove unused anti-aliasing filter GPIO entry.
2026-06-11 13:24:42 +02:00
Arne Luehrs c7a0846645 EPIRB RX: add receiver-overload indicator on channel bar (#3212)
* EPIRB RX: add receiver-overload indicator on channel bar

Tint the channel-power bar red when the front-end overloads (ADC near
full scale). EPIRB demodulation relies on the constant-envelope carrier
and the +/-1.1 rad biphase phase jumps; ADC clipping distorts both, so a
visible cue to reduce RF-amp/LNA/VGA gain is useful.

Implemented in the shared ui::Channel widget via an opt-in
set_overload_threshold(): when the last ChannelStatistics max_db reaches
the threshold the bar is drawn red instead of blue. Default threshold is
above the 0 dBFS ceiling, so all existing users are unaffected. The
EPIRB view enables it at -3 dBFS.

Done in the widget (main firmware) rather than the EPIRB app because the
epirb_rx external app is already at the 32 KB flash limit; the app only
pays for a single setter call.
2026-06-11 11:10:23 +02:00
Arne Luehrs 569bcaad22 Epirb afc wide capture (#3213)
* EPIRB RX: widen AFC capture range to +/-5 kHz

The AFC estimate was only applied after carrier lock, so carrier
acquisition ran on raw phase deltas and could only tolerate offsets of
a few hundred Hz before the 0.6/0.7/1.6 rad detection thresholds (on
the 12-sample accumulator) broke down.

Track the carrier offset continuously in the IDLE state with a
first-order loop (AFC_TRACK_ALPHA), so the de-biased accumulator
self-centers for any offset within the discriminator Nyquist (~24 kHz)
*before* the thresholds run. ALPHA = 0.005 pulls a +/-5 kHz offset
under the 0.6 rad lock threshold in ~11 ms, well inside the 160 ms
preamble / 80 ms stability window. The IDLE rise-detect threshold is
also made symmetric (fabsf) now that the bias is removed.

Also add the missing <cstdint> include to test_convert.cpp so the
application_test suite compiles under the current toolchain.

Verified: baseband_epirb_rx.elf builds (flash 53%, RAM 12%) and
baseband_test passes.
2026-06-11 11:01:54 +02:00
Totoo 6b5955e0b5 Enhance battery detection and configuration handling (#3197)
* indicate batt changed event
* added sleep config, and use it.
* reinit if new batt detected.
* more detect
* repl batt setting
* disabled some things
* copilot fixes
2026-06-11 10:52:20 +02:00
Synray 9042d1c433 Use UI_ macros in looking glass (#3208) 2026-06-09 20:46:35 +02:00
未来方舟 378b2808ba Looking Glass frequency list now supports muiti file loads (#3215) 2026-06-08 15:22:29 +02:00
Erwin Ried 5b363b79aa Update links and images in README.md 2026-05-31 22:56:20 +02:00
Brumi-2021 0d51787099 Clean_gcc_14_2_1_compiler_errors (#3206)
* Clean_gcc_14_2_1_compiler_errors

* format issues
2026-05-31 17:34:30 +02:00
gullradriel 8f5b25b10d PRALINE Tx and clock fix (Pr 3192 takeover (#3198) )
* fix clock init and add mayhem tx capacity
* aligned external clock and RX config
* fix(clock): cap TX interpolation to x16 to prevent doubled waveform speed

  The TX gateware only implements interpolation factors x1..x16 (tx_intrp
  0..4), but the rate search could select n==5 (x32) for very low TX
  sample rates. That left the AFE/CLK0/CLK1 running at the x32 rate while
  the FPGA only interpolated by x16, clocking the TX datapath at twice the
  gateware output rate. Cap afe_rate and _resampling_n to the x16 limit in
  TX so the clocks stay aligned with the interpolation setting.

* fix(praline): source PLL1 from 40MHz GP_CLKIN instead of 12MHz XTAL

  For the intermediate 90-110MHz PLL1 stepping stage, source PLL1 from
  GP_CLKIN (Si5351 CLK2/MCU_CLK at 40MHz) giving N=2, M=10, Fclk=100MHz,
  rather than the on-chip 12MHz XTAL bootstrap path. This matches the
  non-PRALINE path. Remove the dead commented-out XTAL enable/PLL code.

* docs(fpga_bridge): correct register 0x03 description for PRALINE gateware

  Register 0x03 is RX_DIGITAL_GAIN in RX and TX_NCO_CTRL in TX, not
  RX_PSTEP.

* Fix fpga_tx_set_nco_enable to use FPGA_REG3_TX_NCO_CTRL (0x03)
2026-05-31 12:44:40 +02:00
Synray 4e56d0f420 Add RSSI toggle button to looking glass (#3204) 2026-05-31 11:24:24 +02:00
gullradriel 2bb3b415d1 update submodule (#3199) 2026-05-30 20:31:01 +02:00
Pezsma 6e0394fb6b Filename scroll (#3193) 2026-05-30 08:24:20 +02:00
Totoo da21ea7866 Fix flashing (#3188)
Fixed some bugs
2026-05-30 01:49:49 +02:00
未来方舟 2c434946cd Display if IQ inverted in DFU overlay menu while the algo decides to (#3196)
* init

* expose more info

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

* reformat a bit
2026-05-21 15:51:56 +02:00
Totoo 8f9246c1ea Enhance transmitter model and UI for better handling of TX and RF ampstates (#3185) 2026-05-21 15:50:47 +02:00
Pezsma f1d52ae78f expanding channels (#3183) 2026-05-21 15:50:10 +02:00
104 changed files with 3225 additions and 2310 deletions
+44 -22
View File
@@ -1,58 +1,80 @@
<!--
👋 Thank you for your contribution!
⚠️ Before you start, please review our Contributing Guidelines:
https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines
-->
## Brief description of what you did
<!--
Replace this line with a description of your changes. What problem does this PR solve? What does it add or fix?
Don't forget to describe how maintainers can test your modifications!
-->
<!-- Replace this line with a description of your changes. What problem does this PR solve? What does it add or fix?
Don't forget to describe how maintainers can test your modifications! -->
---
## Proof that your changes work
<!--
1. PRs without proof will not be merged. Please provide evidence for every section below that applies to your change.
2. Trusted contributors: If you have a track record of merged PRs that were properly tested and documented, maintainers may waive one or more of the proof sections below at their discretion. You are still encouraged to include whatever evidence you have, but sections may be marked `N/A — trusted contributor` with a short note.
3. First-time contributors must provide full proof. See the Contributing Guidelines(https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines) and Trusted Contributors (https://github.com/portapack-mayhem/mayhem-firmware/wiki/Trusted-Contributors) articles for details.
-->
> ⚠️ **PRs without proof will not be merged.** Please provide evidence for every section below that applies to your change.
>
> 🛈 **Trusted contributors:** If you have a track record of merged PRs that were properly tested and documented, maintainers may waive one or more of the proof sections below at their discretion. You are still encouraged to include whatever evidence you have, but sections may be marked `N/A — trusted contributor` with a short note. First-time contributors must provide full proof. See the [Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines) and [Trusted Contributors](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Trusted-Contributors) articles for details.
### 🖥️ Proof it compiles
<!--
Attach a log snippet showing a successful build from the final firmware size/space summary in the build output. Example: `Space remaining in flash ROM: 19552 bytes ( 1.9 % )`.
Paste log below.
-->
<!-- Paste log below -->
### 📱 Proof of testing on a real device
<!--
Attach photos, screenshots, or a short video of your changes running on actual PortaPack hardware. Emulator or simulator output is not a substitute for real-device testing.
Paste photo / video / screenshot below
-->
Attach photos, screenshots, or a short video of your changes running on actual PortaPack hardware. **Emulator or simulator output is not a substitute for real-device testing.**
<!-- Paste photo / video / screenshot below -->
### 📡 Proof against a real emitter/receiver (if applicable)
<!--
1. If your PR involves RX/TX, protocol decoding, signal generation, or any RF-related functionality, you **must** demonstrate it working against a real emitter or receiver. Include photos, videos, logs, or waterfall/spectrum screenshots.
2. If this section does not apply to your PR, write `N/A` below and briefly explain why.
3. Paste RF evidence, or write N/A with a short justification
-->
If your PR involves RX/TX, protocol decoding, signal generation, or any RF-related functionality, you **must** demonstrate it working against a real emitter or receiver. Include photos, videos, logs, or waterfall/spectrum screenshots.
If this section does not apply to your PR, write `N/A` below and briefly explain why.
<!-- Paste RF evidence, or write N/A with a short justification -->
---
## 📚 Wiki documentation commitment
**By submitting this PR, you acknowledge that once it is merged you are responsible for creating or updating the corresponding page on the [project wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki).**
<!--
By submitting this PR, you acknowledge that once it is merged you are responsible for creating or updating the corresponding page on the project wiki(https://github.com/portapack-mayhem/mayhem-firmware/wiki).
Your wiki article should include:
- A **screenshot of the main app screen** (strongly recommended)
- A clear **description** of what the app does
- An explanation of the **controls** and UI elements
- Any known **limitations**, quirks, or caveats
- A screenshot of the main app screen (strongly recommended)
- A clear description of what the app does
- An explanation of the controls and UI elements
- Any known limitations, quirks, or caveats
- Anything else a user should know to use the app effectively (dependencies, required hardware, file formats, etc.)
- If you are modifying an existing app, make sure the wiki page reflects your changes.
-->
- [ ] I will (or already) create(d) wiki document for my newly added feature
If you are modifying an existing app, make sure the wiki page reflects your changes.
---
+5 -5
View File
@@ -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="Its 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: ![Static Badge](https://img.shields.io/badge/NEW-yellow) 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: ![Static Badge](https://img.shields.io/badge/NEW-yellow) 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:
+2
View File
@@ -192,6 +192,7 @@ set(CPPSRC
${COMMON}/battery.cpp
${COMMON}/performance_counter.cpp
${COMMON}/bmpfile.cpp
${COMMON}/gpio.cpp
app_settings.cpp
audio.cpp
baseband_api.cpp
@@ -332,6 +333,7 @@ set(CPPSRC
${CPLD_H4M_DATA_CPP}
${HACKRF_CPLD_DATA_CPP}
ui_external_items_menu_loader.cpp
view_factory_base.cpp
)
set_source_files_properties(${CPPSRC} PROPERTIES COMPILE_FLAGS -flto) # Add lto flag to the non-external sources only
+2 -1
View File
@@ -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;
+62 -53
View File
@@ -116,7 +116,7 @@ static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
break;
default:
freq_hz = UINT64_MAX;
freq_hz = 2'402'000'000ull; // Default to channel 37 frequency if invalid channel number
}
return freq_hz;
@@ -254,7 +254,7 @@ void BLETxView::send_packet() {
}
void BLETxView::start() {
if (file_path.empty()) {
if (file_path.empty() || num_packets == 0) {
file_error();
check_loop.set_value(false);
return;
@@ -266,6 +266,8 @@ void BLETxView::start() {
button_play.set_bitmap(&bitmap_stop);
is_running = true;
advCount = 0;
send_packet();
}
@@ -296,55 +298,61 @@ void BLETxView::on_timer() {
void BLETxView::on_tx_progress(const bool done, uint32_t progress) {
// TODO: make use of progress variable
(void)progress;
if (!is_active() || num_packets == 0) {
return;
}
if (done) {
if (is_active()) {
transmitter_model.disable();
if (auto_channel) {
switch (advCount) {
case 0:
channel_number = 37;
break;
case 1:
channel_number = 38;
break;
case 2:
channel_number = 39;
break;
}
field_frequency.set_value(get_freq_by_channel_number(channel_number));
advCount++;
if (advCount == 3) {
channel_number = 37;
packet_counter--;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
if (packet_counter > 0) packet_counter--;
packetDone = true;
advCount = 0;
} else {
channel_number = 37 + advCount;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
send_packet();
}
} else if (all_channels) {
advCount++;
if (advCount == 40) {
channel_number = 0;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
if (packet_counter > 0) packet_counter--;
packetDone = true;
advCount = 0;
} else {
channel_number = advCount;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
send_packet();
advCount++;
}
} else {
packet_counter--;
if (packet_counter > 0) packet_counter--;
packetDone = true;
}
}
}
// Reached end of current packet repeats.
if (packet_counter == 0) {
// Done sending all packets.
if (current_packet == (num_packets - 1)) {
current_packet = 0;
// Reached end of current packet repeats.
if (packet_counter == 0) {
// Done sending all packets.
current_packet++;
if (current_packet >= num_packets) {
current_packet = 0;
// If looping, restart from beginning.
if (check_loop.value()) {
update_current_packet(packets[current_packet], current_packet);
} else {
stop();
}
} else {
current_packet++;
// If looping, restart from beginning.
if (check_loop.value()) {
update_current_packet(packets[current_packet], current_packet);
} else {
stop();
}
} else {
update_current_packet(packets[current_packet], current_packet);
}
}
}
@@ -385,16 +393,20 @@ BLETxView::BLETxView(NavigationView& nav)
};
options_channel.on_change = [this](size_t, int32_t i) {
// If we selected Auto don't do anything and Auto will handle changing.
if (i == 40) {
auto_channel = true;
return;
auto_channel = (i == BLT_CHAN_AUTO);
all_channels = (i == BLT_CHAN_ALL);
advCount = 0;
if (auto_channel) {
channel_number = 37;
} else if (all_channels) {
channel_number = 0;
} else {
auto_channel = false;
channel_number = i;
}
field_frequency.set_value(get_freq_by_channel_number(i));
channel_number = i;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
};
options_speed.on_change = [this](size_t, int32_t i) {
@@ -403,7 +415,7 @@ BLETxView::BLETxView(NavigationView& nav)
};
options_speed.set_selected_index(0);
options_channel.set_selected_index(3);
options_channel.set_by_value(BLT_CHAN_AUTO);
check_rand_mac.set_value(false);
check_rand_mac.on_select = [this](Checkbox&, bool v) {
@@ -413,8 +425,7 @@ BLETxView::BLETxView(NavigationView& nav)
button_open.on_select = [this](Button&) {
auto open_view = nav_.push<FileLoadView>(".TXT");
open_view->on_changed = [this](std::filesystem::path new_file_path) {
on_file_changed(new_file_path);
this->on_file_changed(new_file_path);
nav_.set_on_pop([this]() { button_play.focus(); });
};
};
@@ -427,7 +438,7 @@ BLETxView::BLETxView(NavigationView& nav)
64,
ENTER_KEYBOARD_MODE_ALPHA,
[this](std::string& buffer) {
on_save_file(buffer);
this->on_save_file(buffer);
});
};
@@ -506,10 +517,10 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
}
do {
readUntil(data_file, packets[num_packets].macAddress, mac_address_size_str, ' ');
readUntil(data_file, packets[num_packets].advertisementData, max_packet_size_str, ' ');
readUntil(data_file, packets[num_packets].packetType, max_packet_type_str, ' ');
readUntil(data_file, packets[num_packets].packetCount, max_packet_repeat_str, '\n');
readUntil(data_file, packets[num_packets].macAddress, sizeof(packets[num_packets].macAddress), ' ');
readUntil(data_file, packets[num_packets].advertisementData, sizeof(packets[num_packets].advertisementData), ' ');
readUntil(data_file, packets[num_packets].packetType, sizeof(packets[num_packets].packetType), ' ');
readUntil(data_file, packets[num_packets].packetCount, sizeof(packets[num_packets].packetCount), '\n');
uint64_t macAddressSize = strlen(packets[num_packets].macAddress);
uint64_t advertisementDataSize = strlen(packets[num_packets].advertisementData);
@@ -542,10 +553,8 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
num_packets++;
} while (num_packets < max_num_packets);
update_current_packet(packets[0], 0);
data_file.close();
update_current_packet(packets[0], 0);
}
}
@@ -570,6 +579,9 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
std::vector<std::string> strings = splitIntoStrings(packet.advertisementData);
packet_counter = packet.packet_count;
current_packet = currentIndex;
text_packet_index.set(to_string_dec_uint(current_packet));
text_packets_sent.set(to_string_dec_uint(packet.packet_count));
@@ -578,9 +590,6 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
progressbar.set_max(packet.packet_count);
packet_counter = packet.packet_count;
current_packet = currentIndex;
dataFile.create(dataTempFilePath);
for (const std::string& str : strings) {
+49 -7
View File
@@ -45,6 +45,9 @@
#include <string>
#include <memory>
#define BLT_CHAN_AUTO 40
#define BLT_CHAN_ALL 41
class BLELoggerTx {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
@@ -94,7 +97,7 @@ struct BLETxPacket {
char advertisementData[63];
char packetCount[11];
char packetType[17];
uint32_t packet_count;
uint32_t packet_count; // repeat the same packet count times
PKT_TYPE pduType;
};
@@ -145,6 +148,7 @@ class BLETxView : public View {
std::filesystem::path packet_save_path{bletx_dir / u"BLETX_????.TXT"};
uint8_t channel_number = 37;
bool auto_channel = false;
bool all_channels = false;
char randomMac[13] = "010203040506";
@@ -156,10 +160,10 @@ class BLETxView : public View {
int16_t auto_channel_period{6};
bool repeatLoop = false;
uint32_t packet_counter{0};
uint32_t num_packets{0};
uint32_t current_packet{0};
uint8_t packetTxCount{0};
uint32_t packet_counter{0}; // the counter (decr) of the actual packet as it can be sent multiple times
uint32_t num_packets{0}; // how many packets in the file
uint32_t current_packet{0}; // the actual packet's num, i'm sending if i have multiple packets
uint8_t packetTxCount{0}; // not used
bool packetDone = false;
bool random_mac = false;
bool file_override = false;
@@ -241,10 +245,48 @@ class BLETxView : public View {
OptionsField options_channel{
{11 * 8, 6 * 8},
5,
{{"Ch.37 ", 37},
{{"Ch.0", 0},
{"Ch.1", 1},
{"Ch.2", 2},
{"Ch.3", 3},
{"Ch.4", 4},
{"Ch.5", 5},
{"Ch.6", 6},
{"Ch.7", 7},
{"Ch.8", 8},
{"Ch.9", 9},
{"Ch.10", 10},
{"Ch.11", 11},
{"Ch.12", 12},
{"Ch.13", 13},
{"Ch.14", 14},
{"Ch.15", 15},
{"Ch.16", 16},
{"Ch.17", 17},
{"Ch.18", 18},
{"Ch.19", 19},
{"Ch.20", 20},
{"Ch.21", 21},
{"Ch.22", 22},
{"Ch.23", 23},
{"Ch.24", 24},
{"Ch.25", 25},
{"Ch.26", 26},
{"Ch.27", 27},
{"Ch.28", 28},
{"Ch.29", 29},
{"Ch.30", 30},
{"Ch.31", 31},
{"Ch.32", 32},
{"Ch.33", 33},
{"Ch.34", 34},
{"Ch.35", 35},
{"Ch.36", 36},
{"Ch.37", 37},
{"Ch.38", 38},
{"Ch.39", 39},
{"Auto", 40}}};
{"Auto", BLT_CHAN_AUTO},
{"All", BLT_CHAN_ALL}}};
OptionsField options_adv_type{
{17 * 8, 6 * 8},
+1
View File
@@ -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
}
+8 -5
View File
@@ -52,7 +52,6 @@ void BattinfoView::update_result() {
text_voltage.set("UNKNOWN");
text_current.set("-");
text_charge.set("-");
// text_cycles.set("-");
text_ttef.set("-");
text_method.set("-");
// text_warn.set("");
@@ -60,7 +59,9 @@ void BattinfoView::update_result() {
}
bool uichg = false;
uint8_t valid_mask = 0;
battery::BatteryManagement::getBatteryInfo(valid_mask, percent, voltage, current);
bool battMayChanged = false;
battery::BatteryManagement::getBatteryInfo(valid_mask, percent, voltage, current, battMayChanged);
text_state.set((battMayChanged || !persistent_memory::battery_cap_valid()) ? "Needs config." : "ok");
// update text fields
if (percent <= 100 && (valid_mask & battery::BatteryManagement::BATT_VALID_VOLTAGE) == battery::BatteryManagement::BATT_VALID_VOLTAGE)
text_percent.set(to_string_dec_uint(percent) + " %");
@@ -76,6 +77,7 @@ void BattinfoView::update_result() {
labels_opt.hidden(false);
text_current.hidden(false);
text_charge.hidden(false);
text_state.hidden(false);
text_current.set(to_string_dec_int(current) + " mA");
if (current >= 25) // when >25mA it is charging in any scenario
text_charge.set("Charging");
@@ -98,7 +100,7 @@ void BattinfoView::update_result() {
labels_opt.hidden(true);
text_current.hidden(true);
text_charge.hidden(true);
// text_cycles.hidden(true);
text_state.hidden(true);
text_ttef.hidden(true);
// text_warn.set("");
}
@@ -143,9 +145,10 @@ void BattinfoView::update_result() {
} else {
text_method.set("Voltage");
}
if (battMayChanged) text_capacity.set_style(Theme::getInstance()->fg_red);
if (uichg) set_dirty();
// to update status bar too, send message in behalf of batt manager
BatteryStateMessage msg{valid_mask, percent, current >= 25, voltage};
BatteryStateMessage msg{valid_mask, percent, current >= 25, voltage, battMayChanged};
EventDispatcher::send_message(msg);
}
@@ -161,7 +164,7 @@ BattinfoView::BattinfoView(NavigationView& nav)
&button_settings,
&button_exit,
&text_capacity,
// &text_cycles,
//&text_state, //disabled for now
// &text_warn,
&text_ttef});
+7 -6
View File
@@ -64,7 +64,7 @@ class BattinfoView : public View {
{{UI_POS_X(2), UI_POS_Y(5)}, "Current:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(6)}, "Charge:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(7)}, "TTF/E:", Theme::getInstance()->fg_light->foreground},
// {{UI_POS_X(2), UI_POS_Y(8)}, "Cycles:", Theme::getInstance()->fg_light->foreground},
//{{UI_POS_X(2), UI_POS_Y(8)}, "State:", Theme::getInstance()->fg_light->foreground}, //for now, this is disabled. see max ic implementation
{{UI_POS_X(2), UI_POS_Y(10)}, "Change settings:", Theme::getInstance()->fg_light->foreground},
};
@@ -90,13 +90,14 @@ class BattinfoView : public View {
{UI_POS_X(13), UI_POS_Y(7), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
"-"};
/* Text text_cycles{
{UI_POS_X(13), UI_POS_Y(8), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
"-"};
Text text_state{
{UI_POS_X(13), UI_POS_Y(8), UI_POS_WIDTH_REMAINING(13), UI_POS_HEIGHT(1)},
"ok"};
/*
Text text_warn{
{UI_POS_X(1), UI_POS_Y(9), screen_width, UI_POS_HEIGHT(2)},
""}; */
""};
*/
Button button_settings{
{UI_POS_X(2), UI_POS_Y(11) + 5, UI_POS_WIDTH(10), UI_POS_HEIGHT(2)},
+6 -2
View File
@@ -103,7 +103,8 @@ DfuMenu2::DfuMenu2(NavigationView& nav)
&text_info_line_8,
&text_info_line_9,
&text_info_line_10,
&text_info_line_11});
&text_info_line_11,
&text_info_line_12});
}
void DfuMenu2::paint(Painter& painter) {
@@ -118,9 +119,12 @@ void DfuMenu2::paint(Painter& painter) {
text_info_line_9.set(to_string_dec_uint(portapack::transmitter_model.target_frequency(), 10));
text_info_line_10.set(to_string_dec_uint(portapack::transmitter_model.baseband_bandwidth(), 10));
text_info_line_11.set(to_string_dec_uint(portapack::transmitter_model.sampling_rate(), 10));
text_info_line_12.set(std::string("M") + (radio::get_mixer_invert() ? "Y" : "N") +
" B" + (radio::get_baseband_invert() ? "Y" : "N") +
" IQ" + ((radio::get_mixer_invert() ^ radio::get_baseband_invert()) ? "Y" : "N"));
constexpr auto margin = 5;
constexpr auto lines = 11 + 2;
constexpr auto lines = 12 + 2;
painter.fill_rectangle(
{{5 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin},
@@ -96,6 +96,7 @@ class DfuMenu2 : public View {
{{5 * CHARACTER_WIDTH, 13 * LINE_HEIGHT}, "TX Freq:", Theme::getInstance()->fg_darkcyan->foreground},
{{5 * CHARACTER_WIDTH, 14 * LINE_HEIGHT}, "TX BW:", Theme::getInstance()->fg_darkcyan->foreground},
{{5 * CHARACTER_WIDTH, 15 * LINE_HEIGHT}, "TX SampR:", Theme::getInstance()->fg_darkcyan->foreground},
{{5 * CHARACTER_WIDTH, 16 * LINE_HEIGHT}, "IQ Inv:", Theme::getInstance()->fg_darkcyan->foreground},
};
Text text_info_line_1{{14 * CHARACTER_WIDTH, 5 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
@@ -109,6 +110,7 @@ class DfuMenu2 : public View {
Text text_info_line_9{{14 * CHARACTER_WIDTH, 13 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_10{{14 * CHARACTER_WIDTH, 14 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_11{{14 * CHARACTER_WIDTH, 15 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_12{{(14 + 1) * CHARACTER_WIDTH, 16 * LINE_HEIGHT, (10 - 1) * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
};
} /* namespace ui */
+7 -4
View File
@@ -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;
@@ -24,6 +24,7 @@
#include "ui_looking_glass_app.hpp"
#include "convert.hpp"
#include "file_reader.hpp"
#include "file.hpp"
#include "string_format.hpp"
#include "audio.hpp"
#include "file_path.hpp"
@@ -164,11 +165,13 @@ void GlassView::add_spectrum_pixel(uint8_t power) {
display.fill_rectangle({{xpos, screen_height - point}, {1, point}}, gradient.lut[spectrum_data[xpos]]);
}
// indicate RSSI min and max power
display.fill_rectangle({{0, screen_height - map(raw_min, 0, 255, 0, spectrum_height)}, {screen_width, 1}}, -gradient.lut[raw_min]);
display.fill_rectangle({{0, screen_height - map(raw_max, 0, 255, 0, spectrum_height)}, {screen_width, 1}}, -gradient.lut[raw_max]);
if (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 {
@@ -394,6 +397,7 @@ GlassView::GlassView(
&button_marker_plus,
&button_jump,
&button_rst,
&button_rssi,
&freq_stats});
load_presets(); // Load available presets from TXT files (or default).
@@ -448,6 +452,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;
@@ -458,6 +463,7 @@ GlassView::GlassView(
freq_stats.hidden(false);
button_jump.hidden(false);
button_rst.hidden(false);
button_rssi.hidden(false);
break;
case 2: // PEAK
@@ -468,6 +474,7 @@ GlassView::GlassView(
freq_stats.hidden(false);
button_jump.hidden(false);
button_rst.hidden(false);
button_rssi.hidden(false);
break;
}
@@ -537,6 +544,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:
@@ -587,34 +604,39 @@ void GlassView::on_freqchg(int64_t freq) {
}
void GlassView::load_presets() {
File presets_file;
auto error = presets_file.open(looking_glass_dir / u"PRESETS.TXT");
presets_db.clear();
// Add the "Manual" entry.
presets_db.push_back({0, 0, "Manual"});
if (!error) {
auto reader = FileLineReader(presets_file);
for (const auto& line : reader) {
if (line.length() == 0 || line[0] == '#')
continue;
scan_root_files(looking_glass_dir, u"*.TXT", [this](const std::filesystem::path& path) {
if (path.empty() || path.native()[0] == u'.')
return;
auto cols = split_string(line, ',');
if (cols.size() != 3)
continue;
File presets_file;
auto error = presets_file.open(looking_glass_dir / path);
if (!error) {
auto reader = FileLineReader(presets_file);
for (const auto& line : reader) {
if (line.length() == 0 || line[0] == '#')
continue;
preset_entry entry{};
parse_int(cols[0], entry.min);
parse_int(cols[1], entry.max);
entry.label = trimr(cols[2]);
auto cols = split_string(line, ',');
if (cols.size() != 3)
continue;
if (entry.min == 0 || entry.max == 0 || entry.min >= entry.max)
continue; // Invalid line.
preset_entry entry{};
parse_int(cols[0], entry.min);
parse_int(cols[1], entry.max);
entry.label = trimr(cols[2]);
presets_db.emplace_back(std::move(entry));
if (entry.min == 0 || entry.max == 0 || entry.min >= entry.max)
continue; // Invalid line.
presets_db.emplace_back(std::move(entry));
}
}
}
});
populate_presets();
}
@@ -164,43 +164,44 @@ 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},
{{0, 1 * 16}, "RANGE: FILTER: AMP:", Theme::getInstance()->fg_light->foreground},
{{0, 2 * 16}, "P:", Theme::getInstance()->fg_light->foreground},
{{0, 3 * 16}, "MARKER( / ): MHz", Theme::getInstance()->fg_light->foreground},
{{0, 4 * 16}, "RES: VOL:", Theme::getInstance()->fg_light->foreground}};
{{0, UI_POS_Y(1)}, "RANGE: FILTER: AMP:", Theme::getInstance()->fg_light->foreground},
{{0, UI_POS_Y(2)}, "P:", Theme::getInstance()->fg_light->foreground},
{{0, UI_POS_Y(3)}, "MARKER( / ): MHz", Theme::getInstance()->fg_light->foreground},
{{0, UI_POS_Y(4)}, "RES: VOL:", Theme::getInstance()->fg_light->foreground}};
NumberField field_frequency_min{
{4 * 8, UI_POS_Y(0)},
{UI_POS_X(4), UI_POS_Y(0)},
4,
{0, 7199},
1, // number of steps by encoder delta
' '};
NumberField field_frequency_max{
{13 * 8, UI_POS_Y(0)},
{UI_POS_X(13), UI_POS_Y(0)},
4,
{1, 7200},
1, // number of steps by encoder delta
' '};
LNAGainField field_lna{
{21 * 8, UI_POS_Y(0)}};
{UI_POS_X(21), UI_POS_Y(0)}};
VGAGainField field_vga{
{27 * 8, UI_POS_Y(0)}};
{UI_POS_X(27), UI_POS_Y(0)}};
TextField field_range{
{6 * 8, 1 * 16, 6 * 8, 16},
{UI_POS_X(6), UI_POS_Y(1), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)},
""};
OptionsField filter_config{
{20 * 8, 1 * 16},
{UI_POS_X(20), UI_POS_Y(1)},
4,
{
{"OFF ", 0},
@@ -209,41 +210,41 @@ class GlassView : public View {
}};
RFAmpField field_rf_amp{
{28 * 8, 1 * 16}};
{UI_POS_X(28), UI_POS_Y(1)}};
OptionsField range_presets{
{2 * 8, 2 * 16},
{UI_POS_X(2), UI_POS_Y(2)},
20,
{}};
ButtonWithEncoder button_beep_squelch{
{screen_width - 8 * 8, 2 * 16 + 4, 8 * 8, 1 * 8},
{UI_POS_X_RIGHT(8), UI_POS_Y(2.25), UI_POS_WIDTH(8), UI_POS_HEIGHT(0.5)},
""};
TextField field_marker{
{12 * 8, 3 * 16, 9 * 8, 16},
{UI_POS_X(12), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(1)},
""};
Button button_marker_minus{
{7 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
{UI_POS_X(7), UI_POS_Y(3.25), UI_POS_WIDTH(1), UI_POS_HEIGHT(0.5)},
"-"};
Button button_marker_plus{
{9 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
{UI_POS_X(9), UI_POS_Y(3.25), UI_POS_WIDTH(1), UI_POS_HEIGHT(0.5)},
"+"};
NumberField field_trigger{
{4 * 8, 4 * 16},
{UI_POS_X(4), UI_POS_Y(4)},
3,
{2, 128},
2,
' '};
AudioVolumeField field_volume{
{13 * 8, 4 * 16}};
{UI_POS_X(13), UI_POS_Y(4)}};
/*OptionsField steps_config{
{13 * 8, 4 * 16},
{UI_POS_X(13), UI_POS_Y(4)},
3,
{
{"1", 1},
@@ -255,7 +256,7 @@ class GlassView : public View {
}};*/
OptionsField scan_type{
{17 * 8, 4 * 16},
{UI_POS_X(17), UI_POS_Y(4)},
2,
{
{"F-", LOOKING_GLASS_FASTSCAN},
@@ -263,7 +264,7 @@ class GlassView : public View {
}};
OptionsField view_config{
{19 * 8, 4 * 16},
{UI_POS_X(19), UI_POS_Y(4)},
7,
{
{"SPCTR-V", 0},
@@ -272,7 +273,7 @@ class GlassView : public View {
}};
OptionsField level_integration{
{27 * 8, 4 * 16},
{UI_POS_X(27), UI_POS_Y(4)},
2,
{
{"x0", 0},
@@ -288,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{
+6 -2
View File
@@ -1163,6 +1163,7 @@ SetBatteryView::SetBatteryView(NavigationView& nav) {
&field_battcap,
&button_help_cap,
&checkbox_overridebatt,
&checkbox_battery_replaceable,
&checkbox_battery_charge_hint});
if (i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055)) add_children({&button_reset, &labels2});
@@ -1170,10 +1171,12 @@ SetBatteryView::SetBatteryView(NavigationView& nav) {
button_save.on_select = [&nav, this](Button&) {
pmem::set_ui_override_batt_calc(checkbox_overridebatt.value());
pmem::set_ui_battery_charge_hint(checkbox_battery_charge_hint.value());
pmem::set_battery_replaceable(checkbox_battery_replaceable.value());
battery::BatteryManagement::set_calc_override(checkbox_overridebatt.value());
if (((uint32_t)field_battcap.value() != pmem::battery_cap_mah()) || (!pmem::battery_cap_valid())) {
i2cdev::I2cDev_MAX17055* dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
if ((((uint32_t)field_battcap.value() != pmem::battery_cap_mah()) || (!pmem::battery_cap_valid())) || (dev && dev->getIsBattChanged())) {
pmem::set_battery_cap_mah(field_battcap.value());
i2cdev::I2cDev_MAX17055* dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
if (dev) dev->resetChangedFlag();
if (dev && !dev->reInit()) {
nav.display_modal("Error", "Battery gauge re-init failed");
return;
@@ -1206,6 +1209,7 @@ SetBatteryView::SetBatteryView(NavigationView& nav) {
checkbox_overridebatt.set_value(pmem::ui_override_batt_calc());
checkbox_battery_charge_hint.set_value(pmem::ui_battery_charge_hint());
checkbox_battery_replaceable.set_value(pmem::battery_replaceable());
field_battcap.set_value(pmem::battery_cap_mah());
+9 -4
View File
@@ -1053,7 +1053,7 @@ class SetBatteryView : public View {
{{UI_POS_X_CENTER(17), UI_POS_Y(8)}, "Battery capacity", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(7), UI_POS_Y(9)}, "mAh", Theme::getInstance()->fg_light->foreground}};
Labels labels2{{{UI_POS_X(1), UI_POS_Y(11)}, "Reset IC's learned params.", Theme::getInstance()->fg_light->foreground}};
Labels labels2{{{UI_POS_X(1), UI_POS_Y_BOTTOM(7)}, "Reset IC's learned params.", Theme::getInstance()->fg_light->foreground}};
NumberField field_battcap{
{UI_POS_X(1), UI_POS_Y(9)},
@@ -1078,17 +1078,22 @@ class SetBatteryView : public View {
23,
"Charge hint"};
Checkbox checkbox_battery_replaceable{
{UI_POS_X(2), UI_POS_Y(11)},
23,
"Replaceable battery"};
Button button_cancel{
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(3), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Cancel",
};
Button button_save{
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(3), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Save"};
Button button_reset{
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y(13), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(6), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Reset",
};
};
@@ -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,
+239 -171
View File
@@ -31,6 +31,11 @@ using namespace hackrf::one;
#include "lpc43xx_cpp.hpp"
using namespace lpc43xx;
#ifdef PRALINE
#include "hackrf_gpio.hpp"
#include "gpio.hpp"
#endif
#ifdef PRALINE
extern "C" {
#include "fpga_bridge.h"
@@ -325,6 +330,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)
@@ -423,77 +434,55 @@ static void portapack_tcxo_disable() {
using namespace hackrf::one;
void ClockManager::init_clock_generator() {
#ifdef PRALINE
// thoese PIN can review platform_scu file
// have many conflict and modify for clock init
// P2_10 -> GPIO0[14] P1_CTRL0
LPC_SCU->SFSP[2][10] = 0xF0;
// P6_8 -> GPIO5[16] P1_CTRL1
LPC_SCU->SFSP[6][8] = 0xF4;
// P6_9 -> GPIO3[5] P1_CTRL2
LPC_SCU->SFSP[6][9] = 0xF0;
// P1_20 -> GPIO0[15] CLKIN_CTRL
LPC_SCU->SFSP[1][20] = 0xF0;
constexpr GPIO gpio_p1_ctrl0 = gpio[GPIO0_14];
constexpr GPIO gpio_p1_ctrl1 = gpio[GPIO5_16];
constexpr GPIO gpio_p1_ctrl2 = gpio[GPIO3_5];
constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15];
gpio_p1_ctrl0.output();
gpio_p1_ctrl1.output();
gpio_p1_ctrl2.output();
gpio_clkin_ctrl.output();
// P1 control = 100 and choose P22 clock external clock on portapack
gpio_p1_ctrl0.write(0);
gpio_p1_ctrl1.write(0);
gpio_p1_ctrl2.write(1);
gpio_clkin_ctrl.write(1);
chThdSleepMilliseconds(20);
#else
// HackRF One r9: GPIO0_8 (mcu_clk_en) gates Si5351 CLK2/CLK7 to GP_CLKIN
if (hackrf_r9) {
gpio_r9_mcu_clk_en.output();
gpio_r9_mcu_clk_en.write(1);
}
#endif
clock_generator.reset();
clock_generator.set_crystal_internal_load_capacitance(CrystalInternalLoadCapacitance::XTAL_CL_8pF);
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
*/
// change there detect method same as hackrf one
// PLLA -> XTALPLLB -> CLKIN
clock_generator.set_pll_input_sources(si5351c_pll_input_sources);
// although the name a and the branch define CLK2 MCU CLOCK
// only use CLK2
auto si5351_clock_control_common = si5351a_clock_control_common;
constexpr size_t clock_generator_output_pro_mcu_clkin = 2;
// clk_src define CLKIN
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(si5351c_pll_input_sources);
clock_generator.set_clock_control(
clock_generator_output_pro_mcu_clkin,
si5351_clock_control_common[clock_generator_output_pro_mcu_clkin]
.clk_src(ClockControl::ClockSource::CLKIN)
.clk_pdn(ClockControl::ClockPowerDown::Power_On));
clock_generator.enable_output(clock_generator_output_pro_mcu_clkin);
chThdSleepMilliseconds(20);
// should be extern icon and 10MHz
reference = choose_reference();
/* Clock control will be set AFTER multisynth configuration - see below */
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
@@ -538,86 +527,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)
@@ -645,15 +598,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
@@ -753,13 +705,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);
@@ -787,12 +739,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);
@@ -851,7 +802,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)
*/
@@ -872,50 +823,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
@@ -1210,3 +1207,74 @@ void ClockManager::enable_clock_output(bool enable) {
}
#endif
}
#ifdef PRALINE
void ClockManager::set_p1_control(P1_Function func) {
// Truth table based on P1_Control.csv (L=clear, H=set)
switch (func) {
case P1_Function::TriggerIn:
gpio_control::p1_ctrl2.clear();
gpio_control::p1_ctrl1.clear();
gpio_control::p1_ctrl0.clear();
break;
case P1_Function::AuxClk1:
gpio_control::p1_ctrl2.clear();
gpio_control::p1_ctrl1.clear();
gpio_control::p1_ctrl0.set();
break;
case P1_Function::ClkIn:
gpio_control::p1_ctrl2.clear();
gpio_control::p1_ctrl1.set();
gpio_control::p1_ctrl0.clear();
break;
case P1_Function::TriggerOut:
gpio_control::p1_ctrl2.clear();
gpio_control::p1_ctrl1.set();
gpio_control::p1_ctrl0.set();
break;
case P1_Function::P22_ClkIn:
gpio_control::p1_ctrl2.set();
gpio_control::p1_ctrl1.clear();
gpio_control::p1_ctrl0.clear();
break;
case P1_Function::P2_5:
gpio_control::p1_ctrl2.set();
gpio_control::p1_ctrl1.clear();
gpio_control::p1_ctrl0.set();
break;
case P1_Function::NotConnected:
gpio_control::p1_ctrl2.set();
gpio_control::p1_ctrl1.set();
gpio_control::p1_ctrl0.clear();
break;
case P1_Function::AuxClk2:
gpio_control::p1_ctrl2.set();
gpio_control::p1_ctrl1.set();
gpio_control::p1_ctrl0.set();
break;
}
}
void ClockManager::set_p2_control(P2_Function func) {
// Ensure all P2 control pins are configured as outputs
// Truth table based on P2_Control.csv (L=clear, H=set)
switch (func) {
case P2_Function::Clk3:
// CTRL0 is 'X' (don't care) according to CSV, we default it to Low (clear)
gpio_control::p2_ctrl1.clear();
gpio_control::p2_ctrl0.clear();
break;
case P2_Function::TriggerIn:
gpio_control::p2_ctrl1.set();
gpio_control::p2_ctrl0.clear();
break;
case P2_Function::TriggerOut:
gpio_control::p2_ctrl1.set();
gpio_control::p2_ctrl0.set();
break;
}
}
#endif
+23
View File
@@ -77,6 +77,29 @@ class ClockManager {
void set_reference_ppb(const int32_t ppb);
#ifdef PRALINE
enum class P1_Function {
TriggerIn, // CTRL2: L, CTRL1: L, CTRL0: L
AuxClk1, // CTRL2: L, CTRL1: L, CTRL0: H
ClkIn, // CTRL2: L, CTRL1: H, CTRL0: L
TriggerOut, // CTRL2: L, CTRL1: H, CTRL0: H
P22_ClkIn, // CTRL2: H, CTRL1: L, CTRL0: L
P2_5, // CTRL2: H, CTRL1: L, CTRL0: H
NotConnected, // CTRL2: H, CTRL1: H, CTRL0: L (NC)
AuxClk2 // CTRL2: H, CTRL1: H, CTRL0: H
};
// Multiplexer 2 (P2) Functions - Based on P2_Control.csv
enum class P2_Function {
Clk3, // CTRL1: L, CTRL0: X (Don't care)
TriggerIn, // CTRL1: H, CTRL0: L
TriggerOut // CTRL1: H, CTRL0: H
};
// Function declarations
void set_p1_control(P1_Function func);
void set_p2_control(P2_Function func);
uint8_t get_resampling_n() const { return _resampling_n; }
// Si5351 diagnostic methods
+7 -5
View File
@@ -56,6 +56,8 @@ using namespace hackrf::one;
#include "ui_navigation.hpp"
#include "gpio.hpp"
static int delayed_error = 0;
extern "C" {
@@ -185,10 +187,11 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
SCB->ICSR |= SCB_ICSR_PENDSTCLR_Msk;
power_control::vaa_power_off();
power_control::core_power_off();
#ifdef PRALINE
// Power management and GPIO configuration for Praline hardware
gpio_vaa_disable.set();
gpio_1v2_enable.clear();
LPC_GPIO->DIR[0] &= ~0xFFFF4000;
LPC_GPIO->DIR[1] &= ~0xFFFF1000;
LPC_GPIO->DIR[2] &= ~((1 << 14) | (1 << 13) | (1 << 12) | (1 << 11) | (1 << 10) | (1 << 6) | (1 << 0));
@@ -196,8 +199,6 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
LPC_GPIO->DIR[5] &= ~(1 << 16);
#else
// Power management and GPIO configuration for legacy hardware
gpio_og_vaa_disable.set();
gpio_r9_vaa_disable.clear();
LPC_GPIO->DIR[0] &= ~0xFFFF4000;
LPC_GPIO->DIR[1] &= ~0xFFFF1000;
LPC_GPIO->DIR[2] &= ~((1 << 14) | (1 << 13) | (1 << 12) | (1 << 11) | (1 << 10) | (1 << 6) | (1 << 0));
@@ -238,7 +239,8 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
// --- Battery Status Check (I2C) ---
detect = battery::BatteryManagement::isDetected();
if (detect) {
battery::BatteryManagement::getBatteryInfo(valid_mask, percent, voltage, current);
bool dummy;
battery::BatteryManagement::getBatteryInfo(valid_mask, percent, voltage, current, dummy);
bool is_full = (valid_mask == 31 && percent == 100 && current <= 10) ||
(valid_mask == 1 && percent == 100);
+12 -15
View File
@@ -41,29 +41,26 @@ void BeaconUIList::paint(Painter& painter) {
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()) {
auto line_position = rect.location() + Point{0, 1 + (int)offset * char_height};
auto is_selected = (offset == selected_index_);
auto style = base_style;
// 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 using stack buffer directly to avoid heap allocation
painter.draw_string(
line_position, (is_selected ? style->invert() : *style), 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.
+76 -39
View File
@@ -39,7 +39,7 @@ namespace ui::external_app::epirb_rx {
// URL templates
#define MAPS_URL_TEMPLATE "https://www.google.com/maps/search/?api=1&query=%s%%2C%s"
#define BEACON_URL_TEMPALTE "https://decoder2.herokuapp.com/decoded/"
#define BEACON_URL_TEMPLATE "https://decoder2.herokuapp.com/decoded/"
#ifndef DISABLE_COUNTRY_CACHE
int CountryManager::cache_count = 0;
@@ -71,15 +71,24 @@ void TextArea::paint(Painter& painter) {
const Style& s = has_focus() ? style().invert() : style();
painter.fill_rectangle(rect, s.background);
// We use \t as line separator since \n is used in STR_COLOR_GREEN
auto rows = split_string(content, '\t');
const int line_height = s.font.line_height();
size_t line_idx = 0;
for (auto row : rows) {
painter.draw_string(rect.location() + Point(0, line_idx * line_height), s, row);
int line_idx = 0;
// Efficiently draw lines separated by \t without heap allocations
std::string_view sv{content};
size_t start = 0;
size_t end;
while ((end = sv.find('\t', start)) != std::string_view::npos) {
painter.draw_string(rect.location() + Point(0, line_idx * line_height), s, sv.substr(start, end - start));
start = end + 1;
line_idx++;
}
if (start < sv.length()) {
painter.draw_string(rect.location() + Point(0, line_idx * line_height), s, sv.substr(start));
}
}
void TextArea::set_content(std::string_view value) {
@@ -102,10 +111,12 @@ bool TextArea::on_key(const KeyEvent key) {
void EPIRBAppView::decode_packet(const baseband::Packet& packet, Beacon& beacon) {
// Convert packet bits to byte array for easier processing
uint8_t data[BEACON_DATA_SIZE]{};
for (size_t i = 0; i < std::min(packet.size() / 8, (size_t)BEACON_DATA_SIZE); i++) {
size_t max_bytes = std::min(packet.size() / 8, (size_t)BEACON_DATA_SIZE);
size_t packet_bit_idx = 0;
for (size_t i = 0; i < max_bytes; i++) {
uint8_t byte_val = 0;
for (int bit = 0; bit < 8 && (i * 8 + bit) < packet.size(); bit++) {
if (packet[i * 8 + bit]) {
for (int bit = 0; bit < 8; bit++, packet_bit_idx++) {
if (packet[packet_bit_idx]) {
byte_val |= (1 << (7 - bit));
}
}
@@ -120,17 +131,31 @@ void EPIRBAppView::decode_packet(const baseband::Packet& packet, Beacon& beacon)
#ifdef LOGGER
void EPIRBLogger::on_packet(Beacon& beacon) {
std::string entry = std::string(beacon.getType()) + "," +
beacon.hexId + "," +
beacon.getProtocolName(); // + ",";
// to_string_dec_uint(static_cast<uint8_t>(beacon.emergency_type)) + ",";
/*if (!beacon.location.isUnknown()) {
entry += beacon.location.toString(Location::LocationFormat::DECIMAL);
} else {
entry += ",";
}
entry += "," + beacon.country.toString() + "," +
beacon.getSatus() + "\n";*/
std::string entry;
// Pre-allocate enough memory to avoid heap fragmentation and resizing
// 128 bytes should be plenty for this specific log line
entry.reserve(128);
entry += beacon.getType();
entry += ",";
entry += beacon.hexId;
entry += ",";
entry += beacon.getProtocolName();
/* If you ever uncomment the rest of the logging,
* keep using the same pattern! Like this:
*
* if (!beacon.location.isUnknown()) {
* entry += beacon.location.toString(Location::LocationFormat::DECIMAL);
* } else {
* entry += ",";
* }
* entry += ",";
* entry += beacon.country.toString();
* entry += ",";
* entry += beacon.getSatus();
* entry += "\n";
*/
log_file.write_entry(beacon.date, entry);
}
#endif
@@ -294,12 +319,12 @@ void EPRIBQRView::set_beacon(Beacon* beacon) {
}
void EPRIBQRView::update_display() {
// Update data => we use a single TextArea component for code size optimizaton
// Update data => we use a single TextArea component for code size optimization
char buffer[128];
char* buffer_pointer = buffer;
buffer_pointer += sprintf(buffer_pointer, "%sQR:%s\t\t\t\t\t\t\t\t", STR_COLOR_CYAN, STR_COLOR_WHITE);
buffer_pointer += sprintf(buffer_pointer, STR_COLOR_CYAN "QR:" STR_COLOR_WHITE "\t\t\t\t\t\t\t\t");
if (current_beacon) {
buffer_pointer += sprintf(buffer_pointer, "%sData:%s\t", STR_COLOR_CYAN, STR_COLOR_WHITE);
buffer_pointer += sprintf(buffer_pointer, STR_COLOR_CYAN "Data:" STR_COLOR_WHITE "\t");
// HEX ID 30 Hexa or HEX ID 22 Hexa bit 26 to 112
buffer_pointer += current_beacon->toHexString(buffer_pointer, current_beacon->frame, true, 3, 11);
(*(buffer_pointer++)) = '\t';
@@ -320,7 +345,7 @@ void EPRIBQRView::update_qr() {
if (show_map) {
// Map is selected
if (!current_beacon->location.isUnknown()) {
// Loation is known => actually draw QR
// Location is known => actually draw QR
current_beacon->location.formatFloatLocation(qr_url, MAPS_URL_TEMPLATE);
show_qr = true;
}
@@ -328,7 +353,7 @@ void EPRIBQRView::update_qr() {
// Detail is selected
char* buffer_pointer = qr_url;
// Send to heroku decoder
buffer_pointer += sprintf(qr_url, BEACON_URL_TEMPALTE);
buffer_pointer += sprintf(qr_url, BEACON_URL_TEMPLATE);
current_beacon->hexString(buffer_pointer, false);
show_qr = true;
}
@@ -389,11 +414,10 @@ EPIRBAppView::EPIRBAppView(ui::NavigationView& nav)
&view_rx,
#endif
&view_qr});
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
options_t frequ_options;
ui::OptionsField::options_t frequ_options;
// Set frequency combo content according to FREQ.TXT file content
for (auto freq : ResourceManager::get_frequencies()) {
for (const auto& freq : ResourceManager::get_frequencies()) {
int32_t freq_value = atol(freq.c_str());
frequ_options.emplace_back(to_string_rounded_freq(freq_value, 3), freq_value);
}
@@ -402,7 +426,7 @@ EPIRBAppView::EPIRBAppView(ui::NavigationView& nav)
options_frequency.on_change = [this](size_t, ui::OptionsField::value_t v) {
receiver_model.set_target_frequency(v);
};
// Restore frequency from preferencies
// Restore frequency from preferences
options_frequency.set_by_value(receiver_model.target_frequency());
// Tick second timer
@@ -456,6 +480,13 @@ EPIRBAppView::EPIRBAppView(ui::NavigationView& nav)
audio::set_rate(audio::Rate::Hz_24000);
audio::output::start();
// Tint the channel-power bar red when the front-end overloads (ADC near
// full scale). Clipping distorts the constant-envelope carrier and the
// +/-1.1 rad biphase jumps the decoder relies on, so this cues the user to
// reduce RF-amp/LNA/VGA gain. -3 dBFS is a heuristic on the post-decimation
// level (not a literal ADC clip count); tune if it trips too early/late.
channel.set_overload_threshold(-3);
update_display();
#ifdef LOGGER
@@ -551,7 +582,10 @@ void EPIRBAppView::update_map() {
if (!beacon.location.isUnknown()) {
hide_map = false;
// Set new position
view_map.set_main_marker(std::string(beacon.getType()) + "-" + beacon.shortId(), beacon.location.latitude.getFloatValue(), beacon.location.longitude.getFloatValue());
std::string tag = beacon.getType();
tag += "-";
tag += beacon.shortId();
view_map.set_main_marker(tag, beacon.location.latitude.getFloatValue(), beacon.location.longitude.getFloatValue());
// Add all beacons with valid locations as markers
for (size_t j = 0; j < size; j++) {
if (cur_index != j) {
@@ -560,8 +594,10 @@ void EPIRBAppView::update_map() {
ui::GeoMarker marker;
marker.lat = other_beacon.location.latitude.getFloatValue();
marker.lon = other_beacon.location.longitude.getFloatValue();
marker.angle = 0;
marker.tag = std::string(other_beacon.getType()) + "-" + other_beacon.shortId();
tag = other_beacon.getType();
tag += "-";
tag += other_beacon.shortId();
marker.tag = tag;
view_map.add_marker(marker);
}
}
@@ -580,15 +616,16 @@ void EPIRBAppView::on_tick_second() {
}
void EPIRBAppView::update_display() {
// We use a single TextArea for code size optimization
char buffer[128];
char* buffer_pointer = buffer;
buffer_pointer += sprintf(buffer_pointer, "%sListening... Beacon:%s%2d/%d\t", STR_COLOR_CYAN, STR_COLOR_WHITE, (beacon_db.get_current_beacon_index() + 1), beacons_received);
buffer_pointer += sprintf(buffer_pointer, "%sStats: %s%03dOK %s%03dCOR %s%03dERR\t", STR_COLOR_CYAN, STR_COLOR_GREEN, packets_valid, STR_COLOR_YELLOW, packets_corrected, STR_COLOR_RED, packets_error);
buffer_pointer += sprintf(buffer_pointer, "%sCurrent:%s ", STR_COLOR_CYAN, STR_COLOR_WHITE);
int len = sprintf(buffer,
STR_COLOR_CYAN "Listening... Beacon:" STR_COLOR_WHITE "%2d/%d\t" STR_COLOR_CYAN "Stats: " STR_COLOR_GREEN "%03dOK " STR_COLOR_YELLOW "%03dCOR " STR_COLOR_RED "%03dERR\t" STR_COLOR_CYAN "Current:" STR_COLOR_WHITE " ",
(beacon_db.get_current_beacon_index() + 1), beacons_received,
packets_valid, packets_corrected, packets_error);
if (!beacon_db.empty()) {
beacon_db.get_current_beacon().formatSummary(buffer_pointer, false);
beacon_db.get_current_beacon().formatSummary(buffer + len, false);
}
text_status.set_content(buffer);
}
+323 -323
View File
@@ -1,324 +1,324 @@
/*
* 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
/*
* 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__
+13 -6
View File
@@ -103,12 +103,6 @@ void MAX2831::flush_dirty() {
void MAX2831::init() {
set_mode(Mode::Shutdown);
/* Configure GPIO pins for MAX2831 control */
gpio_max283x_enable.output();
gpio_max2831_rx_enable.output();
gpio_max2831_rxhp.output();
gpio_max2831_rxhp.write(0); /* RXHP low = 100 Hz HPF (default) */
/* Reset to default register values */
std::memcpy(_regs.data(), default_regs.data(), sizeof(_regs));
_regs_dirty = 0xFFFF;
@@ -348,6 +342,16 @@ void MAX2831::set_lpf_rf_bandwidth_rx(const uint32_t bandwidth_minimum) {
#ifdef PRALINE
uint32_t actual_bw = bandwidth_minimum;
/* The MAX2831 internal analog low-pass filter cannot go below 1.75 MHz.
* For narrow-band signals (bandwidth < 1.75 MHz), we enable the custom
* external Anti-Aliasing (AA) filter on pin P1_14 to prevent aliasing.
*/
if (actual_bw <= 1750000) {
gpio_control::aa_en.set(); // Enable external narrow AA filter
} else {
gpio_control::aa_en.clear(); // Disable external AA filter for wideband operations
}
_desired_lpf_bw = actual_bw;
if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) {
set_lpf_bandwidth_internal(actual_bw);
@@ -361,6 +365,9 @@ void MAX2831::set_lpf_rf_bandwidth_rx(const uint32_t bandwidth_minimum) {
void MAX2831::set_lpf_rf_bandwidth_tx(const uint32_t bandwidth_minimum) {
_desired_lpf_bw = bandwidth_minimum;
#ifdef PRALINE
gpio_control::aa_en.clear(); // Disable external AA filter for wideband operations
#endif
if (_mode == Mode::Transmit || _mode == Mode::Tx_Calibration) {
set_lpf_bandwidth_internal(bandwidth_minimum);
}
+6
View File
@@ -99,9 +99,11 @@ constexpr uint32_t pll_factor = 1.0 / (4.0 / 3.0 / reference_frequency) + 0.5;
void MAX2837::init() {
set_mode(Mode::Shutdown);
#ifndef PRALINE
gpio_max283x_enable.output();
gpio_max2837_rxenable.output();
gpio_max2837_txenable.output();
#endif
_map.r.tx_gain.TXVGA_GAIN_SPI_EN = 1;
_map.r.tx_gain.TXVGA_GAIN_MSB_SPI_EN = 1;
@@ -157,9 +159,11 @@ void MAX2837::set_tx_LO_iq_phase_calibration(const size_t v) {
// TX calibration , Logic pins , ENABLE, RXENABLE, TXENABLE = 1,0,1 (5dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
set_mode(Mode::Tx_Calibration); // write to ram 3 LOGIC Pins .
#ifndef PRALINE
gpio_max283x_enable.output();
gpio_max2837_rxenable.output();
gpio_max2837_txenable.output();
#endif
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
@@ -348,9 +352,11 @@ void MAX2837::set_rx_LO_iq_phase_calibration(const size_t v) {
// RX calibration , Logic pins , ENABLE, RXENABLE, TXENABLE = 1,1,0 (3dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
set_mode(Mode::Rx_Calibration); // write to ram 3 LOGIC Pins .
#ifndef PRALINE
gpio_max283x_enable.output();
gpio_max2837_rxenable.output();
gpio_max2837_txenable.output();
#endif
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
+6
View File
@@ -100,8 +100,10 @@ static int_fast8_t requested_rx_vga_gain = 0;
void MAX2839::init() {
set_mode(Mode::Shutdown);
#ifndef PRALINE
gpio_max283x_enable.output();
gpio_max2839_rxtx.output();
#endif
_map.r.rxrf_1.MIMOmode = 1; /* enable RXINB */
@@ -150,8 +152,10 @@ void MAX2839::set_tx_LO_iq_phase_calibration(const size_t v) {
// TX calibration , 2 x Logic pins , ENABLE, RXENABLE = 1,0, (2dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
set_mode(Mode::Tx_Calibration); // write to ram 3 LOGIC Pins .
#ifndef PRALINE
gpio_max283x_enable.output(); // max2839 has only 2 x pins + regs to decide mode.
gpio_max2839_rxtx.output(); // Here is combined rx & tx pin in one port.
#endif
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
@@ -390,8 +394,10 @@ void MAX2839::set_rx_LO_iq_phase_calibration(const size_t v) {
// RX calibration , Logic pins , ENABLE, RXENABLE, TXENABLE = 1,1,0 (3dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
set_mode(Mode::Rx_Calibration); // write to ram 3 LOGIC Pins .
#ifndef PRALINE
gpio_max283x_enable.output(); // max2839 has only 2 x pins + regs to decide mode.
gpio_max2839_rxtx.output(); // Here is combined rx & tx pin in one port.
#endif
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
+6
View File
@@ -175,8 +175,14 @@ struct SynthConfig {
*/
void RFFC507x::init() {
#ifdef PRALINE
gpio_control::rf5072_mix_en.set(); // RF5072_MIX_EN
#endif
gpio_rffc5072_resetx.set();
#ifndef PRALINE
gpio_rffc5072_resetx.output();
#endif
reset();
_bus.init();
+2
View File
@@ -33,8 +33,10 @@ void SPI::init() {
gpio_rffc5072_select.set();
gpio_rffc5072_clock.clear();
#ifndef PRALINE
gpio_rffc5072_select.output();
gpio_rffc5072_clock.output();
#endif
gpio_rffc5072_data.input();
gpio_rffc5072_data.clear();
+5
View File
@@ -144,6 +144,9 @@ Continuous (Fox-oring)
#include "lpc43xx.inc"
#include "rffc507x.hpp" /* c/m, avoiding initial short ON Ant_DC_Bias pulse, from cold reset */
#include "gpio.hpp"
rffc507x::RFFC507x first_if;
ui::SystemView* system_view_ptr;
@@ -185,6 +188,8 @@ static void event_loop() {
int main(void) {
rtc_reset_default();
power_control::vaa_power_on();
#ifndef PRALINE // Do not perform quick set up of GP01_RFF507X = 1 for PRALINE
first_if.init(); /* To avoid initial short Ant_DC_Bias pulse ,we need quick set up GP01_RFF507X =1 */
#endif
+17 -22
View File
@@ -56,11 +56,14 @@ using asahi_kasei::ak4951::AK4951;
#include "i2cdevmanager.hpp"
#include "battery.hpp"
#include "gpio.hpp"
extern "C" {
#include "platform_detect.h"
#ifdef PRALINE
#include "fpga_bridge.h"
#include "board.h"
#endif
}
@@ -360,19 +363,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 +377,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
@@ -567,12 +564,6 @@ static void initialize_boot_splash_screen() {
*/
init_status_t init() {
#ifdef PRALINE
/* 1. HOLD FPGA IN RESET (Active Low) */
// P5_2 is GPIO2[11] (FPGA CRESET)
palClearPad(GPIO2, 11);
#endif
set_idivc_base_clocks(cgu::CLK_SEL::IDIVC);
i2c0.start(i2c_config_boot_clock);
@@ -600,9 +591,12 @@ init_status_t init() {
}
/* Cache some configuration data from persistent memory. */
rtc_time::dst_init();
chThdSleepMilliseconds(10);
power_control::core_power_on();
clock_manager.init_clock_generator();
i2c0.stop();
@@ -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
-1
View File
@@ -67,7 +67,6 @@ extern ReceiverModel receiver_model;
extern TransmitterModel transmitter_model;
extern uint32_t bl_tick_counter;
extern bool antenna_bias;
extern uint16_t touch_threshold;
extern TemperatureLogger temperature_logger;
+11 -5
View File
@@ -390,11 +390,9 @@ 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);
/* Praline: P2_12 = GPIO1[12], ANT_BIAS_EN_N (active LOW) */
rf_path.set_ant_bias(on);
#else
if (hackrf_r9) {
gpio_r9_not_ant_pwr.write(on ? 0 : 1);
@@ -404,6 +402,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);
}
@@ -619,4 +625,4 @@ uint32_t register_read(const size_t register_number) {
} /* namespace debug */
} /* namespace radio */
} /* namespace radio */
+2
View File
@@ -54,6 +54,8 @@ void set_baseband_filter_bandwidth_rx(const uint32_t bandwidth_minimum);
void set_baseband_filter_bandwidth_tx(const uint32_t bandwidth_minimum);
void set_baseband_rate(const uint32_t rate);
void set_antenna_bias(const bool on);
bool get_mixer_invert();
bool get_baseband_invert();
void set_tx_max283x_iq_phase_calibration(const size_t v);
void set_rx_max283x_iq_phase_calibration(const size_t v);
+11 -11
View File
@@ -46,14 +46,6 @@ struct PralineConfig {
bool rf_amp_en;
bool ant_bias_en_n; // Inverted: 0 = bias enabled
static void gpio_init() {
gpio_tx_enable.output();
gpio_mix_bypass.output();
gpio_lpf_enable.output();
gpio_rf_amp_enable.output();
gpio_ant_bias_disable.output();
}
void apply() const {
gpio_tx_enable.write(tx_en);
gpio_mix_bypass.write(mix_bypass); // Control RF path mixer
@@ -231,7 +223,6 @@ constexpr Config get_config(
void Path::init() {
#ifdef PRALINE
PralineConfig::gpio_init();
/* Set safe initial state: RX mode, mixer enabled, LPF on, amp off, no bias */
PralineConfig config = {
.tx_en = false,
@@ -263,6 +254,15 @@ void Path::set_rf_amp(const bool new_rf_amp) {
update();
}
void Path::set_ant_bias(const bool new_ant_bias) {
ant_bias = new_ant_bias;
update();
}
bool Path::get_ant_bias() const {
return ant_bias;
}
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 +301,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;
/* Antenna bias */
config.ant_bias_en_n = !ant_bias;
config.apply();
+3 -1
View File
@@ -65,13 +65,15 @@ class Path {
void set_direction(const Direction direction);
void set_band(const Band band);
void set_rf_amp(const bool rf_amp);
void set_ant_bias(const bool ant_bias);
bool get_ant_bias() const;
Band get_band() const { return _band; } //_band is used solely for debugging purposes.
private:
Direction direction{Direction::Receive};
Band band{Band::Mid};
bool rf_amp{false};
bool ant_bias{false};
void update();
+1
View File
@@ -27,6 +27,7 @@
#include "lpc43xx_cpp.hpp"
#include "portapack_persistent_memory.hpp"
#include <ctime>
namespace rtc_time {
+14 -2
View File
@@ -72,19 +72,31 @@ void TransmitterModel::set_channel_bandwidth(uint32_t v) {
}
uint8_t TransmitterModel::tx_gain() const {
if (persistent_memory::config_tx_disabled()) {
return 0;
}
return std::min(settings_.tx_gain_db, portapack::persistent_memory::config_tx_gain_max_db());
}
void TransmitterModel::set_tx_gain(uint8_t v_db) {
settings_.tx_gain_db = v_db;
uint8_t newgain = v_db;
if (persistent_memory::config_tx_disabled()) {
newgain = 0;
} else if (v_db > portapack::persistent_memory::config_tx_gain_max_db()) {
newgain = portapack::persistent_memory::config_tx_gain_max_db();
}
settings_.tx_gain_db = newgain;
update_tx_gain();
}
bool TransmitterModel::rf_amp() const {
return settings_.rf_amp && !portapack::persistent_memory::config_tx_amp_disabled();
return settings_.rf_amp && !portapack::persistent_memory::config_tx_amp_disabled() && !portapack::persistent_memory::config_tx_disabled();
}
void TransmitterModel::set_rf_amp(bool enabled) {
if (enabled && (portapack::persistent_memory::config_tx_amp_disabled() || portapack::persistent_memory::config_tx_disabled())) {
return;
}
settings_.rf_amp = enabled;
update_rf_amp();
}
+4 -2
View File
@@ -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;
};
+2 -7
View File
@@ -28,7 +28,6 @@
#include "rtc_time.hpp"
#include "sd_card.hpp"
#include <algorithm>
#include "ui_external_items_menu_loader.hpp"
namespace ui {
@@ -61,7 +60,6 @@ BtnGridView::BtnGridView(
}
BtnGridView::~BtnGridView() {
ExternalItemsMenuLoader::unload_external_items();
}
void BtnGridView::set_max_rows(int rows) {
@@ -89,7 +87,6 @@ void BtnGridView::set_parent_rect(const Rect new_parent_rect) {
remove_child(item.get());
menu_item_views.clear();
menu_item_views.shrink_to_fit();
}
button_w = screen_width / rows_;
@@ -138,17 +135,15 @@ void BtnGridView::set_arrow_down_enabled(bool enabled) {
};
void BtnGridView::clear() {
// clear vector and release memory, not using swap since it's causing capture to glitch/fault
// clear vector, not using swap since it's causing capture to glitch/fault
menu_items.clear();
menu_items.shrink_to_fit();
// TODO(u-foka): Clean up my mess, move this somewhere to clear memory when the view is not visible, but not to be confused with clearing the menu items...
for (auto& item : menu_item_views)
remove_child(item.get());
// clear vector and release memory, not using swap since it's causing capture to glitch/fault
// clear vector, not using swap since it's causing capture to glitch/fault
menu_item_views.clear();
menu_item_views.shrink_to_fit();
}
void BtnGridView::add_items(std::initializer_list<GridItem> new_items, bool inhibit_update) {
+5 -1
View File
@@ -36,10 +36,14 @@ void Channel::paint(Painter& painter) {
const range_t<int> x_max_range{0, r.width() - 1};
const auto x_max = x_max_range.clip((max_db_ - db_min) * r.width() / db_delta);
const auto bar_style = (max_db_ >= overload_threshold_)
? Theme::getInstance()->fg_red
: Theme::getInstance()->fg_blue;
const Rect r0{r.left(), r.top(), x_max, r.height()};
painter.fill_rectangle(
r0,
Theme::getInstance()->fg_blue->foreground);
bar_style->foreground);
const Rect r1{r.left() + x_max, r.top(), 1, r.height()};
painter.fill_rectangle(
+11
View File
@@ -44,8 +44,19 @@ class Channel : public Widget {
void paint(Painter& painter) override;
// Opt-in receiver-overload tint: when the channel power (peak IQ magnitude
// in dBFS, 0 = full scale) reaches this threshold the bar is drawn red
// instead of blue, flagging that the analog gain is too high and the ADC is
// clipping. Default is disabled (threshold above the 0 dBFS ceiling) so
// existing users are unaffected.
void set_overload_threshold(int32_t db) {
overload_threshold_ = db;
set_dirty();
}
private:
int32_t max_db_;
int32_t overload_threshold_{INT32_MAX};
MessageHandlerRegistration message_handler_stats{
Message::ID::ChannelStatistics,
+93 -19
View File
@@ -22,6 +22,7 @@
#include "ui_menu.hpp"
#include "rtc_time.hpp"
#include <string_view>
namespace ui {
@@ -29,57 +30,112 @@ namespace ui {
void MenuItemView::set_item(MenuItem* item_) {
item = item_;
scroll_offset = 0;
can_scroll = false;
}
void MenuItemView::highlight() {
set_highlighted(true);
scroll_offset = 0;
set_dirty();
}
void MenuItemView::unhighlight() {
set_highlighted(false);
scroll_offset = 0;
set_dirty();
}
void MenuItemView::paint(Painter& painter) {
Coord offset_x{};
void MenuItemView::set_scroll_offset(size_t offset) {
if (can_scroll && scroll_offset != offset) {
scroll_offset = offset;
set_dirty();
}
}
void MenuItemView::paint(Painter& painter) {
if (!item) return;
const auto r = screen_rect();
const auto rect = screen_rect();
const auto paint_style = (highlighted() && (parent()->has_focus() || keep_highlight)) ? style().invert() : style();
const auto font_height = paint_style.font.line_height();
const int char_width = paint_style.font.char_width();
const int line_height = paint_style.font.line_height();
if (char_width == 0 || line_height == 0) return;
const int margin_x = char_width / 2;
ui::Color final_item_color = (highlighted() && (parent()->has_focus() || keep_highlight)) ? paint_style.foreground : item->color;
ui::Color final_bg_color = (highlighted() && (parent()->has_focus() || keep_highlight)) ? item->color : paint_style.background;
if (final_item_color.v == final_bg_color.v) final_item_color = paint_style.foreground;
painter.fill_rectangle(
r,
final_bg_color);
painter.fill_rectangle(rect, final_bg_color);
Coord offset_x = 0;
if (item->bitmap) {
painter.draw_bitmap(
{r.location().x() + 4, r.location().y() + 4},
{rect.location().x() + 4, rect.location().y() + 4},
*item->bitmap,
final_item_color,
final_bg_color);
offset_x = 26;
} else
} else {
offset_x = 0;
}
Style text_style{
.font = paint_style.font,
.background = final_bg_color,
.foreground = final_item_color};
Style text_style{.font = paint_style.font, .background = final_bg_color, .foreground = final_item_color};
painter.draw_string(
{r.location().x() + offset_x, r.location().y() + (r.size().height() - font_height) / 2},
text_style,
item->text);
std::string_view full_text = item->text;
std::string_view file_name = full_text;
std::string_view file_size_text = "";
auto tab_pos = full_text.find('\t');
if (tab_pos != std::string_view::npos) {
file_size_text = full_text.substr(tab_pos + 1);
file_name = full_text.substr(0, tab_pos);
}
int available_width_px = rect.width() - offset_x;
if (available_width_px <= 0) return;
size_t max_name_chars = available_width_px / char_width;
if (max_name_chars == 0) return;
if (!file_size_text.empty() && max_name_chars > file_size_text.length()) {
max_name_chars = max_name_chars - file_size_text.length() - 1;
}
if (max_name_chars == 0) return;
can_scroll = (file_name.length() > max_name_chars);
std::string_view display_name = file_name;
if (file_name.length() > max_name_chars) {
if (highlighted()) {
size_t max_scroll = file_name.length() - max_name_chars;
size_t actual_offset = scroll_offset % (max_scroll + 1);
display_name = file_name.substr(actual_offset, max_name_chars);
} else {
display_name = file_name.substr(0, max_name_chars);
}
}
Coord text_y = rect.location().y() + (rect.height() - line_height) / 2;
painter.draw_string({rect.location().x() + offset_x, text_y}, text_style, display_name);
if (!file_size_text.empty()) {
int file_size_width = static_cast<int>(file_size_text.length()) * char_width;
int file_size_x = rect.width() - file_size_width - margin_x;
// Csak akkor rajzoljuk ki, ha pozitív koordinátára esik és nem takarja el az ikont/nevet
if (file_size_x > offset_x) {
painter.draw_string({rect.location().x() + static_cast<Coord>(file_size_x), text_y}, text_style, file_size_text);
}
}
}
/* MenuView **************************************************************/
@@ -134,6 +190,20 @@ void MenuView::set_parent_rect(const Rect new_parent_rect) {
update_items();
}
void MenuView::increment_scroll() {
if (menu_items.empty()) return;
const size_t view_index = highlighted_item - offset;
if (view_index >= menu_item_views.size()) {
return;
}
// The MenuView checks if the currently highlighted item needs scrolling
scroll_offset++;
auto* view = item_view(view_index);
if (view) {
view->set_scroll_offset(scroll_offset);
}
}
void MenuView::on_tick_second() {
if (more && blink)
arrow_more.set_foreground(Theme::getInstance()->bg_darkest->foreground);
@@ -141,8 +211,9 @@ void MenuView::on_tick_second() {
arrow_more.set_foreground(Theme::getInstance()->bg_darkest->background);
blink = !blink;
arrow_more.set_dirty();
increment_scroll();
}
void MenuView::clear() {
@@ -153,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();
+10
View File
@@ -67,9 +67,13 @@ class MenuItemView : public Widget {
void highlight();
void unhighlight();
void set_scroll_offset(size_t offset);
private:
MenuItem* item{nullptr};
bool keep_highlight = false;
size_t scroll_offset{0};
bool can_scroll{false};
};
class MenuView : public View {
@@ -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 */
+63 -7
View File
@@ -39,7 +39,13 @@ using namespace portapack;
namespace ui {
/* Gets a style indicating total TX gain level. */
static const Style* get_style_for_gain(uint8_t tot_gain) {
static const Style* get_style_for_gain(uint8_t tot_gain, uint8_t gain) {
if (persistent_memory::config_tx_disabled()) {
return Theme::getInstance()->fg_dark; // disabled color
}
if (gain == persistent_memory::config_tx_gain_max_db() && gain != max2837::tx::gain_db_range.maximum) { // only if limited
return Theme::getInstance()->fg_cyan;
}
if (tot_gain > POWER_THRESHOLD_HIGH) return Theme::getInstance()->fg_red;
if (tot_gain > POWER_THRESHOLD_MED)
@@ -50,7 +56,21 @@ static const Style* get_style_for_gain(uint8_t tot_gain) {
return nullptr; // Uses default.
}
static const Style* get_style_for_amp(uint8_t tot_gain) {
if (persistent_memory::config_tx_disabled() || persistent_memory::config_tx_amp_disabled()) {
return Theme::getInstance()->fg_dark; // disabled color
}
if (tot_gain > POWER_THRESHOLD_HIGH) return Theme::getInstance()->fg_red;
if (tot_gain > POWER_THRESHOLD_MED)
return Theme::getInstance()->fg_orange;
if (tot_gain > POWER_THRESHOLD_LOW)
return Theme::getInstance()->fg_yellow;
return nullptr; // Uses default.
}
/* TransmitterView *******************************************************/
void TransmitterView::paint(Painter& painter) {
@@ -95,12 +115,12 @@ void TransmitterView::on_tx_amp_changed(bool rf_amp) {
void TransmitterView::update_gainlevel_styles() {
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
auto style = get_style_for_gain(tot_gain);
auto style = get_style_for_gain(tot_gain, transmitter_model.tx_gain());
auto styleamp = get_style_for_amp(tot_gain);
field_gain.set_style(style);
text_gain.set_style(style);
field_amp.set_style(style);
text_amp.set_style(style);
field_amp.set_style(styleamp);
text_amp.set_style(styleamp);
}
void TransmitterView::set_transmitting(const bool transmitting) {
@@ -199,6 +219,24 @@ TransmitterView::TransmitterView(
on_start();
}
};
if (persistent_memory::config_tx_amp_disabled() || persistent_memory::config_tx_disabled()) { // amp disabled / tx disabled
field_amp.set_value(0);
field_amp.set_focusable(false);
}
if (persistent_memory::config_tx_disabled()) { // tx disabled
field_gain.set_value(0);
field_gain.set_focusable(false);
} else {
if (persistent_memory::config_tx_gain_max_db() < max2837::tx::gain_db_range.maximum) {
field_gain.set_range(max2837::tx::gain_db_range.minimum, persistent_memory::config_tx_gain_max_db());
if (field_gain.value() > persistent_memory::config_tx_gain_max_db()) {
field_gain.set_value(persistent_memory::config_tx_gain_max_db());
}
}
}
update_gainlevel_styles();
}
TransmitterView::~TransmitterView() {
@@ -252,16 +290,34 @@ TransmitterView2::TransmitterView2(Point pos, bool short_ui) {
update_gainlevel_styles();
};
if (persistent_memory::config_tx_amp_disabled() || persistent_memory::config_tx_disabled()) { // amp disabled / tx disabled
field_amp.set_value(0);
field_amp.set_focusable(false);
}
if (persistent_memory::config_tx_disabled()) { // tx disabled
field_gain.set_value(0);
field_gain.set_focusable(false);
} else {
if (persistent_memory::config_tx_gain_max_db() < max2837::tx::gain_db_range.maximum) {
field_gain.set_range(max2837::tx::gain_db_range.minimum, persistent_memory::config_tx_gain_max_db());
if (field_gain.value() > persistent_memory::config_tx_gain_max_db()) {
field_gain.set_value(persistent_memory::config_tx_gain_max_db());
}
}
}
update_gainlevel_styles();
}
void TransmitterView2::update_gainlevel_styles() {
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
auto style = get_style_for_gain(tot_gain);
auto style = get_style_for_gain(tot_gain, transmitter_model.tx_gain());
auto styleamp = get_style_for_amp(tot_gain);
text_labels.set_style(style);
field_gain.set_style(style);
field_amp.set_style(style);
field_amp.set_style(styleamp);
}
} /* namespace ui */
@@ -9,13 +9,7 @@
namespace ui {
/* static */ std::vector<std::unique_ptr<DynamicBitmap<16, 16>>> ExternalItemsMenuLoader::bitmaps;
// to save ram when entering an app
void ExternalItemsMenuLoader::unload_external_items() {
bitmaps.clear();
bitmaps.shrink_to_fit();
}
/* static */ std::vector<DynamicBitmap<16, 16>> ExternalItemsMenuLoader::bitmaps;
// iterates over all possible ext apps-s, and if it is runnable on the current system, it'll call the callback, and pass minimal info. used to print to console, and for autostart setting's app list. where the minimal info is enough
// please keep in sync with load_external_items
@@ -103,9 +97,8 @@ void ExternalItemsMenuLoader::unload_external_items() {
}
}
std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_external_items(app_location_t app_location, NavigationView& nav) {
/* static */ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_external_items(app_location_t app_location, NavigationView& nav) {
bitmaps.clear();
bitmaps.shrink_to_fit();
std::vector<GridItemEx> external_apps;
@@ -133,8 +126,8 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
gridItem.color = Color((uint16_t)appInfo->icon_color);
auto dyn_bmp = std::make_unique<DynamicBitmap<16, 16>>(appInfo->bitmap_data);
gridItem.bitmap = dyn_bmp->bitmap();
auto dyn_bmp = DynamicBitmap<16, 16>{appInfo->bitmap_data};
gridItem.bitmap = dyn_bmp.bitmap();
bitmaps.push_back(std::move(dyn_bmp));
gridItem.on_select = [&nav, appInfo, i]() {
@@ -211,8 +204,8 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
if (versionMatches) {
gridItem.color = Color((uint16_t)application_information.icon_color);
auto dyn_bmp = std::make_unique<DynamicBitmap<16, 16>>(application_information.bitmap_data);
gridItem.bitmap = dyn_bmp->bitmap();
auto dyn_bmp = DynamicBitmap<16, 16>{application_information.bitmap_data};
gridItem.bitmap = dyn_bmp.bitmap();
bitmaps.push_back(std::move(dyn_bmp));
gridItem.on_select = [&nav, app_location, filePath]() {
@@ -262,8 +255,8 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
gridItem.color = Color((uint16_t)application_information.icon_color);
auto dyn_bmp = std::make_unique<DynamicBitmap<16, 16>>(application_information.bitmap_data);
gridItem.bitmap = dyn_bmp->bitmap();
auto dyn_bmp = DynamicBitmap<16, 16>{application_information.bitmap_data};
gridItem.bitmap = dyn_bmp.bitmap();
bitmaps.push_back(std::move(dyn_bmp));
gridItem.on_select = [&nav, app_location, filePath]() {
@@ -357,8 +350,6 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
if (checksum != EXT_APP_EXPECTED_CHECKSUM)
return false;
nav.pop();
nav.set_last_menu_went_deeper(true);
application_information.externalAppEntry(nav);
return true;
}
@@ -395,8 +386,7 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
}
}
nav.set_last_menu_went_deeper(true);
nav.replace<StandaloneView>(app_image);
nav.push<StandaloneView>(app_image);
return true;
}
@@ -410,8 +400,7 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
}
}
nav.set_last_menu_went_deeper(true);
nav.replace<StandaloneView>(app_image);
nav.push<StandaloneView>(app_image);
return true;
}
@@ -33,38 +33,24 @@
#define EXT_APP_EXPECTED_CHECKSUM 0x00000000
namespace ui {
template <size_t Width, size_t Height>
class DynamicBitmap {
public:
static constexpr size_t buffer_size = Width * Height / (sizeof(uint8_t) * 8);
static constexpr size_t buffer_size = Width * Height / (sizeof(uint8_t) * 8); // one bit per pixel
// Main constructor
DynamicBitmap(const uint8_t data[buffer_size])
: _bitmap{{Width, Height}, _buffer.data()} {
std::memcpy(_buffer.data(), data, buffer_size);
: _buffer(buffer_size, 0),
_bitmap{new Bitmap{{Width, Height}, &_buffer[0]}} {
memcpy(&_buffer[0], data, buffer_size);
}
DynamicBitmap(const DynamicBitmap& other)
: _buffer(other._buffer),
_bitmap{{Width, Height}, _buffer.data()} {}
DynamicBitmap(DynamicBitmap&& other) noexcept
: _buffer(std::move(other._buffer)),
_bitmap{{Width, Height}, _buffer.data()} {}
DynamicBitmap& operator=(const DynamicBitmap& other) = delete;
DynamicBitmap& operator=(DynamicBitmap&& other) noexcept = delete;
// Destructor (Default is fine, no heap memory to free manually)
~DynamicBitmap() = default;
const Bitmap* bitmap() const { return &_bitmap; }
const Bitmap* bitmap() { return _bitmap.get(); }
private:
// Order matters: _buffer must be declared before _bitmap
// so it is initialized first and its .data() pointer is valid.
std::array<uint8_t, buffer_size> _buffer{};
Bitmap _bitmap{};
// Allocating both members so the class is movable without invalidation.
std::vector<uint8_t> _buffer;
std::unique_ptr<Bitmap> _bitmap;
};
class ExternalItemsMenuLoader {
@@ -79,10 +65,9 @@ class ExternalItemsMenuLoader {
static bool run_standalone_app(ui::NavigationView&, std::filesystem::path);
static bool run_module_app(ui::NavigationView&, uint8_t*, size_t);
static void load_all_external_items_callback(std::function<void(AppInfoConsole&)> callback, bool module_included = false);
static void unload_external_items();
private:
static std::vector<std::unique_ptr<DynamicBitmap<16, 16>>> bitmaps;
static std::vector<DynamicBitmap<16, 16>> bitmaps;
};
} // namespace ui
+48 -79
View File
@@ -73,6 +73,8 @@
#include "file_path.hpp"
#include "ff.h"
#include "i2cdev_max17055.hpp"
#include <locale>
#include <codecvt>
@@ -85,47 +87,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 +132,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;
}
}
@@ -337,10 +336,10 @@ void SystemStatusView::on_battery_data(const BatteryStateMessage* msg) {
was_charging = msg->on_charger;
if (!pmem::ui_hide_numeric_battery()) {
battery_text.set_battery(msg->valid_mask, msg->percent, msg->on_charger);
battery_text.set_battery(msg->valid_mask, msg->percent, msg->on_charger, msg->battMayChanged);
}
if (!pmem::ui_hide_battery_icon()) {
battery_icon.set_battery(msg->valid_mask, msg->percent, msg->on_charger);
battery_icon.set_battery(msg->valid_mask, msg->percent, msg->on_charger, msg->battMayChanged);
}
}
@@ -633,7 +632,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 +755,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 +785,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 {
@@ -896,6 +882,10 @@ void SystemMenuView::hackrf_mode(NavigationView& nav) {
YESNO,
[this](bool choice) {
if (choice) {
i2cdev::I2cDev_MAX17055* dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
if (dev) {
dev->sleep_config(false); // don't enable sleep even if no i2c communication. so we won't lose any data. on reboot (like exit hackrf mode) we set it to true again.
}
EventDispatcher::request_stop();
}
});
@@ -931,27 +921,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 +937,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 +997,16 @@ void SystemView::toggle_overlay() {
this->set_dirty();
shared_memory.request_m4_performance_counter = last_perf_counter_status;
overlay_active = 0;
break;
}
}
void SystemView::paint_overlay() {
static bool last_paint_state = false;
if (overlay_active) {
// paint background only every other second
if ((((chTimeNow() >> 10) & 0x01) == 0x01) == last_paint_state)
return;
last_paint_state = !last_paint_state;
if (overlay_active == 1 && overlay)
overlay->set_dirty();
+3 -14
View File
@@ -50,6 +50,7 @@
#include "lfsr_random.hpp"
#include "sd_card.hpp"
#include "external_app.hpp"
#include "view_factory.hpp"
#include "battery.hpp"
// for incrementing fake date when RTC battery is dead
@@ -59,9 +60,6 @@ using namespace sd_card;
namespace ui {
class NavigationView;
using ViewProducer = std::unique_ptr<View> (*)(NavigationView&);
void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc);
void add_external_items(NavigationView& nav, app_location_t location, BtnGridView& grid, uint8_t error_tile_pos, bool show_error_tile = true);
@@ -71,7 +69,7 @@ enum modal_t {
ABORT
};
// Should only be used as part of the appList in NavigationView.
// Should only be used as part of the appList in NavigationView, the viewFactory will never be destroyed.
class AppInfo {
public:
const char* id; // MUST be unique! Used by serial command to start the app so it also has to make sense
@@ -79,7 +77,7 @@ class AppInfo {
app_location_t menuLocation;
Color iconColor;
const Bitmap* icon;
ViewProducer producer;
ViewFactoryBase* viewFactory; // Never destroyed, and I believe it's ok ;) Having a unique_ptr here breaks the initializer list of appList
};
struct AppInfoConsole {
@@ -140,13 +138,6 @@ class NavigationView : public View {
bool StartAppByName(const char* name); // Starts a View (app) by name stored in appListFC. This is to start apps from console
void handle_autostart();
void store_last_menu_name(const std::string& name) { last_menu_name_ = name; }
std::string get_last_menu_name() const { return last_menu_name_; }
size_t view_stack_size() const { return view_stack.size(); }
bool get_last_menu_went_deeper() { return last_menu_went_deeper; }
void set_last_menu_went_deeper(bool went_deeper) { last_menu_went_deeper = went_deeper; }
private:
struct ViewState {
std::unique_ptr<View> view;
@@ -159,8 +150,6 @@ class NavigationView : public View {
void free_view();
void update_view();
std::string last_menu_name_{}; // this stores the last menu name, when we replace the menu with the app, we'll know, where to navigate back
bool last_menu_went_deeper = false;
};
/* Holds widgets and grows dynamically toward the left.
+1 -1
View File
@@ -157,7 +157,7 @@ static void cmd_flash(BaseSequentialStream* chp, int argc, char* argv[]) {
// call nav with flash
auto open_view = nav->push<ui::FlashUtilityView>();
chprintf(chp, "Flashing started\r\n");
chThdSleepMilliseconds(50); // to give display some time to paint the screen
chThdSleepMilliseconds(150); // to give display some time to paint the screen
open_view->wait_till_loaded(); // also wait for first frame sync
if (!open_view->flash_firmware(path.native())) {
chprintf(chp, "error\r\n");
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright 2024 Tamas Eisenberger <e.tamas@iwstudio.hu>
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __VIEW_FACTORY_HPP__
#define __VIEW_FACTORY_HPP__
#include "view_factory_base.hpp"
namespace ui {
template <typename T>
class ViewFactory : public ViewFactoryBase {
public:
virtual std::unique_ptr<View> produce(NavigationView& nav) const override {
return std::unique_ptr<View>(new T(nav));
}
};
} // namespace ui
#endif //__VIEW_FACTORY_HPP__
@@ -0,0 +1,28 @@
/*
* Copyright 2024 Tamas Eisenberger <e.tamas@iwstudio.hu>
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "view_factory_base.hpp"
namespace ui {
ViewFactoryBase::~ViewFactoryBase() {}
} // namespace ui
@@ -0,0 +1,40 @@
/*
* Copyright 2024 Tamas Eisenberger <e.tamas@iwstudio.hu>
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __VIEW_FACTORY_BASE_HPP__
#define __VIEW_FACTORY_BASE_HPP__
#include <memory>
#include "ui_widget.hpp"
namespace ui {
class NavigationView;
class ViewFactoryBase {
public:
virtual ~ViewFactoryBase();
virtual std::unique_ptr<View> produce(NavigationView& nav) const = 0;
};
} // namespace ui
#endif //__VIEW_FACTORY_BASE_HPP__
+338 -272
View File
@@ -1,272 +1,338 @@
/*
* 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;
}
/*
* 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 <algorithm>
#include <cmath>
#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);
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];
// AFC: remove the estimated carrier frequency offset from the raw delta
// before any further processing. Done on the per-sample value so the
// 12-sample accumulator below tracks it naturally.
phase_delta -= freq_offset_est;
// Keep the (de-biased) per-sample delta for AFC averaging over the carrier.
const float sample_phase_delta = phase_delta;
// Let's sum phase delta over a 12 sample window to get the full phase jump
phase_delta_acc -= phase_delta_buffer[phase_delta_index];
phase_delta_buffer[phase_delta_index] = phase_delta;
phase_delta_acc += phase_delta_buffer[phase_delta_index];
phase_delta_index = (phase_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: {
// Continuously pull the AFC estimate toward the mean per-sample
// rotation so the accumulator self-centers for any offset up to
// the discriminator Nyquist (~+/-24 kHz). On noise the de-biased
// deltas average to ~0, so the estimate stays put; on a real
// carrier it converges within a few ms and the thresholds below
// then see a de-biased signal regardless of the actual offset.
// Only update AFC when the per-sample phase delta is small
// (large jumps indicate noise or transient, which would cause
// a random-walk drift if used for AFC updates).
if (fabsf(sample_phase_delta) <= AFC_UPDATE_PHASE_MAX) {
freq_offset_est += AFC_TRACK_ALPHA * sample_phase_delta;
// Bounds checking: limit to ±5 kHz (~0.654 rad/sample at 48 kHz)
freq_offset_est = std::clamp(freq_offset_est, -0.654f, 0.654f);
// Track AFC convergence using Welford's online algorithm
afc_convergence_n++;
float delta = freq_offset_est - afc_mean;
afc_mean += delta / afc_convergence_n;
float delta2 = freq_offset_est - afc_mean;
afc_m2 += delta * delta2;
}
// We are waiting for a 160ms empty carrier => phase should be stable during this period
// Use a symmetric threshold: once AFC has removed the bias a stable
// carrier sits near 0, so both positive and negative excursions of
// the accumulated delta indicate the carrier is not yet stable.
if (filtered_rise_detect(fabsf(phase_delta) >= 0.6f)) {
stability_counter = 0;
// Reset convergence tracking when the carrier is not stable,
// so variance is measured only over the current stable window.
afc_mean = 0.0f;
afc_m2 = 0.0f;
afc_convergence_n = 0;
} else {
stability_counter++;
// Check both phase stability AND AFC convergence before transitioning
if (stability_counter > CARRIER_SAMPLES_THRESHOLD) {
float afc_variance = (afc_convergence_n > 1) ? afc_m2 / (afc_convergence_n - 1) : 0.0f;
if (afc_variance < AFC_CONVERGENCE_THRESHOLD) {
// Both phase and AFC have converged, go to locked state
current_state = CARRIER_LOCKED;
// Reset carrier accumulators so the latched update uses
// only the residual measured while in the locked window
carrier_phase_sum = 0.0f;
carrier_phase_n = 0;
frame_sample_count = 0;
}
}
}
} break;
case CARRIER_LOCKED:
// Carrier is locked: this is the clean unmodulated carrier window.
// Average the per-sample phase delta here to estimate the residual
// frequency offset (rad/sample) used for AFC.
carrier_phase_sum += sample_phase_delta;
carrier_phase_n++;
// Carrier is locked, we now wait for a phase 1.1 rad phase jump corresponding to the beginning of the frame
// Let's use a 0.7 phase jump threshold
if (filtered_rise_detect(phase_delta >= 0.7f)) {
// Latch the AFC estimate from the carrier we just measured so it
// applies to the data burst that starts now. Accumulate so the
// residual is folded into any prior estimate.
if (carrier_phase_n > 0) {
freq_offset_est += carrier_phase_sum / carrier_phase_n;
// Bounds checking: limit to ±5 kHz (~0.654 rad/sample at 48 kHz)
freq_offset_est = std::clamp(freq_offset_est, -0.654f, 0.654f);
}
// 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 opposite 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 occurred 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;
// Reset AFC so the next burst is re-estimated from its own carrier preamble.
freq_offset_est = 0.0f;
carrier_phase_sum = 0.0f;
carrier_phase_n = 0;
// Reset AFC convergence tracking for next frame
afc_mean = 0.0f;
afc_m2 = 0.0f;
afc_convergence_n = 0;
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;
}
+296 -268
View File
@@ -1,268 +1,296 @@
/*
* 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__*/
/*
* 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
// Size of short frame (bits)
#define COSPAS_SHORT_FRAME_SIZE 112
// Preamble for real frames
#define COSPAS_REAL_PREAMBLE 0b1111'1111'1111'1110'0010'1111
// Preamble for test frames
#define COSPAS_TEST_PREAMBLE 0b1111'1111'1111'1110'1101'0000
// Dedicated EPIRB PacketBuilder
// Uses dedicated 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_ACCUMULATOR = 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 (12 samples)
static constexpr size_t PHASE_DELTA_ACC_SIZE = SAMPLES_PER_SYMBOL / 5; // 12 samples
float phase_delta_buffer[PHASE_DELTA_ACC_SIZE] = {0.0f};
size_t phase_delta_index = 0;
float phase_delta_acc = 0.0f;
// Automatic Frequency Control (AFC)
// A residual carrier frequency offset between the tuner and the beacon shows
// up as a constant per-sample phase rotation. We measure its mean over the
// unmodulated carrier preamble and subtract it from every raw phase delta so
// the carrier-stability detection and the +/-2.2 rad data jumps stay centered.
// Current estimate (rad/sample), removed from each raw phase delta.
float freq_offset_est = 0.0f;
// Carrier-tracking loop gain. Applied per sample in IDLE so the estimate
// pulls in any offset within the discriminator's +/-SAMPLE_RATE/2 (~24 kHz)
// range *before* the carrier-detection thresholds run. First-order loop with
// time constant ~1/ALPHA samples (= 200 samples ~ 4 ms at 48 kHz). Tuned so a
// +/-5 kHz offset (0.654 rad/sample) decays the 12-sample accumulator below
// the 0.6 rad lock threshold in ~11 ms (~13 ms at 7 kHz) -- well inside the
// 160 ms preamble / 80 ms stability window, even if reception starts partway
// through the carrier -- while keeping added acquisition jitter negligible.
static constexpr float AFC_TRACK_ALPHA = 0.005f;
// AFC update gating: ignore large per-sample phase jumps (likely noise)
static constexpr float AFC_UPDATE_PHASE_MAX = 0.8f; // rad/sample
// AFC Convergence detection: track variance of AFC estimate to ensure it has stabilized
// before transitioning from IDLE to CARRIER_LOCKED state
static constexpr float AFC_CONVERGENCE_THRESHOLD = 0.001f; // Max variance for convergence
float afc_mean = 0.0f; // Running mean of AFC estimate
float afc_m2 = 0.0f; // Sum of squared differences (Welford's algorithm)
uint32_t afc_convergence_n = 0; // Sample count for AFC convergence calculation
// Running mean of the raw phase delta while a stable carrier is present.
float carrier_phase_sum = 0.0f;
uint32_t carrier_phase_n = 0;
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__*/
+10 -10
View File
@@ -148,8 +148,8 @@ static struct gpio gpio_h1r9_hw_sync_enable = GPIO(5, 5);
#endif
// clang-format on
/* i2c0, i2c1, and i2c_config_si5351c_fast_clock are defined in
* hackrf/firmware/common/i2c_bus.c and si5351c.c; use those instead of
/* 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 = {
@@ -553,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);
@@ -561,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:
@@ -615,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_PLL_BOTH);
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.
@@ -784,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;
}
File diff suppressed because it is too large Load Diff
@@ -33,12 +33,10 @@
#ifdef __cplusplus
extern "C" {
#endif
void boardInit(void);
void boardInit(void);
void configure_pins_portapack(void);
void configure_pins_portapack(void);
void vaa_power_on(void);
void vaa_power_off(void);
#ifdef __cplusplus
}
#endif
@@ -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 {
@@ -198,34 +198,6 @@ static void ssp1_init_ice40(void) {
SSP1_CR1_LOCAL = SSP_CR1_SSE;
}
// Configure SSP1 pins via SCU
static void configure_ssp1_pins(void) {
// P1_3 = SSP1_MISO (function 5)
MMIO32_LOCAL(SCU_SSP1_CIPO_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION5_LOCAL;
// P1_4 = SSP1_MOSI (function 5)
MMIO32_LOCAL(SCU_SSP1_COPI_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION5_LOCAL;
// P1_19 = SSP1_SCK (function 1)
MMIO32_LOCAL(SCU_SSP1_SCK_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION1_LOCAL;
}
// Configure FPGA control pins via SCU and GPIO
static void configure_fpga_control_pins(void) {
// P5_2 = GPIO2[11] = CRESET (function 0, output)
MMIO32_LOCAL(SCU_FPGA_CRESET_LOCAL) = SCU_GPIO_NOPULL_LOCAL | SCU_CONF_FUNCTION0_LOCAL;
// P4_10 = GPIO5[14] = CDONE (function 4, input with pullup)
MMIO32_LOCAL(SCU_FPGA_CDONE_LOCAL) = SCU_GPIO_PUP_LOCAL | SCU_CONF_FUNCTION4_LOCAL;
// P5_1 = GPIO2[10] = SPI_CS (function 0, output)
MMIO32_LOCAL(SCU_FPGA_SPI_CS_LOCAL) = SCU_GPIO_NOPULL_LOCAL | SCU_CONF_FUNCTION0_LOCAL;
// Set CRESET and SPI_CS as outputs (GPIO2[11] and GPIO2[10])
GPIO_DIR(FPGA_CRESET_PORT) |= (1 << FPGA_CRESET_PIN) | (1 << FPGA_SPI_CS_PIN);
// Clear both initially
GPIO_CLR(FPGA_CRESET_PORT) = (1 << FPGA_CRESET_PIN) | (1 << FPGA_SPI_CS_PIN);
// CDONE is input (GPIO5[14])
GPIO_DIR(FPGA_CDONE_PORT) &= ~(1 << FPGA_CDONE_PIN);
}
// GPIO control helpers
static void fpga_creset_low(void) {
GPIO_CLR(FPGA_CRESET_PORT) = (1 << FPGA_CRESET_PIN);
@@ -315,7 +287,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 +300,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 +314,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 +325,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 +394,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);
}
// ============================================================================
@@ -578,12 +550,6 @@ int fpga_bridge_init(uint8_t* mem_base) {
CGU_BASE_SSP1_CLK = CGU_BASE_SSP1_CLK_AUTOBLOCK(1) |
CGU_BASE_SSP1_CLK_CLK_SEL(CGU_SRC_PLL1);
// Configure SSP1 pins
configure_ssp1_pins();
// Configure FPGA control pins
configure_fpga_control_pins();
// Initialize SSP1 for iCE40 programming
ssp1_init_ice40();
@@ -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 */
@@ -52,8 +52,6 @@
/* Driver exported functions. */
/*===========================================================================*/
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
/**
* @brief LPC43xx I/O ports configuration.
* @details Ports 0 through 8.
@@ -62,15 +60,15 @@
*
* @notapi
*/
void _pal_lld_init(const PALConfig *config) {
for(size_t i=0; i<8; i++) {
LPC_GPIO->PIN[i] = config->P[i].data;
LPC_GPIO->DIR[i] = config->P[i].dir;
}
void _pal_lld_init(const PALConfig* config) {
for (size_t i = 0; i < 8; i++) {
LPC_GPIO->PIN[i] = config->P[i].data;
LPC_GPIO->DIR[i] = config->P[i].dir;
}
for(size_t i=0; i<ARRAY_SIZE(config->SCU); i++) {
LPC_SCU->SFSP[config->SCU[i].port][config->SCU[i].pin] = config->SCU[i].config.word;
}
for (size_t i = 0; i < SCU_ARRAY_SIZE; i++) {
LPC_SCU->SFSP[config->SCU[i].port][config->SCU[i].pin] = config->SCU[i].config.word;
}
}
/**
@@ -87,14 +85,14 @@ void _pal_lld_init(const PALConfig *config) {
void _pal_lld_setgroupmode(ioportid_t port,
ioportmask_t mask,
iomode_t mode) {
/* TODO: Handling pull-up, pull-down modes not implemented, as it would
* require a map from GPIO to SCU pins. Not today.
*/
if( mode == PAL_MODE_OUTPUT_PUSHPULL ) {
LPC_GPIO->DIR[port] |= mask;
} else {
LPC_GPIO->DIR[port] &= ~mask;
}
/* TODO: Handling pull-up, pull-down modes not implemented, as it would
* require a map from GPIO to SCU pins. Not today.
*/
if (mode == PAL_MODE_OUTPUT_PUSHPULL) {
LPC_GPIO->DIR[port] |= mask;
} else {
LPC_GPIO->DIR[port] &= ~mask;
}
}
#endif /* HAL_USE_PAL */
@@ -50,10 +50,18 @@
* @note Implementations may extend this structure to contain more,
* architecture dependent, fields.
*/
#ifdef PRALINE
#define SCU_ARRAY_SIZE 78
#else
#define SCU_ARRAY_SIZE 56
#endif
typedef struct {
/** @brief GPIO setup data.*/
gpio_setup_t P[8];
scu_setup_t SCU[60];
/** @brief GPIO setup data.*/
gpio_setup_t P[8];
scu_setup_t SCU[SCU_ARRAY_SIZE];
} PALConfig;
/**
@@ -94,50 +102,50 @@ typedef uint8_t iopadid_t;
/**
* @brief GPIO0 port identifier.
*/
#define IOPORT1 0
#define GPIO0 0
#define IOPORT1 0
#define GPIO0 0
/**
* @brief GPIO1 port identifier.
*/
#define IOPORT2 1
#define GPIO1 1
#define IOPORT2 1
#define GPIO1 1
/**
* @brief GPIO1 port identifier.
*/
#define IOPORT3 2
#define GPIO2 2
#define IOPORT3 2
#define GPIO2 2
/**
* @brief GPIO1 port identifier.
*/
#define IOPORT4 3
#define GPIO3 3
#define IOPORT4 3
#define GPIO3 3
/**
* @brief GPIO1 port identifier.
*/
#define IOPORT5 4
#define GPIO4 4
#define IOPORT5 4
#define GPIO4 4
/**
* @brief GPIO1 port identifier.
*/
#define IOPORT6 5
#define GPIO5 5
#define IOPORT6 5
#define GPIO5 5
/**
* @brief GPIO1 port identifier.
*/
#define IOPORT7 6
#define GPIO6 6
#define IOPORT7 6
#define GPIO6 6
/**
* @brief GPIO1 port identifier.
*/
#define IOPORT8 7
#define GPIO7 7
#define IOPORT8 7
#define GPIO7 7
/*===========================================================================*/
/* Implementation, some of the following macros could be implemented as */
@@ -237,7 +245,7 @@ typedef uint8_t iopadid_t;
*
* @notapi
*/
//#define pal_lld_readgroup(port, mask, offset) 0
// #define pal_lld_readgroup(port, mask, offset) 0
/**
* @brief Writes a group of bits.
@@ -253,7 +261,7 @@ typedef uint8_t iopadid_t;
*
* @notapi
*/
//#define pal_lld_writegroup(port, mask, offset, bits) (void)bits
// #define pal_lld_writegroup(port, mask, offset, bits) (void)bits
/**
* @brief Pads group mode setup.
@@ -268,8 +276,8 @@ typedef uint8_t iopadid_t;
*
* @notapi
*/
#define pal_lld_setgroupmode(port, mask, offset, mode) \
_pal_lld_setgroupmode(port, mask << offset, mode)
#define pal_lld_setgroupmode(port, mask, offset, mode) \
_pal_lld_setgroupmode(port, mask << offset, mode)
/**
* @brief Reads a logical state from an I/O pad.
@@ -285,8 +293,8 @@ typedef uint8_t iopadid_t;
*
* @notapi
*/
#define pal_lld_readpad(port, pad) \
(LPC_GPIO->B[((port) * 32) + (pad)])
#define pal_lld_readpad(port, pad) \
(LPC_GPIO->B[((port) * 32) + (pad)])
/**
* @brief Writes a logical state on an output pad.
@@ -303,8 +311,8 @@ typedef uint8_t iopadid_t;
*
* @notapi
*/
#define pal_lld_writepad(port, pad, bit) \
((LPC_GPIO->B[((port) * 32) + (pad)]) = (bit))
#define pal_lld_writepad(port, pad, bit) \
((LPC_GPIO->B[((port) * 32) + (pad)]) = (bit))
/**
* @brief Sets a pad logical state to @p PAL_HIGH.
@@ -317,8 +325,8 @@ typedef uint8_t iopadid_t;
*
* @notapi
*/
#define pal_lld_setpad(port, pad) \
(LPC_GPIO->SET[(port)] = 1 << (pad))
#define pal_lld_setpad(port, pad) \
(LPC_GPIO->SET[(port)] = 1 << (pad))
/**
* @brief Clears a pad logical state to @p PAL_LOW.
@@ -331,7 +339,7 @@ typedef uint8_t iopadid_t;
*
* @notapi
*/
#define pal_lld_clearpad(port, pad) \
#define pal_lld_clearpad(port, pad) \
(LPC_GPIO->CLR[(port)] = 1 << (pad))
/**
@@ -345,8 +353,8 @@ typedef uint8_t iopadid_t;
*
* @notapi
*/
#define pal_lld_togglepad(port, pad) \
(LPC_GPIO->NOT[(port)] = 1 << (pad))
#define pal_lld_togglepad(port, pad) \
(LPC_GPIO->NOT[(port)] = 1 << (pad))
/**
* @brief Pad mode setup.
@@ -362,7 +370,7 @@ typedef uint8_t iopadid_t;
*
* @notapi
*/
//#define pal_lld_setpadmode(port, pad, mode)
// #define pal_lld_setpadmode(port, pad, mode)
#if !defined(__DOXYGEN__)
extern const PALConfig pal_default_config;
@@ -371,10 +379,10 @@ extern const PALConfig pal_default_config;
#ifdef __cplusplus
extern "C" {
#endif
void _pal_lld_init(const PALConfig *config);
void _pal_lld_setgroupmode(ioportid_t port,
ioportmask_t mask,
iomode_t mode);
void _pal_lld_init(const PALConfig* config);
void _pal_lld_setgroupmode(ioportid_t port,
ioportmask_t mask,
iomode_t mode);
#ifdef __cplusplus
}
#endif
+10 -2
View File
@@ -30,10 +30,11 @@ uint8_t BatteryManagement::calc_percent_voltage(uint16_t voltage) {
}
// helper function to get data from ANY batt management ic
void BatteryManagement::getBatteryInfo(uint8_t& valid_mask, uint8_t& percent, uint16_t& voltage, int32_t& current) {
void BatteryManagement::getBatteryInfo(uint8_t& valid_mask, uint8_t& percent, uint16_t& voltage, int32_t& current, bool& battMayChanged) {
auto dev = i2cdev::I2CDevManager::get_dev_by_model(I2CDEVMDL_MAX17055);
battMayChanged = false;
if (dev) {
((i2cdev::I2cDev_MAX17055*)dev)->getBatteryInfo(valid_mask, percent, voltage, current);
((i2cdev::I2cDev_MAX17055*)dev)->getBatteryInfo(valid_mask, percent, voltage, current, battMayChanged);
return;
}
dev = i2cdev::I2CDevManager::get_dev_by_model(I2CDEVMDL_ADS1110);
@@ -47,6 +48,13 @@ void BatteryManagement::getBatteryInfo(uint8_t& valid_mask, uint8_t& percent, ui
valid_mask = 0;
}
void BatteryManagement::reset_changed_flag() {
auto dev = i2cdev::I2CDevManager::get_dev_by_model(I2CDEVMDL_MAX17055);
if (dev) {
((i2cdev::I2cDev_MAX17055*)dev)->resetChangedFlag();
}
}
// helper function to get data from ANY batt management ic
uint16_t BatteryManagement::get_cycles() {
auto dev = i2cdev::I2CDevManager::get_dev_by_model(I2CDEVMDL_MAX17055);
+2 -1
View File
@@ -46,10 +46,11 @@ class BatteryManagement {
static void set_calc_override(bool override);
static uint8_t calc_percent_voltage(uint16_t); // calculates battery percentage from the voltage
static bool calcOverride; // if set to true, it'll override the battery percent calculation based on current voltage.
static void getBatteryInfo(uint8_t& valid_mask, uint8_t& percent, uint16_t& voltage, int32_t& current);
static void getBatteryInfo(uint8_t& valid_mask, uint8_t& percent, uint16_t& voltage, int32_t& current, bool& battMayChanged);
static uint16_t get_cycles();
static float get_tte();
static float get_ttf();
static void reset_changed_flag();
private:
};
+184
View File
@@ -0,0 +1,184 @@
/*
* 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 "gpio.hpp"
#include "platform.hpp"
namespace power_control {
/* VAA powers:
* MAX5864 analog section.
* MAX2837 registers and other functions.
* RFFC5072 analog section.
*
* Beware that power applied to pins of the MAX2837 may
* show up on VAA and start powering other components on the
* VAA net. So turn on VAA before driving pins from MCU to
* MAX2837.
*/
void vaa_power_on(void) {
/* Very twitchy process for powering up VAA without glitching the 3.3V rail,
* which can send the microcontroller into reset.
*/
#ifdef PRALINE
/* P8_1 (GPIO4[1]) does not have MOTOCONPWM hardware routing.
* Using software bit-banging (pseudo-PWM) for VAA soft-start.
* VAA is active LOW (0 = ON).
*/
/* Software soft-start loop to prevent brown-out */
for (uint32_t i = 0; i < 1000; i++) {
LPC_GPIO->W4[1] = 0; /* Turn ON briefly */
LPC_GPIO->W4[1] = 1; /* Turn OFF briefly */
}
/* Latch VAA to ON state (Active LOW) */
LPC_GPIO->CLR[4] = (1 << 1);
#else
if (hackrf_r9) {
/*
* There is enough VCC->VAA leakage prior to VAA activation from IO pins on
* HackRF One r9 that slowing down activation like this isn't necessary, but
* we do it just in case a different start-up sequence in the future results
* in less leakage.
*/
setup_pin(pin_setup_vaa_enablex_gpio_r9); // P6_10 GPIO3[ 6]: !VAA_ENABLE, 10K PU
for (uint32_t i = 0; i < 1000; i++) {
LPC_GPIO->W3[6] = 1;
LPC_GPIO->W3[6] = 0;
}
} else {
/* Configure and enable MOTOCONPWM peripheral clocks.
* Assume IDIVC is running the post-bootloader configuration, outputting 96MHz derived from PLL1.
*/
base_clock_enable(&motocon_pwm_resources.base);
branch_clock_enable(&motocon_pwm_resources.branch);
peripheral_reset(&motocon_pwm_resources.reset);
/* Combination of pulse duration and duty cycle was arrived at empirically, to keep supply glitching
* to +/- 0.15V.
*/
const uint32_t cycle_period = 256;
uint32_t enable_period = 2;
LPC_MCPWM->TC2 = 0;
LPC_MCPWM->MAT2 = cycle_period - enable_period;
LPC_MCPWM->LIM2 = cycle_period;
/* Switch !VAA_ENABLE pin from GPIO to MOTOCONPWM peripheral output, now that the peripheral is configured. */
setup_pin(pin_setup_vaa_enablex_pwm); // P5_0 /GPIO2[ 9]/MCOB2: !VAA_ENABLE, 10K PU
/* Start the PWM operation. */
LPC_MCPWM->CON_SET = (1 << 16);
/* Wait until VAA rises to approximately 90% of final voltage. */
/* Timing assumes we're running immediately after the bootloader: 96 MHz from IRC+PLL1
*/
while (enable_period < cycle_period) {
{
volatile uint32_t delay = 2000;
while (delay--);
}
enable_period <<= 1;
LPC_MCPWM->MAT2 = cycle_period - enable_period;
}
/* Hold !VAA_ENABLE active using a GPIO, so we can reclaim and shut down the MOTOCONPWM peripheral. */
LPC_GPIO->CLR[2] = (1 << 9); // !VAA_ENABLE
LPC_GPIO->DIR[2] |= (1 << 9);
setup_pin(pin_setup_vaa_enablex_gpio_og); // P5_0 /GPIO2[ 9]/MCOB2: !VAA_ENABLE, 10K PU
peripheral_reset(&motocon_pwm_resources.reset);
branch_clock_disable(&motocon_pwm_resources.branch);
base_clock_disable(&motocon_pwm_resources.base);
}
#endif
}
void vaa_power_off(void) {
/* TODO: There's a lot of other stuff that must be done to prevent
* leakage from +3V3 into VAA.
*/
#ifdef PRALINE
/* Safe state: OFF (VAA RF is active LOW, so Set = OFF) */
LPC_GPIO->SET[4] = (1 << 1);
/* Turn OFF LED3 (TX) */
LPC_GPIO->SET[2] = (1 << 8);
#else
if (hackrf_r9) {
LPC_GPIO->W3[6] = 1; // Turn OFF VAA for r9 P6_10
} else {
LPC_GPIO->W2[9] = 1; // Turn OFF VAA for OG P5_0
}
#endif
}
#ifdef PRALINE
void aux_power_on(void) {
// 3.3V Aux - P6_7 = GPIO5[15], Active LOW (Clear = ON)
LPC_GPIO->CLR[5] = (1 << 15);
}
void aux_power_off(void) {
// 3.3V Aux - P6_7 = GPIO5[15], Active LOW (Set = OFF)
LPC_GPIO->SET[5] = (1 << 15);
}
#endif /* PRALINE */
void core_power_on(void) { // Core power enable
#ifdef PRALINE
// 1.2V FPGA - P8_7 = GPIO4[7], Active HIGH (Set = ON)
LPC_GPIO->SET[4] = (1 << 7);
#else
if (hackrf_r9) {
// P5_0 (GPIO2[9]) is the EN1V8 pin
LPC_GPIO->SET[2] = (1 << 9);
} else {
// On older OG HackRF boards, P6_10 (GPIO3[6]) is the EN1V8 pin
LPC_GPIO->SET[3] = (1 << 6);
}
#endif
}
void core_power_off(void) { // Core power disable
#ifdef PRALINE
// 1.2V FPGA - P8_7 = GPIO4[7], Active HIGH (Clear = OFF) */
LPC_GPIO->CLR[4] = (1 << 7);
#else
if (hackrf_r9) {
// P5_0 (GPIO2[9]) is the EN1V8 pin
LPC_GPIO->CLR[2] = (1 << 9);
} else {
// On older OG HackRF boards, P6_10 (GPIO3[6]) is the EN1V8 pin
LPC_GPIO->CLR[3] = (1 << 6);
}
#endif
}
} // namespace power_control
+108
View File
@@ -22,10 +22,13 @@
#ifndef __GPIO_H__
#define __GPIO_H__
#include <array>
#include <cstddef>
#include <cstdint>
#include "ch.h"
#include "hal.h"
#include "pal.h"
struct PinConfig {
const uint32_t mode;
@@ -280,4 +283,109 @@ struct GPIO {
const uint16_t _gpio_mode;
};
constexpr uint32_t boot_bit(const GPIO& pin, bool state) {
return (state ? 1UL : 0UL) << pin.pad();
}
inline void setup_pin(const scu_setup_t& pin_setup) {
LPC_SCU->SFSP[pin_setup.port][pin_setup.pin] = pin_setup.config;
}
template <size_t N>
inline void setup_gpios(const std::array<gpio_setup_t, N>& pins_setup) {
for (size_t i = 0; i < N; i++) {
LPC_GPIO->PIN[i] |= pins_setup[i].data;
LPC_GPIO->DIR[i] |= pins_setup[i].dir;
}
}
template <size_t N>
inline void setup_pins(const std::array<scu_setup_t, N>& pins_setup) {
for (const auto& pin_setup : pins_setup) {
setup_pin(pin_setup);
}
}
#ifdef PRALINE
namespace gpio_control {
struct PinMap {
uint8_t scu_port;
uint8_t scu_pin;
uint8_t gpio_port;
uint8_t gpio_pad;
};
constexpr PinMap map_p1_ctrl0{2, 10, 0, 14}; // SCU: 2, 10 | GPIO: 0, 14
constexpr PinMap map_p1_ctrl1{6, 8, 5, 16};
constexpr PinMap map_p1_ctrl2{6, 9, 3, 5};
constexpr PinMap map_p2_ctrl0{8, 3, 7, 3};
constexpr PinMap map_p2_ctrl1{8, 4, 7, 4};
constexpr PinMap map_trigger_out{2, 6, 5, 6};
constexpr PinMap map_trigger_in{13, 12, 6, 26};
constexpr PinMap map_rf5072_mix_en{9, 0, 4, 12};
constexpr PinMap map_aa_en{1, 14, 1, 7}; // Anti-Aliasing filter
// P1 Multiplexer control pins
constexpr Pin pin_p1_ctrl0{map_p1_ctrl0.scu_port, map_p1_ctrl0.scu_pin}; // SCU: P2_10
constexpr GPIO p1_ctrl0{pin_p1_ctrl0, map_p1_ctrl0.gpio_port, map_p1_ctrl0.gpio_pad, PAL_MODE_OUTPUT_PUSHPULL}; // GPIO[0]14
constexpr Pin pin_p1_ctrl1{map_p1_ctrl1.scu_port, map_p1_ctrl1.scu_pin}; // SCU: P6_8
constexpr GPIO p1_ctrl1{pin_p1_ctrl1, map_p1_ctrl1.gpio_port, map_p1_ctrl1.gpio_pad, PAL_MODE_OUTPUT_PUSHPULL}; // GPIO[5]16
constexpr Pin pin_p1_ctrl2{map_p1_ctrl2.scu_port, map_p1_ctrl2.scu_pin}; // SCU: P6_9
constexpr GPIO p1_ctrl2{pin_p1_ctrl2, map_p1_ctrl2.gpio_port, map_p1_ctrl2.gpio_pad, PAL_MODE_OUTPUT_PUSHPULL}; // GPIO[3]5
// P2 Multiplexer control pins
constexpr Pin pin_p2_ctrl0{map_p2_ctrl0.scu_port, map_p2_ctrl0.scu_pin}; // SCU: PE_3
constexpr GPIO p2_ctrl0{pin_p2_ctrl0, map_p2_ctrl0.gpio_port, map_p2_ctrl0.gpio_pad, PAL_MODE_OUTPUT_PUSHPULL}; // GPIO[7]3
constexpr Pin pin_p2_ctrl1{map_p2_ctrl1.scu_port, map_p2_ctrl1.scu_pin}; // SCU: PE_4
constexpr GPIO p2_ctrl1{pin_p2_ctrl1, map_p2_ctrl1.gpio_port, map_p2_ctrl1.gpio_pad, PAL_MODE_OUTPUT_PUSHPULL}; // GPIO[7]4
// Trigger pins
constexpr Pin trigger_out_pin{map_trigger_out.scu_port, map_trigger_out.scu_pin}; // SCU: P2_6
constexpr GPIO trigger_out{trigger_out_pin, map_trigger_out.gpio_port, map_trigger_out.gpio_pad, PAL_MODE_OUTPUT_PUSHPULL}; // GPIO[5]6
constexpr Pin trigger_in_pin{map_trigger_in.scu_port, map_trigger_in.scu_pin}; // SCU: PD_12
constexpr GPIO trigger_in{trigger_in_pin, map_trigger_in.gpio_port, map_trigger_in.gpio_pad, PAL_MODE_INPUT}; // GPIO[6]26
// RF507x
constexpr Pin pin_rf5072_mix_en{map_rf5072_mix_en.scu_port, map_rf5072_mix_en.scu_pin};
constexpr GPIO rf5072_mix_en{pin_rf5072_mix_en, map_rf5072_mix_en.gpio_port, map_rf5072_mix_en.gpio_pad, 0};
// Anti-Aliasing filter
constexpr Pin pin_aa_en{map_aa_en.scu_port, map_aa_en.scu_pin};
constexpr GPIO aa_en{pin_aa_en, map_aa_en.gpio_port, map_aa_en.gpio_pad, 0};
} // namespace gpio_control
#endif
namespace power_control {
void vaa_power_on(void);
void vaa_power_off(void);
#ifdef PRALINE
void aux_power_on(void);
void aux_power_off(void);
#endif
void core_power_on(void);
void core_power_off(void);
static const motocon_pwm_resources_t motocon_pwm_resources = {
.base = {.clk = &LPC_CGU->BASE_APB1_CLK, .stat = &LPC_CCU1->BASE_STAT, .stat_mask = (1 << 1)},
.branch = {.cfg = &LPC_CCU1->CLK_APB1_MOTOCON_PWM_CFG, .stat = &LPC_CCU1->CLK_APB1_MOTOCON_PWM_STAT},
.reset = {.output_index = 38},
};
static const scu_setup_t pin_setup_vaa_enablex_pwm = {5, 0, scu_config_normal_drive_t{.mode = 1, .epd = 0, .epun = 1, .ehs = 0, .ezi = 0, .zif = 0}};
static const scu_setup_t pin_setup_vaa_enablex_gpio_og = {5, 0, scu_config_normal_drive_t{.mode = 0, .epd = 0, .epun = 1, .ehs = 0, .ezi = 0, .zif = 0}};
static const scu_setup_t pin_setup_vaa_enablex_gpio_r9 = {6, 10, scu_config_normal_drive_t{.mode = 0, .epd = 0, .epun = 1, .ehs = 0, .ezi = 0, .zif = 0}};
} // namespace power_control
#endif /*__GPIO_H__*/
+6 -11
View File
@@ -41,14 +41,6 @@ constexpr GPIO gpio_led_tx = gpio[GPIO2_8];
constexpr GPIO gpio_og_1v8_enable = gpio[GPIO3_6];
constexpr GPIO gpio_r9_1v8_enable = gpio[GPIO2_9];
constexpr GPIO gpio_vregmode = gpio[GPIO3_7];
#ifdef PRALINE
// PRALINE uses different power control pins
constexpr GPIO gpio_og_vaa_disable = gpio[GPIO4_1]; // PRALINE VAA disable (P8_1)
constexpr GPIO gpio_r9_vaa_disable = gpio[GPIO4_1]; // PRALINE VAA disable (P8_1)
#else
constexpr GPIO gpio_og_vaa_disable = gpio[GPIO2_9];
constexpr GPIO gpio_r9_vaa_disable = gpio[GPIO3_6];
#endif
constexpr GPIO gpio_rx_mix_bp = gpio[GPIO2_12];
constexpr GPIO gpio_tx_mix_bp = gpio[GPIO2_11];
@@ -70,7 +62,6 @@ constexpr GPIO gpio_hp = gpio[GPIO2_0];
constexpr GPIO gpio_rx_amp = gpio[GPIO1_11];
constexpr GPIO gpio_tx_amp = gpio[GPIO2_15];
constexpr GPIO gpio_amp_bypass = gpio[GPIO0_14];
constexpr GPIO gpio_not_rx_amp_pwr = gpio[GPIO1_12];
constexpr GPIO gpio_not_tx_amp_pwr = gpio[GPIO3_5];
#ifdef PRALINE
@@ -80,6 +71,7 @@ constexpr GPIO gpio_rffc5072_resetx = gpio[GPIO2_14]; // P5_5: LPC43xx controls
#else
constexpr GPIO gpio_rffc5072_select = gpio[GPIO2_13];
constexpr GPIO gpio_rffc5072_resetx = gpio[GPIO2_14];
constexpr GPIO gpio_not_rx_amp_pwr = gpio[GPIO1_12];
#endif
#ifdef PRALINE
@@ -128,14 +120,16 @@ constexpr GPIO gpio_q_invert = gpio[GPIO0_13];
constexpr GPIO gpio_vaa_disable = gpio[GPIO4_1]; // VAA disable (P8_1)
constexpr GPIO gpio_1v2_enable = gpio[GPIO4_7]; // 1V2 enable (P8_7)
constexpr GPIO gpio_3v3aux_disable = gpio[GPIO5_15]; // 3V3 aux disable (P6_7)
constexpr GPIO gpio_vbus_enable = gpio[GPIO8_4]; // VBUS_IN_EN P8_4 ->LOW
constexpr GPIO gpio_vin_enable = gpio[GPIO8_5]; // VIN_IN_EN P8_5 ->HIGH
constexpr GPIO gpio_3v3aux_oc = gpio[GPIO1_11]; // 3.3V aux overcurrent input (P2_11)
constexpr GPIO gpio_vbus_enable = gpio[GPIO4_4]; // VBUS_IN_EN P8_4 ->LOW
constexpr GPIO gpio_vin_enable = gpio[GPIO4_5]; // VIN_IN_EN P8_5 ->HIGH
// PRALINE RF path control
constexpr GPIO gpio_tx_enable = gpio[GPIO3_4]; // TX enable (P6_5)
// constexpr GPIO gpio_mix_enable_n = gpio[GPIO3_2]; // Mixer enable inverted (P6_3)
constexpr GPIO gpio_lpf_enable = gpio[GPIO4_8]; // LPF enable (PA_1)
constexpr GPIO gpio_rf_amp_enable = gpio[GPIO4_9]; // RF amp enable (PA_2)
constexpr GPIO gpio_ant_bias_disable = gpio[GPIO1_12]; // Antenna bias disable (P2_12)
constexpr GPIO gpio_bias_oc = gpio[GPIO3_7]; // Bias tee overcurrent input (P6_11)
// PRALINE mixer lock detect (gpio_max2831_ld defined above at line 93)
constexpr GPIO gpio_rffc5072_ld = gpio[GPIO6_25]; // Mixer lock detect (PD_11)
@@ -155,6 +149,7 @@ constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15]; // CLKIN control (P1_20)
constexpr GPIO gpio_trigger_in = gpio[GPIO6_26]; // Trigger input (PD_12)
constexpr GPIO gpio_trigger_out = gpio[GPIO5_6]; // Trigger output (P2_6)
constexpr GPIO gpio_pps_out = gpio[GPIO5_5]; // PPS output (P2_5)
#endif
#ifdef PRALINE
+6
View File
@@ -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 */
+1 -1
View File
@@ -57,7 +57,7 @@ void I2cDev_ADS1110::update() {
uint16_t voltage = readVoltage();
uint8_t batteryPercentage = battery::BatteryManagement::calc_percent_voltage(voltage);
// send local message
BatteryStateMessage msg{1, batteryPercentage, false, voltage};
BatteryStateMessage msg{1, batteryPercentage, false, voltage, false};
EventDispatcher::send_message(msg);
}
+56 -25
View File
@@ -22,10 +22,10 @@ Boston, MA 02110-1301, USA.
#include "i2cdev_max17055.hpp"
#include "battery.hpp"
#include "utility.hpp"
#include "portapack_persistent_memory.hpp"
#include <cstring>
#include <algorithm>
#include <cstdint>
#include "portapack_persistent_memory.hpp"
namespace i2cdev {
@@ -189,13 +189,33 @@ void I2cDev_MAX17055::update() {
uint8_t batteryPercentage = 102;
uint16_t voltage = 0;
int32_t current = 0;
getBatteryInfo(validity, batteryPercentage, voltage, current);
bool battChanged = false;
getBatteryInfo(validity, batteryPercentage, voltage, current, battChanged);
// send local message
BatteryStateMessage msg{validity, batteryPercentage, current >= 25, voltage};
BatteryStateMessage msg{validity, batteryPercentage, current >= 25, voltage, battChanged};
EventDispatcher::send_message(msg);
}
void I2cDev_MAX17055::sleep_config(bool enable_sleep) {
uint16_t config1 = read_register(0x1D);
if (enable_sleep) {
// A COMMSH bit (bit 6)
config1 |= 0x0040;
// ShdnTimer (0x3F):
// THR = 0 -> ~45 sec (def, 0x0000)
// THR = 1 -> ~90 sec (0x1000)
// THR = 2 -> ~180 sec (0x2000)
write_register(0x3F, 0x2000);
} else {
// A COMMSH bit (bit 6)
config1 &= ~0x0040;
}
// write config register
write_register(0x1D, config1);
}
bool I2cDev_MAX17055::init(uint8_t addr_) {
if (addr_ != I2CDEV_MAX17055_ADDR_1) return false;
addr = addr_;
@@ -207,8 +227,8 @@ bool I2cDev_MAX17055::init(uint8_t addr_) {
// First-time or POR initialization
return_status = full_reset_and_init();
}
partialInit(); // If you always want hibernation disabled
// statusClear(); // I am not sure if this should be here or not (Clear all bits in the Status register (0x00))
chThdSleepMilliseconds(300); // wait for adc to fully wake up!
partialInit();
return return_status;
}
return false;
@@ -232,9 +252,10 @@ bool I2cDev_MAX17055::full_reset_and_init() {
if (!load_custom_parameters()) {
return false;
}
if (!clear_por()) {
/*if (!clear_por()) {
return false;
}
}*/
// don't clear it, we'll need to know the state
return true;
}
@@ -253,7 +274,7 @@ bool I2cDev_MAX17055::initialize_custom_parameters() {
if (!write_register(0x1E, 0x03C0)) return false; // IChgTerm
if (!write_register(0x3A, 0x9661)) return false; // VEmpty
if (!write_register(0x60, 0x0090)) return false; // Unknown register
if (!write_register(0x46, ((designcap / 32) * 44138) * designcap)) return false; // dPAcc
if (!write_register(0x46, ((designcap / 32) * 44138) / designcap)) return false; // dPAcc = dQAcc * 44138 / DesignCap (Maxim EZ config)
if (!write_register(0xDB, 0x8000)) return false; // ModelCfg --we should wait till it loads here. While (ReadRegister(0xDB)&0x8000) Wait(10)//do not continue until ModelCFG.Refresh == 0
if (!write_register(0x40, 0x0001)) return false; // Set user mem to 1
return true;
@@ -284,17 +305,18 @@ bool I2cDev_MAX17055::clear_por() {
}
bool I2cDev_MAX17055::needsInitialization() {
uint16_t UserMem1 = read_register(0x40);
if (UserMem1 == 0) {
return true;
uint16_t status = read_register(0x00);
if (status == 0xFFFF) {
chThdSleepMilliseconds(15);
status = read_register(0x00);
}
return false;
return (status & 0x0002) != 0;
}
void I2cDev_MAX17055::partialInit() {
// Only update necessary volatile settings
setHibCFG(0x0000); // If you always want hibernation disabled
// setHibCFG(0x0000); // If you always want hibernation disabled. this is a lower resolution mode, when the ic is on, and measuring, but on lower freq. depends un tha current (mA).
sleep_config(true); // shut down the comm
// Add any other volatile settings that need updating
}
@@ -312,7 +334,12 @@ bool I2cDev_MAX17055::detect() {
if (dev_name == 0x4010) {
return true;
}
// try again, we we just woke up.
chThdSleepMilliseconds(5);
dev_name = read_register(0x21);
if (dev_name == 0x4010) {
return true;
}
// If DevName doesn't match, try reading Status register as a fallback
uint16_t status = read_register(0x00);
if (status != 0xFFFF && status != 0x0000) {
@@ -354,14 +381,27 @@ bool I2cDev_MAX17055::write_register(const uint8_t reg, const uint16_t value) {
return success;
}
void I2cDev_MAX17055::getBatteryInfo(uint8_t& valid_mask, uint8_t& batteryPercentage, uint16_t& voltage, int32_t& current) {
void I2cDev_MAX17055::getBatteryInfo(uint8_t& valid_mask, uint8_t& batteryPercentage, uint16_t& voltage, int32_t& current, bool& battMayChanged) {
// Read Status Register
uint16_t status = read_register(0x00);
battMayChanged = false;
voltage = averageMVoltage();
if ((status == 0 && voltage == 0) || (status == 0x0002 && voltage == 3600) || (status == 0x0002 && voltage == 0)) {
valid_mask = 0;
return;
}
bool requires_reset = false;
if (((status & 0xFF) >> 1) & 0x01) { // POR FLAG
requires_reset = true;
}
// Execute the reset if either flag was tripped
if (requires_reset) {
reInit(); // todo maybe don't do it automatically, just signal. but for now it is safer to do it.
// battMayChanged = true && portapack::persistent_memory::battery_replaceable(); // only signal a change if the battery is replaceable. othervise do it silently
// We don't want to signal this for now. the Voltage drop during power up messes this up a bit.
}
batteryPercentage = stateOfCharge();
current = instantCurrent();
valid_mask = 31; // BATT_VALID_VOLTAGE + CURRENT + PERCENT + BATT_VALID_CYCLES + BATT_VALID_TTEF
@@ -551,15 +591,6 @@ 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);
}
+8 -5
View File
@@ -139,7 +139,7 @@
// Tmn, Vmx, Vmn, Imx, and Imn bits of the Status register
// (00h) are not disabled.
#ifndef MAX17055_Aen
#define MAX17055_Aen 1
#define MAX17055_Aen 0
#endif
// This allows the host to control the bias of the thermistor switch or
@@ -175,7 +175,7 @@
// Set to 1 and set ETHRM or FTHRM to 1 to enable temperature measurements selected by Config.TSel.
#ifndef MAX17055_Ten
#define MAX17055_Ten 1
#define MAX17055_Ten 0
#endif
// Set to 1 to enable device shutdown when the IC is mounted host side and the battery is removed.
@@ -231,7 +231,7 @@
// Set this bit to 1 to enable temperature based alerts.
// Write this bit to 0 to disable temperature alerts. This bit is set to 1 at power-up.
#ifndef MAX17055_TAIrtEN
#define MAX17055_TAIrtEN 1
#define MAX17055_TAIrtEN 0
#endif
// Set this bit to 1 to enable alert output with the Status.dSOCi bit function.
@@ -283,14 +283,17 @@ class I2cDev_MAX17055 : public I2cDev {
void update() override;
bool detect();
void getBatteryInfo(uint8_t& valid_mask, uint8_t& batteryPercentage, uint16_t& voltage, int32_t& current);
void getBatteryInfo(uint8_t& valid_mask, uint8_t& batteryPercentage, uint16_t& voltage, int32_t& current, bool& battMayChanged);
bool reset_learned();
float getValue(const char* entityName);
uint16_t averageMVoltage(void);
int32_t instantCurrent(void);
uint16_t stateOfCharge(void);
bool reInit(); // call when battery parameters changed from ui. don't call if not needed, or the battery is not changed!!!
bool reInit(); // call when battery parameters changed from ui. don't call if not needed, or the battery is not changed!!!
bool getIsBattChanged() { return statusControl(MAX17055_POR); } // true if the ic thinks we have a new battery
void resetChangedFlag() { clear_por(); } // reset the flag
void sleep_config(bool enable_sleep); // if true, the ic can sleep after 3 minutes of inactivity (over i2c). can be waken up on any i2c communication (first packet may be dropped)
private:
const RegisterEntry* findEntry(const char* name) const;
+3 -3
View File
@@ -31,13 +31,13 @@ struct LED {
void setup() const {
#ifdef PRALINE
/* PRALINE LEDs are active-low (GPIO LOW = LED ON) */
_gpio.set(); /* Start with LED OFF (HIGH) */
// No-op: GPIO direction and initial OFF state are already
// enforced globally at boot by pal_default_config in board.cpp.
#else
_gpio.clear();
#endif
_gpio.output();
_gpio.configure();
#endif
}
void on() const {
+6 -3
View File
@@ -1214,7 +1214,7 @@ class SSTVRXConfigureMessage : public Message {
public:
constexpr SSTVRXConfigureMessage(
const uint8_t code)
: Message{id : ID::SSTVRXConfigure},
: Message{ID::SSTVRXConfigure},
code(code) {
}
@@ -1610,17 +1610,20 @@ class BatteryStateMessage : public Message {
uint8_t valid_mask,
uint8_t percent,
bool on_charger,
uint16_t voltage)
uint16_t voltage,
bool battMayChanged)
: Message{ID::BatteryStateData},
valid_mask{valid_mask},
percent{percent},
on_charger{on_charger},
voltage{voltage} {
voltage{voltage},
battMayChanged{battMayChanged} {
}
uint8_t valid_mask = 0;
uint8_t percent = 0;
bool on_charger = false;
uint16_t voltage = 0; // mV
bool battMayChanged = false;
};
class ProtoViewDataMessage : public Message {
+6 -6
View File
@@ -337,8 +337,8 @@ enum GPIOs {
GPIO4_14,
// Power control and LED4
GPIO4_1,
GPIO8_4,
GPIO8_5,
GPIO4_4, // VBUS_IN_EN
GPIO4_5, // VIN_IN_EN
GPIO4_6,
GPIO4_7,
// RF path control
@@ -451,8 +451,8 @@ constexpr GPIO gpio[] = {
#ifdef PRALINE
[GPIO4_14] = {pins[P9_2], 4, 14, 0}, // RFFC5072 mixer data
[GPIO4_1] = {pins[P8_1], 4, 1, 0}, // VAA disable
[GPIO8_4] = {pins[P8_4], 8, 4, 0}, // VBUS_IN_EN
[GPIO8_5] = {pins[P8_5], 8, 5, 0}, // VIN_IN_EN
[GPIO4_4] = {pins[P8_4], 4, 4, 0}, // VBUS_IN_EN
[GPIO4_5] = {pins[P8_5], 4, 5, 0}, // VIN_IN_EN
[GPIO4_6] = {pins[P8_6], 4, 6, 0}, // LED4
[GPIO4_7] = {pins[P8_7], 4, 7, 0}, // 1V2 enable
[GPIO4_8] = {pins[PA_1], 4, 8, 0}, // LPF enable
@@ -470,8 +470,8 @@ constexpr GPIO gpio[] = {
// Placeholder entries for non-PRALINE builds (use P0_0 as dummy)
[GPIO4_14] = {pins[P0_0], 4, 14, 0},
[GPIO4_1] = {pins[P0_0], 4, 1, 0},
[GPIO8_4] = {pins[P0_0], 8, 4, 0},
[GPIO8_5] = {pins[P0_0], 8, 5, 0},
[GPIO4_4] = {pins[P0_0], 4, 4, 0},
[GPIO4_5] = {pins[P0_0], 4, 5, 0},
[GPIO4_6] = {pins[P0_0], 4, 6, 0},
[GPIO4_7] = {pins[P0_0], 4, 7, 0},
[GPIO4_8] = {pins[P0_0], 4, 8, 0},
@@ -212,7 +212,7 @@ struct data_t {
bool updown_frequency_tx_correction;
bool lcd_normally_black : 1;
bool encoder_dial_direction : 1; // false = normal, true = reverse
bool UNUSED_6 : 1;
bool battery_replaceable : 1;
bool UNUSED_7 : 1;
// up/down converter offset
@@ -290,7 +290,7 @@ struct data_t {
updown_frequency_tx_correction(false),
lcd_normally_black(false),
encoder_dial_direction(false),
UNUSED_6(false),
battery_replaceable(false),
UNUSED_7(false),
converter_frequency_offset(0),
@@ -446,6 +446,7 @@ void defaults() {
set_config_tx_gain_max_db(47);
set_battery_cap_mah(0);
set_battery_replaceable(false);
}
void init() {
@@ -1258,6 +1259,14 @@ void set_battery_cap_mah(uint16_t mah) {
}
}
void set_battery_replaceable(bool v) {
data->battery_replaceable = v;
}
bool battery_replaceable() {
return data->battery_replaceable;
}
uint32_t battery_cap_mah() {
if (battery_cap_valid()) {
return data->misc_config.batt_cap_mah;
@@ -395,6 +395,8 @@ uint32_t pmem_calculated_checksum(void);
void set_battery_cap_mah(uint16_t mah); // 0 is not known; use assumed default based on device type/build config
uint32_t battery_cap_mah();
bool battery_cap_valid();
void set_battery_replaceable(bool v);
bool battery_replaceable();
size_t data_size();
+15 -7
View File
@@ -2184,7 +2184,7 @@ bool TextField::on_touch(TouchEvent event) {
BatteryIcon::BatteryIcon(Rect parent_rect, uint8_t percent)
: Widget(parent_rect) {
this->set_battery(percent <= 100 ? 1 : 0, percent, false);
this->set_battery(percent <= 100 ? 1 : 0, percent, false, false);
set_focusable(true);
}
@@ -2195,11 +2195,13 @@ void BatteryIcon::getWidgetName(std::string& result) {
result = "Battery percent";
}
void BatteryIcon::set_battery(uint8_t valid_mask, uint8_t percentage, bool charge) {
if (charge == charge_ && percent_ == percentage && valid_ == valid_mask) return;
void BatteryIcon::set_battery(uint8_t valid_mask, uint8_t percentage, bool charge, bool batt_changed) {
if (charge == charge_ && percent_ == percentage && valid_ == valid_mask && batt_changed == batt_changed_) return;
percent_ = percentage;
charge_ = charge;
valid_ = valid_mask;
batt_changed_ = batt_changed;
if ((valid_mask & battery::BatteryManagement::BATT_VALID_VOLTAGE) != battery::BatteryManagement::BATT_VALID_VOLTAGE) percent_ = 102; // to indicate error
set_dirty();
}
@@ -2227,6 +2229,7 @@ void BatteryIcon::paint(Painter& painter) {
ui::Rect rect = screen_rect(); // 10, 1 * 16
painter.fill_rectangle(rect, has_focus() || highlighted() ? Theme::getInstance()->fg_light->foreground : Theme::getInstance()->bg_dark->background); // clear
ui::Color battColor = (charge_) ? Theme::getInstance()->fg_blue->foreground : Theme::getInstance()->fg_green->foreground;
if (batt_changed_) battColor = Theme::getInstance()->fg_orange->foreground;
// batt body:
painter.draw_vline({rect.left() + 1, rect.top() + 2}, rect.height() - 4, battColor);
painter.draw_vline({rect.right() - 2, rect.top() + 2}, rect.height() - 4, battColor);
@@ -2251,6 +2254,7 @@ void BatteryIcon::paint(Painter& painter) {
else
battColor = Theme::getInstance()->fg_red->foreground;
}
if (batt_changed_) battColor = Theme::getInstance()->fg_orange->foreground;
// fill the bars
for (int y = pp; y < ppx; y++) {
painter.draw_hline({rect.left() + 2, rect.top() + 3 + y}, rect.width() - 4, battColor);
@@ -2261,7 +2265,7 @@ void BatteryIcon::paint(Painter& painter) {
BatteryTextField::BatteryTextField(Rect parent_rect, uint8_t percent)
: Widget(parent_rect) {
this->set_battery(percent <= 100 ? 1 : 0, percent, false);
this->set_battery(percent <= 100 ? 1 : 0, percent, false, false);
set_focusable(true);
}
@@ -2275,8 +2279,11 @@ void BatteryTextField::paint(Painter& painter) {
xdelta = 5;
else if (txt_batt.length() == 2)
xdelta = 2;
std::string post = " %";
if (charge_) post = "+%";
if (batt_changed_) post = "!%";
painter.draw_string({rect.left() + xdelta, rect.top()}, font::fixed_5x8, Theme::getInstance()->bg_dark->foreground, bg, txt_batt);
painter.draw_string({rect.left(), rect.top() + 8}, font::fixed_5x8, Theme::getInstance()->bg_dark->foreground, bg, (charge_) ? "+%" : " %");
painter.draw_string({rect.left(), rect.top() + 8}, font::fixed_5x8, Theme::getInstance()->bg_dark->foreground, bg, post);
}
void BatteryTextField::getAccessibilityText(std::string& result) {
@@ -2286,11 +2293,12 @@ void BatteryTextField::getWidgetName(std::string& result) {
result = "Battery percent";
}
void BatteryTextField::set_battery(uint8_t valid_mask, uint8_t percentage, bool charge) {
if (charge == charge_ && percent_ == percentage && valid_ == valid_mask) return;
void BatteryTextField::set_battery(uint8_t valid_mask, uint8_t percentage, bool charge, bool batt_changed) {
if (charge == charge_ && percent_ == percentage && valid_ == valid_mask && batt_changed == batt_changed_) return;
charge_ = charge;
percent_ = percentage;
valid_ = valid_mask;
batt_changed_ = batt_changed;
if ((valid_mask & battery::BatteryManagement::BATT_VALID_VOLTAGE) != battery::BatteryManagement::BATT_VALID_VOLTAGE) percent_ = 102; // to indicate error
set_dirty();
}
+4 -2
View File
@@ -808,7 +808,7 @@ class BatteryTextField : public Widget {
BatteryTextField(Rect parent_rect, uint8_t percent);
void paint(Painter& painter) override;
void set_battery(uint8_t valid_mask, uint8_t percentage, bool charge);
void set_battery(uint8_t valid_mask, uint8_t percentage, bool charge, bool batt_changed);
void set_text(std::string_view value);
bool on_key(KeyEvent key) override;
@@ -821,6 +821,7 @@ class BatteryTextField : public Widget {
uint8_t percent_{102};
uint8_t valid_{0};
bool charge_{false};
bool batt_changed_{false};
Style style{
.font = font::fixed_5x8,
@@ -835,7 +836,7 @@ class BatteryIcon : public Widget {
BatteryIcon(Rect parent_rect, uint8_t percent);
void paint(Painter& painter) override;
void set_battery(uint8_t valid_mask, uint8_t percentage, bool charge);
void set_battery(uint8_t valid_mask, uint8_t percentage, bool charge, bool batt_changed);
bool on_key(KeyEvent key) override;
bool on_touch(TouchEvent event) override;
@@ -847,6 +848,7 @@ class BatteryIcon : public Widget {
uint8_t percent_{102};
uint8_t valid_{0};
bool charge_{false};
bool batt_changed_{false};
};
class NumberField : public Widget {
@@ -21,6 +21,7 @@
#include "doctest.h"
#include "convert.hpp"
#include <cstdint>
#include <string>
#include <string_view>
+1 -1
Submodule hackrf updated: 0bf478ed25...216949a844
+6
View File
@@ -0,0 +1,6 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# 433 MHz BAND
423,443,433 BAND
+6
View File
@@ -0,0 +1,6 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# AUS UHF CB
476,478,AUS UHF CB
+10
View File
@@ -0,0 +1,10 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# AVIATION
108,118,AVIATION 108 NAVAIDS
118,137,AVIATION 118 VOICE
960,1215,AVIATION 960
1089,1091,ADS-B 1090
128,137,ACARS
+9
View File
@@ -0,0 +1,9 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# BROADCAST RADIO
87,108,FM BROADCAST USA
76,108,FM BROADCAST BRAZIL
76,95,FM BROADCAST JAPAN
65,74,FM OIRT EAST EU
@@ -0,0 +1,6 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# CITIZENS BAND RADIO
26,28,CB RADIO
@@ -0,0 +1,8 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# COMMON PUBLIC SERVICE BANDS
150,160,PUBLIC SERVICE 155
450,470,PUBLIC SERVICE 460
769,775,PUBLIC SERVICE 770
+7
View File
@@ -0,0 +1,7 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# CubeSats
144,146,145 CubeSats
435,438,436 CubeSats
+6
View File
@@ -0,0 +1,6 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# DECT
1879,1931,DECT
+6
View File
@@ -0,0 +1,6 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# FRS/GMRS
462,468,FRS/GMRS
+6
View File
@@ -0,0 +1,6 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# FULL
10,7250,FULL RANGE
+12
View File
@@ -0,0 +1,12 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# HAM RADIO
28,30,HAM 10-METER
50,54,HAM 6-METER
144,148,HAM 2-METER
219,225,HAM 1.25-METER
420,450,HAM 70-CM
902,928,HAM 33-CM
1240,1300,HAM 23-CM
+10
View File
@@ -0,0 +1,10 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# ITU-DEFINED BANDS
1,3,MF BAND
3,30,HF BAND
30,300,VHF BAND
300,3000,UHF BAND
3000,7250,SHF BAND
+11
View File
@@ -0,0 +1,11 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# KEYFOBS
305,325,315 MHz KEYFOBS
380,400,390 MHz KEYFOBS
423,443,433 MHz KEYFOBS
858,878,868 MHz KEYFOBS
902,929,915 MHz KEYFOBS
# 260,500,315/433 MHz KEYFOBS
+9
View File
@@ -0,0 +1,9 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# LoRa
433,435,LoRa 443EU
863,870,LoRa 868EU
902,928,LoRa 915US
779,787,LoRa 780CN
+6
View File
@@ -0,0 +1,6 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# MARINE BAND
155,163,MARINE VHF
+8
View File
@@ -0,0 +1,8 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# MICS
140,380,MICS (VHF) AND MARINE
420,660,MICS (UHF)
169,172,MICS COMMON
@@ -0,0 +1,6 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# MULTI-USE RADIO SERVICE - MURS
151,155,MULTI-USE RADIO SRVS
@@ -0,0 +1,6 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# NOAA WEATHER RADIO
161,164,NOAA WEATHER RADIO
+11
View File
@@ -0,0 +1,11 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# PAGERS https://www.fcc.gov/wireless/bureau-divisions/mobility-division/paging
34,36,USA PAGERS 35
43,45,USA PAGERS 44
151,153,USA PAGERS 152
157,159,USA PAGERS 158
453,454,USA PAGERS 454
929,931,USA PAGERS 930
-87
View File
@@ -1,87 +0,0 @@
# INI,END,DESCRIPTION
# (Freq range in MHz, min separation 3MHz)
# (Description up to 20 char)
# (fields comma delimiter)
# 433 MHz BAND
423,443,433 BAND
# AVIATION
108,118,AVIATION 108 NAVAIDS
118,137,AVIATION 118 VOICE
960,1215,AVIATION 960
1089,1091,ADS-B 1090
128,137,ACARS
# BROADCAST RADIO
87,108,FM BROADCAST USA
76,108,FM BROADCAST BRAZIL
76,95,FM BROADCAST JAPAN
65,74,FM OIRT EAST EU
# CITIZENS BAND RADIO
26,28,CB RADIO
# COMMON PUBLIC SERVICE BANDS
150,160, PUBLIC SERVICE 155
450,470,PUBLIC SERVICE 460
769,775,PUBLIC SERVICE 770
# CubeSats
144,146,145 CubeSats
435,438,436 CubeSats
# DECT
1879,1931,DECT
# FRS/GMRS
462,468,FRS/GMRS
# AUS UHF CB
476,478,AUS UHF CB
# HAM RADIO
28,30,HAM 10-METER
50,54,HAM 6-METER
144,148,HAM 2-METER
219,225,HAM 1.25-METER
420,450,HAM 70-CM
902,928,HAM 33-CM
1240,1300,HAM 23-CM
# KEYFOBS
305,325,315 MHz KEYFOBS
380,400,390 MHz KEYFOBS
423,443,433 MHz KEYFOBS
858,878,868 MHz KEYFOBS
902,929,915 MHz KEYFOBS
# 260,500,315/433 MHz KEYFOBS
# LoRa
433,435,LoRa 443EU
863,870,LoRa 868EU
902,928,LoRa 915US
779,787,LoRa 780CN
# MARINE BAND
155,163,MARINE VHF
# MICS
140,380,MICS (VHF) AND MARINE
420,660,MICS (UHF)
169,172,MICS COMMON
# MULTI-USE RADIO SERVICE - MURS
151,155,MULTI-USE RADIO SRVS
# NOAA WEATHER RADIO
161,164,NOAA WEATHER RADIO
# PAGERS https://www.fcc.gov/wireless/bureau-divisions/mobility-division/paging
34,36,USA PAGERS 35
43,45,USA PAGERS 44
151,153,USA PAGERS 152
157,159,USA PAGERS 158
453,454,USA PAGERS 454
929,931,USA PAGERS 930
# RADIOSONDES
390,420,RADIOSONDES
# RAILROAD
159,162,RAILROAD
# WATER METERS
850,900,Water meters
902,928,ISM 900MHz
# WIFI
2320,2560,BL / WIFI 2.4GHz
5160,5900,WIFI 5GHz
# ITU-DEFINED BANDS
1,3,MF BAND
3,30,HF BAND
30,300,VHF BAND
300,3000,UHF BAND
3000,7250,SHF BAND
# FULL
10,7250,FULL RANGE

Some files were not shown because too many files have changed in this diff Show More