mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-18 13:44:00 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe2ba80a10 | |||
| b5d84fcc4d | |||
| 7481aefe9f | |||
| 6b641c8a5d | |||
| 2ad34bbc09 | |||
| 9c74a1f392 | |||
| abadf35e06 | |||
| 288e87537a | |||
| d25899de10 | |||
| 8a16ec829c | |||
| f6d6e3be4c | |||
| c16bac24c7 | |||
| 4e6a2cc934 | |||
| cafe62564e | |||
| 960e5b8329 | |||
| f5a5e8e5e8 | |||
| d8a1e2530a | |||
| b84a424875 | |||
| 62d0e598d4 | |||
| a93f2ff583 | |||
| e2586ceb22 | |||
| 5a2b0cdc2a | |||
| f852900cd0 | |||
| 9ab367f2e0 | |||
| c7a0846645 | |||
| 569bcaad22 | |||
| 6b5955e0b5 | |||
| 9042d1c433 | |||
| 378b2808ba | |||
| 5b363b79aa | |||
| 0d51787099 | |||
| 8f5b25b10d | |||
| 4e56d0f420 | |||
| 2bb3b415d1 | |||
| 6e0394fb6b | |||
| da21ea7866 | |||
| 2c434946cd | |||
| f3aee44e3b | |||
| 165c190d92 | |||
| 8f9246c1ea | |||
| f1d52ae78f |
@@ -1,58 +1,80 @@
|
||||
<!--
|
||||
👋 Thank you for your contribution!
|
||||
|
||||
⚠️ Before you start, please review our Contributing Guidelines:
|
||||
https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines
|
||||
-->
|
||||
|
||||
## Brief description of what you did
|
||||
<!--
|
||||
Replace this line with a description of your changes. What problem does this PR solve? What does it add or fix?
|
||||
Don't forget to describe how maintainers can test your modifications!
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Replace this line with a description of your changes. What problem does this PR solve? What does it add or fix?
|
||||
Don't forget to describe how maintainers can test your modifications! -->
|
||||
|
||||
---
|
||||
|
||||
## Proof that your changes work
|
||||
<!--
|
||||
1. PRs without proof will not be merged. Please provide evidence for every section below that applies to your change.
|
||||
2. Trusted contributors: If you have a track record of merged PRs that were properly tested and documented, maintainers may waive one or more of the proof sections below at their discretion. You are still encouraged to include whatever evidence you have, but sections may be marked `N/A — trusted contributor` with a short note.
|
||||
3. First-time contributors must provide full proof. See the Contributing Guidelines(https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines) and Trusted Contributors (https://github.com/portapack-mayhem/mayhem-firmware/wiki/Trusted-Contributors) articles for details.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
> ⚠️ **PRs without proof will not be merged.** Please provide evidence for every section below that applies to your change.
|
||||
>
|
||||
> 🛈 **Trusted contributors:** If you have a track record of merged PRs that were properly tested and documented, maintainers may waive one or more of the proof sections below at their discretion. You are still encouraged to include whatever evidence you have, but sections may be marked `N/A — trusted contributor` with a short note. First-time contributors must provide full proof. See the [Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines) and [Trusted Contributors](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Trusted-Contributors) articles for details.
|
||||
|
||||
### 🖥️ Proof it compiles
|
||||
|
||||
<!--
|
||||
Attach a log snippet showing a successful build from the final firmware size/space summary in the build output. Example: `Space remaining in flash ROM: 19552 bytes ( 1.9 % )`.
|
||||
Paste log below.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Paste log below -->
|
||||
|
||||
### 📱 Proof of testing on a real device
|
||||
<!--
|
||||
Attach photos, screenshots, or a short video of your changes running on actual PortaPack hardware. Emulator or simulator output is not a substitute for real-device testing.
|
||||
Paste photo / video / screenshot below
|
||||
-->
|
||||
|
||||
|
||||
|
||||
Attach photos, screenshots, or a short video of your changes running on actual PortaPack hardware. **Emulator or simulator output is not a substitute for real-device testing.**
|
||||
|
||||
<!-- Paste photo / video / screenshot below -->
|
||||
|
||||
### 📡 Proof against a real emitter/receiver (if applicable)
|
||||
<!--
|
||||
1. If your PR involves RX/TX, protocol decoding, signal generation, or any RF-related functionality, you **must** demonstrate it working against a real emitter or receiver. Include photos, videos, logs, or waterfall/spectrum screenshots.
|
||||
2. If this section does not apply to your PR, write `N/A` below and briefly explain why.
|
||||
3. Paste RF evidence, or write N/A with a short justification
|
||||
-->
|
||||
|
||||
|
||||
If your PR involves RX/TX, protocol decoding, signal generation, or any RF-related functionality, you **must** demonstrate it working against a real emitter or receiver. Include photos, videos, logs, or waterfall/spectrum screenshots.
|
||||
|
||||
If this section does not apply to your PR, write `N/A` below and briefly explain why.
|
||||
|
||||
<!-- Paste RF evidence, or write N/A with a short justification -->
|
||||
|
||||
---
|
||||
|
||||
## 📚 Wiki documentation commitment
|
||||
|
||||
**By submitting this PR, you acknowledge that once it is merged you are responsible for creating or updating the corresponding page on the [project wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki).**
|
||||
|
||||
<!--
|
||||
By submitting this PR, you acknowledge that once it is merged you are responsible for creating or updating the corresponding page on the project wiki(https://github.com/portapack-mayhem/mayhem-firmware/wiki).
|
||||
Your wiki article should include:
|
||||
|
||||
- A **screenshot of the main app screen** (strongly recommended)
|
||||
- A clear **description** of what the app does
|
||||
- An explanation of the **controls** and UI elements
|
||||
- Any known **limitations**, quirks, or caveats
|
||||
- A screenshot of the main app screen (strongly recommended)
|
||||
- A clear description of what the app does
|
||||
- An explanation of the controls and UI elements
|
||||
- Any known limitations, quirks, or caveats
|
||||
- Anything else a user should know to use the app effectively (dependencies, required hardware, file formats, etc.)
|
||||
- If you are modifying an existing app, make sure the wiki page reflects your changes.
|
||||
-->
|
||||
|
||||
|
||||
- [ ] I will (or already) create(d) wiki document for my newly added feature
|
||||
|
||||
If you are modifying an existing app, make sure the wiki page reflects your changes.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -83,3 +83,4 @@ venv/
|
||||
# generated bitmap arr file
|
||||
# TODO: generate bitmap during build, since we use python during build anyway, lemme know if this is a bad idea @zxkmm
|
||||
/firmware/tools/bitmap.hpp
|
||||
tmp/*
|
||||
|
||||
@@ -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 Mayhem’s architecture, understand M0‑M4 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 AI‑ASSISTED CONTRIBUTIONS
|
||||
|
||||
When submitting AI‑assisted 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** – Floating‑point operations are expensive. Prefer fixed‑point or integer math.
|
||||
- **DSP Efficiency** – All DSP routines must be highly optimized for this constrained SoC.
|
||||
- **External App Format** – New applications must follow the external‑app 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 repository’s Pull Requests or Issue trackers.
|
||||
@@ -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="It’s TOO Easy to Accidentally Do Illegal Stuff with This" src="https://img.youtube.com/vi/OPckpjBSAOw/maxresdefault.jpg" width="172">](https://share.hackrf.app/X4D5TF)
|
||||
[<img alt="HackRF Portapack H4M - Getting Started Guide" src="https://img.youtube.com/vi/wzP0zWi85SI/maxresdefault.jpg" width="172">](https://share.hackrf.app/F9MPOO)
|
||||
[<img alt="The new HackRF Portapack H4M" src="https://img.youtube.com/vi/onQRdCITmuk/maxresdefault.jpg" width="172">](https://share.hackrf.app/0JUHZ6)
|
||||
|
||||
|
||||
# Frequently Asked Questions
|
||||
@@ -32,11 +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:  The fabulous H4M [complete](https://share.hackrf.app/DF2BZS) or [upgrade](https://share.hackrf.app/OCSJYI), featuring numerous improvements and accessories. Sold by our friends at [OpenSourceSDRLab](https://share.hackrf.app/99SAMT). Join their giveaways on discord (check the badge on top). _EU customers_ can purchase via [Lab401](https://share.hackrf.app/0CI2CR).
|
||||
:heavy_check_mark:  The fabulous H4M [complete](https://share.hackrf.app/O4OC90) or [upgrade](https://share.hackrf.app/OCSJYI), featuring numerous improvements and accessories. Sold by our friends at [OpenSourceSDRLab](https://share.hackrf.app/99SAMT). Join their giveaways on discord (check the badge on top). _EU customers_ can purchase via [Lab401](https://share.hackrf.app/0CI2CR).
|
||||
|
||||
:heavy_check_mark: A recommended one is this [PortaPack H2](https://share.hackrf.app/QT5JMB), that includes everything you need with the plastic case "inspired" on [this](https://github.com/portapack-mayhem/mayhem-firmware/wiki/3d-printed-enclosure).
|
||||
:heavy_check_mark: A recommended one is this [PortaPack H2](https://share.hackrf.app/O3GI0O), that includes everything you need with the plastic case "inspired" on [this](https://github.com/portapack-mayhem/mayhem-firmware/wiki/3d-printed-enclosure).
|
||||
|
||||
:heavy_check_mark: Some individuals lean towards the [H2 with a metal enclosure](https://share.hackrf.app/24T3TO), but its advantages remain debated. Share your insights on our [wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Hardware-overview).
|
||||
:heavy_check_mark: Some individuals lean towards the [H2 with a metal enclosure](https://share.hackrf.app/H7OR74), but its advantages remain debated. Share your insights on our [wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Hardware-overview).
|
||||
|
||||
:warning: Be cautious , *ask* the seller about compatibility with the latest releases. Look out for the description of the item, if they provide the firmware files for an older version or they have custom setup instructions, this means it might be **NOT compatible**, for example:
|
||||
|
||||
@@ -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 [](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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -192,6 +192,7 @@ set(CPPSRC
|
||||
${COMMON}/battery.cpp
|
||||
${COMMON}/performance_counter.cpp
|
||||
${COMMON}/bmpfile.cpp
|
||||
${COMMON}/gpio.cpp
|
||||
app_settings.cpp
|
||||
audio.cpp
|
||||
baseband_api.cpp
|
||||
@@ -332,6 +333,7 @@ set(CPPSRC
|
||||
${CPLD_H4M_DATA_CPP}
|
||||
${HACKRF_CPLD_DATA_CPP}
|
||||
ui_external_items_menu_loader.cpp
|
||||
view_factory_base.cpp
|
||||
)
|
||||
|
||||
set_source_files_properties(${CPPSRC} PROPERTIES COMPILE_FLAGS -flto) # Add lto flag to the non-external sources only
|
||||
@@ -511,3 +513,5 @@ add_custom_target(
|
||||
${PROJECT_NAME}
|
||||
DEPENDS ${PROJECT_NAME}.bin
|
||||
)
|
||||
|
||||
add_dependencies(${PROJECT_NAME} baseband)
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -116,7 +116,7 @@ static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
|
||||
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
|
||||
break;
|
||||
default:
|
||||
freq_hz = UINT64_MAX;
|
||||
freq_hz = 2'402'000'000ull; // Default to channel 37 frequency if invalid channel number
|
||||
}
|
||||
|
||||
return freq_hz;
|
||||
@@ -254,7 +254,7 @@ void BLETxView::send_packet() {
|
||||
}
|
||||
|
||||
void BLETxView::start() {
|
||||
if (file_path.empty()) {
|
||||
if (file_path.empty() || num_packets == 0) {
|
||||
file_error();
|
||||
check_loop.set_value(false);
|
||||
return;
|
||||
@@ -266,6 +266,8 @@ void BLETxView::start() {
|
||||
button_play.set_bitmap(&bitmap_stop);
|
||||
is_running = true;
|
||||
|
||||
advCount = 0;
|
||||
|
||||
send_packet();
|
||||
}
|
||||
|
||||
@@ -296,55 +298,61 @@ void BLETxView::on_timer() {
|
||||
void BLETxView::on_tx_progress(const bool done, uint32_t progress) {
|
||||
// TODO: make use of progress variable
|
||||
(void)progress;
|
||||
if (!is_active() || num_packets == 0) {
|
||||
return;
|
||||
}
|
||||
if (done) {
|
||||
if (is_active()) {
|
||||
transmitter_model.disable();
|
||||
|
||||
if (auto_channel) {
|
||||
switch (advCount) {
|
||||
case 0:
|
||||
channel_number = 37;
|
||||
break;
|
||||
case 1:
|
||||
channel_number = 38;
|
||||
break;
|
||||
case 2:
|
||||
channel_number = 39;
|
||||
break;
|
||||
}
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
|
||||
advCount++;
|
||||
if (advCount == 3) {
|
||||
channel_number = 37;
|
||||
packet_counter--;
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
if (packet_counter > 0) packet_counter--;
|
||||
packetDone = true;
|
||||
advCount = 0;
|
||||
} else {
|
||||
channel_number = 37 + advCount;
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
send_packet();
|
||||
}
|
||||
} else if (all_channels) {
|
||||
advCount++;
|
||||
if (advCount == 40) {
|
||||
channel_number = 0;
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
if (packet_counter > 0) packet_counter--;
|
||||
packetDone = true;
|
||||
advCount = 0;
|
||||
} else {
|
||||
channel_number = advCount;
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
send_packet();
|
||||
advCount++;
|
||||
}
|
||||
} else {
|
||||
packet_counter--;
|
||||
if (packet_counter > 0) packet_counter--;
|
||||
packetDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reached end of current packet repeats.
|
||||
if (packet_counter == 0) {
|
||||
// Done sending all packets.
|
||||
if (current_packet == (num_packets - 1)) {
|
||||
current_packet = 0;
|
||||
// Reached end of current packet repeats.
|
||||
if (packet_counter == 0) {
|
||||
// Done sending all packets.
|
||||
current_packet++;
|
||||
if (current_packet >= num_packets) {
|
||||
current_packet = 0;
|
||||
|
||||
// If looping, restart from beginning.
|
||||
if (check_loop.value()) {
|
||||
update_current_packet(packets[current_packet], current_packet);
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
} else {
|
||||
current_packet++;
|
||||
// If looping, restart from beginning.
|
||||
if (check_loop.value()) {
|
||||
update_current_packet(packets[current_packet], current_packet);
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
} else {
|
||||
update_current_packet(packets[current_packet], current_packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,16 +393,20 @@ BLETxView::BLETxView(NavigationView& nav)
|
||||
};
|
||||
|
||||
options_channel.on_change = [this](size_t, int32_t i) {
|
||||
// If we selected Auto don't do anything and Auto will handle changing.
|
||||
if (i == 40) {
|
||||
auto_channel = true;
|
||||
return;
|
||||
auto_channel = (i == BLT_CHAN_AUTO);
|
||||
all_channels = (i == BLT_CHAN_ALL);
|
||||
|
||||
advCount = 0;
|
||||
|
||||
if (auto_channel) {
|
||||
channel_number = 37;
|
||||
} else if (all_channels) {
|
||||
channel_number = 0;
|
||||
} else {
|
||||
auto_channel = false;
|
||||
channel_number = i;
|
||||
}
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(i));
|
||||
channel_number = i;
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
};
|
||||
|
||||
options_speed.on_change = [this](size_t, int32_t i) {
|
||||
@@ -403,7 +415,7 @@ BLETxView::BLETxView(NavigationView& nav)
|
||||
};
|
||||
|
||||
options_speed.set_selected_index(0);
|
||||
options_channel.set_selected_index(3);
|
||||
options_channel.set_by_value(BLT_CHAN_AUTO);
|
||||
|
||||
check_rand_mac.set_value(false);
|
||||
check_rand_mac.on_select = [this](Checkbox&, bool v) {
|
||||
@@ -413,8 +425,7 @@ BLETxView::BLETxView(NavigationView& nav)
|
||||
button_open.on_select = [this](Button&) {
|
||||
auto open_view = nav_.push<FileLoadView>(".TXT");
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
on_file_changed(new_file_path);
|
||||
|
||||
this->on_file_changed(new_file_path);
|
||||
nav_.set_on_pop([this]() { button_play.focus(); });
|
||||
};
|
||||
};
|
||||
@@ -427,7 +438,7 @@ BLETxView::BLETxView(NavigationView& nav)
|
||||
64,
|
||||
ENTER_KEYBOARD_MODE_ALPHA,
|
||||
[this](std::string& buffer) {
|
||||
on_save_file(buffer);
|
||||
this->on_save_file(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -506,10 +517,10 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
|
||||
}
|
||||
|
||||
do {
|
||||
readUntil(data_file, packets[num_packets].macAddress, mac_address_size_str, ' ');
|
||||
readUntil(data_file, packets[num_packets].advertisementData, max_packet_size_str, ' ');
|
||||
readUntil(data_file, packets[num_packets].packetType, max_packet_type_str, ' ');
|
||||
readUntil(data_file, packets[num_packets].packetCount, max_packet_repeat_str, '\n');
|
||||
readUntil(data_file, packets[num_packets].macAddress, sizeof(packets[num_packets].macAddress), ' ');
|
||||
readUntil(data_file, packets[num_packets].advertisementData, sizeof(packets[num_packets].advertisementData), ' ');
|
||||
readUntil(data_file, packets[num_packets].packetType, sizeof(packets[num_packets].packetType), ' ');
|
||||
readUntil(data_file, packets[num_packets].packetCount, sizeof(packets[num_packets].packetCount), '\n');
|
||||
|
||||
uint64_t macAddressSize = strlen(packets[num_packets].macAddress);
|
||||
uint64_t advertisementDataSize = strlen(packets[num_packets].advertisementData);
|
||||
@@ -542,10 +553,8 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
|
||||
num_packets++;
|
||||
|
||||
} while (num_packets < max_num_packets);
|
||||
|
||||
update_current_packet(packets[0], 0);
|
||||
|
||||
data_file.close();
|
||||
update_current_packet(packets[0], 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,6 +579,9 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
|
||||
|
||||
std::vector<std::string> strings = splitIntoStrings(packet.advertisementData);
|
||||
|
||||
packet_counter = packet.packet_count;
|
||||
current_packet = currentIndex;
|
||||
|
||||
text_packet_index.set(to_string_dec_uint(current_packet));
|
||||
|
||||
text_packets_sent.set(to_string_dec_uint(packet.packet_count));
|
||||
@@ -578,9 +590,6 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
|
||||
|
||||
progressbar.set_max(packet.packet_count);
|
||||
|
||||
packet_counter = packet.packet_count;
|
||||
current_packet = currentIndex;
|
||||
|
||||
dataFile.create(dataTempFilePath);
|
||||
|
||||
for (const std::string& str : strings) {
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
#define BLT_CHAN_AUTO 40
|
||||
#define BLT_CHAN_ALL 41
|
||||
|
||||
class BLELoggerTx {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
@@ -94,7 +97,7 @@ struct BLETxPacket {
|
||||
char advertisementData[63];
|
||||
char packetCount[11];
|
||||
char packetType[17];
|
||||
uint32_t packet_count;
|
||||
uint32_t packet_count; // repeat the same packet count times
|
||||
PKT_TYPE pduType;
|
||||
};
|
||||
|
||||
@@ -145,6 +148,7 @@ class BLETxView : public View {
|
||||
std::filesystem::path packet_save_path{bletx_dir / u"BLETX_????.TXT"};
|
||||
uint8_t channel_number = 37;
|
||||
bool auto_channel = false;
|
||||
bool all_channels = false;
|
||||
|
||||
char randomMac[13] = "010203040506";
|
||||
|
||||
@@ -156,10 +160,10 @@ class BLETxView : public View {
|
||||
int16_t auto_channel_period{6};
|
||||
|
||||
bool repeatLoop = false;
|
||||
uint32_t packet_counter{0};
|
||||
uint32_t num_packets{0};
|
||||
uint32_t current_packet{0};
|
||||
uint8_t packetTxCount{0};
|
||||
uint32_t packet_counter{0}; // the counter (decr) of the actual packet as it can be sent multiple times
|
||||
uint32_t num_packets{0}; // how many packets in the file
|
||||
uint32_t current_packet{0}; // the actual packet's num, i'm sending if i have multiple packets
|
||||
uint8_t packetTxCount{0}; // not used
|
||||
bool packetDone = false;
|
||||
bool random_mac = false;
|
||||
bool file_override = false;
|
||||
@@ -241,10 +245,48 @@ class BLETxView : public View {
|
||||
OptionsField options_channel{
|
||||
{11 * 8, 6 * 8},
|
||||
5,
|
||||
{{"Ch.37 ", 37},
|
||||
{{"Ch.0", 0},
|
||||
{"Ch.1", 1},
|
||||
{"Ch.2", 2},
|
||||
{"Ch.3", 3},
|
||||
{"Ch.4", 4},
|
||||
{"Ch.5", 5},
|
||||
{"Ch.6", 6},
|
||||
{"Ch.7", 7},
|
||||
{"Ch.8", 8},
|
||||
{"Ch.9", 9},
|
||||
{"Ch.10", 10},
|
||||
{"Ch.11", 11},
|
||||
{"Ch.12", 12},
|
||||
{"Ch.13", 13},
|
||||
{"Ch.14", 14},
|
||||
{"Ch.15", 15},
|
||||
{"Ch.16", 16},
|
||||
{"Ch.17", 17},
|
||||
{"Ch.18", 18},
|
||||
{"Ch.19", 19},
|
||||
{"Ch.20", 20},
|
||||
{"Ch.21", 21},
|
||||
{"Ch.22", 22},
|
||||
{"Ch.23", 23},
|
||||
{"Ch.24", 24},
|
||||
{"Ch.25", 25},
|
||||
{"Ch.26", 26},
|
||||
{"Ch.27", 27},
|
||||
{"Ch.28", 28},
|
||||
{"Ch.29", 29},
|
||||
{"Ch.30", 30},
|
||||
{"Ch.31", 31},
|
||||
{"Ch.32", 32},
|
||||
{"Ch.33", 33},
|
||||
{"Ch.34", 34},
|
||||
{"Ch.35", 35},
|
||||
{"Ch.36", 36},
|
||||
{"Ch.37", 37},
|
||||
{"Ch.38", 38},
|
||||
{"Ch.39", 39},
|
||||
{"Auto", 40}}};
|
||||
{"Auto", BLT_CHAN_AUTO},
|
||||
{"All", BLT_CHAN_ALL}}};
|
||||
|
||||
OptionsField options_adv_type{
|
||||
{17 * 8, 6 * 8},
|
||||
|
||||
@@ -238,6 +238,7 @@ APRSTXView::APRSTXView(NavigationView& nav) {
|
||||
process_coordinates(lat, lon);
|
||||
});
|
||||
};
|
||||
|
||||
// process_coordinates(last_lat, last_lon); //don't load last, so won't confuse users
|
||||
}
|
||||
|
||||
|
||||
@@ -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});
|
||||
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -103,7 +103,8 @@ DfuMenu2::DfuMenu2(NavigationView& nav)
|
||||
&text_info_line_8,
|
||||
&text_info_line_9,
|
||||
&text_info_line_10,
|
||||
&text_info_line_11});
|
||||
&text_info_line_11,
|
||||
&text_info_line_12});
|
||||
}
|
||||
|
||||
void DfuMenu2::paint(Painter& painter) {
|
||||
@@ -118,9 +119,12 @@ void DfuMenu2::paint(Painter& painter) {
|
||||
text_info_line_9.set(to_string_dec_uint(portapack::transmitter_model.target_frequency(), 10));
|
||||
text_info_line_10.set(to_string_dec_uint(portapack::transmitter_model.baseband_bandwidth(), 10));
|
||||
text_info_line_11.set(to_string_dec_uint(portapack::transmitter_model.sampling_rate(), 10));
|
||||
text_info_line_12.set(std::string("M") + (radio::get_mixer_invert() ? "Y" : "N") +
|
||||
" B" + (radio::get_baseband_invert() ? "Y" : "N") +
|
||||
" IQ" + ((radio::get_mixer_invert() ^ radio::get_baseband_invert()) ? "Y" : "N"));
|
||||
|
||||
constexpr auto margin = 5;
|
||||
constexpr auto lines = 11 + 2;
|
||||
constexpr auto lines = 12 + 2;
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{5 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin},
|
||||
|
||||
@@ -96,6 +96,7 @@ class DfuMenu2 : public View {
|
||||
{{5 * CHARACTER_WIDTH, 13 * LINE_HEIGHT}, "TX Freq:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 14 * LINE_HEIGHT}, "TX BW:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 15 * LINE_HEIGHT}, "TX SampR:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 16 * LINE_HEIGHT}, "IQ Inv:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
};
|
||||
|
||||
Text text_info_line_1{{14 * CHARACTER_WIDTH, 5 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
@@ -109,6 +110,7 @@ class DfuMenu2 : public View {
|
||||
Text text_info_line_9{{14 * CHARACTER_WIDTH, 13 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_10{{14 * CHARACTER_WIDTH, 14 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_11{{14 * CHARACTER_WIDTH, 15 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_12{{(14 + 1) * CHARACTER_WIDTH, 16 * LINE_HEIGHT, (10 - 1) * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -390,6 +390,7 @@ std::string get_stem(std::string t) {
|
||||
return t.substr(0, index);
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_filename(std::string _s) {
|
||||
const auto index = _s.find_last_of("/");
|
||||
if (index == _s.npos) {
|
||||
@@ -398,6 +399,7 @@ std::string get_filename(std::string _s) {
|
||||
return _s.substr(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void FileManBaseView::refresh_list() {
|
||||
if (on_refresh_widgets)
|
||||
on_refresh_widgets(false);
|
||||
@@ -408,8 +410,6 @@ void FileManBaseView::refresh_list() {
|
||||
menu_view.clear();
|
||||
|
||||
for (const auto& entry : entry_list) {
|
||||
auto entry_name = std::string{entry.path.length() <= max_filename_length ? entry.path : entry.path.substr(0, max_filename_length)};
|
||||
|
||||
if (entry.is_directory) {
|
||||
std::string size_str{};
|
||||
if (entry.path == str_next || entry.path == str_back) {
|
||||
@@ -418,8 +418,10 @@ void FileManBaseView::refresh_list() {
|
||||
size_str = (entry.path == parent_dir_path.string()) ? "" : to_string_dec_uint(file_count(current_path / entry.path));
|
||||
}
|
||||
|
||||
// CRITICAL: Use "\t" (Tab) to separate the path and the size!
|
||||
// Do not use spaces or padding here. The menu will handle the alignment.
|
||||
menu_view.add_item(
|
||||
{entry_name.substr(0, max_filename_length) + std::string((max_filename_length + 1) - entry_name.length(), ' ') + size_str,
|
||||
{entry.path + "\t" + size_str,
|
||||
Theme::getInstance()->fg_yellow->foreground,
|
||||
&bitmap_icon_dir,
|
||||
[this](KeyEvent key) {
|
||||
@@ -431,8 +433,9 @@ void FileManBaseView::refresh_list() {
|
||||
const auto& assoc = get_assoc(get_extension(entry.path));
|
||||
auto size_str = to_string_file_size(entry.size);
|
||||
|
||||
// CRITICAL: Use "\t" (Tab) to separate the path and the size!
|
||||
menu_view.add_item(
|
||||
{entry_name.substr(0, max_filename_length) + std::string((max_filename_length + 1) - entry_name.length(), ' ') + size_str,
|
||||
{entry.path + "\t" + size_str,
|
||||
assoc.color,
|
||||
assoc.icon,
|
||||
[this](KeyEvent key) {
|
||||
|
||||
@@ -32,7 +32,7 @@ static const char* hackrf_magic = "HACKRFFW";
|
||||
#define FIRST_CHECKSUM_NIGHTLY 240125
|
||||
|
||||
Thread* FlashUtilityView::thread{nullptr};
|
||||
static constexpr size_t max_filename_length = 26;
|
||||
static constexpr size_t max_filename_length = 60; // max length of filename
|
||||
|
||||
bool valid_firmware_file(std::filesystem::path::string_type path) {
|
||||
File firmware_file;
|
||||
@@ -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,13 +161,14 @@ 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(
|
||||
{0, 0, portapack::display.width(), portapack::display.height()},
|
||||
Theme::getInstance()->bg_darkest->background);
|
||||
|
||||
painter.draw_string({12, 24}, this->nav_.style(), "This will take 15 seconds.");
|
||||
painter.draw_string({12, 24}, this->nav_.style(), "This will take 15-45 sec.");
|
||||
painter.draw_string({12, 64}, this->nav_.style(), "Please wait while LED RX");
|
||||
painter.draw_string({12, 84}, this->nav_.style(), "is on and TX is flashing.");
|
||||
painter.draw_string({12, 124}, this->nav_.style(), "Device will then restart.");
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,14 +1171,20 @@ 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 (dev) dev->resetChangedFlag();
|
||||
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->reInit()) {
|
||||
nav.display_modal("Error", "Battery gauge re-init failed");
|
||||
return;
|
||||
}
|
||||
if (dev) {
|
||||
dev->resetChangedFlag();
|
||||
dev->update(); // resend the new data
|
||||
}
|
||||
}
|
||||
send_system_refresh();
|
||||
nav.pop();
|
||||
@@ -1206,6 +1213,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());
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -361,6 +361,16 @@ void set_noaaapt_config() {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_vor_config(bool enabled) {
|
||||
const VorRxConfigureMessage message{enabled};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_vor_tx_config(uint16_t radial_deg, bool ident_enabled, const std::string& ident, bool enabled) {
|
||||
const VorTxConfigureMessage message{radial_deg, ident_enabled, ident.c_str(), enabled};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_flex_config() {
|
||||
const FlexConfigureMessage message{};
|
||||
send_message(&message);
|
||||
|
||||
@@ -113,6 +113,8 @@ void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta);
|
||||
void set_morsetx_key(bool key_down);
|
||||
void set_wefax_config(uint8_t lpm, uint8_t ioc);
|
||||
void set_noaaapt_config();
|
||||
void set_vor_config(bool enabled = true);
|
||||
void set_vor_tx_config(uint16_t radial_deg, bool ident_enabled = true, const std::string& ident = "", bool enabled = true);
|
||||
void set_flex_config();
|
||||
void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk
|
||||
void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload = nullptr, uint16_t payload_length = 0); // baud*100
|
||||
|
||||
@@ -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)
|
||||
@@ -406,7 +419,11 @@ std::string ClockManager::get_freq() {
|
||||
to_string_dec_uint((reference.frequency % 1000000) / 100, 4, '0') + " MHz";
|
||||
}
|
||||
|
||||
static void portapack_tcxo_enable() {
|
||||
void ClockManager::portapack_tcxo_enable() {
|
||||
#ifdef PRALINE
|
||||
gpio_control::clkin_ctrl.setActive();
|
||||
set_p1_control(P1_Function::P22_ClkIn);
|
||||
#endif
|
||||
portapack::io.reference_oscillator(true);
|
||||
|
||||
/* Delay >10ms at 96MHz clock speed for reference oscillator to start. */
|
||||
@@ -415,7 +432,10 @@ static void portapack_tcxo_enable() {
|
||||
while (delay--);
|
||||
}
|
||||
|
||||
static void portapack_tcxo_disable() {
|
||||
void ClockManager::portapack_tcxo_disable() {
|
||||
#ifdef PRALINE
|
||||
gpio_control::clkin_ctrl.setInactive();
|
||||
#endif
|
||||
portapack::io.reference_oscillator(false);
|
||||
}
|
||||
|
||||
@@ -423,77 +443,50 @@ 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 -> XTAL,PLLB -> CLKIN
|
||||
clock_generator.set_pll_input_sources(si5351c_pll_input_sources);
|
||||
// although the name a and the branch define CLK2 MCU CLOCK
|
||||
// only use CLK2
|
||||
auto si5351_clock_control_common = si5351a_clock_control_common;
|
||||
constexpr size_t clock_generator_output_pro_mcu_clkin = 2;
|
||||
// clk_src define CLKIN
|
||||
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 +531,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 +602,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
|
||||
@@ -724,7 +680,7 @@ ClockManager::Reference ClockManager::choose_reference() {
|
||||
}
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
gpio_r9_clkin_en.write(1);
|
||||
gpio_control::r9_clkin_en.setActive();
|
||||
volatile uint32_t delay = 240000 + 24000;
|
||||
while (delay--);
|
||||
}
|
||||
@@ -739,7 +695,7 @@ ClockManager::Reference ClockManager::choose_reference() {
|
||||
}
|
||||
|
||||
if (hackrf_r9) {
|
||||
gpio_r9_clkin_en.write(0);
|
||||
gpio_control::r9_clkin_en.setInactive();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -753,13 +709,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 +743,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 +806,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 +827,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
|
||||
@@ -1181,8 +1182,7 @@ void ClockManager::enable_clock_output(bool enable) {
|
||||
}
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
gpio_r9_clkout_en.output();
|
||||
gpio_r9_clkout_en.write(enable);
|
||||
gpio_control::r9_clkout_en.setState(enable);
|
||||
|
||||
// NOTE: RETURNING HERE IF HACKRF_R9 TO PREVENT CLK2 FROM BEING DISABLED OR FREQ MODIFIED SINCE CLK2 ON R9 IS
|
||||
// USED FOR BOTH CLKOUT AND FOR THE MCU_CLOCK (== GP_CLKIN) WHICH OTHER LP43XX CLOCKS CURRENTLY RELY ON.
|
||||
@@ -1210,3 +1210,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
|
||||
@@ -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
|
||||
@@ -93,6 +116,9 @@ class ClockManager {
|
||||
|
||||
void enable_clock_output(bool enable);
|
||||
|
||||
void portapack_tcxo_enable();
|
||||
void portapack_tcxo_disable();
|
||||
|
||||
private:
|
||||
I2C& i2c0;
|
||||
si5351::Si5351& clock_generator;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
+66
-4
@@ -294,11 +294,11 @@ void BLESpamView::createWindowsPacket() {
|
||||
std::copy(res.begin(), res.end(), advertisementData);
|
||||
}
|
||||
|
||||
void BLESpamView::createNameSpamPacket() {
|
||||
const char* names[] = {"PortaHack", "PwnBt", "iSpam", "GenericFoodVagon", "SignalSnoop", "ByteBandit", "RadioRogue", "RadioRebel", "ByteBlast"};
|
||||
|
||||
const char* name = names[rand() % 9]; //"PortaHack";
|
||||
// Builds a "Complete Local Name" HID advertisement for the given name.
|
||||
// Shared by NameSpam (built-in list) and Custom (SD card list) modes.
|
||||
void BLESpamView::buildNamePacket(const char* name) {
|
||||
uint8_t name_len = strlen(name);
|
||||
if (name_len > 19) name_len = 19; // clamp so the hex string fits advertisementData[63]
|
||||
|
||||
uint8_t size = 12 + name_len;
|
||||
uint8_t i = 0;
|
||||
@@ -327,6 +327,65 @@ void BLESpamView::createNameSpamPacket() {
|
||||
std::copy(res.begin(), res.end(), advertisementData);
|
||||
}
|
||||
|
||||
void BLESpamView::createNameSpamPacket() {
|
||||
const char* names[] = {"PortaHack", "PwnBt", "iSpam", "GenericFoodVagon", "SignalSnoop", "ByteBandit", "RadioRogue", "RadioRebel", "ByteBlast"};
|
||||
buildNamePacket(names[rand() % 9]);
|
||||
}
|
||||
|
||||
// Opens /BLESPAM/NAME.txt once and keeps it open for the session.
|
||||
void BLESpamView::open_custom_file() {
|
||||
custom_loaded_ = true; // attempt only once
|
||||
auto open_error = custom_file_.open(u"/BLESPAM/NAME.txt");
|
||||
custom_file_valid_ = !open_error; // false -> caller falls back to NameSpam
|
||||
}
|
||||
|
||||
// Reads the next line into custom_name_ (clamped to 19 chars), advancing the
|
||||
// file position. At end of file it seeks back to the start to cycle. Returns
|
||||
// false if the file holds no usable name. Only one line is ever held in RAM.
|
||||
bool BLESpamView::read_next_custom_name() {
|
||||
if (!custom_file_valid_) return false;
|
||||
|
||||
custom_name_.clear();
|
||||
bool wrapped = false;
|
||||
char c;
|
||||
|
||||
while (true) {
|
||||
auto read_result = custom_file_.read(&c, 1);
|
||||
if (read_result.is_error()) return false;
|
||||
|
||||
if (read_result.value() == 0) {
|
||||
// end of file
|
||||
if (!custom_name_.empty()) break;
|
||||
// use the final line
|
||||
if (wrapped) return false;
|
||||
// no usable name in file
|
||||
if (custom_file_.seek(0).is_error()) return false;
|
||||
// loop back to the first line
|
||||
wrapped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\n' || c == '\r') {
|
||||
if (!custom_name_.empty()) break; // got a complete line
|
||||
continue; // skip blank lines / CRLF pairs
|
||||
}
|
||||
|
||||
if (custom_name_.size() < 19) // clamp; keep scanning rest of long line
|
||||
custom_name_.push_back(c);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLESpamView::createCustomPacket() {
|
||||
if (!custom_loaded_) open_custom_file();
|
||||
|
||||
if (!read_next_custom_name()) { // no file / unreadable / empty -> fall back
|
||||
createNameSpamPacket();
|
||||
return;
|
||||
}
|
||||
buildNamePacket(custom_name_.c_str());
|
||||
}
|
||||
|
||||
char BLESpamView::randomNameChar() {
|
||||
int randVal = rand() % 62; // 26 uppercase + 26 lowercase + 10 digits
|
||||
if (randVal < 26) {
|
||||
@@ -622,6 +681,9 @@ void BLESpamView::createPacket(ATK_TYPE attackType) {
|
||||
case ATK_NAMERANDOM:
|
||||
createNameRandomPacket();
|
||||
break;
|
||||
case ATK_CUSTOM:
|
||||
createCustomPacket();
|
||||
break;
|
||||
case ATK_ALL_SAFE:
|
||||
createAnyPacket(true);
|
||||
break;
|
||||
|
||||
+14
-2
@@ -39,6 +39,7 @@
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "file.hpp"
|
||||
|
||||
using namespace ui;
|
||||
|
||||
@@ -52,6 +53,7 @@ enum ATK_TYPE {
|
||||
ATK_SAMSUNG,
|
||||
ATK_NAMESPAM,
|
||||
ATK_NAMERANDOM,
|
||||
ATK_CUSTOM,
|
||||
ATK_ALL_SAFE,
|
||||
ATK_ALL
|
||||
};
|
||||
@@ -131,8 +133,9 @@ class BLESpamView : public View {
|
||||
{"Samsung", 4},
|
||||
{"NameSpam", 5},
|
||||
{"NameRandom", 6},
|
||||
{"All-Safe", 7},
|
||||
{"All", 8}}};
|
||||
{"NameCustom", 7},
|
||||
{"All-Safe", 8},
|
||||
{"All", 9}}};
|
||||
|
||||
bool is_running{false};
|
||||
|
||||
@@ -144,6 +147,11 @@ class BLESpamView : public View {
|
||||
bool randomMac{true};
|
||||
bool randomDev{true};
|
||||
|
||||
File custom_file_{}; // kept open while in Custom mode
|
||||
std::string custom_name_{}; // holds only the current name (clamped)
|
||||
bool custom_loaded_{false}; // true once an open attempt has been made
|
||||
bool custom_file_valid_{false}; // true if /BLESPAM/NAME.txt opened successfully
|
||||
|
||||
uint8_t channel_number = 37;
|
||||
char mac[13] = "010203040407";
|
||||
char advertisementData[63] = {"03032CFE06162CFED5A59E020AB4\0"};
|
||||
@@ -156,7 +164,11 @@ class BLESpamView : public View {
|
||||
void createIosPacket(bool crash);
|
||||
void createSamsungPacket();
|
||||
void createWindowsPacket();
|
||||
void buildNamePacket(const char* name); // shared name advertisement builder (NameSpam / Custom)
|
||||
void createNameSpamPacket();
|
||||
void createCustomPacket();
|
||||
void open_custom_file(); // open /BLESPAM/NAME.txt once (one name per line)
|
||||
bool read_next_custom_name(); // read next line into custom_name_, looping at EOF
|
||||
void createNameRandomPacket();
|
||||
void createAnyPacket(bool safe);
|
||||
void createPacket(ATK_TYPE attackType);
|
||||
|
||||
+12
-15
@@ -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
@@ -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
@@ -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__
|
||||
+17
-1
@@ -133,6 +133,14 @@ set(EXTCPPSRC
|
||||
external/noaaapt_rx/main.cpp
|
||||
external/noaaapt_rx/ui_noaaapt_rx.cpp
|
||||
|
||||
#vor_rx
|
||||
external/vor_rx/main.cpp
|
||||
external/vor_rx/ui_vor_rx.cpp
|
||||
|
||||
#vor_tx
|
||||
external/vor_tx/main.cpp
|
||||
external/vor_tx/ui_vor_tx.cpp
|
||||
|
||||
#shoppingcart_lock 272 bytes
|
||||
external/shoppingcart_lock/main.cpp
|
||||
external/shoppingcart_lock/shoppingcart_lock.cpp
|
||||
@@ -327,7 +335,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 +370,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
|
||||
@@ -396,6 +409,8 @@ set(EXTAPPLIST
|
||||
acars_rx
|
||||
wefax_rx
|
||||
noaaapt_rx
|
||||
vor_rx
|
||||
vor_tx
|
||||
shoppingcart_lock
|
||||
ookbrute
|
||||
ook_editor
|
||||
@@ -451,6 +466,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
|
||||
|
||||
+21
@@ -110,6 +110,9 @@ 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
|
||||
ram_external_app_vor_rx (rwx) : org = 0xAE090000, len = 32k
|
||||
ram_external_app_vor_tx (rwx) : org = 0xAE0A0000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -390,6 +393,18 @@ SECTIONS
|
||||
*(*ui*external_app*noaaapt_rx*);
|
||||
} > ram_external_app_noaaapt_rx
|
||||
|
||||
.external_app_vor_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_vor_rx.application_information));
|
||||
*(*ui*external_app*vor_rx*);
|
||||
} > ram_external_app_vor_rx
|
||||
|
||||
.external_app_vor_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_vor_tx.application_information));
|
||||
*(*ui*external_app*vor_tx*);
|
||||
} > ram_external_app_vor_tx
|
||||
|
||||
.external_app_breakout : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_breakout.application_information));
|
||||
@@ -635,4 +650,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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; if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_vor_rx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::vor_rx {
|
||||
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<VorRxView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::vor_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_vor_rx.application_information"), used)) application_information_t _application_information_vor_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::vor_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "VOR RX",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xC0,
|
||||
0x03,
|
||||
0x80,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_vor_rx */ {'P', 'V', 'R', 'X'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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; if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_vor_rx.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
using namespace portapack;
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::vor_rx {
|
||||
|
||||
void VorLogger::write_header() {
|
||||
log_file.write_raw("Time;Course;Radial;Deviation;Quality;Flag;");
|
||||
}
|
||||
|
||||
void VorLogger::log_status(const VorRxStatusDataMessage& message, uint16_t course_deg) {
|
||||
const int16_t deviation = static_cast<int16_t>((static_cast<int32_t>(message.radial_deg) - static_cast<int32_t>(course_deg) + 540) % 360 - 180);
|
||||
std::string row = ";";
|
||||
row += to_string_dec_uint(course_deg);
|
||||
row += ";" + to_string_dec_uint(message.radial_deg);
|
||||
row += ";" + std::to_string(deviation);
|
||||
row += ";" + to_string_dec_uint(message.quality);
|
||||
row += ";" + std::string(message.valid ? (message.to_from ? "TO" : "FROM") : "--");
|
||||
log_file.write_entry(rtc_time::now(), row);
|
||||
}
|
||||
|
||||
VorCdiIndicator::VorCdiIndicator(Point position)
|
||||
: Widget{{position, {screen_width, 32}}} {
|
||||
}
|
||||
|
||||
void VorCdiIndicator::set_course(uint16_t course_deg) {
|
||||
if (course_deg_ != course_deg) {
|
||||
course_deg_ = course_deg;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void VorCdiIndicator::set_radial(uint16_t radial_deg) {
|
||||
if (radial_deg_ != radial_deg) {
|
||||
radial_deg_ = radial_deg;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void VorCdiIndicator::set_valid(bool valid) {
|
||||
if (valid_ != valid) {
|
||||
valid_ = valid;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
int32_t VorCdiIndicator::normalize_signed_degrees(int32_t degrees) {
|
||||
while (degrees <= -180) {
|
||||
degrees += 360;
|
||||
}
|
||||
while (degrees > 180) {
|
||||
degrees -= 360;
|
||||
}
|
||||
return degrees;
|
||||
}
|
||||
|
||||
void VorCdiIndicator::paint(Painter& painter) {
|
||||
const auto r = screen_rect();
|
||||
const auto theme = Theme::getInstance();
|
||||
const auto line_color = valid_ ? theme->fg_light->foreground : theme->bg_medium->foreground;
|
||||
const auto needle_color = valid_ ? theme->fg_red->foreground : theme->bg_medium->foreground;
|
||||
|
||||
// Clear background so old needle positions don't linger.
|
||||
painter.fill_rectangle(r, theme->bg_darkest->background);
|
||||
|
||||
const auto center_x = static_cast<Coord>(r.left() + r.width() / 2);
|
||||
const auto center_y = static_cast<Coord>(r.top() + r.height() / 2);
|
||||
|
||||
// Horizontal scale line.
|
||||
painter.draw_hline({static_cast<Coord>(r.left() + 8), center_y}, r.width() - 16, line_color);
|
||||
|
||||
// Evenly spaced tick marks, longer at center.
|
||||
constexpr int32_t tick_spacing = 40;
|
||||
for (int32_t tick = -2; tick <= 2; ++tick) {
|
||||
const auto x = static_cast<Coord>(center_x + tick * tick_spacing);
|
||||
const auto len = (tick == 0) ? 14 : 8;
|
||||
painter.draw_vline({x, static_cast<Coord>(center_y - len / 2)}, len, line_color);
|
||||
}
|
||||
|
||||
// Deviation needle, clamped to +/-10 degrees full-scale.
|
||||
const int32_t deviation = normalize_signed_degrees(static_cast<int32_t>(radial_deg_) - static_cast<int32_t>(course_deg_));
|
||||
const int32_t scaled = std::max<int32_t>(-10, std::min<int32_t>(10, deviation));
|
||||
// +/-10 deg full-scale maps to +/-2 ticks (2 * tick_spacing); the division
|
||||
// is exact for the clamped range so no rounding is needed.
|
||||
const int16_t needle_offset = static_cast<int16_t>((scaled * (2 * tick_spacing)) / 10);
|
||||
const auto needle_x = static_cast<Coord>(center_x + needle_offset);
|
||||
painter.draw_vline({needle_x, static_cast<Coord>(r.top() + 4)}, r.height() - 8, needle_color);
|
||||
}
|
||||
|
||||
VorRxView::VorRxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_volume,
|
||||
&field_frequency,
|
||||
&rssi,
|
||||
&channel,
|
||||
&audio,
|
||||
&labels,
|
||||
&text_status,
|
||||
&text_band,
|
||||
&text_next,
|
||||
&field_course,
|
||||
&text_course_unit,
|
||||
&field_calibration,
|
||||
&text_calib_unit,
|
||||
&text_radial,
|
||||
&text_flag,
|
||||
&text_cdi_title,
|
||||
&cdi_indicator,
|
||||
&check_log,
|
||||
&button_start_stop});
|
||||
|
||||
field_frequency.set_step(8333);
|
||||
if (field_frequency.value() == 0) {
|
||||
field_frequency.set_value(110'000'000);
|
||||
}
|
||||
|
||||
field_course.set_value(0);
|
||||
field_course.on_change = [this](int32_t) {
|
||||
update_cdi();
|
||||
};
|
||||
|
||||
field_calibration.set_value(0);
|
||||
field_calibration.on_change = [this](int32_t) {
|
||||
refresh_radial();
|
||||
};
|
||||
|
||||
logger = std::make_unique<VorLogger>();
|
||||
check_log.set_value(logging_);
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
logging_ = v;
|
||||
update_logging();
|
||||
};
|
||||
update_logging();
|
||||
|
||||
button_start_stop.on_select = [this](Button&) {
|
||||
if (running_) {
|
||||
stop_receiver();
|
||||
} else {
|
||||
start_receiver();
|
||||
}
|
||||
};
|
||||
|
||||
start_receiver();
|
||||
}
|
||||
|
||||
VorRxView::~VorRxView() {
|
||||
stop_receiver();
|
||||
}
|
||||
|
||||
void VorRxView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void VorRxView::start_receiver() {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
baseband::set_vor_config(true);
|
||||
|
||||
radial_filter_valid_ = false;
|
||||
flag_state_ = 0;
|
||||
|
||||
receiver_model.set_hidden_offset(0);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
|
||||
receiver_model.set_frequency_step(8333);
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.enable();
|
||||
|
||||
// enable() applies the AM configuration, which forces the audio codec to
|
||||
// 12 kHz. The VOR baseband keeps its channel at 48 kHz (so the 9960 Hz
|
||||
// subcarrier stays representable), so re-assert 48 kHz audio afterwards.
|
||||
audio::set_rate(audio::Rate::Hz_48000);
|
||||
audio::output::start();
|
||||
|
||||
running_ = true;
|
||||
update_status();
|
||||
}
|
||||
|
||||
void VorRxView::stop_receiver() {
|
||||
if (!running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
running_ = false;
|
||||
baseband::set_vor_config(false);
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
update_status();
|
||||
}
|
||||
|
||||
void VorRxView::update_status() {
|
||||
text_status.set(running_ ? "Running" : "Idle");
|
||||
button_start_stop.set_text(running_ ? "Stop" : "Start");
|
||||
}
|
||||
|
||||
void VorRxView::update_logging() {
|
||||
if (logger && logging_) {
|
||||
logger->append(logs_dir / (std::string("VOR_") + to_string_timestamp(rtc_time::now()) + ".CSV"));
|
||||
logger->write_header();
|
||||
}
|
||||
}
|
||||
|
||||
void VorRxView::on_vor_status(const VorRxStatusDataMessage& message) {
|
||||
if (!running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
last_radial_deg_ = message.radial_deg;
|
||||
last_valid_ = message.valid;
|
||||
have_status_ = true;
|
||||
|
||||
if (message.valid) {
|
||||
last_radial_deg_ = smooth_radial(message.radial_deg);
|
||||
} else {
|
||||
// Drop the filter history so a fresh lock doesn't drag from stale data.
|
||||
radial_filter_valid_ = false;
|
||||
}
|
||||
|
||||
text_next.set(message.valid ? "Locked" : "Searching");
|
||||
refresh_radial();
|
||||
if (logger && logging_) {
|
||||
VorRxStatusDataMessage calibrated = message;
|
||||
calibrated.radial_deg = calibrated_radial(last_radial_deg_);
|
||||
calibrated.to_from = (flag_state_ == 2);
|
||||
logger->log_status(calibrated, field_course.value());
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t VorRxView::smooth_radial(uint16_t radial_deg) {
|
||||
// Wrapped exponential moving average. At ~10 updates/s blending 20% of each
|
||||
// new reading (alpha = 0.8) gives a ~0.5 s time constant while cutting the
|
||||
// jitter by ~3x. Stepping along the shortest arc in 1/64 deg fixed point
|
||||
// handles the 0/360 deg wrap without any trig (which would otherwise pull
|
||||
// newlib's float sin/cos/atan2 argument-reduction code into flash ROM).
|
||||
constexpr int32_t scale = 64;
|
||||
constexpr int32_t full_circle = 360 * scale;
|
||||
const int32_t target = static_cast<int32_t>(radial_deg) * scale;
|
||||
|
||||
if (!radial_filter_valid_) {
|
||||
radial_smoothed_fp_ = target;
|
||||
radial_filter_valid_ = true;
|
||||
} else {
|
||||
int32_t diff = (target - radial_smoothed_fp_) % full_circle;
|
||||
if (diff < -full_circle / 2) {
|
||||
diff += full_circle;
|
||||
} else if (diff > full_circle / 2) {
|
||||
diff -= full_circle;
|
||||
}
|
||||
// 20% step toward the new reading (1 - alpha).
|
||||
radial_smoothed_fp_ += diff / 5;
|
||||
radial_smoothed_fp_ %= full_circle;
|
||||
if (radial_smoothed_fp_ < 0) {
|
||||
radial_smoothed_fp_ += full_circle;
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<uint16_t>((radial_smoothed_fp_ + scale / 2) / scale) % 360;
|
||||
}
|
||||
|
||||
uint16_t VorRxView::calibrated_radial(uint16_t radial_deg) const {
|
||||
int32_t value = (static_cast<int32_t>(radial_deg) + field_calibration.value()) % 360;
|
||||
if (value < 0) {
|
||||
value += 360;
|
||||
}
|
||||
return static_cast<uint16_t>(value);
|
||||
}
|
||||
|
||||
void VorRxView::refresh_radial() {
|
||||
if (!have_status_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint16_t radial = calibrated_radial(last_radial_deg_);
|
||||
text_radial.set(to_string_dec_uint(radial, 3) + " deg");
|
||||
text_flag.set(to_from_label(radial));
|
||||
cdi_indicator.set_radial(radial);
|
||||
cdi_indicator.set_valid(last_valid_);
|
||||
}
|
||||
|
||||
const char* VorRxView::to_from_label(uint16_t radial_deg) {
|
||||
if (!last_valid_) {
|
||||
flag_state_ = 0;
|
||||
return "--";
|
||||
}
|
||||
|
||||
// TO/FROM is set by the selected OBS course relative to the received radial,
|
||||
// not by the radial alone. Take the difference wrapped to [-180, 180]: the
|
||||
// switch is at the 90 deg abeam point. |diff| < 90 means the selected course
|
||||
// leads away from the station (FROM); |diff| > 90 means toward it (TO).
|
||||
int32_t diff = (static_cast<int32_t>(radial_deg) - field_course.value() + 540) % 360 - 180;
|
||||
const int32_t adiff = (diff < 0) ? -diff : diff;
|
||||
|
||||
// Hysteresis: hold the current flag within a +/-5 deg dead zone around the
|
||||
// 90 deg boundary so noise near abeam doesn't rapidly toggle TO/FROM.
|
||||
if (adiff < 85) {
|
||||
flag_state_ = 1; // FROM
|
||||
} else if (adiff > 95) {
|
||||
flag_state_ = 2; // TO
|
||||
}
|
||||
|
||||
if (flag_state_ == 1) return "FROM";
|
||||
if (flag_state_ == 2) return "TO";
|
||||
return "--"; // abeam / ambiguous
|
||||
}
|
||||
|
||||
void VorRxView::update_cdi() {
|
||||
cdi_indicator.set_course(field_course.value());
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::vor_rx
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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; if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __UI_VOR_RX_H__
|
||||
#define __UI_VOR_RX_H__
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_audio.hpp"
|
||||
#include "ui_channel.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
|
||||
namespace ui::external_app::vor_rx {
|
||||
|
||||
class VorLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void write_header();
|
||||
void log_status(const VorRxStatusDataMessage& message, uint16_t course_deg);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
class VorCdiIndicator : public Widget {
|
||||
public:
|
||||
VorCdiIndicator(Point position);
|
||||
|
||||
void set_course(uint16_t course_deg);
|
||||
void set_radial(uint16_t radial_deg);
|
||||
void set_valid(bool valid);
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
static int32_t normalize_signed_degrees(int32_t degrees);
|
||||
|
||||
uint16_t course_deg_{0};
|
||||
uint16_t radial_deg_{0};
|
||||
bool valid_{false};
|
||||
};
|
||||
|
||||
class VorRxView : public View {
|
||||
public:
|
||||
VorRxView(NavigationView& nav);
|
||||
~VorRxView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "VOR RX"; }
|
||||
|
||||
private:
|
||||
void start_receiver();
|
||||
void stop_receiver();
|
||||
void update_status();
|
||||
void on_vor_status(const VorRxStatusDataMessage& message);
|
||||
void update_cdi();
|
||||
void refresh_radial();
|
||||
uint16_t calibrated_radial(uint16_t radial_deg) const;
|
||||
uint16_t smooth_radial(uint16_t radial_deg);
|
||||
const char* to_from_label(uint16_t radial_deg);
|
||||
void update_logging();
|
||||
|
||||
NavigationView& nav_;
|
||||
bool running_{false};
|
||||
bool logging_{false};
|
||||
bool have_status_{false};
|
||||
uint16_t last_radial_deg_{0};
|
||||
bool last_valid_{false};
|
||||
uint8_t flag_state_{0}; // TO/FROM hysteresis: 0=unknown, 1=FROM, 2=TO
|
||||
// Circular exponential moving average of the radial. Each 100 ms estimate
|
||||
// is noisy (~10 deg std even when locked), so blend it along the shortest
|
||||
// arc in 1/64 deg fixed point to average correctly across the 0/360 deg
|
||||
// wrap without trig.
|
||||
bool radial_filter_valid_{false};
|
||||
int32_t radial_smoothed_fp_{0};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
nav_};
|
||||
RFAmpField field_rf_amp{
|
||||
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||
LNAGainField field_lna{
|
||||
{UI_POS_X(15), UI_POS_Y(0)}};
|
||||
VGAGainField field_vga{
|
||||
{UI_POS_X(18), UI_POS_Y(0)}};
|
||||
AudioVolumeField field_volume{
|
||||
{screen_width - 2 * 8, UI_POS_Y(0)}};
|
||||
|
||||
RSSI rssi{
|
||||
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(21) - UI_POS_WIDTH(2), 4}};
|
||||
Channel channel{
|
||||
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(21) - UI_POS_WIDTH(2), 4}};
|
||||
Audio audio{
|
||||
{UI_POS_X(21), 10, UI_POS_WIDTH_REMAINING(21) - UI_POS_WIDTH(2), 4}};
|
||||
|
||||
NumberField field_course{
|
||||
{UI_POS_X(14), UI_POS_Y(4)},
|
||||
3,
|
||||
{0, 359},
|
||||
1,
|
||||
'0'};
|
||||
|
||||
NumberField field_calibration{
|
||||
{UI_POS_X(14), UI_POS_Y(7)},
|
||||
4,
|
||||
{-359, 359},
|
||||
1,
|
||||
' '};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(1)}, "Status:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(2)}, "VOR Band:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(3)}, "Decoder:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(4)}, "Course (OBS):", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(5)}, "Rec. Radial:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(6)}, "Flag:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(7)}, "Calibration:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_status{
|
||||
{UI_POS_X(14), UI_POS_Y(1), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
|
||||
"Idle"};
|
||||
Text text_band{
|
||||
{UI_POS_X(14), UI_POS_Y(2), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
|
||||
"108.00-117.95MHz"};
|
||||
Text text_next{
|
||||
{UI_POS_X(14), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
|
||||
"Pending"};
|
||||
Text text_course_unit{
|
||||
{UI_POS_X(18), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(18), UI_POS_HEIGHT(1)},
|
||||
"deg"};
|
||||
Text text_radial{
|
||||
{UI_POS_X(14), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
|
||||
"--"};
|
||||
Text text_flag{
|
||||
{UI_POS_X(14), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
|
||||
"--"};
|
||||
Text text_calib_unit{
|
||||
{UI_POS_X(19), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(19), UI_POS_HEIGHT(1)},
|
||||
"deg"};
|
||||
|
||||
Text text_cdi_title{
|
||||
{UI_POS_X(0), UI_POS_Y(9), UI_POS_WIDTH_REMAINING(0), UI_POS_HEIGHT(1)},
|
||||
"Course Deviation Indicator"};
|
||||
|
||||
VorCdiIndicator cdi_indicator{
|
||||
{UI_POS_X(0), UI_POS_Y(10)}};
|
||||
|
||||
std::unique_ptr<VorLogger> logger{};
|
||||
|
||||
Button button_start_stop{
|
||||
{UI_POS_X(9), UI_POS_Y(14), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
|
||||
"Start"};
|
||||
|
||||
Checkbox check_log{
|
||||
{UI_POS_X(7), UI_POS_Y(17)},
|
||||
3,
|
||||
"LOG to SD Card",
|
||||
true};
|
||||
|
||||
MessageHandlerRegistration message_handler_vor_status{
|
||||
Message::ID::VorRxStatusData,
|
||||
[this](const Message* p) {
|
||||
const auto message = *reinterpret_cast<const VorRxStatusDataMessage*>(p);
|
||||
on_vor_status(message);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::vor_rx
|
||||
|
||||
#endif // __UI_VOR_RX_H__
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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; if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_vor_tx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::vor_tx {
|
||||
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<VorTxView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::vor_tx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_vor_tx.application_information"), used)) application_information_t _application_information_vor_tx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::vor_tx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "VOR TX",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xC0,
|
||||
0x03,
|
||||
0x80,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_vor_tx */ {'P', 'V', 'T', 'X'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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; if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_vor_tx.hpp"
|
||||
|
||||
#include "string_format.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::vor_tx {
|
||||
|
||||
VorTxView::VorTxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
add_children({&labels,
|
||||
&field_radial,
|
||||
&text_radial_unit,
|
||||
&check_ident,
|
||||
&button_ident_text,
|
||||
&text_status,
|
||||
&tx_view});
|
||||
|
||||
field_radial.set_value(radial_);
|
||||
field_radial.on_change = [this](int32_t v) {
|
||||
radial_ = v;
|
||||
if (transmitting_) {
|
||||
update_config();
|
||||
}
|
||||
};
|
||||
|
||||
check_ident.set_value(ident_);
|
||||
check_ident.on_select = [this](Checkbox&, bool v) {
|
||||
ident_ = v;
|
||||
if (transmitting_) {
|
||||
update_config();
|
||||
}
|
||||
};
|
||||
|
||||
button_ident_text.set_text(ident_text_);
|
||||
button_ident_text.on_select = [this](Button&) {
|
||||
text_prompt(nav_, ident_text_, 7, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string&) {
|
||||
button_ident_text.set_text(ident_text_);
|
||||
if (transmitting_) {
|
||||
update_config();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
if (tx_acknowledged_) {
|
||||
start_tx();
|
||||
return;
|
||||
}
|
||||
nav_.display_modal(
|
||||
"TX WARNING",
|
||||
"VOR is an aviation nav\nband! TX only into a\ndummy load.",
|
||||
YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice) {
|
||||
tx_acknowledged_ = true;
|
||||
start_tx();
|
||||
} else {
|
||||
tx_view.set_transmitting(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
stop_tx();
|
||||
};
|
||||
}
|
||||
|
||||
VorTxView::~VorTxView() {
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void VorTxView::focus() {
|
||||
tx_view.focus();
|
||||
}
|
||||
|
||||
void VorTxView::update_config() {
|
||||
baseband::set_vor_tx_config(static_cast<uint16_t>(radial_), ident_, ident_text_);
|
||||
}
|
||||
|
||||
void VorTxView::start_tx() {
|
||||
transmitter_model.set_sampling_rate(1536000);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
update_config();
|
||||
transmitting_ = true;
|
||||
tx_view.set_transmitting(true);
|
||||
text_status.set("TX radial " + to_string_dec_uint(radial_));
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void VorTxView::stop_tx() {
|
||||
transmitter_model.disable();
|
||||
transmitting_ = false;
|
||||
tx_view.set_transmitting(false);
|
||||
text_status.set("Idle");
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::vor_tx
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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; if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __UI_VOR_TX_H__
|
||||
#define __UI_VOR_TX_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
namespace ui::external_app::vor_tx {
|
||||
|
||||
class VorTxView : public View {
|
||||
public:
|
||||
VorTxView(NavigationView& nav);
|
||||
~VorTxView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "VOR TX"; }
|
||||
|
||||
private:
|
||||
void start_tx();
|
||||
void stop_tx();
|
||||
void update_config();
|
||||
|
||||
NavigationView& nav_;
|
||||
bool transmitting_{false};
|
||||
bool tx_acknowledged_{false};
|
||||
|
||||
uint32_t radial_{0};
|
||||
bool ident_{true};
|
||||
std::string ident_text_{"VOR"};
|
||||
|
||||
TxRadioState radio_state_{
|
||||
113'500'000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
1536000 /* sampling rate */
|
||||
};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_vor",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"radial"sv, &radial_},
|
||||
{"ident"sv, &ident_},
|
||||
{"ident_text"sv, &ident_text_},
|
||||
}};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(1)}, "Radial:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(5)}, "Ident:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(8)}, "Status:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(10)}, "Lab use: set any freq &", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(11)}, "TX into a dummy load.", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
NumberField field_radial{
|
||||
{UI_POS_X(8), UI_POS_Y(1)},
|
||||
3,
|
||||
{0, 359},
|
||||
1,
|
||||
'0'};
|
||||
|
||||
Text text_radial_unit{
|
||||
{UI_POS_X(12), UI_POS_Y(1), UI_POS_WIDTH(4), UI_POS_HEIGHT(1)},
|
||||
"deg"};
|
||||
|
||||
Checkbox check_ident{
|
||||
{UI_POS_X(0), UI_POS_Y(3)},
|
||||
13,
|
||||
"CW identifier",
|
||||
true};
|
||||
|
||||
Button button_ident_text{
|
||||
{UI_POS_X(8), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)},
|
||||
"VOR"};
|
||||
|
||||
Text text_status{
|
||||
{UI_POS_X(8), UI_POS_Y(8), UI_POS_WIDTH_REMAINING(8), UI_POS_HEIGHT(1)},
|
||||
"Idle"};
|
||||
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
8333,
|
||||
1750000};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::vor_tx
|
||||
|
||||
#endif // __UI_VOR_TX_H__
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -129,6 +131,8 @@ void MAX2839::init() {
|
||||
|
||||
_map.r.rssi_vga.RSSI_MODE = 1; /* RSSI independent of RXHP */
|
||||
|
||||
_map.r.rssi_vga.RSSI_INPUT = 0; /* Measure RSSI at VGA Output (stronger signal) */
|
||||
|
||||
/*
|
||||
* There are two LNA band settings, but we only use one of them.
|
||||
* Switching to the other one doesn't make the overall spectrum any
|
||||
@@ -150,8 +154,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)
|
||||
@@ -301,9 +307,8 @@ void MAX2839::configure_rx_gain() {
|
||||
}
|
||||
|
||||
_map.r.lpf_vga_2.L = lna::gain_ordinal(lna_gain);
|
||||
_dirty[Register::RXRF_2] = 1;
|
||||
_map.r.lpf_vga_2.VGA = vga::gain_ordinal(vga_gain);
|
||||
_dirty[Register::LPF_VGA_2] = 1;
|
||||
_dirty[Register::LPF_VGA_2] = 1; /* both L (LNA) and VGA live in LPF_VGA_2 */
|
||||
flush();
|
||||
}
|
||||
|
||||
@@ -390,8 +395,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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -148,10 +151,6 @@ void init() {
|
||||
/* PRALINE uses MAX2831 transceiver */
|
||||
second_if = (max283x::MAX283x*)&second_if_max2831;
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
gpio_r9_not_ant_pwr.write(1);
|
||||
gpio_r9_not_ant_pwr.output();
|
||||
}
|
||||
second_if = hackrf_r9
|
||||
? (max283x::MAX283x*)&second_if_max2839
|
||||
: (max283x::MAX283x*)&second_if_max2837;
|
||||
@@ -280,9 +279,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,20 +389,26 @@ 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);
|
||||
rf_path.set_ant_bias(on);
|
||||
} else {
|
||||
first_if.set_gpo1(on ? 0 : 1);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool get_mixer_invert() {
|
||||
return mixer_invert;
|
||||
}
|
||||
|
||||
bool get_baseband_invert() {
|
||||
return baseband_invert;
|
||||
}
|
||||
|
||||
void set_tx_max283x_iq_phase_calibration(const size_t v) {
|
||||
second_if->set_tx_LO_iq_phase_calibration(v);
|
||||
}
|
||||
@@ -436,8 +441,8 @@ void disable() {
|
||||
first_if.disable();
|
||||
set_rf_amp(false);
|
||||
|
||||
led_rx.off();
|
||||
led_tx.off();
|
||||
led_rx.setInactive();
|
||||
led_tx.setInactive();
|
||||
}
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -619,4 +624,4 @@ uint32_t register_read(const size_t register_number) {
|
||||
|
||||
} /* namespace debug */
|
||||
|
||||
} /* namespace radio */
|
||||
} /* namespace radio */
|
||||
|
||||
@@ -54,6 +54,8 @@ void set_baseband_filter_bandwidth_rx(const uint32_t bandwidth_minimum);
|
||||
void set_baseband_filter_bandwidth_tx(const uint32_t bandwidth_minimum);
|
||||
void set_baseband_rate(const uint32_t rate);
|
||||
void set_antenna_bias(const bool on);
|
||||
bool get_mixer_invert();
|
||||
bool get_baseband_invert();
|
||||
void set_tx_max283x_iq_phase_calibration(const size_t v);
|
||||
void set_rx_max283x_iq_phase_calibration(const size_t v);
|
||||
|
||||
|
||||
@@ -21,230 +21,26 @@
|
||||
|
||||
#include "rf_path.hpp"
|
||||
#include "platform.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "hackrf_gpio.hpp"
|
||||
using namespace hackrf::one;
|
||||
|
||||
#include "utility.hpp"
|
||||
#include "gpio.hpp"
|
||||
using namespace gpio_control;
|
||||
|
||||
namespace rf {
|
||||
namespace path {
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef PRALINE
|
||||
/* PRALINE uses a simplified RF path with only 5 control signals.
|
||||
* The RF path architecture is completely different from HackRF One.
|
||||
*/
|
||||
struct PralineConfig {
|
||||
bool tx_en;
|
||||
bool mix_bypass; // RF path mixer bypass (GPIO3[2])
|
||||
bool lpf_en;
|
||||
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
|
||||
gpio_lpf_enable.write(lpf_en);
|
||||
gpio_rf_amp_enable.write(rf_amp_en);
|
||||
gpio_ant_bias_disable.write(ant_bias_en_n);
|
||||
}
|
||||
};
|
||||
|
||||
#else
|
||||
/* HackRF One uses 11 GPIOs for RF path control */
|
||||
using GPIOs = std::array<GPIO, 11>;
|
||||
|
||||
/* TODO: ARM GCC 4.8 2014q3 doesn't like this array inside struct Config.
|
||||
* No idea why.
|
||||
*/
|
||||
constexpr GPIOs gpios{
|
||||
gpio_mix_bypass,
|
||||
gpio_not_mix_bypass,
|
||||
gpio_tx_mix_bp,
|
||||
gpio_rx_mix_bp,
|
||||
gpio_hp,
|
||||
gpio_lp,
|
||||
gpio_amp_bypass,
|
||||
gpio_tx_amp,
|
||||
gpio_not_tx_amp_pwr,
|
||||
gpio_rx_amp,
|
||||
gpio_not_rx_amp_pwr,
|
||||
};
|
||||
|
||||
/* HackRF One Config struct - not used on PRALINE */
|
||||
struct Config {
|
||||
using base_type = uint16_t;
|
||||
|
||||
union {
|
||||
struct {
|
||||
bool tx : 1;
|
||||
bool rx : 1;
|
||||
bool mix_bypass : 1;
|
||||
bool not_mix_bypass : 1;
|
||||
bool tx_mix_bp : 1;
|
||||
bool rx_mix_bp : 1;
|
||||
bool hp : 1;
|
||||
bool lp : 1;
|
||||
bool amp_bypass : 1;
|
||||
bool tx_amp : 1;
|
||||
bool not_tx_amp : 1;
|
||||
bool rx_amp : 1;
|
||||
bool not_rx_amp : 1;
|
||||
};
|
||||
base_type w;
|
||||
};
|
||||
|
||||
constexpr Config(
|
||||
const Direction direction,
|
||||
const Band band,
|
||||
const bool amplify)
|
||||
: tx(direction == Direction::Transmit),
|
||||
rx(direction == Direction::Receive),
|
||||
mix_bypass(band == Band::Mid),
|
||||
not_mix_bypass(band != Band::Mid),
|
||||
tx_mix_bp((direction == Direction::Transmit) && (band == Band::Mid)),
|
||||
rx_mix_bp((direction == Direction::Receive) && (band == Band::Mid)),
|
||||
hp(band == Band::High),
|
||||
lp(band == Band::Low),
|
||||
amp_bypass(!amplify),
|
||||
tx_amp((direction == Direction::Transmit) && amplify),
|
||||
not_tx_amp(!tx_amp),
|
||||
rx_amp((direction == Direction::Receive) && amplify),
|
||||
not_rx_amp(!rx_amp) {
|
||||
}
|
||||
|
||||
constexpr Config()
|
||||
: Config(Direction::Receive, Band::Mid, false) {
|
||||
}
|
||||
|
||||
constexpr Config(
|
||||
const base_type w)
|
||||
: w(w) {
|
||||
}
|
||||
|
||||
constexpr Config operator^(const Config& r) const {
|
||||
return w ^ r.w;
|
||||
}
|
||||
|
||||
constexpr Config operator&(const Config& r) const {
|
||||
return w & r.w;
|
||||
}
|
||||
|
||||
constexpr bool operator[](const size_t n) const {
|
||||
return (w >> n) & 1;
|
||||
}
|
||||
|
||||
static void gpio_init() {
|
||||
if (hackrf_r9) {
|
||||
gpio_r9_rx.output();
|
||||
} else {
|
||||
gpio_og_tx.output();
|
||||
gpio_og_rx.output();
|
||||
}
|
||||
for (auto gpio : gpios) {
|
||||
gpio.output();
|
||||
}
|
||||
}
|
||||
|
||||
void apply() const {
|
||||
/* NOTE: Assumes order in gpios[] and Config bitfield match,
|
||||
* after the 'tx' and 'rx' fields which are handled specially. */
|
||||
for (size_t n = 0; n < gpios.size() + 2; n++) {
|
||||
bool value = (*this)[n];
|
||||
switch (n) {
|
||||
case 0:
|
||||
if (!hackrf_r9) {
|
||||
gpio_og_tx.write(value);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (hackrf_r9) {
|
||||
gpio_r9_rx.write(value);
|
||||
} else {
|
||||
gpio_og_rx.write(value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
gpios[n - 2].write(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* HackRF One config table - not used on PRALINE */
|
||||
using ConfigAmp = std::array<Config, 2>;
|
||||
using ConfigDirection = std::array<ConfigAmp, 2>;
|
||||
using ConfigBand = std::array<ConfigDirection, 3>;
|
||||
|
||||
constexpr ConfigAmp config_amp(
|
||||
const Direction direction,
|
||||
const Band band) {
|
||||
return {{
|
||||
Config(direction, band, false),
|
||||
Config(direction, band, true),
|
||||
}};
|
||||
}
|
||||
|
||||
constexpr ConfigDirection config_rx_tx(
|
||||
const Band band) {
|
||||
return {
|
||||
config_amp(Direction::Receive, band),
|
||||
config_amp(Direction::Transmit, band),
|
||||
};
|
||||
}
|
||||
|
||||
constexpr ConfigBand config_band() {
|
||||
return {
|
||||
config_rx_tx(Band::Low),
|
||||
config_rx_tx(Band::Mid),
|
||||
config_rx_tx(Band::High),
|
||||
};
|
||||
}
|
||||
|
||||
constexpr ConfigBand config_table = config_band();
|
||||
|
||||
static_assert(sizeof(config_table) == sizeof(Config::base_type) * 3 * 2 * 2, "rf path config table unexpected size");
|
||||
|
||||
constexpr Config get_config(
|
||||
const Direction direction,
|
||||
const Band band,
|
||||
const bool amplify) {
|
||||
return config_table[toUType(band)][toUType(direction)][amplify ? 1 : 0];
|
||||
}
|
||||
#endif /* PRALINE */
|
||||
|
||||
} /* namespace */
|
||||
|
||||
void Path::init() {
|
||||
/* Set safe initial default states */
|
||||
direction = Direction::Receive;
|
||||
rf_amp_en = false;
|
||||
ant_bias_en = false;
|
||||
|
||||
#ifdef PRALINE
|
||||
PralineConfig::gpio_init();
|
||||
/* Set safe initial state: RX mode, mixer enabled, LPF on, amp off, no bias */
|
||||
PralineConfig config = {
|
||||
.tx_en = false,
|
||||
.mix_bypass = false, // RF path mixer bypass (GPIO3[2])
|
||||
.lpf_en = true, // LPF on for low band
|
||||
.rf_amp_en = false, // Amp off
|
||||
.ant_bias_en_n = true // Bias off (inverted)
|
||||
};
|
||||
config.apply();
|
||||
band = Band::Low;
|
||||
#else
|
||||
update();
|
||||
Config::gpio_init();
|
||||
band = Band::Mid;
|
||||
#endif
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void Path::set_direction(const Direction new_direction) {
|
||||
@@ -254,64 +50,82 @@ void Path::set_direction(const Direction new_direction) {
|
||||
|
||||
void Path::set_band(const Band new_band) {
|
||||
band = new_band;
|
||||
_band = new_band;
|
||||
update();
|
||||
}
|
||||
|
||||
void Path::set_rf_amp(const bool new_rf_amp) {
|
||||
rf_amp = new_rf_amp;
|
||||
rf_amp_en = new_rf_amp;
|
||||
update();
|
||||
}
|
||||
|
||||
void Path::set_ant_bias(const bool new_ant_bias) {
|
||||
ant_bias_en = new_ant_bias;
|
||||
update();
|
||||
}
|
||||
|
||||
bool Path::get_ant_bias() const {
|
||||
return ant_bias_en;
|
||||
}
|
||||
|
||||
void Path::update() {
|
||||
/* 0 ^ 0 => 0 & 0 = 0 ^ 0 = 0 (no change)
|
||||
* 0 ^ 1 => 1 & 0 = 0 ^ 0 = 0 (ignore change to 1)
|
||||
* 1 ^ 0 => 1 & 1 = 1 ^ 1 = 0 (allow change to 0)
|
||||
* 1 ^ 1 => 0 & 1 = 0 ^ 1 = 1 (no change) */
|
||||
const bool is_tx = (direction == Direction::Transmit);
|
||||
|
||||
#ifdef PRALINE
|
||||
/* PRALINE RF path control:
|
||||
* - tx_en: 1 for TX, 0 for RX
|
||||
* - mix_bypass: 0 to enable RF path mixer bypass (GPIO3[2])
|
||||
* - lpf_en: 1 for low band (< 2.4 GHz), 0 for high band
|
||||
* - rf_amp_en: 1 to enable RF amplifier
|
||||
* - ant_bias_en_n: 0 to enable antenna bias (inverted)
|
||||
*/
|
||||
// const Config changed = _config ^ config_next;
|
||||
// const Config turned_off = _config & changed;
|
||||
// PRALINE specific RF path control directly applied to pins.
|
||||
// Active-low pin inversion is handled internally inside setState().
|
||||
|
||||
PralineConfig config;
|
||||
tx_enable.setState(is_tx);
|
||||
|
||||
/* In transition, ignore the bits that are turning on. So this transition phase
|
||||
* only turns off signals. It doesn't turn on signals.
|
||||
*/
|
||||
// const Config transition_config = _config ^ turned_off;
|
||||
// update_signals(transition_config);
|
||||
// On the PRALINE board, the mixer is used ONLY on the Low band.
|
||||
// Since setState() internally handles the active-low (MIX_ENABLE_N) hardware inversion,
|
||||
// we simply pass 'true' to enable the mixer on Low band, and 'false' for Mid/High bands.
|
||||
|
||||
config.tx_en = (direction == Direction::Transmit);
|
||||
mix_bypass.setState(band == Band::Low);
|
||||
|
||||
// RF path mixer bypass: 0=enabled, 1=bypassed
|
||||
config.mix_bypass = (band == Band::Mid);
|
||||
|
||||
/* Move to the final state by turning on required signals. */
|
||||
/* LPF for low band */
|
||||
|
||||
config.lpf_en = (band == Band::Low);
|
||||
|
||||
/* RF amp when amplification requested */
|
||||
config.rf_amp_en = rf_amp;
|
||||
|
||||
/* Antenna bias off by default */
|
||||
config.ant_bias_en_n = true;
|
||||
|
||||
config.apply();
|
||||
lpf.setState(band == Band::Low);
|
||||
rf_amp_enable.setState(rf_amp_en);
|
||||
ant_bias.setState(ant_bias_en);
|
||||
|
||||
#else
|
||||
/* HackRF One RF path control */
|
||||
const auto config = get_config(direction, band, rf_amp);
|
||||
config.apply();
|
||||
|
||||
const bool is_rx = (direction == Direction::Receive);
|
||||
|
||||
// HackRF One (OG & R9) RF path control
|
||||
const bool mix_bypass_en = (band == Band::Mid);
|
||||
const bool amplify = rf_amp_en;
|
||||
|
||||
// Primary TX/RX routing switches
|
||||
if (!hackrf_r9) {
|
||||
og_tx.setState(is_tx);
|
||||
}
|
||||
|
||||
if (hackrf_r9) {
|
||||
r9_rx.setState(is_rx); // Single pin handles directional switching on R9
|
||||
} else {
|
||||
og_rx.setState(is_rx);
|
||||
}
|
||||
|
||||
// RF path switch configuration matrix
|
||||
rx_mix_bypass.setState(mix_bypass_en);
|
||||
tx_mix_bp.setState(is_tx && mix_bypass_en);
|
||||
rx_mix_bp.setState(is_rx && mix_bypass_en);
|
||||
|
||||
hpf.setState(band == Band::High);
|
||||
lpf.setState(band == Band::Low);
|
||||
|
||||
amp_bypass.setState(!amplify);
|
||||
tx_amp.setState(is_tx && amplify);
|
||||
rx_amp.setState(is_rx && amplify);
|
||||
|
||||
tx_mix_bypass.setState(mix_bypass_en);
|
||||
tx_amp_pwr.setState(is_tx && amplify);
|
||||
rx_amp_pwr.setState(is_rx && amplify);
|
||||
|
||||
if (hackrf_r9) {
|
||||
ant_bias.setState(ant_bias_en);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace path
|
||||
} // namespace rf
|
||||
} // namespace rf
|
||||
@@ -41,15 +41,15 @@ namespace path {
|
||||
|
||||
#ifdef PRALINE
|
||||
/* PRALINE: MAX2831 direct path is 2320-2740 MHz */
|
||||
constexpr FrequencyRange band_low{0, 2320'000'000};
|
||||
constexpr FrequencyRange band_high{2740'000'000, 7250'000'000};
|
||||
constexpr FrequencyRange band_mid{band_low.maximum, band_high.minimum};
|
||||
constexpr Frequency TRANSITION = 2320'000'000;
|
||||
#else
|
||||
/* HackRF One: Original band boundaries */
|
||||
constexpr FrequencyRange band_low{0, 2170'000'000};
|
||||
constexpr Frequency TRANSITION = 2170'000'000;
|
||||
#endif
|
||||
|
||||
constexpr FrequencyRange band_low{0, TRANSITION};
|
||||
constexpr FrequencyRange band_high{2740'000'000, 7250'000'000};
|
||||
constexpr FrequencyRange band_mid{band_low.maximum, band_high.minimum};
|
||||
#endif
|
||||
|
||||
enum class Band {
|
||||
/* Zero-based, used as index into frequency_bands table */
|
||||
@@ -61,21 +61,20 @@ enum class Band {
|
||||
class Path {
|
||||
public:
|
||||
void init();
|
||||
|
||||
void set_direction(const Direction direction);
|
||||
void set_band(const Band band);
|
||||
void set_rf_amp(const bool rf_amp);
|
||||
|
||||
Band get_band() const { return _band; } //_band is used solely for debugging purposes.
|
||||
void set_ant_bias(const bool ant_bias);
|
||||
bool get_ant_bias() const;
|
||||
Band get_band() const { return band; }
|
||||
|
||||
private:
|
||||
Direction direction{Direction::Receive};
|
||||
Band band{Band::Mid};
|
||||
bool rf_amp{false};
|
||||
bool rf_amp_en{false};
|
||||
bool ant_bias_en{false};
|
||||
|
||||
void update();
|
||||
|
||||
Band _band{Band::Mid}; //_band is solely used of debugging purposes
|
||||
};
|
||||
|
||||
} // namespace path
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
#include "lpc43xx_cpp.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include <ctime>
|
||||
|
||||
namespace rtc_time {
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -72,19 +74,31 @@ void TransmitterModel::set_channel_bandwidth(uint32_t v) {
|
||||
}
|
||||
|
||||
uint8_t TransmitterModel::tx_gain() const {
|
||||
if (persistent_memory::config_tx_disabled()) {
|
||||
return 0;
|
||||
}
|
||||
return std::min(settings_.tx_gain_db, portapack::persistent_memory::config_tx_gain_max_db());
|
||||
}
|
||||
|
||||
void TransmitterModel::set_tx_gain(uint8_t v_db) {
|
||||
settings_.tx_gain_db = v_db;
|
||||
uint8_t newgain = v_db;
|
||||
if (persistent_memory::config_tx_disabled()) {
|
||||
newgain = 0;
|
||||
} else if (v_db > portapack::persistent_memory::config_tx_gain_max_db()) {
|
||||
newgain = portapack::persistent_memory::config_tx_gain_max_db();
|
||||
}
|
||||
settings_.tx_gain_db = newgain;
|
||||
update_tx_gain();
|
||||
}
|
||||
|
||||
bool TransmitterModel::rf_amp() const {
|
||||
return settings_.rf_amp && !portapack::persistent_memory::config_tx_amp_disabled();
|
||||
return settings_.rf_amp && !portapack::persistent_memory::config_tx_amp_disabled() && !portapack::persistent_memory::config_tx_disabled();
|
||||
}
|
||||
|
||||
void TransmitterModel::set_rf_amp(bool enabled) {
|
||||
if (enabled && (portapack::persistent_memory::config_tx_amp_disabled() || portapack::persistent_memory::config_tx_disabled())) {
|
||||
return;
|
||||
}
|
||||
settings_.rf_amp = enabled;
|
||||
update_rf_amp();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <cstddef>
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "hackrf_hal.hpp"
|
||||
#include "max2837.hpp"
|
||||
#include "message.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
@@ -40,8 +41,9 @@ class TransmitterModel {
|
||||
uint32_t baseband_bandwidth = max283x::filter::bandwidth_minimum;
|
||||
uint32_t sampling_rate = 3'072'000;
|
||||
uint32_t channel_bandwidth = 1;
|
||||
/* 35 should give approx 1m transmission range. */
|
||||
uint8_t tx_gain_db = 35;
|
||||
/* Platform default chosen to avoid PRALINE MAX2831 overdrive while
|
||||
* preserving legacy HackRF One behavior. */
|
||||
uint8_t tx_gain_db = hackrf::one::default_tx_gain_db;
|
||||
bool rf_amp = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include "rtc_time.hpp"
|
||||
#include "sd_card.hpp"
|
||||
#include <algorithm>
|
||||
#include "ui_external_items_menu_loader.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -61,7 +60,6 @@ BtnGridView::BtnGridView(
|
||||
}
|
||||
|
||||
BtnGridView::~BtnGridView() {
|
||||
ExternalItemsMenuLoader::unload_external_items();
|
||||
}
|
||||
|
||||
void BtnGridView::set_max_rows(int rows) {
|
||||
@@ -89,7 +87,6 @@ void BtnGridView::set_parent_rect(const Rect new_parent_rect) {
|
||||
remove_child(item.get());
|
||||
|
||||
menu_item_views.clear();
|
||||
menu_item_views.shrink_to_fit();
|
||||
}
|
||||
|
||||
button_w = screen_width / rows_;
|
||||
@@ -138,17 +135,15 @@ void BtnGridView::set_arrow_down_enabled(bool enabled) {
|
||||
};
|
||||
|
||||
void BtnGridView::clear() {
|
||||
// clear vector and release memory, not using swap since it's causing capture to glitch/fault
|
||||
// clear vector, not using swap since it's causing capture to glitch/fault
|
||||
menu_items.clear();
|
||||
menu_items.shrink_to_fit();
|
||||
|
||||
// TODO(u-foka): Clean up my mess, move this somewhere to clear memory when the view is not visible, but not to be confused with clearing the menu items...
|
||||
for (auto& item : menu_item_views)
|
||||
remove_child(item.get());
|
||||
|
||||
// clear vector and release memory, not using swap since it's causing capture to glitch/fault
|
||||
// clear vector, not using swap since it's causing capture to glitch/fault
|
||||
menu_item_views.clear();
|
||||
menu_item_views.shrink_to_fit();
|
||||
}
|
||||
|
||||
void BtnGridView::add_items(std::initializer_list<GridItem> new_items, bool inhibit_update) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include "ui_menu.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include <string_view>
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -29,57 +30,112 @@ namespace ui {
|
||||
|
||||
void MenuItemView::set_item(MenuItem* item_) {
|
||||
item = item_;
|
||||
scroll_offset = 0;
|
||||
can_scroll = false;
|
||||
}
|
||||
|
||||
void MenuItemView::highlight() {
|
||||
set_highlighted(true);
|
||||
scroll_offset = 0;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void MenuItemView::unhighlight() {
|
||||
set_highlighted(false);
|
||||
scroll_offset = 0;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void MenuItemView::paint(Painter& painter) {
|
||||
Coord offset_x{};
|
||||
void MenuItemView::set_scroll_offset(size_t offset) {
|
||||
if (can_scroll && scroll_offset != offset) {
|
||||
scroll_offset = offset;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void MenuItemView::paint(Painter& painter) {
|
||||
if (!item) return;
|
||||
|
||||
const auto r = screen_rect();
|
||||
|
||||
const auto rect = screen_rect();
|
||||
const auto paint_style = (highlighted() && (parent()->has_focus() || keep_highlight)) ? style().invert() : style();
|
||||
|
||||
const auto font_height = paint_style.font.line_height();
|
||||
const int char_width = paint_style.font.char_width();
|
||||
const int line_height = paint_style.font.line_height();
|
||||
|
||||
if (char_width == 0 || line_height == 0) return;
|
||||
|
||||
const int margin_x = char_width / 2;
|
||||
|
||||
ui::Color final_item_color = (highlighted() && (parent()->has_focus() || keep_highlight)) ? paint_style.foreground : item->color;
|
||||
ui::Color final_bg_color = (highlighted() && (parent()->has_focus() || keep_highlight)) ? item->color : paint_style.background;
|
||||
|
||||
if (final_item_color.v == final_bg_color.v) final_item_color = paint_style.foreground;
|
||||
|
||||
painter.fill_rectangle(
|
||||
r,
|
||||
final_bg_color);
|
||||
painter.fill_rectangle(rect, final_bg_color);
|
||||
|
||||
Coord offset_x = 0;
|
||||
|
||||
if (item->bitmap) {
|
||||
painter.draw_bitmap(
|
||||
{r.location().x() + 4, r.location().y() + 4},
|
||||
{rect.location().x() + 4, rect.location().y() + 4},
|
||||
*item->bitmap,
|
||||
final_item_color,
|
||||
final_bg_color);
|
||||
offset_x = 26;
|
||||
} else
|
||||
} else {
|
||||
offset_x = 0;
|
||||
}
|
||||
|
||||
Style text_style{
|
||||
.font = paint_style.font,
|
||||
.background = final_bg_color,
|
||||
.foreground = final_item_color};
|
||||
Style text_style{.font = paint_style.font, .background = final_bg_color, .foreground = final_item_color};
|
||||
|
||||
painter.draw_string(
|
||||
{r.location().x() + offset_x, r.location().y() + (r.size().height() - font_height) / 2},
|
||||
text_style,
|
||||
item->text);
|
||||
std::string_view full_text = item->text;
|
||||
std::string_view file_name = full_text;
|
||||
std::string_view file_size_text = "";
|
||||
|
||||
auto tab_pos = full_text.find('\t');
|
||||
if (tab_pos != std::string_view::npos) {
|
||||
file_size_text = full_text.substr(tab_pos + 1);
|
||||
file_name = full_text.substr(0, tab_pos);
|
||||
}
|
||||
|
||||
int available_width_px = rect.width() - offset_x;
|
||||
if (available_width_px <= 0) return;
|
||||
|
||||
size_t max_name_chars = available_width_px / char_width;
|
||||
if (max_name_chars == 0) return;
|
||||
|
||||
if (!file_size_text.empty() && max_name_chars > file_size_text.length()) {
|
||||
max_name_chars = max_name_chars - file_size_text.length() - 1;
|
||||
}
|
||||
|
||||
if (max_name_chars == 0) return;
|
||||
|
||||
can_scroll = (file_name.length() > max_name_chars);
|
||||
|
||||
std::string_view display_name = file_name;
|
||||
if (file_name.length() > max_name_chars) {
|
||||
if (highlighted()) {
|
||||
size_t max_scroll = file_name.length() - max_name_chars;
|
||||
size_t actual_offset = scroll_offset % (max_scroll + 1);
|
||||
display_name = file_name.substr(actual_offset, max_name_chars);
|
||||
} else {
|
||||
display_name = file_name.substr(0, max_name_chars);
|
||||
}
|
||||
}
|
||||
|
||||
Coord text_y = rect.location().y() + (rect.height() - line_height) / 2;
|
||||
|
||||
painter.draw_string({rect.location().x() + offset_x, text_y}, text_style, display_name);
|
||||
|
||||
if (!file_size_text.empty()) {
|
||||
int file_size_width = static_cast<int>(file_size_text.length()) * char_width;
|
||||
int file_size_x = rect.width() - file_size_width - margin_x;
|
||||
|
||||
// Csak akkor rajzoljuk ki, ha pozitív koordinátára esik és nem takarja el az ikont/nevet
|
||||
if (file_size_x > offset_x) {
|
||||
painter.draw_string({rect.location().x() + static_cast<Coord>(file_size_x), text_y}, text_style, file_size_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* MenuView **************************************************************/
|
||||
@@ -134,6 +190,20 @@ void MenuView::set_parent_rect(const Rect new_parent_rect) {
|
||||
update_items();
|
||||
}
|
||||
|
||||
void MenuView::increment_scroll() {
|
||||
if (menu_items.empty()) return;
|
||||
const size_t view_index = highlighted_item - offset;
|
||||
if (view_index >= menu_item_views.size()) {
|
||||
return;
|
||||
}
|
||||
// The MenuView checks if the currently highlighted item needs scrolling
|
||||
scroll_offset++;
|
||||
auto* view = item_view(view_index);
|
||||
if (view) {
|
||||
view->set_scroll_offset(scroll_offset);
|
||||
}
|
||||
}
|
||||
|
||||
void MenuView::on_tick_second() {
|
||||
if (more && blink)
|
||||
arrow_more.set_foreground(Theme::getInstance()->bg_darkest->foreground);
|
||||
@@ -141,8 +211,9 @@ void MenuView::on_tick_second() {
|
||||
arrow_more.set_foreground(Theme::getInstance()->bg_darkest->background);
|
||||
|
||||
blink = !blink;
|
||||
|
||||
arrow_more.set_dirty();
|
||||
|
||||
increment_scroll();
|
||||
}
|
||||
|
||||
void MenuView::clear() {
|
||||
@@ -153,6 +224,7 @@ void MenuView::clear() {
|
||||
menu_items.shrink_to_fit();
|
||||
highlighted_item = 0;
|
||||
offset = 0;
|
||||
reset_scroll();
|
||||
}
|
||||
|
||||
size_t MenuView::item_count() const {
|
||||
@@ -225,6 +297,8 @@ bool MenuView::set_highlighted(int32_t new_value) {
|
||||
item_view(highlighted_item - offset)->highlight();
|
||||
}
|
||||
|
||||
reset_scroll();
|
||||
|
||||
if (on_highlight)
|
||||
on_highlight();
|
||||
|
||||
|
||||
@@ -67,9 +67,13 @@ class MenuItemView : public Widget {
|
||||
void highlight();
|
||||
void unhighlight();
|
||||
|
||||
void set_scroll_offset(size_t offset);
|
||||
|
||||
private:
|
||||
MenuItem* item{nullptr};
|
||||
bool keep_highlight = false;
|
||||
size_t scroll_offset{0};
|
||||
bool can_scroll{false};
|
||||
};
|
||||
|
||||
class MenuView : public View {
|
||||
@@ -108,6 +112,10 @@ class MenuView : public View {
|
||||
void update_items();
|
||||
void on_tick_second();
|
||||
|
||||
// SCROLLING
|
||||
void increment_scroll();
|
||||
inline void reset_scroll() { scroll_offset = 0; }
|
||||
|
||||
bool keep_highlight{false};
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
@@ -126,6 +134,8 @@ class MenuView : public View {
|
||||
size_t displayed_max{0};
|
||||
size_t highlighted_item{0};
|
||||
size_t offset{0};
|
||||
|
||||
size_t scroll_offset{0};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -26,10 +26,7 @@
|
||||
#include "string_format.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
#ifndef PRALINE
|
||||
#include "audio.hpp"
|
||||
#endif
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
@@ -595,18 +592,13 @@ AudioVolumeField::AudioVolumeField(
|
||||
/* fill char */ ' '} {
|
||||
set_value(receiver_model.normalized_headphone_volume());
|
||||
|
||||
#ifdef PRALINE
|
||||
on_change = [](int32_t v) {
|
||||
receiver_model.set_normalized_headphone_volume(v);
|
||||
#else
|
||||
on_change = [](int32_t vol) {
|
||||
// don't call receiver model, because this widget shuld be able to handle volume settinsg from any app, regardless of the receiver model's enables state. like the tx apps should be able to set volume too.
|
||||
// this is identical to the receiver model's method
|
||||
// Don't call receiver_model here so volume can be adjusted even when ReceiverModel is disabled (e.g. TX apps/games).
|
||||
// The dB mapping mirrors ReceiverModel::set_normalized_headphone_volume().
|
||||
uint8_t v = clip<uint8_t>(vol, 0, 99);
|
||||
auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max;
|
||||
persistent_memory::set_headphone_volume(new_volume);
|
||||
audio::headphone::set_volume(new_volume);
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,13 @@ using namespace portapack;
|
||||
namespace ui {
|
||||
|
||||
/* Gets a style indicating total TX gain level. */
|
||||
static const Style* get_style_for_gain(uint8_t tot_gain) {
|
||||
static const Style* get_style_for_gain(uint8_t tot_gain, uint8_t gain) {
|
||||
if (persistent_memory::config_tx_disabled()) {
|
||||
return Theme::getInstance()->fg_dark; // disabled color
|
||||
}
|
||||
if (gain == persistent_memory::config_tx_gain_max_db() && gain != max2837::tx::gain_db_range.maximum) { // only if limited
|
||||
return Theme::getInstance()->fg_cyan;
|
||||
}
|
||||
if (tot_gain > POWER_THRESHOLD_HIGH) return Theme::getInstance()->fg_red;
|
||||
|
||||
if (tot_gain > POWER_THRESHOLD_MED)
|
||||
@@ -50,7 +56,21 @@ static const Style* get_style_for_gain(uint8_t tot_gain) {
|
||||
|
||||
return nullptr; // Uses default.
|
||||
}
|
||||
static const Style* get_style_for_amp(uint8_t tot_gain) {
|
||||
if (persistent_memory::config_tx_disabled() || persistent_memory::config_tx_amp_disabled()) {
|
||||
return Theme::getInstance()->fg_dark; // disabled color
|
||||
}
|
||||
|
||||
if (tot_gain > POWER_THRESHOLD_HIGH) return Theme::getInstance()->fg_red;
|
||||
|
||||
if (tot_gain > POWER_THRESHOLD_MED)
|
||||
return Theme::getInstance()->fg_orange;
|
||||
|
||||
if (tot_gain > POWER_THRESHOLD_LOW)
|
||||
return Theme::getInstance()->fg_yellow;
|
||||
|
||||
return nullptr; // Uses default.
|
||||
}
|
||||
/* TransmitterView *******************************************************/
|
||||
|
||||
void TransmitterView::paint(Painter& painter) {
|
||||
@@ -95,12 +115,12 @@ void TransmitterView::on_tx_amp_changed(bool rf_amp) {
|
||||
|
||||
void TransmitterView::update_gainlevel_styles() {
|
||||
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
|
||||
auto style = get_style_for_gain(tot_gain);
|
||||
|
||||
auto style = get_style_for_gain(tot_gain, transmitter_model.tx_gain());
|
||||
auto styleamp = get_style_for_amp(tot_gain);
|
||||
field_gain.set_style(style);
|
||||
text_gain.set_style(style);
|
||||
field_amp.set_style(style);
|
||||
text_amp.set_style(style);
|
||||
field_amp.set_style(styleamp);
|
||||
text_amp.set_style(styleamp);
|
||||
}
|
||||
|
||||
void TransmitterView::set_transmitting(const bool transmitting) {
|
||||
@@ -199,6 +219,24 @@ TransmitterView::TransmitterView(
|
||||
on_start();
|
||||
}
|
||||
};
|
||||
|
||||
if (persistent_memory::config_tx_amp_disabled() || persistent_memory::config_tx_disabled()) { // amp disabled / tx disabled
|
||||
field_amp.set_value(0);
|
||||
field_amp.set_focusable(false);
|
||||
}
|
||||
|
||||
if (persistent_memory::config_tx_disabled()) { // tx disabled
|
||||
field_gain.set_value(0);
|
||||
field_gain.set_focusable(false);
|
||||
} else {
|
||||
if (persistent_memory::config_tx_gain_max_db() < max2837::tx::gain_db_range.maximum) {
|
||||
field_gain.set_range(max2837::tx::gain_db_range.minimum, persistent_memory::config_tx_gain_max_db());
|
||||
if (field_gain.value() > persistent_memory::config_tx_gain_max_db()) {
|
||||
field_gain.set_value(persistent_memory::config_tx_gain_max_db());
|
||||
}
|
||||
}
|
||||
}
|
||||
update_gainlevel_styles();
|
||||
}
|
||||
|
||||
TransmitterView::~TransmitterView() {
|
||||
@@ -252,16 +290,34 @@ TransmitterView2::TransmitterView2(Point pos, bool short_ui) {
|
||||
update_gainlevel_styles();
|
||||
};
|
||||
|
||||
if (persistent_memory::config_tx_amp_disabled() || persistent_memory::config_tx_disabled()) { // amp disabled / tx disabled
|
||||
field_amp.set_value(0);
|
||||
field_amp.set_focusable(false);
|
||||
}
|
||||
|
||||
if (persistent_memory::config_tx_disabled()) { // tx disabled
|
||||
field_gain.set_value(0);
|
||||
field_gain.set_focusable(false);
|
||||
} else {
|
||||
if (persistent_memory::config_tx_gain_max_db() < max2837::tx::gain_db_range.maximum) {
|
||||
field_gain.set_range(max2837::tx::gain_db_range.minimum, persistent_memory::config_tx_gain_max_db());
|
||||
if (field_gain.value() > persistent_memory::config_tx_gain_max_db()) {
|
||||
field_gain.set_value(persistent_memory::config_tx_gain_max_db());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_gainlevel_styles();
|
||||
}
|
||||
|
||||
void TransmitterView2::update_gainlevel_styles() {
|
||||
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
|
||||
auto style = get_style_for_gain(tot_gain);
|
||||
auto style = get_style_for_gain(tot_gain, transmitter_model.tx_gain());
|
||||
auto styleamp = get_style_for_amp(tot_gain);
|
||||
|
||||
text_labels.set_style(style);
|
||||
field_gain.set_style(style);
|
||||
field_amp.set_style(style);
|
||||
field_amp.set_style(styleamp);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -9,13 +9,7 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* static */ std::vector<std::unique_ptr<DynamicBitmap<16, 16>>> ExternalItemsMenuLoader::bitmaps;
|
||||
|
||||
// to save ram when entering an app
|
||||
void ExternalItemsMenuLoader::unload_external_items() {
|
||||
bitmaps.clear();
|
||||
bitmaps.shrink_to_fit();
|
||||
}
|
||||
/* static */ std::vector<DynamicBitmap<16, 16>> ExternalItemsMenuLoader::bitmaps;
|
||||
|
||||
// iterates over all possible ext apps-s, and if it is runnable on the current system, it'll call the callback, and pass minimal info. used to print to console, and for autostart setting's app list. where the minimal info is enough
|
||||
// please keep in sync with load_external_items
|
||||
@@ -103,9 +97,8 @@ void ExternalItemsMenuLoader::unload_external_items() {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_external_items(app_location_t app_location, NavigationView& nav) {
|
||||
/* static */ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_external_items(app_location_t app_location, NavigationView& nav) {
|
||||
bitmaps.clear();
|
||||
bitmaps.shrink_to_fit();
|
||||
|
||||
std::vector<GridItemEx> external_apps;
|
||||
|
||||
@@ -133,8 +126,8 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
|
||||
|
||||
gridItem.color = Color((uint16_t)appInfo->icon_color);
|
||||
|
||||
auto dyn_bmp = std::make_unique<DynamicBitmap<16, 16>>(appInfo->bitmap_data);
|
||||
gridItem.bitmap = dyn_bmp->bitmap();
|
||||
auto dyn_bmp = DynamicBitmap<16, 16>{appInfo->bitmap_data};
|
||||
gridItem.bitmap = dyn_bmp.bitmap();
|
||||
bitmaps.push_back(std::move(dyn_bmp));
|
||||
|
||||
gridItem.on_select = [&nav, appInfo, i]() {
|
||||
@@ -211,8 +204,8 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
|
||||
if (versionMatches) {
|
||||
gridItem.color = Color((uint16_t)application_information.icon_color);
|
||||
|
||||
auto dyn_bmp = std::make_unique<DynamicBitmap<16, 16>>(application_information.bitmap_data);
|
||||
gridItem.bitmap = dyn_bmp->bitmap();
|
||||
auto dyn_bmp = DynamicBitmap<16, 16>{application_information.bitmap_data};
|
||||
gridItem.bitmap = dyn_bmp.bitmap();
|
||||
bitmaps.push_back(std::move(dyn_bmp));
|
||||
|
||||
gridItem.on_select = [&nav, app_location, filePath]() {
|
||||
@@ -262,8 +255,8 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
|
||||
|
||||
gridItem.color = Color((uint16_t)application_information.icon_color);
|
||||
|
||||
auto dyn_bmp = std::make_unique<DynamicBitmap<16, 16>>(application_information.bitmap_data);
|
||||
gridItem.bitmap = dyn_bmp->bitmap();
|
||||
auto dyn_bmp = DynamicBitmap<16, 16>{application_information.bitmap_data};
|
||||
gridItem.bitmap = dyn_bmp.bitmap();
|
||||
bitmaps.push_back(std::move(dyn_bmp));
|
||||
|
||||
gridItem.on_select = [&nav, app_location, filePath]() {
|
||||
@@ -357,8 +350,6 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
|
||||
if (checksum != EXT_APP_EXPECTED_CHECKSUM)
|
||||
return false;
|
||||
|
||||
nav.pop();
|
||||
nav.set_last_menu_went_deeper(true);
|
||||
application_information.externalAppEntry(nav);
|
||||
return true;
|
||||
}
|
||||
@@ -395,8 +386,7 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
|
||||
}
|
||||
}
|
||||
|
||||
nav.set_last_menu_went_deeper(true);
|
||||
nav.replace<StandaloneView>(app_image);
|
||||
nav.push<StandaloneView>(app_image);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -410,8 +400,7 @@ std::vector<ExternalItemsMenuLoader::GridItemEx> ExternalItemsMenuLoader::load_e
|
||||
}
|
||||
}
|
||||
|
||||
nav.set_last_menu_went_deeper(true);
|
||||
nav.replace<StandaloneView>(app_image);
|
||||
nav.push<StandaloneView>(app_image);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,38 +33,24 @@
|
||||
#define EXT_APP_EXPECTED_CHECKSUM 0x00000000
|
||||
|
||||
namespace ui {
|
||||
|
||||
template <size_t Width, size_t Height>
|
||||
class DynamicBitmap {
|
||||
public:
|
||||
static constexpr size_t buffer_size = Width * Height / (sizeof(uint8_t) * 8);
|
||||
static constexpr size_t buffer_size = Width * Height / (sizeof(uint8_t) * 8); // one bit per pixel
|
||||
|
||||
// Main constructor
|
||||
DynamicBitmap(const uint8_t data[buffer_size])
|
||||
: _bitmap{{Width, Height}, _buffer.data()} {
|
||||
std::memcpy(_buffer.data(), data, buffer_size);
|
||||
: _buffer(buffer_size, 0),
|
||||
_bitmap{new Bitmap{{Width, Height}, &_buffer[0]}} {
|
||||
memcpy(&_buffer[0], data, buffer_size);
|
||||
}
|
||||
|
||||
DynamicBitmap(const DynamicBitmap& other)
|
||||
: _buffer(other._buffer),
|
||||
_bitmap{{Width, Height}, _buffer.data()} {}
|
||||
|
||||
DynamicBitmap(DynamicBitmap&& other) noexcept
|
||||
: _buffer(std::move(other._buffer)),
|
||||
_bitmap{{Width, Height}, _buffer.data()} {}
|
||||
|
||||
DynamicBitmap& operator=(const DynamicBitmap& other) = delete;
|
||||
DynamicBitmap& operator=(DynamicBitmap&& other) noexcept = delete;
|
||||
|
||||
// Destructor (Default is fine, no heap memory to free manually)
|
||||
~DynamicBitmap() = default;
|
||||
|
||||
const Bitmap* bitmap() const { return &_bitmap; }
|
||||
const Bitmap* bitmap() { return _bitmap.get(); }
|
||||
|
||||
private:
|
||||
// Order matters: _buffer must be declared before _bitmap
|
||||
// so it is initialized first and its .data() pointer is valid.
|
||||
std::array<uint8_t, buffer_size> _buffer{};
|
||||
Bitmap _bitmap{};
|
||||
// Allocating both members so the class is movable without invalidation.
|
||||
std::vector<uint8_t> _buffer;
|
||||
std::unique_ptr<Bitmap> _bitmap;
|
||||
};
|
||||
|
||||
class ExternalItemsMenuLoader {
|
||||
@@ -79,10 +65,9 @@ class ExternalItemsMenuLoader {
|
||||
static bool run_standalone_app(ui::NavigationView&, std::filesystem::path);
|
||||
static bool run_module_app(ui::NavigationView&, uint8_t*, size_t);
|
||||
static void load_all_external_items_callback(std::function<void(AppInfoConsole&)> callback, bool module_included = false);
|
||||
static void unload_external_items();
|
||||
|
||||
private:
|
||||
static std::vector<std::unique_ptr<DynamicBitmap<16, 16>>> bitmaps;
|
||||
static std::vector<DynamicBitmap<16, 16>> bitmaps;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "ui_recon.hpp"
|
||||
#include "ui_search.hpp"
|
||||
#include "ui_settings.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "ui_sonde.hpp"
|
||||
#include "ui_ss_viewer.hpp"
|
||||
// #include "ui_test.hpp"
|
||||
@@ -73,6 +74,8 @@
|
||||
#include "file_path.hpp"
|
||||
#include "ff.h"
|
||||
|
||||
#include "i2cdev_max17055.hpp"
|
||||
|
||||
#include <locale>
|
||||
#include <codecvt>
|
||||
|
||||
@@ -85,47 +88,44 @@ namespace ui {
|
||||
// TODO(u-foka): Check consistency of command names (where we add rx/tx postfix)
|
||||
const NavigationView::AppList NavigationView::appList = {
|
||||
/* HOME ******************************************************************/
|
||||
{nullptr, "Receive", HOME, Color::cyan(), &bitmap_icon_receivers, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<ReceiversMenuView>(nav); }},
|
||||
{nullptr, "Transmit", HOME, Color::cyan(), &bitmap_icon_transmit, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<TransmittersMenuView>(nav); }},
|
||||
{nullptr, "Transceiver", HOME, Color::cyan(), &bitmap_icon_transceivers, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<TransceiversMenuView>(nav); }},
|
||||
{"recon", "Recon", HOME, Color::green(), &bitmap_icon_scanner, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<ReconView>(nav); }},
|
||||
{"capture", "Capture", HOME, Color::red(), &bitmap_icon_capture, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<CaptureAppView>(nav); }},
|
||||
{"replay", "Replay", HOME, Color::green(), &bitmap_icon_replay, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<PlaylistView>(nav); }},
|
||||
{"lookingglass", "Looking Glass", HOME, Color::green(), &bitmap_icon_looking, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<GlassView>(nav); }},
|
||||
{nullptr, "Utilities", HOME, Color::cyan(), &bitmap_icon_utilities, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<UtilitiesMenuView>(nav); }},
|
||||
{nullptr, "Games", HOME, Color::cyan(), &bitmap_icon_games, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<GamesMenuView>(nav); }},
|
||||
{nullptr, "Settings", HOME, Color::cyan(), &bitmap_icon_setup, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<SettingsMenuView>(nav); }},
|
||||
|
||||
{nullptr, "Receive", HOME, Color::cyan(), &bitmap_icon_receivers, new ViewFactory<ReceiversMenuView>()},
|
||||
{nullptr, "Transmit", HOME, Color::cyan(), &bitmap_icon_transmit, new ViewFactory<TransmittersMenuView>()},
|
||||
{nullptr, "Transceiver", HOME, Color::cyan(), &bitmap_icon_transceivers, new ViewFactory<TransceiversMenuView>()},
|
||||
{"recon", "Recon", HOME, Color::green(), &bitmap_icon_scanner, new ViewFactory<ReconView>()},
|
||||
{"capture", "Capture", HOME, Color::red(), &bitmap_icon_capture, new ViewFactory<CaptureAppView>()},
|
||||
{"replay", "Replay", HOME, Color::green(), &bitmap_icon_replay, new ViewFactory<PlaylistView>()},
|
||||
{"lookingglass", "Looking Glass", HOME, Color::green(), &bitmap_icon_looking, new ViewFactory<GlassView>()},
|
||||
{nullptr, "Utilities", HOME, Color::cyan(), &bitmap_icon_utilities, new ViewFactory<UtilitiesMenuView>()},
|
||||
{nullptr, "Games", HOME, Color::cyan(), &bitmap_icon_games, new ViewFactory<GamesMenuView>()},
|
||||
{nullptr, "Settings", HOME, Color::cyan(), &bitmap_icon_setup, new ViewFactory<SettingsMenuView>()},
|
||||
/* RX ********************************************************************/
|
||||
{"adsbrx", "ADS-B", RX, Color::green(), &bitmap_icon_adsb, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<ADSBRxView>(nav); }},
|
||||
{"ais", "AIS Boats", RX, Color::green(), &bitmap_icon_ais, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<AISAppView>(nav); }},
|
||||
{"aprsrx", "APRS", RX, Color::green(), &bitmap_icon_aprs, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<APRSRXView>(nav); }},
|
||||
{"audio", "Audio", RX, Color::green(), &bitmap_icon_speaker, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<AnalogAudioView>(nav); }},
|
||||
{"blerx", "BLE Rx", RX, Color::green(), &bitmap_icon_btle, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<BLERxView>(nav); }},
|
||||
{"pocsag", "POCSAG", RX, Color::green(), &bitmap_icon_pocsag, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<POCSAGAppView>(nav); }},
|
||||
{"radiosonde", "Radiosnde", RX, Color::green(), &bitmap_icon_sonde, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<SondeView>(nav); }},
|
||||
{"search", "Search", RX, Color::yellow(), &bitmap_icon_search, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<SearchView>(nav); }},
|
||||
{"subghzd", "SubGhzD", RX, Color::yellow(), &bitmap_icon_remote, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<SubGhzDView>(nav); }},
|
||||
{"weather", "Weather", RX, Color::green(), &bitmap_icon_thermometer, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<WeatherView>(nav); }},
|
||||
|
||||
{"adsbrx", "ADS-B", RX, Color::green(), &bitmap_icon_adsb, new ViewFactory<ADSBRxView>()},
|
||||
{"ais", "AIS Boats", RX, Color::green(), &bitmap_icon_ais, new ViewFactory<AISAppView>()},
|
||||
{"aprsrx", "APRS", RX, Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSRXView>()},
|
||||
{"audio", "Audio", RX, Color::green(), &bitmap_icon_speaker, new ViewFactory<AnalogAudioView>()},
|
||||
{"blerx", "BLE Rx", RX, Color::green(), &bitmap_icon_btle, new ViewFactory<BLERxView>()},
|
||||
{"pocsag", "POCSAG", RX, Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGAppView>()},
|
||||
{"radiosonde", "Radiosnde", RX, Color::green(), &bitmap_icon_sonde, new ViewFactory<SondeView>()},
|
||||
{"search", "Search", RX, Color::yellow(), &bitmap_icon_search, new ViewFactory<SearchView>()},
|
||||
{"subghzd", "SubGhzD", RX, Color::yellow(), &bitmap_icon_remote, new ViewFactory<SubGhzDView>()},
|
||||
{"weather", "Weather", RX, Color::green(), &bitmap_icon_thermometer, new ViewFactory<WeatherView>()},
|
||||
/* TX ********************************************************************/
|
||||
{"aprstx", "APRS TX", TX, ui::Color::green(), &bitmap_icon_aprs, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<APRSTXView>(nav); }},
|
||||
{"bletx", "BLE Tx", TX, ui::Color::green(), &bitmap_icon_btle, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<BLETxView>(nav); }},
|
||||
{"ooktx", "OOK", TX, ui::Color::yellow(), &bitmap_icon_remote, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<EncodersView>(nav); }},
|
||||
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<RDSView>(nav); }},
|
||||
{"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<TouchTunesView>(nav); }},
|
||||
|
||||
{"aprstx", "APRS TX", TX, ui::Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSTXView>()},
|
||||
{"bletx", "BLE Tx", TX, ui::Color::green(), &bitmap_icon_btle, new ViewFactory<BLETxView>()},
|
||||
{"ooktx", "OOK", TX, ui::Color::yellow(), &bitmap_icon_remote, new ViewFactory<EncodersView>()},
|
||||
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory<RDSView>()},
|
||||
{"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, new ViewFactory<TouchTunesView>()},
|
||||
/* TRX ********************************************************************/
|
||||
{"microphone", "Mic", TRX, Color::green(), &bitmap_icon_microphone, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<MicTXView>(nav); }},
|
||||
|
||||
{"microphone", "Mic", TRX, Color::green(), &bitmap_icon_microphone, new ViewFactory<MicTXView>()},
|
||||
/* UTILITIES *************************************************************/
|
||||
{"filemanager", "File Manager", UTILITIES, Color::green(), &bitmap_icon_dir, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<FileManagerView>(nav); }},
|
||||
{"freqman", "Freq. Manager", UTILITIES, Color::green(), &bitmap_icon_freqman, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<FrequencyManagerView>(nav); }},
|
||||
{"iqtrim", "IQ Trim", UTILITIES, Color::orange(), &bitmap_icon_trim, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<IQTrimView>(nav); }},
|
||||
{"notepad", "Notepad", UTILITIES, Color::dark_cyan(), &bitmap_icon_notepad, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<TextEditorView>(nav); }},
|
||||
{nullptr, "Debug", UTILITIES, Color::light_grey(), &bitmap_icon_debug, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<DebugMenuView>(nav); }},
|
||||
{"filemanager", "File Manager", UTILITIES, Color::green(), &bitmap_icon_dir, new ViewFactory<FileManagerView>()},
|
||||
{"freqman", "Freq. Manager", UTILITIES, Color::green(), &bitmap_icon_freqman, new ViewFactory<FrequencyManagerView>()},
|
||||
{"iqtrim", "IQ Trim", UTILITIES, Color::orange(), &bitmap_icon_trim, new ViewFactory<IQTrimView>()},
|
||||
{"notepad", "Notepad", UTILITIES, Color::dark_cyan(), &bitmap_icon_notepad, new ViewFactory<TextEditorView>()},
|
||||
{nullptr, "Debug", UTILITIES, Color::light_grey(), &bitmap_icon_debug, new ViewFactory<DebugMenuView>()},
|
||||
//{"testapp", "Test App", UTILITIES, Color::dark_grey(), nullptr, new ViewFactory<TestView>()},
|
||||
// Dangerous apps.
|
||||
{nullptr, "Flash Utility", UTILITIES, Color::red(), &bitmap_icon_peripherals_details, [](NavigationView& nav) -> std::unique_ptr<View> { return std::make_unique<FlashUtilityView>(nav); }},
|
||||
{nullptr, "Flash Utility", UTILITIES, Color::red(), &bitmap_icon_peripherals_details, new ViewFactory<FlashUtilityView>()},
|
||||
};
|
||||
|
||||
bool NavigationView::StartAppByName(const char* name) {
|
||||
@@ -133,13 +133,122 @@ bool NavigationView::StartAppByName(const char* name) {
|
||||
|
||||
for (const auto& app : appList) {
|
||||
if (app.id != nullptr && strcmp(app.id, name) == 0) {
|
||||
push_view(app.producer(*this));
|
||||
push_view(app.viewFactory->produce(*this));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* App search ***********************************************************/
|
||||
|
||||
namespace {
|
||||
|
||||
// ASCII, allocation-free, case-insensitive lowering. Kept local so this TU
|
||||
// doesn't need to include <cctype> just for one small comparison.
|
||||
char ascii_lower(char c) {
|
||||
return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
|
||||
}
|
||||
|
||||
// True when 'needle' occurs anywhere in 'haystack', case-insensitive.
|
||||
bool name_contains(const char* haystack, const std::string& needle) {
|
||||
const size_t n = needle.size();
|
||||
if (n == 0)
|
||||
return false;
|
||||
for (size_t i = 0; haystack[i] != '\0'; ++i) {
|
||||
size_t j = 0;
|
||||
while (j < n && haystack[i + j] != '\0' &&
|
||||
ascii_lower(haystack[i + j]) == ascii_lower(needle[j]))
|
||||
++j;
|
||||
if (j == n)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void NavigationView::start_app_search() {
|
||||
app_search_query_.clear();
|
||||
app_search_committed_ = false;
|
||||
|
||||
// Reuse the shared keyboard view; it is destroyed as soon as the user
|
||||
// confirms or cancels, so no search UI ever lingers in RAM.
|
||||
text_prompt(*this, app_search_query_, 20, ENTER_KEYBOARD_MODE_ALPHA,
|
||||
[this](std::string& query) {
|
||||
// Runs while the keyboard is still alive: only record that a
|
||||
// query was confirmed; results are built once it has popped.
|
||||
app_search_committed_ = !query.empty();
|
||||
});
|
||||
|
||||
// Defer building/showing the results until the keyboard view has popped.
|
||||
set_on_pop([this]() { open_app_search_results(); });
|
||||
}
|
||||
|
||||
void NavigationView::open_app_search_results() {
|
||||
if (!app_search_committed_)
|
||||
return; // user cancelled with Back, or the query was empty
|
||||
app_search_committed_ = false;
|
||||
|
||||
std::vector<AppSearchEntry> matches;
|
||||
|
||||
// Internal apps: match on the name shown in the menus.
|
||||
for (const auto& app : appList) {
|
||||
if (app.displayName != nullptr && app.viewFactory != nullptr &&
|
||||
name_contains(app.displayName, app_search_query_))
|
||||
matches.push_back({app.displayName, app.viewFactory, {}});
|
||||
}
|
||||
|
||||
// External (.ppma) and standalone (.ppmp) apps on the SD card. The
|
||||
// enumerator hands back pointers to short-lived buffers, so the names are
|
||||
// copied into owned strings here. Match on both the friendly name and the
|
||||
// file (call) name. module_included=false: PPmod apps can't be launched by
|
||||
// path, so they're intentionally excluded.
|
||||
ExternalItemsMenuLoader::load_all_external_items_callback(
|
||||
[this, &matches](AppInfoConsole& info) {
|
||||
if (name_contains(info.appFriendlyName, app_search_query_) ||
|
||||
name_contains(info.appCallName, app_search_query_))
|
||||
matches.push_back({info.appFriendlyName, nullptr, info.appCallName});
|
||||
},
|
||||
false);
|
||||
|
||||
if (matches.empty()) {
|
||||
display_modal("Search", "No matching app found.");
|
||||
return;
|
||||
}
|
||||
|
||||
push<AppSearchResultsView>(std::move(matches));
|
||||
}
|
||||
|
||||
void NavigationView::launch_search_entry(ViewFactoryBase* factory, std::string call_name) {
|
||||
// Same close-then-open contract as StartAppByName: drop the search UI
|
||||
// (freeing the results view and the very button that called us) before
|
||||
// starting the target, so only one app is ever resident. Everything used
|
||||
// below is a by-value argument or this resident NavigationView.
|
||||
home(false);
|
||||
|
||||
if (factory != nullptr) {
|
||||
push_view(factory->produce(*this));
|
||||
return;
|
||||
}
|
||||
|
||||
// External/standalone: AppInfoConsole doesn't record which kind it is, so
|
||||
// try both extensions the way handle_autostart() does.
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> conv;
|
||||
|
||||
std::string appwithpath = "/" + apps_dir.string() + "/" + call_name + ".ppma";
|
||||
std::filesystem::path pth = conv.from_bytes(appwithpath.c_str());
|
||||
if (ExternalItemsMenuLoader::run_external_app(*this, pth))
|
||||
return;
|
||||
|
||||
appwithpath = "/" + apps_dir.string() + "/" + call_name + ".ppmp";
|
||||
pth = conv.from_bytes(appwithpath.c_str());
|
||||
if (ExternalItemsMenuLoader::run_standalone_app(*this, pth))
|
||||
return;
|
||||
|
||||
display_modal("Search", "Failed to start:\n" + call_name);
|
||||
}
|
||||
|
||||
/* StatusTray ************************************************************/
|
||||
|
||||
StatusTray::StatusTray(Point pos)
|
||||
@@ -337,10 +446,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,6 +671,7 @@ InformationView::InformationView(
|
||||
NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
add_children({&backdrop,
|
||||
&search_icon,
|
||||
&version,
|
||||
<ime});
|
||||
|
||||
@@ -605,6 +715,19 @@ bool InformationView::firmware_checksum_error() {
|
||||
return fw_checksum_error;
|
||||
}
|
||||
|
||||
bool InformationView::on_touch(const TouchEvent event) {
|
||||
// The whole info bar is one touch target. Capture on Start so the End
|
||||
// event is delivered here, then act on release like a normal button tap.
|
||||
switch (event.type) {
|
||||
case TouchEvent::Type::End:
|
||||
if (nav_.is_valid())
|
||||
nav_.start_app_search();
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Navigation ************************************************************/
|
||||
|
||||
bool NavigationView::is_top() const {
|
||||
@@ -633,7 +756,6 @@ void NavigationView::pop(bool trigger_update) {
|
||||
|
||||
free_view();
|
||||
view_stack.pop_back();
|
||||
view_stack.shrink_to_fit();
|
||||
|
||||
// NB: These are executed _after_ the view has been
|
||||
// destroyed. The old view MUST NOT be referenced in
|
||||
@@ -757,22 +879,10 @@ void NavigationView::handle_autostart() {
|
||||
void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc) {
|
||||
for (auto& app : NavigationView::appList) {
|
||||
if (app.menuLocation == loc) {
|
||||
const AppInfo* p_app = &app;
|
||||
grid.add_item({app.displayName, app.iconColor, app.icon,
|
||||
[&nav, p_app]() {
|
||||
NavigationView& local_nav = nav;
|
||||
if (p_app->menuLocation == HOME) {
|
||||
local_nav.store_last_menu_name(p_app->displayName);
|
||||
local_nav.set_last_menu_went_deeper(false);
|
||||
auto new_view = p_app->producer(local_nav);
|
||||
local_nav.push_view(std::move(new_view));
|
||||
} else {
|
||||
auto new_view = p_app->producer(local_nav);
|
||||
local_nav.pop();
|
||||
local_nav.set_last_menu_went_deeper(true);
|
||||
local_nav.push_view(std::move(new_view));
|
||||
}
|
||||
}},
|
||||
[&nav, &app]() {
|
||||
i2cdev::I2CDevManager::set_autoscan_interval(0); //if i navigate away from any menu, turn off autoscan
|
||||
nav.push_view(app.viewFactory->produce(nav)); }},
|
||||
true);
|
||||
}
|
||||
};
|
||||
@@ -799,7 +909,7 @@ void add_external_items(NavigationView& nav, app_location_t location, BtnGridVie
|
||||
return a.desired_position < b.desired_position;
|
||||
});
|
||||
|
||||
for (auto & gridItem : externalItems) {
|
||||
for (auto const& gridItem : externalItems) {
|
||||
if (gridItem.desired_position < 0) {
|
||||
grid.add_item(std::move(gridItem), true);
|
||||
} else {
|
||||
@@ -887,6 +997,32 @@ void GamesMenuView::on_populate() {
|
||||
add_external_items(nav_, app_location_t::GAMES, *this, return_icon ? 1 : 0);
|
||||
}
|
||||
|
||||
/* AppSearchResultsView *************************************************/
|
||||
|
||||
AppSearchResultsView::AppSearchResultsView(NavigationView& nav, std::vector<AppSearchEntry>&& entries)
|
||||
: nav_(nav), entries_(std::move(entries)) {
|
||||
set_max_rows(2); // wider buttons: app names need the room
|
||||
}
|
||||
|
||||
void AppSearchResultsView::on_populate() {
|
||||
for (size_t i = 0; i < entries_.size(); ++i) {
|
||||
const auto& entry = entries_[i];
|
||||
// Internal apps are green, external/standalone orange, matching the
|
||||
// "yellow-ish external" convention used elsewhere. No icon: the cheap
|
||||
// enumerator doesn't decode external bitmaps.
|
||||
add_item({entry.display,
|
||||
entry.factory ? Color::green() : Color::orange(),
|
||||
nullptr,
|
||||
[this, i]() {
|
||||
// Read the target off entries_ and pass it by value: the
|
||||
// call below frees this view via home(), so nothing after
|
||||
// it may touch 'this' or its members.
|
||||
nav_.launch_search_entry(entries_[i].factory, entries_[i].call_name);
|
||||
}},
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
/* SystemMenuView ********************************************************/
|
||||
|
||||
void SystemMenuView::hackrf_mode(NavigationView& nav) {
|
||||
@@ -896,6 +1032,10 @@ void SystemMenuView::hackrf_mode(NavigationView& nav) {
|
||||
YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice) {
|
||||
i2cdev::I2cDev_MAX17055* dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
|
||||
if (dev) {
|
||||
dev->sleep_config(false); // don't enable sleep even if no i2c communication. so we won't lose any data. on reboot (like exit hackrf mode) we set it to true again.
|
||||
}
|
||||
EventDispatcher::request_stop();
|
||||
}
|
||||
});
|
||||
@@ -931,27 +1071,6 @@ SystemView::SystemView(
|
||||
{{0, 0},
|
||||
{parent_rect.width(), status_view_height}});
|
||||
status_view.on_back = [this]() {
|
||||
if (this->navigation_view.view_stack_size() == 2) {
|
||||
const AppInfo* lastmenu = nullptr;
|
||||
const auto last_menu_name = this->navigation_view.get_last_menu_name();
|
||||
this->navigation_view.store_last_menu_name("");
|
||||
if (!last_menu_name.empty()) {
|
||||
for (const auto& app : NavigationView::appList) {
|
||||
if (app.displayName == last_menu_name) {
|
||||
lastmenu = &app;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastmenu && this->navigation_view.get_last_menu_went_deeper()) {
|
||||
this->navigation_view.pop();
|
||||
this->navigation_view.push_view(lastmenu->producer(this->navigation_view));
|
||||
this->navigation_view.store_last_menu_name(lastmenu->displayName); // since we opened it again, store it again
|
||||
this->navigation_view.set_last_menu_went_deeper(false); // not yet
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->navigation_view.pop();
|
||||
};
|
||||
|
||||
@@ -968,7 +1087,6 @@ SystemView::SystemView(
|
||||
navigation_view.on_view_changed = [this](const View& new_view) {
|
||||
if (!this->navigation_view.is_top()) {
|
||||
remove_child(&info_view);
|
||||
i2cdev::I2CDevManager::set_autoscan_interval(0);
|
||||
} else {
|
||||
add_child(&info_view);
|
||||
info_view.refresh();
|
||||
@@ -1029,15 +1147,16 @@ void SystemView::toggle_overlay() {
|
||||
this->set_dirty();
|
||||
shared_memory.request_m4_performance_counter = last_perf_counter_status;
|
||||
overlay_active = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SystemView::paint_overlay() {
|
||||
static bool last_paint_state = false;
|
||||
if (overlay_active) {
|
||||
// paint background only every other second
|
||||
if ((((chTimeNow() >> 10) & 0x01) == 0x01) == last_paint_state)
|
||||
return;
|
||||
|
||||
last_paint_state = !last_paint_state;
|
||||
if (overlay_active == 1 && overlay)
|
||||
overlay->set_dirty();
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
#include "lfsr_random.hpp"
|
||||
#include "sd_card.hpp"
|
||||
#include "external_app.hpp"
|
||||
#include "view_factory.hpp"
|
||||
#include "battery.hpp"
|
||||
|
||||
// for incrementing fake date when RTC battery is dead
|
||||
@@ -59,9 +60,6 @@ using namespace sd_card;
|
||||
|
||||
namespace ui {
|
||||
|
||||
class NavigationView;
|
||||
using ViewProducer = std::unique_ptr<View> (*)(NavigationView&);
|
||||
|
||||
void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc);
|
||||
void add_external_items(NavigationView& nav, app_location_t location, BtnGridView& grid, uint8_t error_tile_pos, bool show_error_tile = true);
|
||||
|
||||
@@ -71,7 +69,7 @@ enum modal_t {
|
||||
ABORT
|
||||
};
|
||||
|
||||
// Should only be used as part of the appList in NavigationView.
|
||||
// Should only be used as part of the appList in NavigationView, the viewFactory will never be destroyed.
|
||||
class AppInfo {
|
||||
public:
|
||||
const char* id; // MUST be unique! Used by serial command to start the app so it also has to make sense
|
||||
@@ -79,7 +77,7 @@ class AppInfo {
|
||||
app_location_t menuLocation;
|
||||
Color iconColor;
|
||||
const Bitmap* icon;
|
||||
ViewProducer producer;
|
||||
ViewFactoryBase* viewFactory; // Never destroyed, and I believe it's ok ;) Having a unique_ptr here breaks the initializer list of appList
|
||||
};
|
||||
|
||||
struct AppInfoConsole {
|
||||
@@ -88,6 +86,14 @@ struct AppInfoConsole {
|
||||
const app_location_t appLocation;
|
||||
};
|
||||
|
||||
// One row in the app-search results list. Strings are owned copies: the
|
||||
// external-app enumerator hands back pointers to short-lived buffers.
|
||||
struct AppSearchEntry {
|
||||
std::string display; // friendly name shown to the user
|
||||
ViewFactoryBase* factory; // internal app factory, or nullptr for external/standalone
|
||||
std::string call_name; // external/standalone file name (no extension); unused when internal
|
||||
};
|
||||
|
||||
class NavigationView : public View {
|
||||
public:
|
||||
std::function<void(const View&)> on_view_changed{};
|
||||
@@ -140,12 +146,19 @@ class NavigationView : public View {
|
||||
bool StartAppByName(const char* name); // Starts a View (app) by name stored in appListFC. This is to start apps from console
|
||||
void handle_autostart();
|
||||
|
||||
void store_last_menu_name(const std::string& name) { last_menu_name_ = name; }
|
||||
std::string get_last_menu_name() const { return last_menu_name_; }
|
||||
size_t view_stack_size() const { return view_stack.size(); }
|
||||
// Opens a keyboard; once confirmed, shows a pick-list of every internal,
|
||||
// external (.ppma) and standalone (.ppmp) app whose name matches the query.
|
||||
// Reuses the transient text-entry view, so the only steady-state cost is
|
||||
// the two members below; the keyboard and the results list are allocated
|
||||
// only while a search is in progress.
|
||||
void start_app_search();
|
||||
|
||||
bool get_last_menu_went_deeper() { return last_menu_went_deeper; }
|
||||
void set_last_menu_went_deeper(bool went_deeper) { last_menu_went_deeper = went_deeper; }
|
||||
// Launches one search result and drops the search UI first, so only one app
|
||||
// is ever resident. 'factory' non-null selects an internal app; otherwise
|
||||
// 'call_name' is an external/standalone app file name (no extension).
|
||||
// Arguments are taken by value on purpose: home() below frees the results
|
||||
// view (and the button that called this), so nothing here may touch it.
|
||||
void launch_search_entry(ViewFactoryBase* factory, std::string call_name);
|
||||
|
||||
private:
|
||||
struct ViewState {
|
||||
@@ -153,14 +166,20 @@ class NavigationView : public View {
|
||||
std::function<void()> on_pop;
|
||||
};
|
||||
|
||||
// Builds and pushes the results list. Runs from the keyboard view's on_pop,
|
||||
// i.e. after that view is fully destroyed, so pushing here can't be
|
||||
// clobbered by the keyboard's own trailing pop().
|
||||
void open_app_search_results();
|
||||
|
||||
std::string app_search_query_{}; // keyboard buffer, owned across its lifetime
|
||||
bool app_search_committed_{false}; // true only when the user confirmed a non-empty query
|
||||
|
||||
std::vector<ViewState> view_stack{};
|
||||
|
||||
Widget* view() const;
|
||||
|
||||
void free_view();
|
||||
void update_view();
|
||||
std::string last_menu_name_{}; // this stores the last menu name, when we replace the menu with the app, we'll know, where to navigate back
|
||||
bool last_menu_went_deeper = false;
|
||||
};
|
||||
|
||||
/* Holds widgets and grows dynamically toward the left.
|
||||
@@ -340,6 +359,9 @@ class InformationView : public View {
|
||||
void refresh();
|
||||
bool firmware_checksum_error();
|
||||
|
||||
// Whole info bar is a touch target that opens the app search.
|
||||
bool on_touch(const TouchEvent event) override;
|
||||
|
||||
private:
|
||||
// static constexpr auto version_string = "v1.4.4"; // This is commented out as we are now setting the version via ENV (VERSION_STRING=v1.0.0)
|
||||
NavigationView& nav_;
|
||||
@@ -348,8 +370,15 @@ class InformationView : public View {
|
||||
{0, 0, screen_width, 16},
|
||||
Theme::getInstance()->bg_darker->background};
|
||||
|
||||
// Visual hint that the info bar starts an app search when tapped.
|
||||
Image search_icon{
|
||||
{2, 0, 16, 16},
|
||||
&bitmap_icon_search,
|
||||
Color::yellow(),
|
||||
Theme::getInstance()->bg_darker->background};
|
||||
|
||||
Text version{
|
||||
{2, 0, 11 * 8, 16},
|
||||
{18, 0, 9 * 8, 16}, // 9 chars keeps room for the "FLASH ERR" indicator; right edge unchanged from before the icon
|
||||
VERSION_STRING};
|
||||
|
||||
LiveDateTime ltime{
|
||||
@@ -433,6 +462,19 @@ class SystemMenuView : public BtnGridView {
|
||||
void hackrf_mode(NavigationView& nav);
|
||||
};
|
||||
|
||||
// Pick-list of apps matching an app-search query. Reuses the button grid so it
|
||||
// behaves (and launches on tap) exactly like the normal app menus.
|
||||
class AppSearchResultsView : public BtnGridView {
|
||||
public:
|
||||
AppSearchResultsView(NavigationView& nav, std::vector<AppSearchEntry>&& entries);
|
||||
std::string title() const override { return "Search"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
std::vector<AppSearchEntry> entries_;
|
||||
void on_populate() override;
|
||||
};
|
||||
|
||||
class SystemView : public View {
|
||||
public:
|
||||
SystemView(
|
||||
|
||||
@@ -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(50); // 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");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2024 Tamas Eisenberger <e.tamas@iwstudio.hu>
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __VIEW_FACTORY_HPP__
|
||||
#define __VIEW_FACTORY_HPP__
|
||||
|
||||
#include "view_factory_base.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
template <typename T>
|
||||
class ViewFactory : public ViewFactoryBase {
|
||||
public:
|
||||
virtual std::unique_ptr<View> produce(NavigationView& nav) const override {
|
||||
return std::unique_ptr<View>(new T(nav));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
#endif //__VIEW_FACTORY_HPP__
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2024 Tamas Eisenberger <e.tamas@iwstudio.hu>
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "view_factory_base.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
ViewFactoryBase::~ViewFactoryBase() {}
|
||||
|
||||
} // namespace ui
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2024 Tamas Eisenberger <e.tamas@iwstudio.hu>
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __VIEW_FACTORY_BASE_HPP__
|
||||
#define __VIEW_FACTORY_BASE_HPP__
|
||||
|
||||
#include <memory>
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class NavigationView;
|
||||
|
||||
class ViewFactoryBase {
|
||||
public:
|
||||
virtual ~ViewFactoryBase();
|
||||
virtual std::unique_ptr<View> produce(NavigationView& nav) const = 0;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
#endif //__VIEW_FACTORY_BASE_HPP__
|
||||
@@ -220,11 +220,17 @@ endif()
|
||||
|
||||
# Upstream hackrf headers now gate on IS_* macros normally populated by
|
||||
# hackrf/firmware/platform-detect.cmake. We don't include that here, so derive
|
||||
# the IS_* set from BOARD ourselves. IS_H1_R9 is forced to 0 (compile-time
|
||||
# constant) so `if (IS_H1_R9)` branches resolve without needing the runtime
|
||||
# detected_platform() symbol, which mayhem doesn't link in.
|
||||
# the IS_* set from BOARD ourselves. IS_H1_R9 must mirror the upstream
|
||||
# HACKRF_ONE definition: a *runtime* check on detected_platform(). The only
|
||||
# baseband image that compiles IS_H1_R9-gated upstream files (rf_path.c,
|
||||
# si5351c.c, sgpio.c, platform_gpio.c, platform_scu.c) is sd_over_usb, which
|
||||
# does link platform_detect.c and calls detect_hardware_platform() at startup.
|
||||
# Forcing IS_H1_R9=0 here silently compiled out all H1R9 behaviour, so a real
|
||||
# HackRF One r9 was configured with the OG clock/GPIO/RF-path setup in that
|
||||
# image. All usages are `#ifdef IS_H1_R9` + `if (IS_H1_R9)` (never arithmetic
|
||||
# `#if`), so a function-call expression is safe.
|
||||
if(BOARD STREQUAL "HACKRF_ONE")
|
||||
set(BOARD_IS_DEFS "-DIS_HACKRF_ONE=1 -DIS_NOT_PRALINE=1 -DIS_H1_R9=0 -DIS_NOT_H1_R9=1 -DIS_NOT_RAD1O=1 -DIS_NOT_JAWBREAKER=1 -DIS_H1_OR_PRALINE=1 -DIS_H1_OR_RAD1O=1 -DIS_H1_OR_JAWBREAKER=1 -DIS_EXPANSION_COMPATIBLE=1")
|
||||
set(BOARD_IS_DEFS "-DIS_HACKRF_ONE=1 -DIS_NOT_PRALINE=1 -D'IS_H1_R9=(detected_platform()==BOARD_ID_HACKRF1_R9)' -D'IS_NOT_H1_R9=(!IS_H1_R9)' -DIS_NOT_RAD1O=1 -DIS_NOT_JAWBREAKER=1 -DIS_H1_OR_PRALINE=1 -DIS_H1_OR_RAD1O=1 -DIS_H1_OR_JAWBREAKER=1 -DIS_EXPANSION_COMPATIBLE=1")
|
||||
elseif(BOARD STREQUAL "PRALINE")
|
||||
set(BOARD_IS_DEFS "-DIS_PRALINE=1 -DIS_NOT_HACKRF_ONE=1 -DIS_NOT_H1_R9=1 -DIS_NOT_RAD1O=1 -DIS_NOT_JAWBREAKER=1 -DIS_H1_OR_PRALINE=1 -DIS_FOUR_LEDS=1 -DIS_EXPANSION_COMPATIBLE=1")
|
||||
elseif(BOARD STREQUAL "RAD1O")
|
||||
@@ -510,6 +516,20 @@ DeclareTargets(PFUT flash_utility)
|
||||
set(add_to_firmware FALSE)
|
||||
set(MODE_FLAGS "-O3")
|
||||
|
||||
### VOR TX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_vor_tx.cpp
|
||||
)
|
||||
DeclareTargets(PVTX vor_tx)
|
||||
|
||||
### VOR RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_vor_rx.cpp
|
||||
)
|
||||
DeclareTargets(PVRX vor_rx)
|
||||
|
||||
### FLEX RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
@@ -776,6 +796,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 +827,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
@@ -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
@@ -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__*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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{};
|
||||
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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_vor_rx.hpp"
|
||||
|
||||
#include "audio_output.hpp"
|
||||
#include "audio_dma.hpp"
|
||||
#include "dsp_iir_config.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "event_m4.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
}
|
||||
|
||||
VorRx::VorRx() {
|
||||
channel_spectrum.set_decimation_factor(1);
|
||||
|
||||
baseband_thread.start();
|
||||
rssi_thread.start();
|
||||
}
|
||||
|
||||
void VorRx::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
|
||||
channel_spectrum.feed(decim_1_out, channel_filter_low_f, channel_filter_high_f, channel_filter_transition);
|
||||
|
||||
// No decim_2 stage: the standard AM decim_2 filter is a ~4.5 kHz low-pass
|
||||
// that would attenuate the 9960 Hz VOR subcarrier by ~60 dB and destroy the
|
||||
// reference phase. The channel stays at 48 kHz so the subcarrier survives.
|
||||
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||
|
||||
feed_channel_stats(channel_out);
|
||||
|
||||
auto audio = demod_am.execute(channel_out, audio_buffer);
|
||||
if (vor_enabled) {
|
||||
process_vor_metrics(audio);
|
||||
// Strip the 9960 Hz subcarrier from the audible signal (the metrics
|
||||
// above still ran on the unfiltered audio so the decode is unaffected).
|
||||
audio_lpf_1.execute_in_place(audio);
|
||||
audio_lpf_2.execute_in_place(audio);
|
||||
}
|
||||
audio_compressor.execute_in_place(audio);
|
||||
audio_output.write(audio);
|
||||
}
|
||||
|
||||
void VorRx::on_message(const Message* const message) {
|
||||
switch (message->id) {
|
||||
case Message::ID::UpdateSpectrum:
|
||||
case Message::ID::SpectrumStreamingConfig:
|
||||
channel_spectrum.on_message(message);
|
||||
break;
|
||||
|
||||
case Message::ID::VorRxConfigure:
|
||||
configure_vor(*reinterpret_cast<const VorRxConfigureMessage*>(message));
|
||||
break;
|
||||
|
||||
case Message::ID::AMConfigure:
|
||||
configure(*reinterpret_cast<const AMConfigureMessage*>(message));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VorRx::configure(const AMConfigureMessage& message) {
|
||||
decim_0.configure(message.decim_0_filter.taps);
|
||||
decim_1.configure(message.decim_1_filter.taps);
|
||||
channel_filter.configure(message.channel_filter.taps, channel_filter_decimation_factor);
|
||||
|
||||
constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor;
|
||||
constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor;
|
||||
constexpr size_t channel_filter_input_fs = decim_1_output_fs;
|
||||
|
||||
channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs;
|
||||
channel_filter_high_f = message.channel_filter.high_frequency_normalized * channel_filter_input_fs;
|
||||
channel_filter_transition = message.channel_filter.transition_normalized * channel_filter_input_fs;
|
||||
|
||||
channel_spectrum.set_decimation_factor(message.channel_spectrum_decimation_factor);
|
||||
audio_output.configure(message.audio_hpf_lpf_config);
|
||||
|
||||
configured = true;
|
||||
}
|
||||
|
||||
void VorRx::PhaseOscillator::configure(float frequency_hz, float sample_rate_hz) {
|
||||
const float step = 2.0f * kPi * frequency_hz / sample_rate_hz;
|
||||
step_sin = sinf(step);
|
||||
step_cos = cosf(step);
|
||||
sin_v = 0.0f;
|
||||
cos_v = 1.0f;
|
||||
}
|
||||
|
||||
void VorRx::PhaseOscillator::advance() {
|
||||
const float next_cos = (cos_v * step_cos) - (sin_v * step_sin);
|
||||
const float next_sin = (sin_v * step_cos) + (cos_v * step_sin);
|
||||
cos_v = next_cos;
|
||||
sin_v = next_sin;
|
||||
}
|
||||
|
||||
void VorRx::PhaseOscillator::renormalize() {
|
||||
// The recursive rotator slowly drifts in amplitude over long runs;
|
||||
// rescale to the unit circle so accumulated tone levels stay meaningful.
|
||||
const float mag = sqrtf((cos_v * cos_v) + (sin_v * sin_v));
|
||||
if (mag > 0.0f) {
|
||||
const float inv = 1.0f / mag;
|
||||
cos_v *= inv;
|
||||
sin_v *= inv;
|
||||
}
|
||||
}
|
||||
|
||||
void VorRx::configure_vor(const VorRxConfigureMessage& message) {
|
||||
vor_enabled = message.enabled;
|
||||
vor_reference_osc.configure(vor_reference_hz, 48000.0f);
|
||||
vor_subcarrier_osc.configure(vor_subcarrier_hz, 48000.0f);
|
||||
audio_lpf_1.configure(audio_48k_lpf_3200hz_config);
|
||||
audio_lpf_2.configure(audio_48k_lpf_3200hz_config);
|
||||
vor_ref_i = 0.0f;
|
||||
vor_ref_q = 0.0f;
|
||||
vor_var_i = 0.0f;
|
||||
vor_var_q = 0.0f;
|
||||
vor_dc_sum = 0.0f;
|
||||
vor_sub_lp_i = 0.0f;
|
||||
vor_sub_lp_q = 0.0f;
|
||||
vor_sub_prev_i = 0.0f;
|
||||
vor_sub_prev_q = 0.0f;
|
||||
vor_sample_count = 0;
|
||||
}
|
||||
|
||||
uint16_t VorRx::normalize_degrees(float degrees) {
|
||||
while (degrees < 0.0f) {
|
||||
degrees += 360.0f;
|
||||
}
|
||||
while (degrees >= 360.0f) {
|
||||
degrees -= 360.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(degrees + 0.5f);
|
||||
}
|
||||
|
||||
void VorRx::send_vor_status(uint16_t phase_deg, uint16_t radial_deg, uint16_t ref_level, uint16_t var_level, uint8_t quality, bool valid, bool to_from) {
|
||||
const VorRxStatusDataMessage message{phase_deg, radial_deg, ref_level, var_level, quality, valid, to_from};
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
|
||||
void VorRx::process_vor_metrics(const buffer_f32_t& audio) {
|
||||
for (size_t i = 0; i < audio.count; ++i) {
|
||||
const float sample = audio.p[i];
|
||||
const float ref_cos = vor_reference_osc.cosine();
|
||||
const float ref_sin = vor_reference_osc.sine();
|
||||
const float sub_cos = vor_subcarrier_osc.cosine();
|
||||
const float sub_sin = vor_subcarrier_osc.sine();
|
||||
|
||||
// Variable signal: 30 Hz amplitude modulation of the carrier envelope.
|
||||
// Correlate the envelope against a local 30 Hz tone (DC rejected because
|
||||
// the window spans an exact integer number of 30 Hz cycles).
|
||||
vor_dc_sum += sample;
|
||||
vor_var_i += sample * ref_cos;
|
||||
vor_var_q -= sample * ref_sin;
|
||||
|
||||
// Reference signal: 30 Hz FM on the 9960 Hz subcarrier. Quadrature
|
||||
// down-convert the subcarrier, low-pass to its +/-480 Hz baseband, then
|
||||
// FM-demodulate (phase difference between successive samples).
|
||||
const float sc_i = sample * sub_cos;
|
||||
const float sc_q = -sample * sub_sin;
|
||||
vor_sub_lp_i += (1.0f - vor_sub_lp_alpha) * (sc_i - vor_sub_lp_i);
|
||||
vor_sub_lp_q += (1.0f - vor_sub_lp_alpha) * (sc_q - vor_sub_lp_q);
|
||||
|
||||
const float num = (vor_sub_lp_q * vor_sub_prev_i) - (vor_sub_lp_i * vor_sub_prev_q);
|
||||
const float den = (vor_sub_lp_i * vor_sub_prev_i) + (vor_sub_lp_q * vor_sub_prev_q);
|
||||
const float fm = atan2f(num, den);
|
||||
vor_sub_prev_i = vor_sub_lp_i;
|
||||
vor_sub_prev_q = vor_sub_lp_q;
|
||||
|
||||
vor_ref_i += fm * ref_cos;
|
||||
vor_ref_q -= fm * ref_sin;
|
||||
|
||||
vor_reference_osc.advance();
|
||||
vor_subcarrier_osc.advance();
|
||||
|
||||
++vor_sample_count;
|
||||
if (vor_sample_count < vor_window_samples) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float inv_n = 1.0f / static_cast<float>(vor_window_samples);
|
||||
const float variable_phase = atan2f(vor_var_q, vor_var_i);
|
||||
const float reference_phase = atan2f(vor_ref_q, vor_ref_i);
|
||||
// Real VOR radials increase clockwise, i.e. the variable tone lags the
|
||||
// reference by the bearing. The decoder therefore reports
|
||||
// (reference - variable); the opposite order mirrors every bearing to
|
||||
// 360 - true. Add back the receive-chain phase lag (see
|
||||
// vor_ref_phase_lag_deg): with this subtraction order the delayed
|
||||
// reference makes the raw radial read low, so the correction is added.
|
||||
float phase_diff = ((reference_phase - variable_phase) * 180.0f / kPi) + vor_ref_phase_lag_deg;
|
||||
while (phase_diff < 0.0f) {
|
||||
phase_diff += 360.0f;
|
||||
}
|
||||
while (phase_diff >= 360.0f) {
|
||||
phase_diff -= 360.0f;
|
||||
}
|
||||
|
||||
// Scale-invariant lock metrics (independent of RF/AGC level):
|
||||
// - var_ratio: 30 Hz AM depth vs. carrier DC (~0.30 for a real VOR).
|
||||
// - ref_amp: FM-demodulated 30 Hz amplitude (phase-only, so it does
|
||||
// not depend on signal amplitude, ~0.065 when locked).
|
||||
const float dc_level = vor_dc_sum * inv_n;
|
||||
const float var_amp = 2.0f * sqrtf((vor_var_i * vor_var_i) + (vor_var_q * vor_var_q)) * inv_n;
|
||||
const float ref_amp = 2.0f * sqrtf((vor_ref_i * vor_ref_i) + (vor_ref_q * vor_ref_q)) * inv_n;
|
||||
const float var_ratio = (dc_level > 0.0f) ? (var_amp / dc_level) : 0.0f;
|
||||
|
||||
const auto phase_deg = normalize_degrees(phase_diff);
|
||||
const auto radial_deg = phase_deg;
|
||||
const auto ref_level = static_cast<uint16_t>(std::min(65535.0f, ref_amp * 10000.0f));
|
||||
const auto var_level = static_cast<uint16_t>(std::min(65535.0f, var_ratio * 1000.0f));
|
||||
const bool valid = (ref_amp > 0.03f) && (var_ratio > 0.15f);
|
||||
// TO/FROM depends on the operator-selected OBS course, which only the
|
||||
// application knows, so it is derived there (see VorRxView). Emit a
|
||||
// stable placeholder here rather than a radial-only guess.
|
||||
const bool to_from = false;
|
||||
uint8_t quality = 0;
|
||||
if (valid) {
|
||||
const float q = (ref_amp / 0.065f) * 100.0f;
|
||||
quality = static_cast<uint8_t>(std::min(100.0f, std::max(0.0f, q)));
|
||||
}
|
||||
|
||||
send_vor_status(phase_deg, radial_deg, ref_level, var_level, quality, valid, to_from);
|
||||
|
||||
vor_reference_osc.renormalize();
|
||||
vor_subcarrier_osc.renormalize();
|
||||
vor_sample_count = 0;
|
||||
vor_ref_i = 0.0f;
|
||||
vor_ref_q = 0.0f;
|
||||
vor_var_i = 0.0f;
|
||||
vor_var_q = 0.0f;
|
||||
vor_dc_sum = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
audio::dma::init_audio_out();
|
||||
|
||||
EventDispatcher event_dispatcher{std::make_unique<VorRx>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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_VOR_RX_H__
|
||||
#define __PROC_VOR_RX_H__
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
|
||||
#include "dsp_decimate.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
#include "dsp_iir.hpp"
|
||||
#include "audio_compressor.hpp"
|
||||
|
||||
#include "audio_output.hpp"
|
||||
#include "spectrum_collector.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
|
||||
// Standalone VOR receiver baseband.
|
||||
// Demodulates the AM channel at 48 kHz (so the 9960 Hz subcarrier stays
|
||||
// representable) and decodes the 30 Hz reference vs. variable phase to derive
|
||||
// the radial.
|
||||
class VorRx : public BasebandProcessor {
|
||||
public:
|
||||
VorRx();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 3072000;
|
||||
// VOR keeps the channel at 48 kHz instead of the AM audio 12 kHz, so the
|
||||
// 9960 Hz subcarrier stays below Nyquist. The standard AM decim_2 stage is
|
||||
// therefore omitted (its ~4.5 kHz low-pass would remove the subcarrier).
|
||||
static constexpr size_t channel_filter_decimation_factor = 1;
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
std::array<float, 32> audio{};
|
||||
const buffer_f32_t audio_buffer{
|
||||
audio.data(),
|
||||
audio.size()};
|
||||
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
int32_t channel_filter_low_f = 0;
|
||||
int32_t channel_filter_high_f = 0;
|
||||
int32_t channel_filter_transition = 0;
|
||||
bool configured{false};
|
||||
bool vor_enabled{false};
|
||||
|
||||
static constexpr float vor_reference_hz{30.0f};
|
||||
static constexpr float vor_subcarrier_hz{9960.0f};
|
||||
// 100 ms at 48 kHz (exactly 3 cycles of 30 Hz)
|
||||
static constexpr uint32_t vor_window_samples{4800};
|
||||
// One-pole low-pass to isolate the +/-480 Hz FM subcarrier baseband
|
||||
// after quadrature down-conversion (corner ~600 Hz at 48 kHz).
|
||||
static constexpr float vor_sub_lp_alpha{0.9245f};
|
||||
// Fixed phase lag the receive chain adds to the recovered 30 Hz reference
|
||||
// tone, expressed in degrees at 30 Hz. It comes almost entirely from the
|
||||
// one-pole subcarrier low-pass above (~2.75 deg of lag at 30 Hz for
|
||||
// alpha = 0.9245) plus a ~0.1 deg half-sample delay in the FM
|
||||
// differentiator. Because the reference tone is delayed, the raw radial
|
||||
// (reference - variable) reads low by this amount, so we add it back.
|
||||
// Value verified by a TX->RX loopback (proc_vor_tx fed through this
|
||||
// decoder): every transmitted radial decoded ~3.2 deg low before this
|
||||
// correction, and on-bearing afterwards.
|
||||
static constexpr float vor_ref_phase_lag_deg{3.2f};
|
||||
|
||||
struct PhaseOscillator {
|
||||
float sin_v{0.0f};
|
||||
float cos_v{1.0f};
|
||||
float step_sin{0.0f};
|
||||
float step_cos{1.0f};
|
||||
|
||||
void configure(float frequency_hz, float sample_rate_hz);
|
||||
float cosine() const { return cos_v; }
|
||||
float sine() const { return sin_v; }
|
||||
void advance();
|
||||
void renormalize();
|
||||
} vor_reference_osc{}, vor_subcarrier_osc{};
|
||||
// 30 Hz DFT accumulators for the variable (AM) and reference (FM) tones.
|
||||
float vor_ref_i{0.0f};
|
||||
float vor_ref_q{0.0f};
|
||||
float vor_var_i{0.0f};
|
||||
float vor_var_q{0.0f};
|
||||
float vor_dc_sum{0.0f};
|
||||
// Subcarrier quadrature down-converter / FM-demodulator state.
|
||||
float vor_sub_lp_i{0.0f};
|
||||
float vor_sub_lp_q{0.0f};
|
||||
float vor_sub_prev_i{0.0f};
|
||||
float vor_sub_prev_q{0.0f};
|
||||
uint32_t vor_sample_count{0};
|
||||
|
||||
dsp::demodulate::AM demod_am{};
|
||||
// Two cascaded 2nd-order Butterworth low-passes (4th-order total) that strip
|
||||
// the 9960 Hz FM subcarrier out of the audio before it reaches the speaker.
|
||||
// The channel is kept wide (48 kHz) so the subcarrier survives for the
|
||||
// radial decode, so it must be filtered out of the audible path here.
|
||||
IIRBiquadFilter audio_lpf_1{};
|
||||
IIRBiquadFilter audio_lpf_2{};
|
||||
FeedForwardCompressor audio_compressor{};
|
||||
AudioOutput audio_output{};
|
||||
|
||||
SpectrumCollector channel_spectrum{};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
#ifdef PRALINE
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive,
|
||||
/*auto_start*/ false};
|
||||
RSSIThread rssi_thread{/*auto_start*/ false};
|
||||
#else
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
#endif
|
||||
|
||||
void configure(const AMConfigureMessage& message);
|
||||
void configure_vor(const VorRxConfigureMessage& message);
|
||||
void process_vor_metrics(const buffer_f32_t& audio);
|
||||
void send_vor_status(uint16_t phase_deg, uint16_t radial_deg, uint16_t ref_level, uint16_t var_level, uint8_t quality, bool valid, bool to_from);
|
||||
static uint16_t normalize_degrees(float degrees);
|
||||
};
|
||||
|
||||
#endif /*__PROC_VOR_RX_H__*/
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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; if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "proc_vor_tx.hpp"
|
||||
|
||||
#include "morse.hpp"
|
||||
#include "sine_table_int8.hpp"
|
||||
#include "event_m4.hpp"
|
||||
|
||||
void VorTxProcessor::execute(const buffer_c8_t& buffer) {
|
||||
for (size_t i = 0; i < buffer.count; i++) {
|
||||
if (!configured) {
|
||||
buffer.p[i] = {0, 0};
|
||||
continue;
|
||||
}
|
||||
|
||||
// 30 Hz reference and variable tones (same frequency, offset by the radial).
|
||||
const int32_t ref_sine = sine_table_i8[(phase_30 >> 24) & 0xFF];
|
||||
const int32_t var_sine = sine_table_i8[((phase_30 - radial_offset) >> 24) & 0xFF];
|
||||
phase_30 += delta_30;
|
||||
|
||||
// 9960 Hz subcarrier, FM-modulated +/- 480 Hz by the 30 Hz reference tone.
|
||||
const int32_t sub_inc = static_cast<int32_t>(delta_sub) + ((ref_sine * static_cast<int32_t>(delta_480)) >> 7);
|
||||
phase_sub += static_cast<uint32_t>(sub_inc);
|
||||
const int32_t sub_sine = sine_table_i8[(phase_sub >> 24) & 0xFF];
|
||||
|
||||
// Optional 1020 Hz identification tone.
|
||||
int32_t id_sine = 0;
|
||||
if (ident_enabled && ident_tone_active()) {
|
||||
id_sine = sine_table_i8[(phase_id >> 24) & 0xFF];
|
||||
phase_id += delta_1020;
|
||||
}
|
||||
|
||||
// DSB-AM envelope (carrier kept at LO -> Q = 0).
|
||||
int32_t env = carrier_level + (((var_sine * var_depth) + (sub_sine * sub_depth) + (id_sine * id_depth)) >> 7);
|
||||
if (env < 0) env = 0;
|
||||
if (env > 127) env = 127;
|
||||
|
||||
buffer.p[i] = {static_cast<int8_t>(env), 0};
|
||||
}
|
||||
}
|
||||
|
||||
void VorTxProcessor::vor_tx_config(const VorTxConfigureMessage& message) {
|
||||
// Real VOR radials increase clockwise: the variable tone lags the 30 Hz
|
||||
// reference by the bearing, so the offset is subtracted (see the matching
|
||||
// (reference - variable) decode in proc_vor_rx). A TX->RX loopback with this
|
||||
// convention still decodes a transmitted radial of N degrees as N degrees,
|
||||
// and it now matches real ground-station VOR bearings.
|
||||
radial_offset = static_cast<uint32_t>(static_cast<uint64_t>(message.radial_deg % 360) * phase_period / 360);
|
||||
ident_enabled = message.ident_enabled;
|
||||
|
||||
for (size_t i = 0; i < sizeof(ident_text); ++i)
|
||||
ident_text[i] = message.ident_text[i];
|
||||
ident_text[sizeof(ident_text) - 1] = '\0';
|
||||
build_ident_schedule();
|
||||
|
||||
configured = message.enabled;
|
||||
}
|
||||
|
||||
// Render the ident text into a repeating sequence of keyed-on / keyed-off
|
||||
// sample runs using the shared ITU Morse table. Timing is derived from the
|
||||
// standard "dot = 1200 / wpm" relation; letters are separated by a 3-unit gap,
|
||||
// words by 7 units, and a trailing gap pads each cycle to a fixed 10 s period.
|
||||
void VorTxProcessor::build_ident_schedule() {
|
||||
ident_segment_count = 0;
|
||||
ident_index = 0;
|
||||
ident_sample_counter = 0;
|
||||
|
||||
const uint32_t dot_samples = static_cast<uint32_t>(1200ULL * baseband_fs / (ident_wpm * 1000ULL));
|
||||
|
||||
uint32_t total_samples = 0;
|
||||
auto push_samples = [&](bool on, uint32_t samples) {
|
||||
if (ident_segment_count >= max_ident_segments) return;
|
||||
ident_segments[ident_segment_count++] = {on, samples};
|
||||
total_samples += samples;
|
||||
};
|
||||
auto push = [&](bool on, uint32_t units) {
|
||||
push_samples(on, units * dot_samples);
|
||||
};
|
||||
|
||||
bool any = false;
|
||||
for (size_t n = 0; n < sizeof(ident_text) && ident_text[n]; ++n) {
|
||||
char ch = ident_text[n];
|
||||
if (ch >= 'a' && ch <= 'z') ch -= 32;
|
||||
|
||||
uint16_t code = 0;
|
||||
if (ch >= '!' && ch <= '_') code = morse::morse_ITU[ch - '!'];
|
||||
|
||||
if (!code) {
|
||||
// Space or unsupported character: word gap between symbols.
|
||||
if (any) push(false, MORSE_WORD_SPACE);
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint16_t code_size = code & 7;
|
||||
for (uint16_t c = 0; c < code_size; ++c) {
|
||||
const bool dash = ((code << c) & 0x8000) != 0;
|
||||
push(true, dash ? MORSE_DASH : MORSE_DOT);
|
||||
if (c + 1 < code_size)
|
||||
push(false, MORSE_SYMBOL_SPACE);
|
||||
}
|
||||
push(false, MORSE_LETTER_SPACE);
|
||||
any = true;
|
||||
}
|
||||
|
||||
if (!any) {
|
||||
// No sendable characters: disable keying entirely.
|
||||
ident_segment_count = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Pad the trailing silence so ident-start to ident-start is a full 10 s.
|
||||
// If the keyed sequence is somehow longer than the period, fall back to a
|
||||
// one word-space gap so repeats never run back-to-back.
|
||||
const uint32_t min_gap = MORSE_WORD_SPACE * dot_samples;
|
||||
const uint32_t gap = (ident_period_samples > total_samples + min_gap)
|
||||
? ident_period_samples - total_samples
|
||||
: min_gap;
|
||||
push_samples(false, gap);
|
||||
}
|
||||
|
||||
// Advances the keying schedule by one sample and returns whether the ident
|
||||
// tone should be sounding right now. Returns false when there is nothing to
|
||||
// send so the caller leaves the ident tone off.
|
||||
bool VorTxProcessor::ident_tone_active() {
|
||||
if (ident_segment_count == 0) return false;
|
||||
|
||||
if (ident_sample_counter == 0)
|
||||
ident_sample_counter = ident_segments[ident_index].length;
|
||||
|
||||
const bool on = ident_segments[ident_index].on;
|
||||
|
||||
if (ident_sample_counter == 0 || --ident_sample_counter == 0) {
|
||||
++ident_index;
|
||||
if (ident_index >= ident_segment_count) ident_index = 0;
|
||||
}
|
||||
|
||||
return on;
|
||||
}
|
||||
|
||||
void VorTxProcessor::on_message(const Message* const msg) {
|
||||
if (msg->id == Message::ID::VorTxConfigure) {
|
||||
vor_tx_config(*reinterpret_cast<const VorTxConfigureMessage*>(msg));
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<VorTxProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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; if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __PROC_VOR_TX_H__
|
||||
#define __PROC_VOR_TX_H__
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
|
||||
// Generates a conventional VOR composite signal as DSB-AM (carrier at LO, Q = 0):
|
||||
// - 30 Hz "variable" tone, amplitude-modulated directly (phase = transmitted radial)
|
||||
// - 9960 Hz subcarrier, FM-modulated (+/- 480 Hz) by a 30 Hz "reference" tone
|
||||
// - optional 1020 Hz identification tone
|
||||
// The radial encoded is the phase difference between the variable and reference 30 Hz tones.
|
||||
class VorTxProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const msg) override;
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 1536000;
|
||||
static constexpr uint64_t phase_period = 1ULL << 32;
|
||||
// Per-sample phase increments (top byte indexes the 256-entry sine table).
|
||||
static constexpr uint32_t delta_30 = static_cast<uint32_t>(30ULL * phase_period / baseband_fs);
|
||||
static constexpr uint32_t delta_sub = static_cast<uint32_t>(9960ULL * phase_period / baseband_fs);
|
||||
static constexpr uint32_t delta_1020 = static_cast<uint32_t>(1020ULL * phase_period / baseband_fs);
|
||||
static constexpr uint32_t delta_480 = static_cast<uint32_t>(480ULL * phase_period / baseband_fs);
|
||||
|
||||
// AM levels (int8 scale). carrier_level + sum of contributions must stay <= 127.
|
||||
static constexpr int32_t carrier_level = 60;
|
||||
static constexpr int32_t var_depth = 18; // ~30% of carrier
|
||||
static constexpr int32_t sub_depth = 18; // ~30% of carrier
|
||||
static constexpr int32_t id_depth = 6; // ~10% of carrier
|
||||
|
||||
// CW identifier keying. Real VOR stations key the 1020 Hz ident tone in
|
||||
// Morse at ~7 words per minute, so the schedule below is expressed in
|
||||
// "dot" time units and rendered to on/off sample runs at config time.
|
||||
static constexpr uint32_t ident_wpm = 7;
|
||||
static constexpr size_t max_ident_segments = 128;
|
||||
|
||||
// VOR ident is transmitted at least every 10 s; repeat the keyed identifier
|
||||
// on a fixed 10 s cycle (ident start to next ident start).
|
||||
static constexpr uint32_t ident_period_samples = 10 * baseband_fs;
|
||||
|
||||
struct IdentSegment {
|
||||
bool on;
|
||||
uint32_t length; // duration in samples
|
||||
};
|
||||
|
||||
uint32_t phase_30{0};
|
||||
uint32_t phase_sub{0};
|
||||
uint32_t phase_id{0};
|
||||
uint32_t radial_offset{0};
|
||||
|
||||
bool ident_enabled{true};
|
||||
bool configured{false};
|
||||
|
||||
char ident_text[8]{};
|
||||
IdentSegment ident_segments[max_ident_segments]{};
|
||||
size_t ident_segment_count{0};
|
||||
size_t ident_index{0};
|
||||
uint32_t ident_sample_counter{0};
|
||||
|
||||
void vor_tx_config(const VorTxConfigureMessage& message);
|
||||
void build_ident_schedule();
|
||||
bool ident_tone_active();
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Transmit};
|
||||
};
|
||||
|
||||
#endif /* __PROC_VOR_TX_H__ */
|
||||
@@ -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),
|
||||
@@ -148,8 +153,8 @@ static struct gpio gpio_h1r9_hw_sync_enable = GPIO(5, 5);
|
||||
#endif
|
||||
// clang-format on
|
||||
|
||||
/* i2c0, i2c1, and i2c_config_si5351c_fast_clock are defined in
|
||||
* hackrf/firmware/common/i2c_bus.c and si5351c.c; use those instead of
|
||||
/* i2c0, i2c1, and i2c_config_fast_clock are defined in
|
||||
* hackrf/firmware/common/i2c_bus.c and i2c_lpc.c; use those instead of
|
||||
* redefining them here to avoid multiple-definition link errors. */
|
||||
|
||||
si5351c_driver_t clock_gen = {
|
||||
@@ -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).
|
||||
@@ -553,7 +567,7 @@ void cpu_clock_init(void) {
|
||||
/* use IRC as clock source for APB3 */
|
||||
CGU_BASE_APB3_CLK = CGU_BASE_APB3_CLK_CLK_SEL(CGU_SRC_IRC);
|
||||
|
||||
i2c_bus_start(clock_gen.bus, &i2c_config_si5351c_fast_clock);
|
||||
i2c_bus_start(clock_gen.bus, &i2c_config_fast_clock);
|
||||
|
||||
si5351c_init(&clock_gen);
|
||||
si5351c_disable_all_outputs(&clock_gen);
|
||||
@@ -561,8 +575,7 @@ void cpu_clock_init(void) {
|
||||
si5351c_power_down_all_clocks(&clock_gen);
|
||||
si5351c_set_crystal_configuration(&clock_gen);
|
||||
si5351c_enable_xo_and_ms_fanout(&clock_gen);
|
||||
si5351c_configure_pll_sources(&clock_gen, PLL_SOURCE_XTAL);
|
||||
si5351c_configure_pll_multisynth(&clock_gen, PLL_SOURCE_XTAL);
|
||||
si5351c_configure_pll_multisynth(&clock_gen, SI5351C_INPUT_XTAL);
|
||||
|
||||
/*
|
||||
* Clocks on HackRF One r9:
|
||||
@@ -615,14 +628,15 @@ void cpu_clock_init(void) {
|
||||
/* Set to 10 MHz, the common rate between Jawbreaker and HackRF One. */
|
||||
sample_rate_set(10000000 * (fp_40_24_t)FP_ONE_HZ, true);
|
||||
|
||||
si5351c_set_clock_source(&clock_gen, PLL_SOURCE_XTAL);
|
||||
si5351c_configure_clock_control(&clock_gen);
|
||||
si5351c_change_input(&clock_gen, SI5351C_INPUT_XTAL);
|
||||
// soft reset
|
||||
si5351c_reset_pll(&clock_gen, SI5351C_PLL_BOTH);
|
||||
si5351c_reset_plls(&clock_gen, SI5351C_PLL_MASK_BOTH);
|
||||
si5351c_enable_clock_outputs(&clock_gen);
|
||||
|
||||
// FIXME disable I2C
|
||||
/* Kick I2C0 down to 400kHz when we switch over to APB1 clock = 204MHz */
|
||||
i2c_bus_start(clock_gen.bus, &i2c_config_si5351c_fast_clock);
|
||||
i2c_bus_start(clock_gen.bus, &i2c_config_fast_clock);
|
||||
|
||||
/*
|
||||
* 12MHz clock is entering LPC XTAL1/OSC input now.
|
||||
@@ -758,7 +772,7 @@ void cpu_clock_init(void) {
|
||||
clock_source_t activate_best_clock_source(void) {
|
||||
#ifdef HACKRF_ONE
|
||||
/* Ensure PortaPack reference oscillator is off while checking for external clock input. */
|
||||
if (portapack_reference_oscillator && portapack()) {
|
||||
if (portapack_present()) {
|
||||
portapack_reference_oscillator(false);
|
||||
}
|
||||
#endif
|
||||
@@ -771,7 +785,7 @@ clock_source_t activate_best_clock_source(void) {
|
||||
} else {
|
||||
#ifdef HACKRF_ONE
|
||||
/* Enable PortaPack reference oscillator (if present), and check for valid clock. */
|
||||
if (portapack_reference_oscillator && portapack()) {
|
||||
if (portapack_present()) {
|
||||
portapack_reference_oscillator(true);
|
||||
delay(510000); /* loop iterations @ 204MHz for >10ms for oscillator to enable. */
|
||||
if (si5351c_clkin_signal_valid(&clock_gen)) {
|
||||
@@ -784,9 +798,9 @@ clock_source_t activate_best_clock_source(void) {
|
||||
/* No external or PortaPack clock was found. Use HackRF Si5351C crystal. */
|
||||
}
|
||||
|
||||
si5351c_set_clock_source(
|
||||
si5351c_change_input(
|
||||
&clock_gen,
|
||||
(source == CLOCK_SOURCE_HACKRF) ? PLL_SOURCE_XTAL : PLL_SOURCE_CLKIN);
|
||||
(source == CLOCK_SOURCE_HACKRF) ? SI5351C_INPUT_XTAL : SI5351C_INPUT_CLKIN);
|
||||
hackrf_ui()->set_clock_source(source);
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user