Compare commits

...

23 Commits

Author SHA1 Message Date
Payalo64 4e6a2cc934 Add 3.3GHz Analog FPV Channels to FPV_ANALOG.TXT (#3230) 2026-06-20 18:28:02 +02:00
Pezsma cafe62564e Gpio modify (#3229)
* Refactor GPIO configuration for PRALINE and non-PRALINE setups

- Updated PinMap structure to include gpio_mode for better flexibility.
- Added new GPIO mappings for SGPIO pins with appropriate configurations.
- Commented out unused GPIO definitions in hackrf_gpio.hpp to improve code clarity.
- Adjusted GPIO initialization for control pins to utilize the new PinMap structure.
- Ensured compatibility for both PRALINE and non-PRALINE configurations by using preprocessor directives.

* Refactor GPIO handling and remove LED abstraction

- Updated GPIO class to support logical polarity, enabling/disabling features based on their configured state.
- Replaced direct GPIO manipulation in power control functions with new GPIO methods for better readability and maintainability.
- Removed the LED class and its associated functionality, as it was deemed unnecessary for the current implementation.
- Adjusted GPIO initialization for various components, ensuring correct polarity settings for VAA and power enable pins.
- Cleaned up unused includes and commented-out code in hackrf_gpio.hpp.

* Refactor GPIO LED control methods to use setActive() and setInactive() for improved clarity

* Refactor GPIO control methods to use setActive() and setInactive() for improved clarity and consistency

* copilot

* Update GPIO control logic and pin definitions for clarity and consistency

* Refactor GPIO methods for improved naming consistency and clarity
2026-06-19 07:48:55 +02:00
gullradriel 960e5b8329 Upgrade subm plus warning fix (#3227)
* fix compilation warning
* update submodule
2026-06-17 11:34:00 +02:00
Totoo f5a5e8e5e8 Map improv. (#3226) 2026-06-16 17:54:32 +02:00
Synray d8a1e2530a Add Security+ 2.0 TX App (#3217)
* Add U64 BoundSetting
* Add secplus-tx
* Add secplus license
* Update icon
* UI update
* Add filesystem::path BoundSetting
* Add file UI
* Rename file
* Add save button
* Fix warnings, add integer conversions to UI macros
* UI update
* Copilot suggestions, name edit UI
* Fix buffer length
* Style update
* Avoid recursion in Widget::style()
* Apply suggestions from code review
* Move secplustx.cpp to external
* Shorten title
* Space optimizations, remove FilePath setting
* Remove assertions
* Change namespace
* Change title to match
2026-06-15 19:28:47 +02:00
Totoo b84a424875 MBT update 2026-06-13 09:50:25 +08:00
gullradriel 62d0e598d4 Update submodule (#3222)
* update submodule to latest GSG modifications
* build: fix delay function
* update submodule to include praline fix
* fix dep to rebuild/reinclude hackrf_usb_ram.bin
2026-06-12 22:15:44 +02:00
Synray a93f2ff583 Remove sine table lookup from proc_ook (#3201) 2026-06-12 16:26:13 +02:00
Totoo e2586ceb22 proper fix for flasher (#3223) 2026-06-12 16:25:43 +02:00
未来方舟 5a2b0cdc2a build tool and agent.md (#2993)
* init

* note

* detect arch

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix typo

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update mbt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* protect rm -rf

* remove wrong ai code

* Update mbt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add agent file

* remove empty file

* agent.md rewrite

* agent.md rewrite

* fix on ubuntu

* err check

* add make

* add python3-yaml

* add binutils

* fix the build order to avoid some compile bugs

* mg

* remove trash

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: gullradriel <3157857+gullradriel@users.noreply.github.com>
Co-authored-by: HTotoo <ttotoo@gmail.com>
2026-06-12 16:52:13 +08:00
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
121 changed files with 4870 additions and 2379 deletions
+65
View File
@@ -0,0 +1,65 @@
# PortaPack Mayhem Agent Guidelines
Welcome to the PortaPack Mayhem repository. This document sets mandatory rules and best practices for human developers and AI coding assistants. It exists to prevent unverified, untested code from overwhelming the maintainers, while still enabling AI to accelerate development.
## 🚫 STRICTLY PROHIBITED
1. **No Fully Automated Pull Requests**
Autonomous agents (bots, crawlers, automated workflows) must **never** open a Pull Request without direct human supervision, review, and explicit approval.
2. **No “Blind” AI-Generated Submissions**
If you cannot explain, debug, or maintain the C++/DSP code an AI produced for you, do **not** submit it. PRs that appear to be untested, AI-only “compile-and-pray” attempts will be closed immediately.
3. **No Unverified Hardware Interactions**
AI cannot test on real hardware. Do **not** submit code that has not been compiled and physically tested on a PortaPack device (or a highly accurate emulator). This includes UI rendering, RF transmission quality, and baseband stability.
## ✅ ENCOURAGED AI USE CASES
We strongly encourage using AI assistants (Copilot, ChatGPT, Claude, local LLMs) as pair-programmers. Excellent use cases include:
- **Bug Triage & Root-Causing:** Analyze crash logs, stack traces, and complex memory issues to identify the root cause of an open issue.
- **Code Comprehension:** Map Mayhems architecture, understand M0M4 IPC, or break down complex DSP algorithms.
- **Development Assistance:** Generate boilerplate UI code, write unit tests, optimize math functions, or draft documentation provided a human reviews and refines the output.
- **Targeted Refactoring:** Suggest cleaner, modern C++ for small legacy code segments. **Do not attempt to rewrite the entire codebase.**
## 📋 MANDATORY RULES FOR AIASSISTED CONTRIBUTIONS
When submitting AIassisted work, you must:
1. **Take Full Ownership**
You are strictly responsible for every line of code you submit, regardless of origin. Be prepared to answer detailed technical questions during review.
2. **Hardware Verification Required**
In your PR, explicitly state: *“I have compiled this code and tested it on physical PortaPack hardware.”* PRs without this statement will be rejected.
3. **Transparency (Recommended)**
If AI significantly helped architect a feature or fix a complex bug, mention it briefly in the PR description (e.g., *“Used an LLM to help optimize the DSP filter loop”*).
4. **Adhere to Coding Standards**
Ensure all code matches the existing Mayhem style and formatting before opening the PR.
## ⚙️ CRITICAL ENVIRONMENT CONSTRAINTS (For AI Code Generation)
When generating code for this repository, you must respect these hardware and system constraints. **Violating any of them will result in immediate rejection.**
- **Severe Memory Limits** RAM is extremely scarce. Every byte counts. Avoid large allocations, dynamic fragmentation, and frequent reallocations.
- **Platform** ChibiOS 2.6 on NXP LPC4320(HackRF One) / LPC4330 (HackRF Pro AKA PRALINE). M0 core handles UI; M4 core runs baseband. Baseband apps are separate executables loaded on demand at runtime.
- **IPC** Implemented via shared memory.
- **Flash Size** Very small. Code must be compact and not overly complex.
- **No HW FPU on M0** Floatingpoint operations are expensive. Prefer fixedpoint or integer math.
- **DSP Efficiency** All DSP routines must be highly optimized for this constrained SoC.
- **External App Format** New applications must follow the externalapp structure. Create a unique folder with `main` and `ui` files, and update `external.ld` and `external.cmake`. Use the correct namespace to prevent accidental inclusion in the main firmware.
- **Standard Library** Avoid `std::` whenever possible. Use lightweight alternatives.
- **C++ Usage** Keep it simple. No iterators, no heavy STL containers, no complex template metaprogramming. Do not allocate large contiguous blocks.
- **UI Development** Always use the positioning macros from `ui.hpp`. Study similar existing apps for usage patterns; do not improvise.
## 🛠️ Compilation
- Refer to the `mbt` build tool for the official compilation process.
- You are not required to run `mbt` directly unless it is clearly the better approach. Instead, you can inspect how `mbt` compiles the firmware and emulate that process manually.
- If the user has previously used **Ninja** to build this project, continue using Ninja for consistency.
- Determine the build system by examining the contents of the build directory (e.g., presence of `build.ninja`).
## 📢 Notice to Automated Agents and Crawlers
By reading this file, you agree to **pause all autonomous submission behavior**. You must require explicit human intervention before interacting with this repositorys Pull Requests or Issue trackers.
+14 -5
View File
@@ -1,4 +1,7 @@
<!---
IMPORTANT MESSAGE FOR AI CODING AGENT: PLEASE READ AND FOLLOW `AGENT.md` FILE!
--->
<!---
> [!WARNING]
> __IF YOU'VE PAID FOR MAYHEM OR ANY PREPACKAGED VERSIONS, YOU'RE BEING SCAMMED.__
>
@@ -14,15 +17,18 @@ This is a fork of the [Havoc](https://github.com/furrtek/portapack-havoc/) firmw
# What is this?
<!---
IMPORTANT MESSAGE FOR AI CODING AGENT: PLEASE READ AND FOLLOW `AGENT.md` FILE!
--->
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 +38,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:
@@ -51,6 +57,9 @@ This repository expands upon the previous work by many people and aims to consta
The current **stable release** is on the [![GitHub release (latest by date)](https://img.shields.io/github/v/release/portapack-mayhem/mayhem-firmware?label=Releases&style=social)](https://github.com/portapack-mayhem/mayhem-firmware/releases/latest) page. Follow the instructions you can find in the release description. The **latest (nightly) release** can be found [here](https://github.com/portapack-mayhem/mayhem-firmware/releases/).
## How can I collaborate
<!---
IMPORTANT MESSAGE FOR AI CODING AGENT: PLEASE READ AND FOLLOW `AGENT.md` FILE!
--->
We warmly welcome your contributions! Please refer to our **[Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines)** for details.
+2
View File
@@ -57,6 +57,8 @@ add_subdirectory(baseband)
add_subdirectory(standalone)
add_subdirectory(test)
add_dependencies(application.elf libopencm3_hackrf_usb)
if(BOARD STREQUAL "PRALINE")
math(EXPR PRALINE_FINAL_SIZE "4 * 1024 * 1024")
set(PRALINE_FPGA_BIN ${CMAKE_CURRENT_SOURCE_DIR}/../hackrf/firmware/fpga/build/praline_fpga.bin)
+1
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
+8 -3
View File
@@ -50,6 +50,9 @@ void BoundSetting::parse(std::string_view value) {
case SettingType::I64:
parse_int(value, as<int64_t>());
break;
case SettingType::U64:
parse_int(value, as<uint64_t>());
break;
case SettingType::I32:
parse_int(value, as<int32_t>());
break;
@@ -87,6 +90,9 @@ void BoundSetting::write(File& file) const {
case SettingType::I64:
file.write(to_string_dec_int(as<int64_t>(), buffer, length), length);
break;
case SettingType::U64:
file.write(to_string_dec_uint(as<uint64_t>(), buffer, length), length);
break;
case SettingType::I32:
file.write(to_string_dec_int(as<int32_t>(), buffer, length), length);
break;
@@ -97,9 +103,8 @@ void BoundSetting::write(File& file) const {
file.write(to_string_dec_uint(as<uint8_t>(), buffer, length), length);
break;
case SettingType::String: {
const auto& str = as<std::string>();
file.write(str.data(), str.length());
break;
file.write_line(as<std::string>());
return;
}
case SettingType::Bool:
file.write(as<bool>() ? "1" : "0", 1);
+6 -1
View File
@@ -33,6 +33,7 @@
#include <variant>
#include "file.hpp"
#include "hackrf_hal.hpp"
#include "max283x.hpp"
#include "string_format.hpp"
@@ -47,6 +48,7 @@ class BoundSetting {
/* The type of bound setting. */
enum class SettingType : uint8_t {
I64,
U64,
I32,
U32,
U8,
@@ -59,6 +61,9 @@ class BoundSetting {
BoundSetting(std::string_view name, int64_t* target)
: name_{name}, target_{target}, type_{SettingType::I64} {}
BoundSetting(std::string_view name, uint64_t* target)
: name_{name}, target_{target}, type_{SettingType::U64} {}
BoundSetting(std::string_view name, int32_t* target)
: name_{name}, target_{target}, type_{SettingType::I32} {}
@@ -134,7 +139,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;
+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)},
+2 -2
View File
@@ -3176,9 +3176,9 @@ void RFFC5072StatusView::refresh_status() {
// Blink LED
/*for (int i = 0; i < 3; i++) {
hackrf::one::led_rx.on();
hackrf::one::led_rx.setActive();
chThdSleepMilliseconds(100);
hackrf::one::led_rx.off();
hackrf::one::led_rx.setInactive();
chThdSleepMilliseconds(100);
}*/
} else {
+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;
@@ -134,12 +134,6 @@ bool FlashUtilityView::endsWith(const std::u16string& str, const std::u16string&
}
}
void FlashUtilityView::wait_till_loaded() {
while (!isLoaded) {
chThdSleepMilliseconds(50);
}
}
std::filesystem::path FlashUtilityView::extract_tar(std::filesystem::path::string_type path, ui::Painter& painter) {
//
painter.fill_rectangle(
@@ -157,6 +151,7 @@ std::filesystem::path FlashUtilityView::extract_tar(std::filesystem::path::strin
bool FlashUtilityView::flash_firmware(std::filesystem::path::string_type path) {
ui::Painter painter;
menu_view.hidden(true);
if (endsWith(path, u".tar")) {
// extract, then update
path = extract_tar(u'/' + path, painter).native();
@@ -166,6 +161,7 @@ bool FlashUtilityView::flash_firmware(std::filesystem::path::string_type path) {
painter.fill_rectangle({0, 50, portapack::display.width(), 90}, Theme::getInstance()->bg_darkest->background);
painter.draw_string({0, 60}, *Theme::getInstance()->fg_red, "BAD FIRMWARE FILE OR W/R ERR");
chThdSleepMilliseconds(5000);
menu_view.hidden(false);
return false;
}
painter.fill_rectangle(
@@ -49,7 +49,6 @@ class FlashUtilityView : public View {
std::string title() const override { return "Flash Utility"; };
bool flash_firmware(std::filesystem::path::string_type path);
void wait_till_loaded();
private:
NavigationView& nav_;
@@ -68,14 +67,6 @@ class FlashUtilityView : public View {
void firmware_selected(std::filesystem::path::string_type path);
bool endsWith(const std::u16string& str, const std::u16string& suffix);
bool isLoaded = false;
uint8_t refreshcnt = 0;
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
refreshcnt++;
if (refreshcnt > 5) isLoaded = true;
}};
};
} /* namespace ui */
@@ -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,
+241 -171
View File
@@ -25,12 +25,19 @@
#include "portapack_io.hpp"
#include "portapack.hpp"
#include "lpc43xx.inc"
#include "hackrf_hal.hpp"
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 +332,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 +436,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 +529,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 +600,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 +707,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 +741,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 +804,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 +825,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 +1209,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=setInactive, H=setActive)
switch (func) {
case P1_Function::TriggerIn:
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::AuxClk1:
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setActive();
break;
case P1_Function::ClkIn:
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::TriggerOut:
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setActive();
break;
case P1_Function::P22_ClkIn:
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::P2_5:
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setActive();
break;
case P1_Function::NotConnected:
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::AuxClk2:
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setActive();
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=setInactive, H=setActive)
switch (func) {
case P2_Function::Clk3:
// CTRL0 is 'X' (don't care) according to CSV, we default it to Low (setInactive)
gpio_control::p2_ctrl1.setInactive();
gpio_control::p2_ctrl0.setInactive();
break;
case P2_Function::TriggerIn:
gpio_control::p2_ctrl1.setActive();
gpio_control::p2_ctrl0.setInactive();
break;
case P2_Function::TriggerOut:
gpio_control::p2_ctrl1.setActive();
gpio_control::p2_ctrl0.setActive();
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
+15 -10
View File
@@ -24,6 +24,11 @@
#include "hackrf_gpio.hpp"
#include "portapack_hal.hpp"
#include "board.h"
#include "gpio.hpp"
using namespace gpio_control;
void config_mode_blink_until_dfu();
void config_mode_set() {
@@ -103,16 +108,16 @@ void config_mode_run() {
auto tx_value = ((tx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
if (tx_value) {
hackrf::one::led_tx.on();
led_tx.setActive();
} else {
hackrf::one::led_tx.off();
led_tx.setInactive();
}
auto rx_value = ((rx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
if (rx_value) {
hackrf::one::led_rx.on();
led_rx.setActive();
} else {
hackrf::one::led_rx.off();
led_rx.setInactive();
}
chThdSleepMilliseconds(100);
@@ -122,14 +127,14 @@ void config_mode_run() {
void config_mode_blink_until_dfu() {
while (true) {
hackrf::one::led_tx.on();
hackrf::one::led_rx.on();
hackrf::one::led_usb.on();
led_tx.setActive();
led_rx.setActive();
led_usb.setActive();
chThdSleepMilliseconds(10);
hackrf::one::led_tx.off();
hackrf::one::led_rx.off();
hackrf::one::led_usb.off();
led_tx.setInactive();
led_rx.setInactive();
led_usb.setInactive();
chThdSleepMilliseconds(115);
auto dfu_btn = portapack::gpio_dfu.read();
+5 -2
View File
@@ -33,6 +33,9 @@
#include "irq_controls.hpp"
#include "file_path.hpp"
#include "gpio.hpp"
using namespace gpio_control;
using namespace ui;
#define DEBUG_LOG_FILE "debug_log.txt"
@@ -134,9 +137,9 @@ void draw_line(int32_t y_offset, const char* label, regarm_t value) {
}
void runtime_error(uint8_t source) {
LED led = (source == CORTEX_M0) ? hackrf::one::led_rx : hackrf::one::led_tx;
const auto& led = (source == CORTEX_M0) ? led_rx : led_tx;
led.off();
led.setInactive();
// wait for DFU button release if pressed, so we don't immediately jump into stack dump
while (swizzled_switches() & (1 << (int)Switch::Dfu));
+17 -17
View File
@@ -37,8 +37,8 @@
#include "ch.h"
#include "hackrf_gpio.hpp"
using namespace hackrf::one;
#include "gpio.hpp"
using namespace gpio_control;
#include "irq_rtc.hpp"
@@ -185,10 +185,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 +197,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));
@@ -228,8 +227,8 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
LPC_ADC0->CR &= ~(1 << 21);
LPC_ADC1->CR &= ~(1 << 21);
led_rx.off();
led_usb.off();
led_rx.setInactive();
led_usb.setInactive();
rtc_wakeup_init();
NVIC_EnableIRQ(I2C0_OR_I2C1_IRQn);
@@ -238,28 +237,29 @@ 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);
if (is_full) {
// Case 1: Battery full (All LEDs off)
led_rx.off();
led_tx.off();
led_rx.setInactive();
led_tx.setInactive();
} else if ((voltage < 4150 && current < 10) || valid_mask == 0) {
// Case 2: Not full but low current draw (<10mA) -> Charging error
led_tx.on(); // LED indicates error/idle
led_rx.off();
led_tx.setActive(); // LED indicates error/idle
led_rx.setInactive();
} else {
// Case 3: Actively charging
led_rx.on(); // LED indicates charging
led_tx.off();
led_rx.setActive(); // LED indicates charging
led_tx.setInactive();
}
} else {
// Case 4: Battery IC not detected -> Error or H2 or older, so don't show that as an error.
led_tx.on();
led_rx.on();
led_tx.setActive();
led_rx.setActive();
}
// Shut down I2C and power down the APB bus for sleep
+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__
+7 -1
View File
@@ -327,7 +327,7 @@ set(EXTCPPSRC
external/rtty_tx/ui_rtty_tx.cpp
external/rtty_tx/baudot.cpp
#pocsag_tx
#pocsag_tx
external/pocsag_tx/main.cpp
external/pocsag_tx/ui_pocsag_tx.cpp
@@ -362,6 +362,11 @@ set(EXTCPPSRC
#hard_reset
external/hard_reset/main.cpp
external/hard_reset/ui_hard_reset.cpp
#secplustx
external/secplustx/main.cpp
external/secplustx/ui_secplustx.cpp
external/secplustx/secplustx.cpp
)
set(EXTAPPLIST
@@ -451,6 +456,7 @@ set(EXTAPPLIST
two_tone_pager
two_tone_rx
hard_reset
secplustx
)
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
+7
View File
@@ -110,6 +110,7 @@ MEMORY
ram_external_app_two_tone_rx (rwx) : org = 0xAE050000, len = 32k
ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k
ram_external_app_hard_reset (rwx) : org = 0xAE070000, len = 32k
ram_external_app_secplustx (rwx) : org = 0xAE080000, len = 32k
}
SECTIONS
@@ -635,4 +636,10 @@ SECTIONS
KEEP(*(.external_app.app_hard_reset.application_information));
*(*ui*external_app*hard_reset*);
} > ram_external_app_hard_reset
.external_app_secplustx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_secplustx.application_information));
*(*ui*external_app*secplustx*);
} > ram_external_app_secplustx
}
+81
View File
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 Synray
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_secplustx.hpp"
#include "external_app.hpp"
namespace ui::external_app::ui_secplustx {
void initialize_app(ui::NavigationView& nav) {
nav.push<SecplusTXView>();
}
} // namespace ui::external_app::ui_secplustx
extern "C" {
__attribute__((section(".external_app.app_secplustx.application_information"), used)) application_information_t _application_information_secplustx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::ui_secplustx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Security+",
/*.bitmap_data = */ {
0b00100000,
0b00000000,
0b00100000,
0b00100000,
0b00100000,
0b01110000,
0b00100000,
0b00100000,
0b11100000,
0b00000111,
0b11110000,
0b00001111,
0b00110000,
0b00001100,
0b00110000,
0b00001100,
0b11110000,
0b00001111,
0b11110000,
0b00001111,
0b01110000,
0b00001101,
0b10110000,
0b00001110,
0b01110000,
0b00001101,
0b10110000,
0b00001110,
0b11110000,
0b00001111,
0b11100000,
0b00000111,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_ook */ {'P', 'O', 'O', 'K'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+150
View File
@@ -0,0 +1,150 @@
/*
* Copyright 2022 Clayton Smith (argilo@gmail.com)
*
* This file is part of secplus.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#include "secplustx.hpp"
namespace ui::external_app::ui_secplustx {
static void v2_calc_parity(const uint64_t fixed, uint32_t* data) {
uint32_t parity = (fixed >> 32) & 0xf;
int8_t offset;
*data &= 0xffff0fff;
for (offset = 0; offset < 32; offset += 4) {
parity ^= ((*data >> offset) & 0xf);
}
*data |= (parity << 12);
}
static void encode_v2_rolling(const uint32_t rolling,
uint32_t* rolling_halves) {
uint32_t rolling_reversed = 0;
int8_t i, half;
for (i = 0; i < 28; i++) {
rolling_reversed |= ((rolling >> i) & 1) << (28 - i - 1);
}
rolling_halves[0] = 0;
rolling_halves[1] = 0;
for (half = 0; half < 2; half++) {
for (i = 0; i < 8; i += 2) {
rolling_halves[half] |= rolling_reversed % 3 << i;
rolling_reversed /= 3;
}
}
for (half = 0; half < 2; half++) {
for (i = 10; i < 18; i += 2) {
rolling_halves[half] |= rolling_reversed % 3 << i;
rolling_reversed /= 3;
}
}
rolling_halves[0] |= (rolling_reversed % 3) << 8;
rolling_reversed /= 3;
rolling_halves[1] |= (rolling_reversed % 3) << 8;
}
static const int8_t ORDER[16] = {9, 33, 6, -1, 24, 18, 36, -1,
24, 36, 6, -1, -1, -1, -1, -1};
static const int8_t INVERT[16] = {6, 2, 1, -1, 7, 5, 3, -1,
4, 0, 5, -1, -1, -1, -1, -1};
static void v2_scramble(const uint32_t* parts, const uint8_t frame_type, uint8_t* packet_half) {
const int8_t order = ORDER[packet_half[0] >> 4];
const int8_t invert = INVERT[packet_half[0] & 0xf];
int8_t i;
uint8_t out_offset = 10;
int8_t end;
uint32_t parts_permuted[3];
end = (frame_type == 0 ? 5 : 8);
for (i = 1; i < end; i++) {
packet_half[i] = 0;
}
parts_permuted[0] =
(invert & 4) ? ~parts[(order >> 4) & 3] : parts[(order >> 4) & 3];
parts_permuted[1] =
(invert & 2) ? ~parts[(order >> 2) & 3] : parts[(order >> 2) & 3];
parts_permuted[2] = (invert & 1) ? ~parts[order & 3] : parts[order & 3];
end = (frame_type == 0 ? 8 : 0);
for (i = 18 - 1; i >= end; i--) {
packet_half[out_offset >> 3] |= ((parts_permuted[0] >> i) & 1)
<< (7 - (out_offset % 8));
out_offset++;
packet_half[out_offset >> 3] |= ((parts_permuted[1] >> i) & 1)
<< (7 - (out_offset % 8));
out_offset++;
packet_half[out_offset >> 3] |= ((parts_permuted[2] >> i) & 1)
<< (7 - (out_offset % 8));
out_offset++;
}
}
static void encode_v2_half_parts(const uint32_t rolling, const uint32_t fixed, const uint16_t data, const uint8_t frame_type, uint8_t* packet_half) {
uint32_t parts[3];
parts[0] = ((fixed >> 10) << 8) | (data >> 8);
parts[1] = ((fixed & 0x3ff) << 8) | (data & 0xff);
parts[2] = rolling;
packet_half[0] = (uint8_t)rolling;
v2_scramble(parts, frame_type, packet_half);
}
static int8_t v2_check_limits(const uint32_t rolling, const uint64_t fixed) {
if ((rolling >> 28) != 0) {
return -1;
}
if ((fixed >> 40) != 0) {
return -1;
}
return 0;
}
static void encode_v2_half(const uint32_t rolling, const uint32_t fixed, const uint16_t data, const uint8_t frame_type, uint8_t* packet_half) {
encode_v2_half_parts(rolling, fixed, data, frame_type, packet_half);
/* shift indicator two bits to the right */
packet_half[1] |= (packet_half[0] & 0x3) << 6;
packet_half[0] >>= 2;
/* set frame type */
packet_half[0] |= (frame_type << 6);
}
int8_t encode_v2(const uint32_t rolling, const uint64_t fixed, uint32_t data, const uint8_t frame_type, uint8_t* packet1, uint8_t* packet2) {
int8_t err = 0;
uint32_t rolling_halves[2];
err = v2_check_limits(rolling, fixed);
if (err < 0) {
return err;
}
encode_v2_rolling(rolling, rolling_halves);
v2_calc_parity(fixed, &data);
encode_v2_half(rolling_halves[0], fixed >> 20, data >> 16, frame_type,
packet1);
encode_v2_half(rolling_halves[1], fixed & 0xfffff, data & 0xffff, frame_type,
packet2);
return 0;
}
}; // namespace ui::external_app::ui_secplustx
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2022 Clayton Smith (argilo@gmail.com)
*
* This file is part of secplus.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#ifndef __SECPLUSTX__
#define __SECPLUSTX__
#include <cstdint>
namespace ui::external_app::ui_secplustx {
int8_t encode_v2(const uint32_t rolling, const uint64_t fixed, uint32_t data, const uint8_t frame_type, uint8_t* packet1, uint8_t* packet2);
};
#endif
+227
View File
@@ -0,0 +1,227 @@
#include "ui_secplustx.hpp"
#include <string_view>
#include <string>
#include <utility>
#include <vector>
#include "secplustx.hpp"
#include "ui_fileman.hpp"
#include "file_reader.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "optional.hpp"
using namespace portapack;
using namespace ui;
namespace fs = std::filesystem;
namespace ui::external_app::ui_secplustx {
static constexpr uint8_t secplus_repeat_count = 3;
static constexpr float secplus_sample_rate = OOK_SAMPLERATE / 4000.0f;
Optional<SecplusData> SecplusTXView::read_secplus_file(const fs::path& file_path) {
File file{};
if (auto error = file.open(file_path)) return {};
std::string raw = *FileLineReader{file}.begin();
std::vector chunks = split_string(raw, ';');
if (chunks.empty()) return {};
SecplusData data{};
data.version = static_cast<SecplusVersion>(std::strtoul(chunks[0].data(), NULL, 10));
switch (data.version) {
case SecplusVersion::V1:
return {}; // unimplemented
case SecplusVersion::V2: {
if (chunks.size() != 6) return {};
data.name = std::string{chunks[1]};
if (data.name.empty()) data.name = "Remote";
data.has_data = std::strtoul(chunks[2].data(), NULL, 10);
data.fixed_code = std::strtoull(chunks[3].data(), NULL, 16);
data.rolling_code = std::strtoul(chunks[4].data(), NULL, 16);
data.data = std::strtoul(chunks[5].data(), NULL, 16);
return data;
}
default:
return {};
}
}
bool SecplusTXView::write_secplus_file(const fs::path& file_path, const SecplusData& data) {
ensure_directory(secplus_dir);
File file{};
if (auto error = file.create(file_path)) {
return false;
}
switch (data.version) {
case SecplusVersion::V1:
return false; // unimplemented
case SecplusVersion::V2: {
std::string formatted = to_string_dec_uint(static_cast<uint8_t>(data.version));
formatted += ';';
formatted += data.name;
formatted += ';';
formatted += to_string_dec_uint(data.has_data);
formatted += ';';
formatted += to_string_hex(data.fixed_code);
formatted += ';';
formatted += to_string_hex(data.rolling_code);
formatted += ';';
formatted += to_string_hex(data.data);
file.write_line(formatted);
file.close();
return true;
}
default:
return false;
}
}
SecplusTXView::SecplusTXView(NavigationView& nav)
: nav_{nav} {
if (!fs::file_exists(file_path)) write_secplus_file(file_path, data);
baseband::run_image(portapack::spi_flash::image_tag_ook);
add_children({&button_open,
&button_save,
&field_name,
&labels,
&field_fixed,
&field_rolling,
&has_data,
&field_data,
&learn_mode,
&autosave,
&progressbar,
&tx_view});
button_open.on_select = [this](const Button&) {
ensure_directory(secplus_dir);
auto open_view = nav_.push<FileLoadView>(".SECPLUS");
open_view->push_dir(secplus_dir);
open_view->on_changed = [this](fs::path path) { file_path = path.string(); };
nav_.set_on_pop([this]() { reload_data(); });
};
button_save.on_select = [this](const Button&) { write_secplus_file(file_path, data); };
field_name.on_select = [this, &nav](TextField&) {
buffer = data.name;
text_prompt(nav_, buffer, field_name.parent_rect().width() / UI_POS_DEFAULT_WIDTH, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& new_name) {
data.name = new_name;
field_name.set_text(new_name);
save_data();
});
};
field_fixed.on_change = [this](SymField& field) { data.fixed_code = field.to_integer(); };
field_rolling.on_change = [this](SymField& field) { data.rolling_code = field.to_integer(); };
field_data.on_change = [this](SymField& field) { data.data = field.to_integer(); };
has_data.on_select = [this](Checkbox& checkbox, bool value) {
data.has_data = value;
// fade if inactive, otherwise use default style
const Style* new_style = value ? &style() : Theme::getInstance()->fg_medium;
checkbox.set_style(new_style);
field_data.set_style(new_style);
field_data.set_focusable(value);
};
tx_view.on_edit_frequency = [this]() {
auto new_view = nav_.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
};
};
tx_view.on_start = [this]() { start_tx(); };
tx_view.on_stop = [this]() {
baseband::kill_ook();
stop_tx();
};
autosave.set_value(true);
reload_data();
}
void SecplusTXView::save_data() {
if (autosave.value()) write_secplus_file(file_path, data);
}
void SecplusTXView::reload_data() {
if (auto result = read_secplus_file(file_path)) data = std::move(*result);
// always update data, use default values if read failed
field_name.set_text(data.name);
has_data.set_value(data.has_data);
field_rolling.set_value(data.rolling_code);
field_fixed.set_value(data.fixed_code);
field_data.set_value(data.data);
}
void SecplusTXView::start_tx() {
uint8_t packet1[8]{};
uint8_t packet2[8]{};
size_t bitstream_length = 0;
constexpr uint8_t packet1_indicator = 0b00;
constexpr uint8_t packet2_indicator = 0b01;
auto encode_packet = [&bitstream_length](uint8_t indicator, auto& packet, bool has_data) {
constexpr uint32_t preamble = 0b0000000000000000'1111;
constexpr uint32_t blank_size = 24;
auto manchester_encode = [&bitstream_length](auto& x, uint32_t size) {
for (uint32_t i = 0; i < size; ++i) {
bool bit = (x >> (size - 1 - i)) & 1;
encoders::bitstream_append(bitstream_length, 2, bit ? 0b01 : 0b10);
}
};
manchester_encode(preamble, 20);
manchester_encode(indicator, 2);
for (uint8_t byte = 0; byte < (has_data ? 8 : 5); ++byte) manchester_encode(packet[byte], 8);
encoders::bitstream_append(bitstream_length, blank_size, 0);
};
if (encode_v2(data.rolling_code, data.fixed_code, data.data, data.has_data, packet1, packet2) < 0) {
nav_.display_modal("Error", "Invalid rolling/fixed code");
return;
}
encode_packet(packet1_indicator, packet1, has_data.value());
encode_packet(packet2_indicator, packet2, has_data.value());
if (!learn_mode.value()) {
field_rolling.set_value(++data.rolling_code);
field_rolling.set_dirty();
}
progressbar.set_max(secplus_repeat_count);
progressbar.set_value(1);
tx_view.set_transmitting(true);
transmitter_model.enable();
baseband::set_ook_data(
bitstream_length,
secplus_sample_rate,
secplus_repeat_count,
0);
}
void SecplusTXView::stop_tx() {
transmitter_model.disable();
progressbar.set_value(0);
tx_view.set_transmitting(false);
}
void SecplusTXView::on_tx_progress(uint32_t progress, bool done) {
progressbar.set_value(progress + 1);
if (done) {
stop_tx();
save_data();
}
}
SecplusTXView::~SecplusTXView() {
transmitter_model.disable();
baseband::shutdown();
save_data();
}
} // namespace ui::external_app::ui_secplustx
@@ -0,0 +1,96 @@
#ifndef __UI_SECPLUSTX__
#define __UI_SECPLUSTX__
#include <cstdint>
#include <string>
#include "ui.hpp"
#include "file.hpp"
#include "ui_transmitter.hpp"
#include "transmitter_model.hpp"
#include "file_path.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "encoders.hpp"
#include "string_format.hpp"
namespace ui::external_app::ui_secplustx {
enum class SecplusVersion : uint8_t {
V1,
V2,
};
struct SecplusData {
SecplusVersion version;
std::string name;
bool has_data;
uint64_t fixed_code;
uint32_t rolling_code;
uint32_t data;
};
class SecplusTXView : public View {
public:
void focus() override { button_open.focus(); }
SecplusTXView(NavigationView& nav);
~SecplusTXView();
std::string title() const override {
return "Security+";
}
private:
std::string file_path = (secplus_dir / "DEFAULT.SECPLUS").string();
SecplusData data{SecplusVersion::V2, "Remote", false, 0, 0, 0};
std::string buffer{};
NavigationView& nav_;
TxRadioState radio_state_{
315000000,
1750000,
OOK_SAMPLERATE};
app_settings::SettingsManager settings_{"tx_secplus", app_settings::Mode::TX, {{"file_path"sv, &file_path}}};
Button button_open{{UI_POS_X(1), UI_POS_Y(1), screen_width / 2 - UI_POS_X(1), UI_POS_HEIGHT(2)}, "Open"};
Button button_save{{screen_width / 2, UI_POS_Y(1), screen_width / 2 - UI_POS_X(1), UI_POS_HEIGHT(2)}, "Save"};
// remote data
TextField field_name{{UI_POS_X(1), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(2), UI_POS_HEIGHT(1)}, "Remote"};
Labels labels{
{{UI_POS_X(1), UI_POS_Y(4)}, "Fixed:", Theme::getInstance()->fg_medium->foreground},
{{UI_POS_X(1), UI_POS_Y(5)}, "Rolling:", Theme::getInstance()->fg_medium->foreground}};
SymField field_fixed{{UI_POS_X(10), UI_POS_Y(4)}, 10, SymField::Type::Hex, true};
SymField field_rolling{{UI_POS_X(10), UI_POS_Y(5)}, 7, SymField::Type::Hex, true};
Checkbox has_data{{UI_POS_X(1), UI_POS_Y(6)}, 5, "Data:", true};
SymField field_data{{UI_POS_X(10), UI_POS_Y(6)}, 8, SymField::Type::Hex, true};
// options
Checkbox learn_mode{{UI_POS_X(1), UI_POS_Y(7)}, 5, "Learn", true};
Checkbox autosave{{UI_POS_X(1), UI_POS_Y(8)}, 8, "Autosave", true};
ProgressBar progressbar{{UI_POS_X(2), UI_POS_Y_BOTTOM(5.75), UI_POS_WIDTH_REMAINING(4), UI_POS_HEIGHT(1)}};
TransmitterView tx_view{UI_POS_Y_BOTTOM(4), 1000000, 0};
Optional<SecplusData> read_secplus_file(const std::filesystem::path& file_path);
bool write_secplus_file(const std::filesystem::path& file_path, const SecplusData& data);
void save_data();
void reload_data();
void start_tx();
void stop_tx();
void on_tx_progress(uint32_t progress, bool done);
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
this->on_tx_progress(message.progress, message.done);
}};
};
} // namespace ui::external_app::ui_secplustx
#endif
+1
View File
@@ -57,4 +57,5 @@ const std::filesystem::path macaddress_dir = u"MACADDRESS";
const std::filesystem::path splash_dot_bmp = u"/splash.bmp";
const std::filesystem::path keeloq_keys_dir = u"KEELOQKEYS";
const std::filesystem::path keeloq_remotes_dir = u"KEELOQREMOTES";
const std::filesystem::path secplus_dir = u"SECPLUS";
const std::filesystem::path epirb_dir = u"EPIRB";
+1
View File
@@ -58,6 +58,7 @@ extern const std::filesystem::path waterfalls_dir;
extern const std::filesystem::path macaddress_dir;
extern const std::filesystem::path keeloq_keys_dir;
extern const std::filesystem::path keeloq_remotes_dir;
extern const std::filesystem::path secplus_dir;
extern const std::filesystem::path epirb_dir;
extern const std::filesystem::path splash_dot_bmp;
+10
View File
@@ -1,5 +1,6 @@
#include "gpio_lpc.h"
#include "gpio.h"
#include "delay.h"
typedef enum {
LED1 = 0,
@@ -34,6 +35,15 @@ void delay_us_at_mhz(uint32_t us, uint32_t mhz) {
delay((uint32_t)cycles64);
}
/* The application M0 core runs from BASE_M4_CLK at 200MHz. */
void delay_us(uint32_t us) {
delay_us_at_mhz(us, 200);
}
void delay_ms(uint32_t ms) {
delay_us_at_mhz(ms * 1000, 200);
}
void led_on(const led_t led) {
gpio_set(&gpio_led[led]);
}
+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.setActive(); // Enable external narrow AA filter
} else {
gpio_control::aa_en.setInactive(); // 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.setActive(); // 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 -1
View File
@@ -132,7 +132,6 @@ Continuous (Fox-oring)
#include "spi_image.hpp"
#include "debug.hpp"
#include "led.hpp"
#include "gcc.hpp"
@@ -144,6 +143,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 +187,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;
+10 -9
View File
@@ -53,6 +53,9 @@ using namespace hackrf::one;
#include "baseband_api.hpp"
#include "hal.h" // For LPC_SGPIO
#include "gpio.hpp"
using namespace gpio_control;
#include <array>
/* Direct access to the radio. Setting values incorrectly can damage
@@ -280,9 +283,9 @@ void set_direction(const rf::Direction new_direction) {
baseband_codec.set_mode((direction == rf::Direction::Transmit) ? max5864::Mode::Transmit : max5864::Mode::Receive);
if (direction == rf::Direction::Receive)
led_rx.on();
led_rx.setActive();
else
led_tx.on();
led_tx.setActive();
}
bool set_tuning_frequency(const rf::Frequency frequency) {
@@ -390,11 +393,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);
@@ -444,8 +445,8 @@ void disable() {
first_if.disable();
set_rf_amp(false);
led_rx.off();
led_tx.off();
led_rx.setInactive();
led_tx.setInactive();
}
#ifdef PRALINE
@@ -627,4 +628,4 @@ uint32_t register_read(const size_t register_number) {
} /* namespace debug */
} /* namespace radio */
} /* namespace radio */
+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 {
+3 -1
View File
@@ -26,7 +26,9 @@
#include "audio.hpp"
#include "baseband_api.hpp"
#include "event_m0.hpp"
#include "hackrf_gpio.hpp"
#include "gpio.hpp"
using namespace gpio_control;
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
#include "radio.hpp"
+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;
};
+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,
+67 -25
View File
@@ -237,26 +237,69 @@ void GeoMap::map_read_line_bin(ui::Color* buffer, uint16_t pixels) {
if (map_zoom == 1) {
map_file.read(buffer, pixels << 1);
} else if (map_zoom > 1) {
map_file.read(buffer, (pixels / map_zoom) << 1);
// Zoom in: Expand each pixel to "map_zoom" number of pixels.
// Future TODO: Add dithering to smooth out the pixelation.
// As long as MOD(width,map_zoom)==0 then we don't need to check buffer overflow case when stretching last pixel;
// For 240 width, than means no check is needed for map_zoom values up to 6.
// (Rectangle height must also divide evenly into map_zoom or we get black lines at end of screen)
// Note that zooming in results in a map offset of (1/map_zoom) pixels to the right & downward directions (see zoom_pixel_offset).
for (int i = (width / map_zoom) - 1; i >= 0; i--) {
for (int j = 0; j < map_zoom; j++) {
buffer[(i * map_zoom) + j] = buffer[i];
// Calculate how many source pixels we actually need from the file
uint16_t src_pixels_needed = (pixels + map_zoom - 1) / map_zoom;
if (src_pixels_needed == 0) src_pixels_needed = 1;
// Position the source data at the very END of the buffer.
// This allows us to overwrite the buffer from left-to-right safely.
uint16_t src_offset = pixels - src_pixels_needed;
// Read directly into the tail of the target buffer. Zero extra RAM needed.
map_file.read(&buffer[src_offset], src_pixels_needed << 1);
// Process forwards. Because `dst` grows by 1 and the source index grows by 1/map_zoom,
// `dst` will never catch up to overwrite a source pixel before we are done with it.
for (uint16_t dst = 0; dst < pixels; ++dst) {
uint16_t i0 = dst / map_zoom;
uint16_t i1 = i0 + 1;
// Clamp the right-hand pixel to avoid reading out of bounds on the last iteration
if (i1 >= src_pixels_needed) {
i1 = src_pixels_needed - 1;
}
// 'frac' represents the integer distance from the left pixel (0 to map_zoom - 1)
uint16_t frac = dst % map_zoom;
// Fetch colors from the tail of our buffer
ui::Color c0 = buffer[src_offset + i0];
ui::Color c1 = buffer[src_offset + i1];
if (frac == 0) {
// Exact pixel match, bypass the math entirely for a speed boost
buffer[dst] = c0;
} else {
uint32_t inv_frac = map_zoom - frac;
uint8_t r = ((uint32_t)c0.r() * inv_frac + (uint32_t)c1.r() * frac) / map_zoom;
uint8_t g = ((uint32_t)c0.g() * inv_frac + (uint32_t)c1.g() * frac) / map_zoom;
uint8_t b = ((uint32_t)c0.b() * inv_frac + (uint32_t)c1.b() * frac) / map_zoom;
buffer[dst] = ui::Color(r, g, b);
}
}
} else {
ui::Color zoom_out_buffer[(pixels * (-map_zoom))];
map_file.read(zoom_out_buffer, (pixels * (-map_zoom)) << 1);
// Zoom out: Collapse each group of "-map_zoom" pixels into one pixel.
// Future TODO: Average each group of pixels (in both X & Y directions if possible).
for (int i = 0; i < width; i++) {
buffer[i] = zoom_out_buffer[i * (-map_zoom)];
const int skip = -map_zoom;
const int MAX_BUFFER_ELEMENTS = 256;
// Fixed-size local array avoids VLA crashes.
ui::Color zoom_out_buffer[MAX_BUFFER_ELEMENTS];
const int total_elements_needed = pixels * skip;
// Use the default size, but strictly cap it at 256 to protect the stack
const int chunk_size = total_elements_needed < MAX_BUFFER_ELEMENTS ? total_elements_needed : MAX_BUFFER_ELEMENTS;
int target_i = 0;
int current_file_offset = 0;
// Sequentially read through the required portion of the file in chunks
while (target_i < width && current_file_offset < total_elements_needed) {
// Calculate how many pixels we can read in this specific pass
int read_size = chunk_size < (total_elements_needed - current_file_offset) ? chunk_size : (total_elements_needed - current_file_offset);
// Read the chunk (<< 1 converts pixel count to byte count)
map_file.read(zoom_out_buffer, read_size << 1);
// Determine where the first valid "zoomed" pixel is located inside this specific chunk
int first_valid_index = 0;
int offset_modulo = current_file_offset % skip;
if (offset_modulo != 0) {
first_valid_index = skip - offset_modulo;
}
// Extract only the pixels we need, skipping the rest
for (int j = first_valid_index; j < read_size; j += skip) {
if (target_i < width) {
buffer[target_i] = zoom_out_buffer[j];
target_i++;
}
}
current_file_offset += read_size;
}
}
}
@@ -415,8 +458,7 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int
return true;
}
BMPFile bmp{};
bmp.open("/OSM/" + to_string_dec_int(zoom) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp", true);
BMPFile* bmp = bmp_cache.get(zoom, tile_x, tile_y);
// 1. Define the source and destination areas, starting with the full tile.
int src_x = 0;
int src_y = 0;
@@ -449,7 +491,7 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int
return true;
}
if (!bmp.is_loaded()) {
if (!bmp || !bmp->is_loaded()) {
// Draw an error rectangle using the calculated clipped dimensions
ui::Rect error_rect{{dest_x + r.left(), dest_y + r.top()}, {clip_w, clip_h}};
display.fill_rectangle(error_rect, Theme::getInstance()->bg_darkest->background);
@@ -458,20 +500,20 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int
map_line_buffer.resize(clip_w);
if (bmp.is_bottomup()) {
if (bmp->is_bottomup()) {
for (int y = clip_h - 1; y >= 0; --y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
bmp.read_next_px_cnt(map_line_buffer.data(), clip_w, false);
bmp->seek(src_x, source_row);
bmp->read_next_px_cnt(map_line_buffer.data(), clip_w, false);
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, map_line_buffer);
}
} else {
for (int y = 0; y < clip_h; ++y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
bmp.read_next_px_cnt(map_line_buffer.data(), clip_w, false);
bmp->seek(src_x, source_row);
bmp->read_next_px_cnt(map_line_buffer.data(), clip_w, false);
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, map_line_buffer);
}
}
+101 -2
View File
@@ -30,13 +30,16 @@
#include "bmpfile.hpp"
#include "mathdef.hpp"
#include <array>
#include <cstdio>
#include <inttypes.h>
#include "portapack.hpp"
namespace ui {
#define MAX_MAP_ZOOM_IN 4000
#define MAX_MAP_ZOOM_OUT 10
#define MAP_ZOOM_RESOLUTION_LIMIT 5 // Max zoom-in to show map; rect height & width must divide into this evenly
#define MAX_MAP_ZOOM_OUT 15
#define MAP_ZOOM_RESOLUTION_LIMIT 10 // Max zoom-in to show map;
#define INVALID_LAT_LON 200
#define INVALID_ANGLE 400
@@ -74,6 +77,101 @@ struct GeoMarker {
}
};
class BMPFileCache {
public:
static constexpr uint8_t SlotsCount = 9;
BMPFileCache() {
}
~BMPFileCache() {
clear();
}
BMPFile* get(const int32_t z, const int32_t x, const int32_t y) {
// Cache hit.
for (auto& slot : slots_) {
if (slot.used && slot.x == x && slot.y == y && slot.z == z) {
slot.last_used = next_stamp();
return &slot.bmp;
}
}
// Select free slot first, otherwise LRU slot.
Slot* target = nullptr;
uint16_t oldest = 0xFFFFu;
for (auto& slot : slots_) {
if (!slot.used) {
target = &slot;
break;
}
if (slot.last_used < oldest) {
oldest = slot.last_used;
target = &slot;
}
}
if (!target) {
return nullptr;
}
if (target->used) {
target->bmp.close();
target->used = false;
}
// OSM tile path convention: <base>/<z>/<x>/<y>.bmp
char path_buffer[64];
snprintf(path_buffer, sizeof(path_buffer), "/OSM/%" PRId32 "/%" PRId32 "/%" PRId32 ".bmp", z, x, y);
if (!target->bmp.open(path_buffer, true)) {
target->bmp.close();
return nullptr;
}
target->x = x;
target->y = y;
target->z = z;
target->last_used = next_stamp();
target->used = true;
return &target->bmp;
}
void clear() {
for (auto& slot : slots_) {
if (slot.used) {
slot.bmp.close();
slot.used = false;
}
}
}
private:
struct Slot {
BMPFile bmp{};
int32_t x{};
int32_t y{};
int32_t z{};
uint16_t last_used{};
bool used{false};
};
uint16_t next_stamp() {
if (++stamp_ == 0) {
uint16_t v = 1;
for (auto& slot : slots_) {
if (slot.used) {
slot.last_used = v++;
}
}
stamp_ = v;
}
return stamp_;
}
std::array<Slot, SlotsCount> slots_{};
uint16_t stamp_{0};
};
class GeoPos : public View {
public:
enum alt_unit {
@@ -276,6 +374,7 @@ class GeoMap : public Widget {
bool hide_center_marker_{false};
GeoMapMode mode_{};
File map_file{};
BMPFileCache bmp_cache{};
bool map_opened{};
bool map_visible{};
uint16_t map_width{}, map_height{};
+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 */
+8 -2
View File
@@ -73,6 +73,8 @@
#include "file_path.hpp"
#include "ff.h"
#include "i2cdev_max17055.hpp"
#include <locale>
#include <codecvt>
@@ -334,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);
}
}
@@ -880,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();
}
});
+1 -2
View File
@@ -157,8 +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(150); // to give display some time to paint the screen
open_view->wait_till_loaded(); // also wait for first frame sync
chThdSleepMilliseconds(150); // to give display some time to paint the screen
if (!open_view->flash_firmware(path.native())) {
chprintf(chp, "error\r\n");
}
+10 -1
View File
@@ -776,6 +776,7 @@ set(MODE_CPPSRC
${HACKRF_PATH}/firmware/common/usb_request.c
${HACKRF_PATH}/firmware/common/usb_standard_request.c
${HACKRF_PATH}/firmware/common/platform_detect.c
${HACKRF_PATH}/firmware/common/delay.c
${HACKRF_PATH}/firmware/common/platform_gpio.c
${HACKRF_PATH}/firmware/common/platform_scu.c
${HACKRF_PATH}/firmware/common/gpio_lpc.c
@@ -806,11 +807,19 @@ endif()
### HackRF "factory" firmware
# The RAM image (${HACKRF_FIRMWARE_BIN_IMAGE}) is produced by the hackrf
# "hackrf_usb_ram.bin" custom target (objcopy of the .elf). The previous
# DEPENDS used the bare filename ${HACKRF_FIRMWARE_BIN_FILENAME}, which CMake
# silently dropped, so the embed (hackrf.lz4/hackrf.img) was never regenerated
# when the hackrf firmware changed -> stale RAM image baked into the build.
# Depend on the full path (marked GENERATED so Make doesn't look for a file
# rule) AND on the producing custom target (for build ordering).
set_source_files_properties(${HACKRF_FIRMWARE_BIN_IMAGE} PROPERTIES GENERATED TRUE)
add_custom_command(
OUTPUT hackrf.img
COMMAND ${LZ4} -f -9 ${HACKRF_FIRMWARE_BIN_IMAGE} hackrf.lz4
COMMAND ${MAKE_IMAGE_CHUNK} hackrf.lz4 HRF1 hackrf.img
DEPENDS ${HACKRF_FIRMWARE_BIN_FILENAME} ${MAKE_IMAGE_CHUNK}
DEPENDS ${HACKRF_FIRMWARE_BIN_IMAGE} hackrf_usb_ram.bin ${MAKE_IMAGE_CHUNK}
VERBATIM
)
+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__*/
+4 -17
View File
@@ -22,25 +22,19 @@
#include "proc_ook.hpp"
#include "portapack_shared_memory.hpp"
#include "sine_table_int8.hpp"
#include "event_m4.hpp"
#include <cstdint>
inline void OOKProcessor::write_sample(const buffer_c8_t& buffer, uint8_t bit_value, size_t i) {
int8_t re, im;
if (bit_value) {
phase = (phase + 200); // What ?
sphase = phase + (64 << 18);
re = (sine_table_i8[(sphase & 0x03FC0000) >> 18]);
im = (sine_table_i8[(phase & 0x03FC0000) >> 18]);
re = INT8_MAX;
im = 0;
} else {
re = 0;
im = 0;
}
buffer.p[i] = {re, im};
}
@@ -125,11 +119,8 @@ inline size_t OOKProcessor::duval_algo_step() {
void OOKProcessor::scan_process(const buffer_c8_t& buffer) {
size_t buf_ptr = 0;
// transmit any leftover bits from previous step
if (!scan_encode(buffer, buf_ptr))
return;
while (1) {
// Encode the sequence into required format. First call transmits any leftover bits from previous step
while (scan_encode(buffer, buf_ptr)) {
// calculate next chunk of deBruijn sequence
duval_length = duval_algo_step();
@@ -151,10 +142,6 @@ void OOKProcessor::scan_process(const buffer_c8_t& buffer) {
duval_bit = 0;
duval_sample_bit = 0;
duval_symbol = 0;
// encode the sequence into required format
if (!scan_encode(buffer, buf_ptr))
break;
}
}
-2
View File
@@ -46,8 +46,6 @@ class OOKProcessor : public BasebandProcessor {
uint16_t bit_pos{0};
uint8_t cur_bit{0};
uint32_t sample_count{0};
uint32_t tone_phase{0}, phase{0}, sphase{0};
int32_t tone_sample{0}, sig{0}, frq{0};
TXProgressMessage txprogress_message{};
@@ -39,6 +39,7 @@
#include "platform_scu.h"
#include "fixed_point.h"
#include "clkin.h"
#include "cpu_clock.h"
#include "usb_type.h"
#include <libopencm3/lpc43xx/cgu.h>
#include <libopencm3/lpc43xx/ccu.h>
@@ -51,6 +52,10 @@
#include "gpio_lpc.h"
/* Referenced by the hackrf common delay.c. The Mayhem firmware already runs
* the CPU at 204MHz when this baseband image is started. */
unsigned int cpu_clock_mhz = 204;
/* GPIO Output PinMux */
static struct gpio gpio_led[] = {
GPIO(2, 1),
@@ -492,6 +497,9 @@ static void cpu_clock_pll1_max_speed(void) {
reg_val |= CGU_BASE_M4_CLK_CLK_SEL(CGU_SRC_IRC) | CGU_BASE_M4_CLK_AUTOBLOCK(1);
CGU_BASE_M4_CLK = reg_val;
/* CPU is now at 12MHz */
cpu_clock_mhz = 12;
/* 2. Enable the crystal oscillator. */
CGU_XTAL_OSC_CTRL &= ~CGU_XTAL_OSC_CTRL_ENABLE_MASK;
@@ -537,11 +545,17 @@ static void cpu_clock_pll1_max_speed(void) {
reg_val |= CGU_BASE_M4_CLK_CLK_SEL(CGU_SRC_PLL1);
CGU_BASE_M4_CLK = reg_val;
/* CPU is now at 102MHz */
cpu_clock_mhz = 102;
/* 9. Wait 50us. */
delay_us_at_mhz(50, 102);
/* 10. Set the PLL1 P-divider to direct output mode (DIRECT=1). */
CGU_PLL1_CTRL |= CGU_PLL1_CTRL_DIRECT_MASK;
/* CPU is now at 204MHz */
cpu_clock_mhz = 204;
}
/* clock startup for LPC4320 configure PLL1 to max speed (204MHz).
+2 -1
View File
@@ -26,11 +26,12 @@
#include <libopencm3/lpc43xx/scu.h>
#include <libopencm3/lpc43xx/rgu.h>
#include <libopencm3/lpc43xx/wwdt.h>
#include "delay.h"
#include "string.h"
volatile bool usb_bulk_block_done = false;
void delay(uint32_t duration);
void usb_bulk_block_cb(void* user_data, unsigned int bytes_transferred) {
usb_bulk_block_done = true;
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:
};
+4
View File
@@ -105,6 +105,7 @@ bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
if (!((bmp_header.signature == 0x4D42) &&
(bmp_header.planes == 1) &&
(bmp_header.compression == 0 || bmp_header.compression == 3))) {
bmpimage.close();
return false;
}
@@ -114,6 +115,7 @@ bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
byte_per_px = 1;
// bmpimage.seek(sizeof(bmp_header_t));
// bmpimage.read(color_palette, 1024);
bmpimage.close();
return false; // niy
break;
@@ -121,6 +123,7 @@ bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
byte_per_px = 2;
type = 5;
if (bmp_header.compression == 3) {
bmpimage.close();
return false;
} // niy
@@ -135,6 +138,7 @@ bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
break;
default:
// not supported
bmpimage.close();
return false;
}
byte_per_row = (bmp_header.width * byte_per_px + 3) & ~3;
+178
View File
@@ -0,0 +1,178 @@
/*
* 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"
using namespace gpio_control;
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) */
VAA_en.setActive();
#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. */
og_VAA_en.output();
og_VAA_en.setActive();
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) */
VAA_en.setInactive();
#else
if (hackrf_r9) {
r9_VAA_en.setInactive(); // Turn OFF VAA for r9 P6_10
} else {
og_VAA_en.setInactive(); // 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) {
#ifdef PRALINE
// 1.2V FPGA - P8_7 = GPIO4[7], Active HIGH (Set = ON)
en_1v2.setActive();
#else
if (hackrf_r9) {
r9_1v8_en.setActive();
} else {
og_1v8_en.setActive();
}
#endif
}
void core_power_off(void) {
#ifdef PRALINE
en_1v2.setInactive();
#else
if (hackrf_r9) {
r9_1v8_en.setInactive();
} else {
og_1v8_en.setInactive();
}
#endif
}
} // namespace power_control
+289 -12
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_lld.h"
struct PinConfig {
const uint32_t mode;
@@ -197,6 +200,12 @@ struct Pin {
uint8_t _pin_pad;
};
// Defines the logical polarity of the GPIO pin
enum class Polarity {
ActiveHigh, // High physical state means the function is ON (default)
ActiveLow // Low physical state means the function is ON (inverted)
};
struct GPIO {
// GPIO() = delete;
// GPIO(const GPIO& gpio) = delete;
@@ -206,22 +215,15 @@ struct GPIO {
const Pin& pin,
const ioportid_t gpio_port,
const iopadid_t gpio_pad,
const uint16_t gpio_mode)
const uint16_t gpio_mode,
const Polarity polarity = Polarity::ActiveHigh) // Default parameter ensures backward compatibility
: _pin{pin},
_gpio_port{gpio_port},
_gpio_pad{gpio_pad},
_gpio_mode{gpio_mode} {
_gpio_mode{gpio_mode},
_polarity{polarity} {
}
/*
constexpr GPIO(
const GPIO& gpio
) : _pin { gpio._pin },
_gpio_port { gpio._gpio_port },
_gpio_pad { gpio._gpio_pad },
_gpio_mode { gpio._gpio_mode }
{
}
*/
constexpr ioportid_t port() const {
return _gpio_port;
}
@@ -234,6 +236,11 @@ struct GPIO {
return _pin;
}
// Returns the configured polarity of the pin
constexpr Polarity polarity() const {
return _polarity;
}
void configure() const {
_pin.mode(_gpio_mode);
}
@@ -242,6 +249,8 @@ struct GPIO {
return _gpio_mode;
}
// Physical Level Operations
void set() const {
palSetPad(_gpio_port, _gpio_pad);
}
@@ -270,6 +279,39 @@ struct GPIO {
return palReadPad(_gpio_port, _gpio_pad);
}
// Turns the feature ON based on its polarity
void setActive() const {
if (_polarity == Polarity::ActiveHigh) {
set();
} else {
clear();
}
}
// Turns the feature OFF based on its polarity
void setInactive() const {
if (_polarity == Polarity::ActiveHigh) {
clear();
} else {
set();
}
}
// Sets the logical state of the feature
void setState(const bool active) const {
if (active) {
setActive();
} else {
setInactive();
}
}
// Returns true if the feature is logically active/enabled
bool isEnabled() const {
const bool physical_state = read();
return (_polarity == Polarity::ActiveHigh) ? physical_state : !physical_state;
}
bool operator!=(const GPIO& other) const {
return (port() != other.port()) || (pad() != other.pad());
}
@@ -278,6 +320,241 @@ struct GPIO {
const ioportid_t _gpio_port;
const iopadid_t _gpio_pad;
const uint16_t _gpio_mode;
const Polarity _polarity;
};
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);
}
}
namespace gpio_control {
struct PinMap {
uint8_t scu_port;
uint8_t scu_pin;
uint8_t gpio_port;
uint8_t gpio_pad;
uint8_t gpio_mode;
};
#ifdef PRALINE
constexpr PinMap map_p1_ctrl0{2, 10, 0, 14, 0}; // SCU: 2, 10 | GPIO: 0, 14
constexpr PinMap map_p1_ctrl1{6, 8, 5, 16, 4};
constexpr PinMap map_p1_ctrl2{6, 9, 3, 5, 0};
constexpr PinMap map_p2_ctrl0{8, 3, 7, 3, 4};
constexpr PinMap map_p2_ctrl1{8, 4, 7, 4, 4};
constexpr PinMap map_trigger_out{2, 6, 5, 6, 4};
constexpr PinMap map_trigger_in{13, 12, 6, 26, 4};
constexpr PinMap map_clkin_ctrl{1, 20, 0, 15, 0};
constexpr PinMap map_rf5072_mix_en{9, 0, 4, 12, 0};
constexpr PinMap map_aa_en{1, 14, 1, 7, 0}; // Anti-Aliasing filter
constexpr PinMap map_sgpio_4{9, 4, 5, 17, 6};
constexpr PinMap map_sgpio_8{8, 0, 4, 0, 4};
constexpr PinMap map_sgpio_9{9, 3, 4, 15, 6};
constexpr PinMap map_sgpio_10{8, 2, 4, 2, 4};
constexpr PinMap map_en_1v2{8, 7, 4, 7, 0};
constexpr PinMap map_vregmode{4, 9, 5, 13, 4};
constexpr PinMap map_VAA_en{8, 1, 4, 1, 0};
constexpr PinMap map_led_mcu{8, 6, 4, 6, 0};
#else
constexpr PinMap map_sgpio_4{6, 3, 3, 2, 2};
constexpr PinMap map_sgpio_8{9, 6, 4, 11, 6};
constexpr PinMap map_sgpio_9{4, 3, 2, 3, 7};
constexpr PinMap map_sgpio_10{1, 14, 1, 7, 6};
constexpr PinMap map_sgpio_13{4, 8, 5, 12, 4};
constexpr PinMap map_og_1v8_en{6, 10, 3, 6, 0};
constexpr PinMap map_r9_1v8_en{5, 0, 2, 9, 0};
constexpr PinMap map_vregmode{6, 11, 3, 7, 0};
constexpr PinMap map_og_VAA_en{5, 0, 2, 9, 0};
constexpr PinMap map_r9_VAA_en{6, 10, 3, 6, 0};
#endif
constexpr PinMap map_sgpio_0{0, 0, 0, 0, 3};
constexpr PinMap map_sgpio_1{0, 1, 0, 1, 3};
constexpr PinMap map_sgpio_2{1, 15, 0, 2, 2};
constexpr PinMap map_sgpio_3{1, 16, 0, 3, 2};
constexpr PinMap map_sgpio_5{6, 6, 0, 5, 2};
constexpr PinMap map_sgpio_6{2, 2, 5, 2, 0};
constexpr PinMap map_sgpio_7{1, 0, 0, 4, 6};
constexpr PinMap map_sgpio_11{1, 17, 0, 12, 6};
constexpr PinMap map_sgpio_12{1, 18, 0, 13, 0};
constexpr PinMap map_led_usb{4, 1, 2, 1, 0};
constexpr PinMap map_led_rx{4, 2, 2, 2, 0};
constexpr PinMap map_led_tx{6, 12, 2, 8, 0};
#ifdef PRALINE
// 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, map_p1_ctrl0.gpio_mode}; // 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, map_p1_ctrl1.gpio_mode}; // 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, map_p1_ctrl2.gpio_mode}; // 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, map_p2_ctrl0.gpio_mode}; // 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, map_p2_ctrl1.gpio_mode}; // 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, map_trigger_out.gpio_mode}; // 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, map_trigger_in.gpio_mode}; // GPIO[6]26
// Clock input control
constexpr Pin pin_clkin_ctrl{map_clkin_ctrl.scu_port, map_clkin_ctrl.scu_pin}; // SCU: P1_20
constexpr GPIO clkin_ctrl{pin_clkin_ctrl, map_clkin_ctrl.gpio_port, map_clkin_ctrl.gpio_pad, map_clkin_ctrl.gpio_mode}; // GPIO[0]15
// RF507x
constexpr Pin pin_rf5072_mix_en{map_rf5072_mix_en.scu_port, map_rf5072_mix_en.scu_pin}; // SCU: P9_0
constexpr GPIO rf5072_mix_en{pin_rf5072_mix_en, map_rf5072_mix_en.gpio_port, map_rf5072_mix_en.gpio_pad, map_rf5072_mix_en.gpio_mode}; // GPIO[4]12
// Anti-Aliasing filter
constexpr Pin pin_aa_en{map_aa_en.scu_port, map_aa_en.scu_pin}; // SCU: P1_14
constexpr GPIO aa_en{pin_aa_en, map_aa_en.gpio_port, map_aa_en.gpio_pad, map_aa_en.gpio_mode}; // GPIO[1]7
constexpr Pin pin_en_1v2{map_en_1v2.scu_port, map_en_1v2.scu_pin}; // SCU: P8_7
constexpr GPIO en_1v2{pin_en_1v2, map_en_1v2.gpio_port, map_en_1v2.gpio_pad, map_en_1v2.gpio_mode}; // GPIO[4]7
constexpr Pin pin_VAA_en{map_VAA_en.scu_port, map_VAA_en.scu_pin}; // SCU: P8_1
constexpr GPIO VAA_en{pin_VAA_en, map_VAA_en.gpio_port, map_VAA_en.gpio_pad, map_VAA_en.gpio_mode, Polarity::ActiveLow}; // GPIO[4]1
constexpr Pin pin_led_usb{map_led_usb.scu_port, map_led_usb.scu_pin}; // SCU: P4_1
constexpr GPIO led_usb{pin_led_usb, map_led_usb.gpio_port, map_led_usb.gpio_pad, map_led_usb.gpio_mode, Polarity::ActiveLow}; // GPIO[2]1
constexpr Pin pin_led_rx{map_led_rx.scu_port, map_led_rx.scu_pin}; // SCU: P4_2
constexpr GPIO led_rx{pin_led_rx, map_led_rx.gpio_port, map_led_rx.gpio_pad, map_led_rx.gpio_mode, Polarity::ActiveLow}; // GPIO[2]2
constexpr Pin pin_led_tx{map_led_tx.scu_port, map_led_tx.scu_pin}; // SCU: P6_12
constexpr GPIO led_tx{pin_led_tx, map_led_tx.gpio_port, map_led_tx.gpio_pad, map_led_tx.gpio_mode, Polarity::ActiveLow}; // GPIO[2]8
constexpr Pin pin_led_mcu{map_led_mcu.scu_port, map_led_mcu.scu_pin}; // SCU: P8_6
constexpr GPIO led_mcu{pin_led_mcu, map_led_mcu.gpio_port, map_led_mcu.gpio_pad, map_led_mcu.gpio_mode, Polarity::ActiveLow}; // GPIO[4]6
#else
constexpr Pin pin_sgpio_13{map_sgpio_13.scu_port, map_sgpio_13.scu_pin}; // SCU: P4_8
constexpr GPIO sgpio_13{pin_sgpio_13, map_sgpio_13.gpio_port, map_sgpio_13.gpio_pad, map_sgpio_13.gpio_mode}; // GPIO[5]12
constexpr Pin pin_og_1v8_en{map_og_1v8_en.scu_port, map_og_1v8_en.scu_pin}; // SCU: P6_10
constexpr GPIO og_1v8_en{pin_og_1v8_en, map_og_1v8_en.gpio_port, map_og_1v8_en.gpio_pad, map_og_1v8_en.gpio_mode}; // GPIO[3]6
constexpr Pin pin_r9_1v8_en{map_r9_1v8_en.scu_port, map_r9_1v8_en.scu_pin}; // SCU: P5_0
constexpr GPIO r9_1v8_en{pin_r9_1v8_en, map_r9_1v8_en.gpio_port, map_r9_1v8_en.gpio_pad, map_r9_1v8_en.gpio_mode}; // GPIO[2]9
constexpr Pin pin_og_VAA_en{map_og_VAA_en.scu_port, map_og_VAA_en.scu_pin}; // SCU: P5_0
constexpr GPIO og_VAA_en{pin_og_VAA_en, map_og_VAA_en.gpio_port, map_og_VAA_en.gpio_pad, map_og_VAA_en.gpio_mode, Polarity::ActiveLow}; // GPIO[2]9
constexpr Pin pin_r9_VAA_en{map_r9_VAA_en.scu_port, map_r9_VAA_en.scu_pin}; // SCU: P6_10
constexpr GPIO r9_VAA_en{pin_r9_VAA_en, map_r9_VAA_en.gpio_port, map_r9_VAA_en.gpio_pad, map_r9_VAA_en.gpio_mode, Polarity::ActiveLow}; // GPIO[3]6
constexpr Pin pin_led_usb{map_led_usb.scu_port, map_led_usb.scu_pin}; // SCU: P4_1
constexpr GPIO led_usb{pin_led_usb, map_led_usb.gpio_port, map_led_usb.gpio_pad, map_led_usb.gpio_mode}; // GPIO[2]1
constexpr Pin pin_led_rx{map_led_rx.scu_port, map_led_rx.scu_pin}; // SCU: P4_2
constexpr GPIO led_rx{pin_led_rx, map_led_rx.gpio_port, map_led_rx.gpio_pad, map_led_rx.gpio_mode}; // GPIO[2]2
constexpr Pin pin_led_tx{map_led_tx.scu_port, map_led_tx.scu_pin}; // SCU: P6_12
constexpr GPIO led_tx{pin_led_tx, map_led_tx.gpio_port, map_led_tx.gpio_pad, map_led_tx.gpio_mode}; // GPIO[2]8
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 = {map_og_VAA_en.scu_port, map_og_VAA_en.scu_pin, scu_config_normal_drive_t{.mode = map_og_VAA_en.gpio_mode, .epd = 0, .epun = 1, .ehs = 0, .ezi = 0, .zif = 0}};
static const scu_setup_t pin_setup_vaa_enablex_gpio_r9 = {map_r9_VAA_en.scu_port, map_r9_VAA_en.scu_pin, scu_config_normal_drive_t{.mode = map_r9_VAA_en.gpio_mode, .epd = 0, .epun = 1, .ehs = 0, .ezi = 0, .zif = 0}};
#endif
// SGPIO
constexpr Pin pin_sgpio_0{map_sgpio_0.scu_port, map_sgpio_0.scu_pin}; // SCU: P0_0
constexpr GPIO sgpio_0{pin_sgpio_0, map_sgpio_0.gpio_port, map_sgpio_0.gpio_pad, map_sgpio_0.gpio_mode}; // GPIO[0]0
constexpr Pin pin_sgpio_1{map_sgpio_1.scu_port, map_sgpio_1.scu_pin}; // SCU: P0_1
constexpr GPIO sgpio_1{pin_sgpio_1, map_sgpio_1.gpio_port, map_sgpio_1.gpio_pad, map_sgpio_1.gpio_mode}; // GPIO[0]1
constexpr Pin pin_sgpio_2{map_sgpio_2.scu_port, map_sgpio_2.scu_pin}; // SCU: P1_15
constexpr GPIO sgpio_2{pin_sgpio_2, map_sgpio_2.gpio_port, map_sgpio_2.gpio_pad, map_sgpio_2.gpio_mode}; // GPIO[0]2
constexpr Pin pin_sgpio_3{map_sgpio_3.scu_port, map_sgpio_3.scu_pin}; // SCU: P1_16
constexpr GPIO sgpio_3{pin_sgpio_3, map_sgpio_3.gpio_port, map_sgpio_3.gpio_pad, map_sgpio_3.gpio_mode}; // GPIO[0]3
constexpr Pin pin_sgpio_4{map_sgpio_4.scu_port, map_sgpio_4.scu_pin}; // SCU: P6_3, PRALINE: P9_4
constexpr GPIO sgpio_4{pin_sgpio_4, map_sgpio_4.gpio_port, map_sgpio_4.gpio_pad, map_sgpio_4.gpio_mode}; // GPIO[6]3 PRALINE: GPIO[5]17
constexpr Pin pin_sgpio_5{map_sgpio_5.scu_port, map_sgpio_5.scu_pin}; // SCU: P6_6
constexpr GPIO sgpio_5{pin_sgpio_5, map_sgpio_5.gpio_port, map_sgpio_5.gpio_pad, map_sgpio_5.gpio_mode}; // GPIO[0]5
constexpr Pin pin_sgpio_6{map_sgpio_6.scu_port, map_sgpio_6.scu_pin}; // SCU: P2_2
constexpr GPIO sgpio_6{pin_sgpio_6, map_sgpio_6.gpio_port, map_sgpio_6.gpio_pad, map_sgpio_6.gpio_mode}; // GPIO[5]2
constexpr Pin pin_sgpio_7{map_sgpio_7.scu_port, map_sgpio_7.scu_pin}; // SCU: P1_0
constexpr GPIO sgpio_7{pin_sgpio_7, map_sgpio_7.gpio_port, map_sgpio_7.gpio_pad, map_sgpio_7.gpio_mode}; // GPIO[0]4
constexpr Pin pin_sgpio_8{map_sgpio_8.scu_port, map_sgpio_8.scu_pin}; // SCU: P9_6, PRALINE: P8_0
constexpr GPIO sgpio_8{pin_sgpio_8, map_sgpio_8.gpio_port, map_sgpio_8.gpio_pad, map_sgpio_8.gpio_mode}; // GPIO[4]11, PRALINE: GPIO[4]0
constexpr Pin pin_sgpio_9{map_sgpio_9.scu_port, map_sgpio_9.scu_pin}; // SCU: P4_3, PRALINE: P9_3
constexpr GPIO sgpio_9{pin_sgpio_9, map_sgpio_9.gpio_port, map_sgpio_9.gpio_pad, map_sgpio_9.gpio_mode}; // GPIO[2]3, PRALINE: GPIO[4]12
constexpr Pin pin_sgpio_10{map_sgpio_10.scu_port, map_sgpio_10.scu_pin}; // SCU: P1_14, PRALINE: P8_2
constexpr GPIO sgpio_10{pin_sgpio_10, map_sgpio_10.gpio_port, map_sgpio_10.gpio_pad, map_sgpio_10.gpio_mode}; // GPIO[1]7, PRALINE: GPIO[4]2
constexpr Pin pin_sgpio_11{map_sgpio_11.scu_port, map_sgpio_11.scu_pin}; // SCU: P1_17
constexpr GPIO sgpio_11{pin_sgpio_11, map_sgpio_11.gpio_port, map_sgpio_11.gpio_pad, map_sgpio_11.gpio_mode}; // GPIO[0]17
constexpr Pin pin_sgpio_12{map_sgpio_12.scu_port, map_sgpio_12.scu_pin}; // SCU: P1_18
constexpr GPIO sgpio_12{pin_sgpio_12, map_sgpio_12.gpio_port, map_sgpio_12.gpio_pad, map_sgpio_12.gpio_mode}; // GPIO[0]13
constexpr Pin pin_vregmode{map_vregmode.scu_port, map_vregmode.scu_pin}; // SCU: P6_11, PRALINE: P4_9
constexpr GPIO vregmode{pin_vregmode, map_vregmode.gpio_port, map_vregmode.gpio_pad, map_vregmode.gpio_mode}; // GPIO[3]7, PRALINE: GPIO[5]13
} // namespace gpio_control
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);
} // namespace power_control
#endif /*__GPIO_H__*/
+11 -55
View File
@@ -23,7 +23,6 @@
#define __HACKRF_GPIO_H__
#include "pins.hpp"
#include "led.hpp"
#include <array>
@@ -34,27 +33,11 @@ namespace one {
/* GPIO */
constexpr GPIO gpio_led_usb = gpio[GPIO2_1];
constexpr GPIO gpio_led_rx = gpio[GPIO2_2];
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];
#ifdef PRALINE
constexpr GPIO gpio_mix_bypass = gpio[GPIO3_2]; // P6_3: PRALINE RF path mixer bypass inverted
constexpr GPIO gpio_mix_en_n_r1_0 = gpio[GPIO5_6]; // P2_6: R1.0 board mixer bypass
constexpr GPIO gpio_mix_bypass = gpio[GPIO3_2]; // P6_3: PRALINE RF path mixer bypass inverted
// constexpr GPIO gpio_mix_en_n_r1_0 = gpio[GPIO5_6]; // P2_6: R1.0 board mixer bypass
#else
constexpr GPIO gpio_mix_bypass = gpio[GPIO5_16];
#endif
@@ -70,7 +53,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 +62,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
@@ -98,11 +81,11 @@ constexpr GPIO gpio_max283x_select = gpio[GPIO0_15];
#ifdef PRALINE
// PRALINE uses MAX2831 transceiver with different control pins
constexpr GPIO gpio_max283x_enable = gpio[GPIO7_1]; // MAX2831 ENABLE (PE_1)
constexpr GPIO gpio_max2831_enable = gpio[GPIO7_1]; // Alias
constexpr GPIO gpio_max283x_enable = gpio[GPIO7_1]; // MAX2831 ENABLE (PE_1)
// constexpr GPIO gpio_max2831_enable = gpio[GPIO7_1]; // Alias
constexpr GPIO gpio_max2831_rx_enable = gpio[GPIO7_2]; // MAX2831 RX_ENABLE (PE_2)
constexpr GPIO gpio_max2831_rxhp = gpio[GPIO6_29]; // MAX2831 RXHP (PD_15)
constexpr GPIO gpio_max2831_ld = gpio[GPIO4_11]; // MAX2831 Lock Detect (P9_6)
// constexpr GPIO gpio_max2831_rxhp = gpio[GPIO6_29]; // MAX2831 RXHP (PD_15)
constexpr GPIO gpio_max2831_ld = gpio[GPIO4_11]; // MAX2831 Lock Detect (P9_6)
// Legacy aliases for code compatibility
constexpr GPIO gpio_max2837_rxenable = gpio[GPIO7_2];
constexpr GPIO gpio_max2837_txenable = gpio[GPIO7_2]; // MAX2831 uses single RX/TX control
@@ -116,7 +99,6 @@ constexpr GPIO gpio_max2839_rxtx = gpio[GPIO2_5];
#ifdef PRALINE
constexpr GPIO gpio_max5864_select = gpio[GPIO6_30]; // PD_16: PRALINE MAX5864 CS
constexpr GPIO gpio_fpga_select = gpio[GPIO2_10]; // FPGA SPI CS (P5_1)
#else
constexpr GPIO gpio_max5864_select = gpio[GPIO2_7];
#endif
@@ -125,36 +107,16 @@ constexpr GPIO gpio_q_invert = gpio[GPIO0_13];
#ifdef PRALINE
// PRALINE power control
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_vaa_disable = gpio[GPIO4_1]; // VAA disable (P8_1)
// 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_tx_enable = gpio[GPIO3_4]; // TX enable (P6_5)
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)
// 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)
// constexpr GPIO gpio_pps_out = gpio[GPIO5_5]; // PPS output (P2_5)
// PRALINE LED4
constexpr GPIO gpio_led4 = gpio[GPIO4_6]; // LED4 (P8_6)
// PRALINE dual port control
constexpr GPIO gpio_p1_ctrl0 = gpio[GPIO0_14]; // P1 control 0 (P2_10)
constexpr GPIO gpio_p1_ctrl1 = gpio[GPIO5_16]; // P1 control 1 (P6_8)
constexpr GPIO gpio_p1_ctrl2 = gpio[GPIO3_5]; // P1 control 2 (P6_9)
constexpr GPIO gpio_p2_ctrl0 = gpio[GPIO7_3]; // P2 control 0 (PE_3)
constexpr GPIO gpio_p2_ctrl1 = gpio[GPIO7_4]; // P2 control 1 (PE_4)
constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15]; // CLKIN control (P1_20)
// PRALINE trigger/sync I/O
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
@@ -175,12 +137,6 @@ constexpr GPIO gpio_r9_clkout_en = gpio[GPIO0_9];
constexpr GPIO gpio_r9_mcu_clk_en = gpio[GPIO0_8];
constexpr GPIO gpio_r9_not_ant_pwr = gpio[GPIO2_4];
/* LEDs */
constexpr LED led_usb{gpio_led_usb};
constexpr LED led_rx{gpio_led_rx};
constexpr LED led_tx{gpio_led_tx};
} /* namespace one */
} /* namespace hackrf */
+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;
-75
View File
@@ -1,75 +0,0 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
*
* 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 __LED_H__
#define __LED_H__
#include "gpio.hpp"
struct LED {
constexpr LED(const GPIO gpio)
: _gpio{gpio} {
}
void setup() const {
#ifdef PRALINE
/* PRALINE LEDs are active-low (GPIO LOW = LED ON) */
_gpio.set(); /* Start with LED OFF (HIGH) */
#else
_gpio.clear();
#endif
_gpio.output();
_gpio.configure();
}
void on() const {
#ifdef PRALINE
_gpio.clear(); /* LOW = ON for PRALINE */
#else
_gpio.set();
#endif
}
void off() const {
#ifdef PRALINE
_gpio.set(); /* HIGH = OFF for PRALINE */
#else
_gpio.clear();
#endif
}
void toggle() const {
_gpio.toggle();
}
void write(const bool value) const {
#ifdef PRALINE
_gpio.write(!value); /* Invert for PRALINE */
#else
_gpio.write(value);
#endif
}
private:
const GPIO _gpio;
};
#endif /*__LED_H__*/
+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();
+2 -2
View File
@@ -23,9 +23,9 @@
#ifndef __SINE_TABLE_I8_H__
#define __SINE_TABLE_I8_H__
#include <cmath>
#include <cstdint>
static const int8_t sine_table_i8[256] = {
static constexpr int8_t sine_table_i8[256] = {
0, 2, 5, 8, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45,
48, 51, 54, 57, 59, 62, 65, 67, 70, 73, 75, 78, 80, 83, 85, 87,
90, 92, 94, 96, 98, 100, 102, 104, 105, 107, 109, 110, 112, 113, 115, 116,
+16 -16
View File
@@ -26,6 +26,9 @@
namespace ui {
using Coord = int16_t;
using Dim = int16_t;
// Positioning helpers PER CHARACTERS (8*16)
// EACH parameters must be used az CHAR position, not PX coordinates. So If you wanna use the 8,16 coordinates (that is the second character in X and second character in Y you must use UI_POS_X(1) UI_POS_Y(1) (since we count from 0)
@@ -36,31 +39,31 @@ namespace ui {
// default font width
#define UI_POS_DEFAULT_WIDTH 8
// px position of the linenum-th character (Y)
#define UI_POS_Y(linenum) ((int)((linenum) * UI_POS_DEFAULT_HEIGHT))
#define UI_POS_Y(linenum) ((Coord)((linenum) * UI_POS_DEFAULT_HEIGHT))
// px position of the linenum-th character from the bottom of the screen (Y) (please calculate the +1 line top-bar to it too if that is visible!)
#define UI_POS_Y_BOTTOM(linenum) ((int)(screen_height - (linenum) * UI_POS_DEFAULT_HEIGHT))
#define UI_POS_Y_BOTTOM(linenum) ((Coord)(screen_height - (int)((linenum) * UI_POS_DEFAULT_HEIGHT)))
// px position of the linenum-th character from the left of the screen (X)
#define UI_POS_X(charnum) ((int)((charnum) * UI_POS_DEFAULT_WIDTH))
#define UI_POS_X(charnum) ((Coord)((charnum) * UI_POS_DEFAULT_WIDTH))
// px position of the linenum-th character from the right of the screen (X)
#define UI_POS_X_RIGHT(charnum) ((int)(screen_width - ((charnum) * UI_POS_DEFAULT_WIDTH)))
#define UI_POS_X_RIGHT(charnum) ((Coord)(screen_width - (int)((charnum) * UI_POS_DEFAULT_WIDTH)))
// px position of the left character from the center of the screen (X) (for N character wide string)
#define UI_POS_X_CENTER(charnum) ((int)((screen_width / 2) - ((charnum) * UI_POS_DEFAULT_WIDTH / 2)))
// px position of the currcol in a table with colnum number of columns, where one coloumn is charnum characters wide maximum
#define UI_POS_X_TABLE(colnum, currcol) ((currcol) * (screen_width / (colnum)))
#define UI_POS_X_CENTER(charnum) ((Coord)((screen_width / 2) - (int)((charnum) * UI_POS_DEFAULT_WIDTH / 2)))
// px position of the currcol in a table with colnum number of columns, where one column is charnum characters wide maximum
#define UI_POS_X_TABLE(colnum, currcol) ((Coord)((currcol) * (screen_width / (colnum))))
// px width of N characters
#define UI_POS_WIDTH(charnum) ((int)((charnum) * UI_POS_DEFAULT_WIDTH))
#define UI_POS_WIDTH(charnum) ((Dim)((charnum) * UI_POS_DEFAULT_WIDTH))
// px width of the screen
#define UI_POS_MAXWIDTH (screen_width)
// px height of N line
#define UI_POS_HEIGHT(linecount) ((int)((linecount) * UI_POS_DEFAULT_HEIGHT))
#define UI_POS_HEIGHT(linecount) ((Dim)((linecount) * UI_POS_DEFAULT_HEIGHT))
// px height of the screen's percent
#define UI_POS_HEIGHT_PERCENT(percent) ((int)(screen_height * (percent) / 100))
#define UI_POS_HEIGHT_PERCENT(percent) ((Dim)(screen_height * (percent) / 100))
// remaining px from the linenum-th line to the bottom of the screen. (please calculate the +1 line top-bar to it too if that is visible!)
#define UI_POS_HEIGHT_REMAINING(linenum) ((int)(screen_height - ((linenum) * UI_POS_DEFAULT_HEIGHT)))
#define UI_POS_HEIGHT_REMAINING(linenum) ((Dim)(screen_height - (int)((linenum) * UI_POS_DEFAULT_HEIGHT)))
// remaining px from the charnum-th character to the right of the screen
#define UI_POS_WIDTH_REMAINING(charnum) ((int)(screen_width - ((charnum) * UI_POS_DEFAULT_WIDTH)))
#define UI_POS_WIDTH_REMAINING(charnum) ((Dim)(screen_width - (int)((charnum) * UI_POS_DEFAULT_WIDTH)))
// px width of the screen's percent
#define UI_POS_WIDTH_PERCENT(percent) ((int)(screen_width * (percent) / 100))
#define UI_POS_WIDTH_PERCENT(percent) ((Dim)(screen_width * (percent) / 100))
// px width of the screen
#define UI_POS_MAXHEIGHT (screen_height)
@@ -85,9 +88,6 @@ namespace ui {
#define DEG_TO_RAD(d) (d * (2 * pi) / 360.0)
using Coord = int16_t;
using Dim = int16_t;
extern uint16_t screen_width;
extern uint16_t screen_height;
+20 -8
View File
@@ -206,7 +206,11 @@ void Widget::set_style(const Style* new_style) {
}
const Style& Widget::style() const {
return style_ ? *style_ : parent()->style();
for (const Widget* curr = this; curr; curr = curr->parent()) {
if (curr->style_) return *curr->style_;
}
// default style should always be set by SystemView
return *style_;
}
void Widget::drawn(bool v) {
@@ -2184,7 +2188,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 +2199,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 +2233,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 +2258,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 +2269,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 +2283,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 +2297,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 {
+4 -8
View File
@@ -201,15 +201,11 @@ void Widget::set_style(const Style* new_style) {
}
const Style& Widget::style() const {
if (style_ != nullptr)
return *style_;
else {
auto p = parent();
if (p == nullptr)
// TODO: debug
while (true);
return p->style();
for (const Widget* curr = this; curr; curr = curr->parent()) {
if (curr->style_) return *curr->style_;
}
// default style should always be set by SystemView
return *style_;
}
void Widget::drawn(bool v) {
@@ -21,6 +21,7 @@
#include "doctest.h"
#include "convert.hpp"
#include <cstdint>
#include <string>
#include <string_view>
+1 -1
View File
@@ -24,4 +24,4 @@
# external app address ranges below must match those in linker file "external.ld"
maximum_application_size = 32*1024
external_apps_address_start = 0xADB00000
external_apps_address_end = 0xAE060000
external_apps_address_end = 0xAE090000
+1 -1
Submodule hackrf updated: f526ddf4eb...ba2045a866
Executable
+647
View File
@@ -0,0 +1,647 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
DEFAULT_TOOLCHAIN_URL="https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/RC2.1/gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2?revision=6e63531f-8cb1-40b9-bbfc-8a57cdfc01b4&la=en&hash=F761343D43A0587E8AC0925B723C04DBFB848339"
mbt_log() {
printf "mbt: %s\n" "$*"
}
mbt_error() {
printf "mbt: error: %s\n" "$*" >&2
}
mbt_die() {
mbt_error "$*"
exit 1
}
mbt_command_exists() {
command -v "$1" >/dev/null 2>&1
}
mbt_default_jobs() {
local jobs
jobs="$(getconf _NPROCESSORS_ONLN 2>/dev/null || true)"
if [[ -z "$jobs" ]] && mbt_command_exists nproc; then
jobs="$(nproc)"
fi
if [[ -z "$jobs" ]]; then
jobs=1
fi
printf "%s" "$jobs"
}
mbt_detect_distro() {
if mbt_command_exists apt-get; then
printf "debian"
return 0
fi
if mbt_command_exists pacman; then
printf "arch"
return 0
fi
printf "unknown"
}
mbt_run_privileged() {
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
"$@"
return
fi
if ! mbt_command_exists sudo; then
mbt_die "sudo is required for dependency installation. Re-run as root or install sudo."
fi
sudo "$@"
}
mbt_install_deps() {
local distro
distro="$(mbt_detect_distro)"
case "$distro" in
debian)
mbt_log "detected Debian-like environment; installing required packages."
mbt_run_privileged apt-get update
mbt_run_privileged apt-get install -y --no-install-recommends \
git tar wget dfu-util cmake make python3 bzip2 lz4 curl hackrf \
python3-setuptools python3-yaml binutils python3-setuptools ccache ninja-build
;;
arch)
mbt_log "detected Arch-like environment; installing required packages."
mbt_run_privileged pacman -Sy --needed --noconfirm \
git tar wget dfu-util cmake make python3 bzip2 lz4 curl hackrf \
python-distutils-extra python-setuptools python-pip python-yaml \
ninja ccache binutils
;;
*)
mbt_die "unsupported distribution. Install dependencies manually (see wiki on github), or you can send PR to support for your distro. Detected distro: $distro"
;;
esac
}
mbt_sync_submodules() {
local jobs
jobs="$(mbt_default_jobs)"
mbt_log "syncing submodules."
git -C "$SCRIPT_PATH" submodule update --init --recursive --jobs "$((jobs * 2))"
}
mbt_enable_toolchain_env() {
export MBT_ROOT="$SCRIPT_PATH"
export MBT_TOOLCHAIN_DIR="${MBT_TOOLCHAIN_DIR:-$SCRIPT_PATH/armbin}"
export MBT_TOOLCHAIN_URL="${MBT_TOOLCHAIN_URL:-$DEFAULT_TOOLCHAIN_URL}"
export MBT_EXPECTED_GCC_VERSION="${MBT_EXPECTED_GCC_VERSION:-9.2.1}"
if [[ -z "${MBT_FORCE_TOOLCHAIN+x}" ]]; then
MBT_FORCE_TOOLCHAIN="${FORCE_TOOLCHAIN:-0}"
fi
export MBT_FORCE_TOOLCHAIN
if [[ -z "${MBT_NO_TOOLCHAIN_DOWNLOAD+x}" ]]; then
MBT_NO_TOOLCHAIN_DOWNLOAD="${NO_TOOLCHAIN_DOWNLOAD:-0}"
fi
export MBT_NO_TOOLCHAIN_DOWNLOAD
export MBT_VERBOSE="${MBT_VERBOSE:-}"
# shellcheck source=/dev/null
. "$SCRIPT_PATH/scripts/toolchain/mbtenv.sh"
}
mbt_use_existing_toolchain_env() {
local repo_toolchain_bin="$SCRIPT_PATH/armbin/bin"
if [[ ! -d "$repo_toolchain_bin" ]]; then
return 0
fi
case ":$PATH:" in
*":$repo_toolchain_bin:"*)
return 0
;;
esac
export PATH="$repo_toolchain_bin:$PATH"
}
mbt_normalize_device() {
local raw
raw="$(printf "%s" "$1" | tr '[:upper:]' '[:lower:]')"
case "$raw" in
1|one|hackrf|hackrf-one)
printf "hackrf-one"
;;
2|pro|hackrf-pro|praline)
printf "hackrf-pro"
;;
3|portarf|porta-rf|porta_rf)
printf "portarf"
;;
*)
return 1
;;
esac
}
mbt_prompt_device() {
local choice normalized
cat >&2 <<'EOF'
Select device type:
1) HackRF One
2) HackRF Pro (PRALINE)
3) PortaRF
EOF
while true; do
printf "Device [1-3]: " >&2
if ! IFS= read -r choice; then
return 1
fi
normalized="$(mbt_normalize_device "$choice" 2>/dev/null || true)"
if [[ -n "$normalized" ]]; then
printf "%s" "$normalized"
return 0
fi
mbt_error "invalid device selection: $choice"
done
}
mbt_print_help() {
cat <<'EOF'
Usage:
./mbt One-step build flow (installs deps, syncs submodules, downloads toolchain, asks for device, builds)
./mbt build [options] Developer build flow
./mbt deps Install distro dependencies only
./mbt toolchain [--force-toolchain]
Download/setup ARM toolchain only
./mbt doctor Validate host setup and report gaps
./mbt shell [options] Open a shell with toolchain exported for this session
Build options:
-d, --device <name> Device: hackrf-one | hackrf-pro | portarf
--ccache / --no-ccache Toggle USE_CCACHE (default: on)
--with-deps / --no-deps Install Linux dependencies before build
--with-submodule-sync Update git submodules before build (default)
--no-submodule-sync Skip submodule sync
--with-toolchain Ensure toolchain exists and matches expected version (default)
--no-toolchain Skip toolchain checks/downloads and use existing PATH (prepends ./armbin/bin if present)
--force-toolchain Re-download and reinstall toolchain
-B, --build-dir <path> Build directory (default: ./build)
-G, --generator <name> CMake generator (default: Ninja)
-j, --jobs <n> Build parallelism (default: host CPU count)
--target <name> Build a specific target
--clean Remove build directory, run libopencm3 clean, and exit
--cmake-arg <arg> Extra CMake configure arg (repeatable)
--build-arg <arg> Extra cmake --build arg (repeatable)
-y, --non-interactive Fail instead of prompting when required input is missing
-h, --help Show this help
Examples:
./mbt
./mbt build --device hackrf-one
./mbt build --device hackrf-pro --no-submodule-sync
./mbt build --device portarf --no-toolchain --no-deps
./mbt build --device hackrf-one --cmake-arg=-DVERSION_STRING=my-build
EOF
}
mbt_doctor() {
local distro missing=0 toolchain_cc toolchain_version cmd
local -a required_cmds
distro="$(mbt_detect_distro)"
mbt_log "doctor report for $SCRIPT_PATH"
mbt_log "distro family: $distro"
required_cmds=(git tar wget dfu-util cmake python3 bzip2 lz4 curl ccache ninja)
if [[ "$distro" == "arch" ]]; then
required_cmds+=(make)
fi
for cmd in "${required_cmds[@]}"; do
if mbt_command_exists "$cmd"; then
printf " [ok] %s\n" "$cmd"
else
printf " [missing] %s\n" "$cmd"
missing=1
fi
done
toolchain_cc="$SCRIPT_PATH/armbin/bin/arm-none-eabi-gcc"
if [[ -x "$toolchain_cc" ]]; then
toolchain_version="$("$toolchain_cc" -dumpfullversion 2>/dev/null || "$toolchain_cc" -dumpversion 2>/dev/null || true)"
printf " [ok] arm-none-eabi-gcc %s (%s)\n" "$toolchain_version" "$toolchain_cc"
if [[ "$toolchain_version" != "9.2.1" ]]; then
printf " [warn] expected arm-none-eabi-gcc 9.2.1\n"
fi
else
if mbt_command_exists arm-none-eabi-gcc; then
toolchain_cc="arm-none-eabi-gcc"
toolchain_version="$("$toolchain_cc" -dumpfullversion 2>/dev/null || "$toolchain_cc" -dumpversion 2>/dev/null || true)"
printf " [ok] arm-none-eabi-gcc %s (%s)\n" "$toolchain_version" "$toolchain_cc"
if [[ "$toolchain_version" != "9.2.1" ]]; then
printf " [warn] expected arm-none-eabi-gcc 9.2.1\n"
fi
else
printf " [missing] %s\n" "$toolchain_cc"
missing=1
fi
fi
if git -C "$SCRIPT_PATH" submodule status --recursive | grep -q '^-'; then
printf " [warn] some submodules are not initialized (run ./mbt build --with-submodule-sync --device hackrf-one)\n"
else
printf " [ok] submodules initialized\n"
fi
if [[ "$missing" -ne 0 ]]; then
mbt_error "doctor found missing prerequisites."
return 1
fi
mbt_log "doctor passed."
return 0
}
mbt_validate_jobs() {
[[ "$1" =~ ^[0-9]+$ ]] && [[ "$1" -gt 0 ]]
}
mbt_detect_cached_device() {
local build_dir="$1"
local cache_file="$build_dir/CMakeCache.txt"
local board flash_limit
if [[ ! -f "$cache_file" ]]; then
return 1
fi
board="$(grep -E '^BOARD:' "$cache_file" | head -n 1 | cut -d= -f2- || true)"
flash_limit="$(grep -E '^FLASH_MB_LIMIT_SIZE:' "$cache_file" | head -n 1 | cut -d= -f2- || true)"
if [[ "$board" == "PRALINE" ]]; then
printf "hackrf-pro"
return 0
fi
case "$flash_limit" in
2|2.0)
printf "portarf"
return 0
;;
1|1.0)
printf "hackrf-one"
return 0
;;
esac
printf "unknown"
return 0
}
mbt_clean() {
local build_dir="$1"
local libopencm3_dir="$SCRIPT_PATH/hackrf/firmware/libopencm3"
mbt_log "cleaning build directory: $build_dir"
if ! rm -rf "$build_dir"; then
mbt_error "failed to delete build directory. Try running ./mbt --clean as root."
return 1
fi
if [[ -d "$libopencm3_dir" ]]; then
mbt_log "cleaning libopencm3: $libopencm3_dir"
make -C "$libopencm3_dir" clean
else
mbt_error "libopencm3 directory not found: $libopencm3_dir"
return 1
fi
}
mbt_report_build_error() {
printf '%s\n' 'Build error happened. Try to fix your code, or run ./mbt --clean and try again' >&2
}
mbt_build() {
local build_dir="$1"
local generator="$2"
local use_ccache="$3"
local device="$4"
local jobs="$5"
local target="$6"
local clean_build="$7"
shift 7
if [[ "$#" -lt 2 ]]; then
mbt_die "internal error: mbt_build expected extra arg array names"
fi
local -n cmake_extra_args_ref="$1"
local -a cmake_extra_args=("${cmake_extra_args_ref[@]}")
shift
local -n build_extra_args_ref="$1"
local -a build_extra_args=("${build_extra_args_ref[@]}")
local -a cmake_args build_args device_args
local marker_file="$build_dir/.mbt_device"
local previous_device=""
if [[ "$clean_build" -eq 1 ]]; then
mbt_clean "$build_dir"
return 0
else
if [[ -f "$marker_file" ]]; then
previous_device="$(<"$marker_file")"
elif [[ -f "$build_dir/CMakeCache.txt" ]]; then
previous_device="$(mbt_detect_cached_device "$build_dir" 2>/dev/null || true)"
fi
if [[ -n "$previous_device" ]] && [[ "$previous_device" != "$device" ]]; then
mbt_log "device changed ($previous_device -> $device), cleaning build directory: $build_dir"
rm -rf "$build_dir"
fi
fi
mkdir -p "$build_dir"
device_args=()
case "$device" in
hackrf-one)
device_args+=()
;;
hackrf-pro)
device_args+=("-DBOARD=PRALINE")
;;
portarf)
device_args+=("-DFLASH_MB_SIZE=2" "-DFLASH_MB_LIMIT_SIZE=2")
;;
*)
mbt_die "unsupported device preset: $device"
;;
esac
if [[ "$generator" == "Ninja" ]] && ! mbt_command_exists ninja; then
if mbt_command_exists make; then
mbt_log "ninja not found; falling back to Unix Makefiles."
generator="Unix Makefiles"
else
mbt_die "ninja not found and make is unavailable. Install dependencies or set a working generator."
fi
fi
cmake_args=(
-S "$SCRIPT_PATH"
-B "$build_dir"
-G "$generator"
"-DUSE_CCACHE=$use_ccache"
)
cmake_args+=("${device_args[@]}")
cmake_args+=("${cmake_extra_args[@]}")
mbt_log "configuring build for $device in $build_dir"
if ! cmake "${cmake_args[@]}"; then
mbt_report_build_error
return 1
fi
printf "%s\n" "$device" > "$marker_file"
build_args=(--build "$build_dir" --parallel "$jobs")
if [[ -n "$target" ]]; then
build_args+=(--target "$target")
fi
build_args+=("${build_extra_args[@]}")
mbt_log "building..."
if ! cmake "${build_args[@]}"; then
mbt_report_build_error
return 1
fi
mbt_log "build complete. Outputs are in $build_dir."
mbt_log "The bare binary (for the hackrf_spiflash host tool), excluding external apps, is located at $build_dir/firmware/portapack-mayhem-firmware.bin"
mbt_log "The full tar.gz package (for MayhemHub and the Flash Utility OTA app), including external apps and the firmware file, is located at $build_dir/firmware/portapack-mayhem_OCI.ppfw.tar"
mbt_log "If you have the dev's SD card switcher hardware, the fast flash script is at $build_dir/flash.py"
# have to hard code the name here cuz it's too expensive to read cmake for just a file name.
}
main() {
local command="build"
local quickstart=0
local device=""
local use_ccache="ON"
local with_deps=0
local with_submodule_sync=1
local with_toolchain=1
local build_dir="$SCRIPT_PATH/build"
local generator="Ninja"
local jobs
local target=""
local clean_build=0
local non_interactive=0
local -a cmake_extra_args=()
local -a build_extra_args=()
FORCE_TOOLCHAIN=0
jobs="$(mbt_default_jobs)"
if [[ "$#" -eq 0 ]]; then
quickstart=1
with_deps=1
fi
if [[ "$#" -gt 0 ]]; then
case "$1" in
build|deps|toolchain|doctor|shell)
command="$1"
shift
;;
-h|--help|help)
mbt_print_help
exit 0
;;
esac
fi
while [[ "$#" -gt 0 ]]; do
case "$1" in
-d|--device)
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
device="$2"
shift 2
;;
--ccache)
use_ccache="ON"
shift
;;
--no-ccache)
use_ccache="OFF"
shift
;;
--with-deps)
with_deps=1
shift
;;
--no-deps)
with_deps=0
shift
;;
--with-submodule-sync)
with_submodule_sync=1
shift
;;
--no-submodule-sync)
with_submodule_sync=0
shift
;;
--with-toolchain)
with_toolchain=1
shift
;;
--no-toolchain)
with_toolchain=0
shift
;;
--force-toolchain)
FORCE_TOOLCHAIN=1
with_toolchain=1
shift
;;
-B|--build-dir)
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
build_dir="$2"
shift 2
;;
-G|--generator)
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
generator="$2"
shift 2
;;
-j|--jobs)
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
jobs="$2"
shift 2
;;
--target)
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
target="$2"
shift 2
;;
--clean)
clean_build=1
shift
;;
--cmake-arg)
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
cmake_extra_args+=("$2")
shift 2
;;
--cmake-arg=*)
cmake_extra_args+=("${1#*=}")
shift
;;
--build-arg)
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
build_extra_args+=("$2")
shift 2
;;
--build-arg=*)
build_extra_args+=("${1#*=}")
shift
;;
-y|--non-interactive)
non_interactive=1
shift
;;
-h|--help)
mbt_print_help
exit 0
;;
*)
mbt_die "unknown option: $1"
;;
esac
done
if ! mbt_validate_jobs "$jobs"; then
mbt_die "invalid jobs value: $jobs"
fi
case "$command" in
deps)
mbt_install_deps
exit 0
;;
toolchain)
NO_TOOLCHAIN_DOWNLOAD=0
mbt_enable_toolchain_env
mbt_log "toolchain ready: $SCRIPT_PATH/armbin"
exit 0
;;
doctor)
mbt_doctor
exit $?
;;
shell)
if [[ "$with_toolchain" -eq 1 ]]; then
mbt_enable_toolchain_env
else
mbt_use_existing_toolchain_env
fi
mbt_log "spawning shell with toolchain exported in this session. Use 'exit' to leave."
exec "${SHELL:-/bin/bash}" -i
;;
build)
;;
*)
mbt_die "unsupported command: $command"
;;
esac
if [[ "$clean_build" -eq 1 ]]; then
mbt_clean "$build_dir"
exit 0
fi
if [[ "$with_deps" -eq 1 ]]; then
mbt_install_deps
fi
if [[ "$with_submodule_sync" -eq 1 ]]; then
mbt_sync_submodules
fi
if [[ "$with_toolchain" -eq 1 ]]; then
mbt_enable_toolchain_env
else
mbt_use_existing_toolchain_env
fi
if [[ -n "$device" ]]; then
device="$(mbt_normalize_device "$device" 2>/dev/null || true)"
fi
if [[ -z "$device" ]]; then
if [[ "$non_interactive" -eq 1 ]]; then
mbt_die "--device is required when --non-interactive is set"
fi
if [[ ! -t 0 ]]; then
mbt_die "missing --device and no interactive terminal available"
fi
device="$(mbt_prompt_device)" || mbt_die "failed to read device selection"
fi
if [[ "$quickstart" -eq 1 ]]; then
mbt_log "quickstart mode enabled."
fi
mbt_build "$build_dir" "$generator" "$use_ccache" "$device" "$jobs" "$target" "$clean_build" \
cmake_extra_args[@] build_extra_args[@]
}
main "$@"
+209
View File
@@ -0,0 +1,209 @@
#!/bin/sh
# shellcheck disable=SC2039
MBT_ROOT="${MBT_ROOT:-$(pwd -P)}"
MBT_VERBOSE="${MBT_VERBOSE:-}"
MBT_TOOLCHAIN_DIR="${MBT_TOOLCHAIN_DIR:-$MBT_ROOT/armbin}"
MBT_TOOLCHAIN_URL="${MBT_TOOLCHAIN_URL:-https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/RC2.1/gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2?revision=6e63531f-8cb1-40b9-bbfc-8a57cdfc01b4&la=en&hash=F761343D43A0587E8AC0925B723C04DBFB848339}"
MBT_EXPECTED_GCC_VERSION="${MBT_EXPECTED_GCC_VERSION:-9.2.1}"
MBT_FORCE_TOOLCHAIN="${MBT_FORCE_TOOLCHAIN:-0}"
MBT_NO_TOOLCHAIN_DOWNLOAD="${MBT_NO_TOOLCHAIN_DOWNLOAD:-0}"
MBT_TOOLCHAIN_BIN="$MBT_TOOLCHAIN_DIR/bin"
MBT_TOOLCHAIN_CC="$MBT_TOOLCHAIN_BIN/arm-none-eabi-gcc"
MBT_TMP_ARCHIVE=""
MBT_DOWNLOADER=""
mbtenv_log() {
printf "mbt: %s\n" "$*"
}
mbtenv_verbose() {
if [ -n "$MBT_VERBOSE" ]; then
mbtenv_log "$*"
fi
}
mbtenv_command_exists() {
command -v "$1" >/dev/null 2>&1
}
mbtenv_cleanup_tmp() {
if [ -n "$MBT_TMP_ARCHIVE" ] && [ -f "$MBT_TMP_ARCHIVE" ]; then
rm -f "$MBT_TMP_ARCHIVE"
fi
}
mbtenv_check_linux() {
if [ "$(uname -s)" != "Linux" ]; then
mbtenv_log "error: mbt currently supports Linux only."
return 1
fi
arch="$(uname -m)"
case "$arch" in
x86_64|amd64)
# Supported architecture for the default x86_64 Linux toolchain archive.
;;
*)
mbtenv_log "error: unsupported architecture '$arch'. mbt currently supports x86_64 Linux only for the bundled toolchain."
return 1
;;
esac
return 0
}
mbtenv_choose_downloader() {
if mbtenv_command_exists curl; then
MBT_DOWNLOADER="curl"
return 0
fi
if mbtenv_command_exists wget; then
MBT_DOWNLOADER="wget"
return 0
fi
mbtenv_log "error: neither curl nor wget found in PATH."
return 1
}
mbtenv_download_archive() {
if [ "$MBT_DOWNLOADER" = "curl" ]; then
curl --fail --location --progress-bar --output "$1" "$2"
return $?
fi
wget --show-progress --progress=bar:force -O "$1" "$2"
}
mbtenv_get_installed_version() {
if [ ! -x "$MBT_TOOLCHAIN_CC" ]; then
return 1
fi
"$MBT_TOOLCHAIN_CC" -dumpfullversion 2>/dev/null || "$MBT_TOOLCHAIN_CC" -dumpversion 2>/dev/null
}
mbtenv_toolchain_is_expected() {
INSTALLED_VERSION="$(mbtenv_get_installed_version 2>/dev/null || true)"
if [ "$INSTALLED_VERSION" = "$MBT_EXPECTED_GCC_VERSION" ]; then
return 0
fi
return 1
}
mbtenv_download_and_unpack_toolchain() {
mbtenv_choose_downloader || return 1
MBT_TMP_ARCHIVE="$(mktemp /tmp/mbt-toolchain-XXXXXX.tar.bz2)"
trap mbtenv_cleanup_tmp EXIT INT TERM
mbtenv_log "downloading ARM GNU toolchain..."
mbtenv_download_archive "$MBT_TMP_ARCHIVE" "$MBT_TOOLCHAIN_URL" || {
mbtenv_log "error: failed to download toolchain from ARM."
return 1
}
mbtenv_log "putting toolchain into $MBT_TOOLCHAIN_DIR"
if [ -d "$MBT_TOOLCHAIN_DIR" ] && [ ! -f "$MBT_TOOLCHAIN_DIR/bin/arm-none-eabi-gcc-9.2.1" ]; then
mbtenv_log "Sorry, i can't verify if the toolchain dir is legit to delete. it either could be becasue you failed to download the full toolchain last time, or the enviroment var were pointed to something else, which is not safe for me to remove. please check if it's safe to remove $MBT_TOOLCHAIN_DIR and manually remove $MBT_TOOLCHAIN_DIR"
return 1
fi
rm -rf "$MBT_TOOLCHAIN_DIR"
mkdir -p "$MBT_TOOLCHAIN_DIR"
tar --strip-components=1 -xjf "$MBT_TMP_ARCHIVE" -C "$MBT_TOOLCHAIN_DIR" || {
mbtenv_log "error: failed to extract toolchain archive."
return 1
}
mbtenv_cleanup_tmp
trap - EXIT INT TERM
MBT_TMP_ARCHIVE=""
return 0
}
mbtenv_ensure_toolchain() {
if mbtenv_toolchain_is_expected && [ "$MBT_FORCE_TOOLCHAIN" != "1" ]; then
mbtenv_verbose "toolchain already installed at $MBT_TOOLCHAIN_DIR"
return 0
fi
if [ "$MBT_NO_TOOLCHAIN_DOWNLOAD" = "1" ]; then
if [ ! -x "$MBT_TOOLCHAIN_CC" ]; then
mbtenv_log "error: arm-none-eabi-gcc not found in $MBT_TOOLCHAIN_BIN"
mbtenv_log "hint: run ./mbt toolchain once, or remove --no-toolchain."
return 1
fi
INSTALLED_VERSION="$(mbtenv_get_installed_version 2>/dev/null || true)"
mbtenv_log "error: found arm-none-eabi-gcc $INSTALLED_VERSION, expected $MBT_EXPECTED_GCC_VERSION"
mbtenv_log "hint: run ./mbt toolchain once, or remove --no-toolchain."
return 1
fi
if [ -x "$MBT_TOOLCHAIN_CC" ]; then
INSTALLED_VERSION="$(mbtenv_get_installed_version 2>/dev/null || true)"
mbtenv_log "toolchain version mismatch (found $INSTALLED_VERSION, expected $MBT_EXPECTED_GCC_VERSION)"
mbtenv_log "reinstalling toolchain..."
fi
mbtenv_download_and_unpack_toolchain || return 1
if ! mbtenv_toolchain_is_expected; then
INSTALLED_VERSION="$(mbtenv_get_installed_version 2>/dev/null || true)"
mbtenv_log "error: installed toolchain version check failed (found $INSTALLED_VERSION)."
return 1
fi
return 0
}
mbtenv_activate_path() {
if [ ! -d "$MBT_TOOLCHAIN_BIN" ]; then
mbtenv_log "error: missing toolchain bin directory: $MBT_TOOLCHAIN_BIN"
return 1
fi
case ":$PATH:" in
*":$MBT_TOOLCHAIN_BIN:"*)
return 0
;;
esac
if [ -z "${MBT_SAVED_PATH+x}" ]; then
MBT_SAVED_PATH="$PATH"
export MBT_SAVED_PATH
fi
PATH="$MBT_TOOLCHAIN_BIN:$PATH"
export PATH
return 0
}
mbtenv_restore() {
if [ -n "${MBT_SAVED_PATH+x}" ]; then
PATH="$MBT_SAVED_PATH"
export PATH
unset MBT_SAVED_PATH
fi
return 0
}
mbtenv_main() {
mbtenv_check_linux || return 1
if [ "${1:-}" = "--restore" ]; then
mbtenv_restore
return 0
fi
if [ "${MBT_NOENV:-0}" = "1" ]; then
return 0
fi
mbtenv_ensure_toolchain || return 1
mbtenv_activate_path || return 1
return 0
}
mbtenv_main "${1:-}"
+18
View File
@@ -58,3 +58,21 @@ f=1258000000,d=FPV Ch 6
f=1280000000,d=FPV Ch 7
f=1320000000,d=FPV Ch 8
f=1360000000,d=FPV Ch 9
# 3.1GHz & 3.3GHz Analog FPV Channels
f=3330000000,d=FPV Ch A-1
f=3350000000,d=FPV Ch A-2
f=3370000000,d=FPV Ch A-3
f=3390000000,d=FPV Ch A-4
f=3410000000,d=FPV Ch A-5
f=3430000000,d=FPV Ch A-6
f=3450000000,d=FPV Ch A-7
f=3470000000,d=FPV Ch A-8
f=3170000000,d=FPV Ch B-1
f=3190000000,d=FPV Ch B-2
f=3210000000,d=FPV Ch B-3
f=3230000000,d=FPV Ch B-4
f=3250000000,d=FPV Ch B-5
f=3270000000,d=FPV Ch B-6
f=3290000000,d=FPV Ch B-7
f=3310000000,d=FPV Ch B-8
+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

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