mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-07-26 10:38:52 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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
|
||||
@@ -512,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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,55 @@ static void portapack_tcxo_disable() {
|
||||
using namespace hackrf::one;
|
||||
|
||||
void ClockManager::init_clock_generator() {
|
||||
#ifdef PRALINE
|
||||
// thoese PIN can review platform_scu file
|
||||
// have many conflict and modify for clock init
|
||||
// P2_10 -> GPIO0[14] P1_CTRL0
|
||||
LPC_SCU->SFSP[2][10] = 0xF0;
|
||||
// P6_8 -> GPIO5[16] P1_CTRL1
|
||||
LPC_SCU->SFSP[6][8] = 0xF4;
|
||||
// P6_9 -> GPIO3[5] P1_CTRL2
|
||||
LPC_SCU->SFSP[6][9] = 0xF0;
|
||||
// P1_20 -> GPIO0[15] CLKIN_CTRL
|
||||
LPC_SCU->SFSP[1][20] = 0xF0;
|
||||
|
||||
constexpr GPIO gpio_p1_ctrl0 = gpio[GPIO0_14];
|
||||
constexpr GPIO gpio_p1_ctrl1 = gpio[GPIO5_16];
|
||||
constexpr GPIO gpio_p1_ctrl2 = gpio[GPIO3_5];
|
||||
constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15];
|
||||
|
||||
gpio_p1_ctrl0.output();
|
||||
gpio_p1_ctrl1.output();
|
||||
gpio_p1_ctrl2.output();
|
||||
gpio_clkin_ctrl.output();
|
||||
|
||||
// P1 control = 100 and choose P22 clock external clock on portapack
|
||||
gpio_p1_ctrl0.write(0);
|
||||
gpio_p1_ctrl1.write(0);
|
||||
gpio_p1_ctrl2.write(1);
|
||||
gpio_clkin_ctrl.write(1);
|
||||
chThdSleepMilliseconds(20);
|
||||
|
||||
#else
|
||||
// HackRF One r9: GPIO0_8 (mcu_clk_en) gates Si5351 CLK2/CLK7 to GP_CLKIN
|
||||
if (hackrf_r9) {
|
||||
gpio_r9_mcu_clk_en.output();
|
||||
gpio_r9_mcu_clk_en.write(1);
|
||||
}
|
||||
#endif
|
||||
|
||||
clock_generator.reset();
|
||||
clock_generator.set_crystal_internal_load_capacitance(CrystalInternalLoadCapacitance::XTAL_CL_8pF);
|
||||
clock_generator.enable_fanout();
|
||||
|
||||
#ifdef PRALINE
|
||||
/* PRALINE has Si5351A (NOT Si5351C like HackRF One OG).
|
||||
* Must use Si5351A configuration: PLLA only, no CLKIN support.
|
||||
*
|
||||
* IMPORTANT: Follow HackRF reference sequence:
|
||||
* 1. Set PLL input sources
|
||||
* 2. Configure PLL and multisynths
|
||||
* 3. Set clock control registers (AFTER multisynths!)
|
||||
* 4. Reset PLLs
|
||||
* 5. Enable outputs
|
||||
*/
|
||||
// change there detect method same as hackrf one
|
||||
// PLLA -> 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 +536,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 +607,14 @@ void ClockManager::init_clock_generator() {
|
||||
|
||||
// Wait for PLL(s) to lock.
|
||||
#ifdef PRALINE
|
||||
// PRALINE: Wait for 0x60 (0x20 | 0x40), PLLA and PLLB to lock (0x20 = LOL_A bit, 0x40 = LOL_B bit)
|
||||
uint8_t device_status_mask = 0x60;
|
||||
uint8_t device_status_mask =
|
||||
(ref_pll == ClockControl::MultiSynthSource::PLLB) ? 0x40 : 0x20;
|
||||
uint32_t pll_timeout = 100000;
|
||||
while ((clock_generator.device_status() & device_status_mask) != 0 && pll_timeout > 0) {
|
||||
pll_timeout--;
|
||||
}
|
||||
// Store PLL lock status for debugging
|
||||
static volatile uint32_t pll_lock_timeout = pll_timeout;
|
||||
(void)pll_lock_timeout;
|
||||
|
||||
clock_generator.enable_output(clock_generator_output_pro_mcu_clkin);
|
||||
|
||||
#else
|
||||
// Wait for PLL(s) to lock - with timeout to prevent hang
|
||||
@@ -753,13 +714,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 +748,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 +811,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 +832,96 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
|
||||
n++;
|
||||
}
|
||||
|
||||
_resampling_n = n;
|
||||
|
||||
// === Stop FPGA processing and flush filters ===
|
||||
fpga_debug_register_write(1, 0x00); // Disable FPGA filters (resets CIC accumulators)
|
||||
|
||||
// Verify we're in RX mode before writing RX registers
|
||||
if (fpga_get_mode() != FPGA_MODE_RX) {
|
||||
// Either set mode or return error
|
||||
fpga_set_mode(FPGA_MODE_RX);
|
||||
/* The TX gateware only implements interpolation factors x1..x16
|
||||
* (tx_intrp 0..4). The rate search above can select n == 5 (x32) for very
|
||||
* low TX sample rates. If we kept afe_rate/CLK0/CLK1 at the x32 rate while
|
||||
* the FPGA only interpolated by x16, the TX datapath would be clocked at
|
||||
* twice the gateware's output rate, doubling the waveform speed. Cap the
|
||||
* AFE rate and _resampling_n to the x16 limit in TX so the clocks and the
|
||||
* interpolation setting stay aligned. */
|
||||
if (radio::debug::get_cached_direction() == rf::Direction::Transmit) {
|
||||
constexpr uint8_t MAX_TX_N = 4; // x16, matches max tx_intrp
|
||||
if (n > MAX_TX_N) {
|
||||
n = MAX_TX_N;
|
||||
afe_rate = frequency << n;
|
||||
}
|
||||
}
|
||||
|
||||
// Set FPGA RX decimation register
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, n);
|
||||
|
||||
/* RX Mode: Register 3 is FPGA_REG_RX_DIGITAL_GAIN.
|
||||
* We shift up by (3 * n) to compensate for CIC bit-growth.
|
||||
* Relationship: ds = (Stages * n) - Offset
|
||||
* For a 3-stage filter, every increment of n grows the signal by 3 bits.
|
||||
* We subtract a baseline offset to keep the signal within 8-bit bounds.
|
||||
* Add a baseline shift to ensure the signal isn't too quiet
|
||||
*/
|
||||
uint8_t ds = (3 * n);
|
||||
ds += 2;
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds);
|
||||
_resampling_n = n;
|
||||
|
||||
radio::invalidate_spi_config();
|
||||
|
||||
// Configure Si5351 clocks
|
||||
// CLK0: AFE_CLK (with r_div=1 for ÷2)
|
||||
// CLK1: SCT_CLK (with r_div=0 for ÷1, runs at 2× AFE for FPGA timing)
|
||||
// Configure Si5351 clocks using the correct AFE VCO
|
||||
// CLK0 always drives the AFE/MAX5864 clock.
|
||||
clock_generator.set_ms_frequency(0, afe_rate * 2, si5351_vco_afe_f, 1);
|
||||
clock_generator.set_ms_frequency(1, afe_rate * 2, si5351_vco_afe_f, 0);
|
||||
|
||||
// === Reset PLL A for phase alignment ===
|
||||
clock_generator.write_register(si5351::Register::PLLReset, 0x20);
|
||||
/* Do not reset PLLA here. On PRALINE, CLK2/MCU_CLKIN is sourced from PLLA,
|
||||
* so a runtime PLLA reset can glitch the LPC43xx peripheral clock tree and
|
||||
* break SGPIO-driven RX apps such as ADSB/APRS. Updating the multisynths is
|
||||
* sufficient for sample-rate changes. */
|
||||
|
||||
// Brief delay for PLL lock and clock stability ===
|
||||
// ~1ms at 96MHz = ~96000 cycles, use 10ms for safety
|
||||
volatile uint32_t delay = 240000; // ~2.5ms
|
||||
while (delay--);
|
||||
if (radio::debug::get_cached_direction() == rf::Direction::Transmit) {
|
||||
/*
|
||||
* TX path:
|
||||
* Baseband generators such as AFSK still synthesize samples at their
|
||||
* legacy logical sample rates (for APRS this is 1.536MHz). On PRALINE,
|
||||
* the FPGA must interpolate those samples up to the active AFE rate.
|
||||
*
|
||||
* The TX datapath is clocked from CLK1/fpgaclk. Therefore CLK1 must
|
||||
* equal the desired complex sample rate at the DAC side. Driving CLK1
|
||||
* at 2x the intended TX sample rate causes the entire APRS waveform to
|
||||
* run twice as fast, which matches the "energy present, undecodable"
|
||||
* symptom seen on HackRF One receivers.
|
||||
*
|
||||
* Gateware mapping from standard.py:
|
||||
* tx_intrp = 0 -> x1
|
||||
* tx_intrp = 1 -> x2
|
||||
* tx_intrp = 2 -> x4
|
||||
* tx_intrp = 3 -> x8
|
||||
* tx_intrp = 4 -> x16
|
||||
*/
|
||||
if (fpga_get_mode() != FPGA_MODE_TX) {
|
||||
fpga_set_mode(FPGA_MODE_TX);
|
||||
}
|
||||
|
||||
// Re-enable FPGA processing with clean state ===
|
||||
fpga_debug_register_write(1, 0x01);
|
||||
// TX gateware consumes complex samples at the rate presented on CLK1.
|
||||
clock_generator.set_ms_frequency(1, afe_rate, si5351_vco_afe_f, 0);
|
||||
|
||||
uint8_t tx_interp = 0;
|
||||
uint32_t tx_rate = frequency;
|
||||
while ((tx_rate < afe_rate) && (tx_interp < 4)) {
|
||||
tx_rate <<= 1;
|
||||
tx_interp++;
|
||||
}
|
||||
fpga_tx_set_interpolation(tx_interp);
|
||||
fpga_tx_set_nco_enable(false);
|
||||
fpga_tx_set_phase_step(0);
|
||||
}
|
||||
// for RX mode
|
||||
else {
|
||||
// RX path keeps CLK1 at 2x AFE for receive timing / SGPIO alignment.
|
||||
clock_generator.set_ms_frequency(1, afe_rate * 2, si5351_vco_afe_f, 0);
|
||||
|
||||
// === Stop FPGA processing and flush filters ===
|
||||
fpga_debug_register_write(1, 0x00); // Disable FPGA filters (resets CIC accumulators)
|
||||
|
||||
if (fpga_get_mode() != FPGA_MODE_RX) {
|
||||
fpga_set_mode(FPGA_MODE_RX);
|
||||
}
|
||||
|
||||
// Set FPGA RX decimation register
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, n);
|
||||
|
||||
/* RX Mode: Register 3 is FPGA_REG_RX_DIGITAL_GAIN.
|
||||
* We shift up by (3 * n) to compensate for CIC bit-growth.
|
||||
*/
|
||||
uint8_t ds = (3 * n);
|
||||
ds += 2;
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds);
|
||||
|
||||
// Re-enable FPGA processing with clean state ===
|
||||
fpga_debug_register_write(1, 0x01);
|
||||
}
|
||||
|
||||
#else
|
||||
/* Codec clock is at sampling frequency, CPLD and SGPIO clocks are at
|
||||
@@ -1210,3 +1216,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
|
||||
|
||||
+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__
|
||||
+7
-1
@@ -327,7 +327,7 @@ set(EXTCPPSRC
|
||||
external/rtty_tx/ui_rtty_tx.cpp
|
||||
external/rtty_tx/baudot.cpp
|
||||
|
||||
#pocsag_tx
|
||||
#pocsag_tx
|
||||
external/pocsag_tx/main.cpp
|
||||
external/pocsag_tx/ui_pocsag_tx.cpp
|
||||
|
||||
@@ -362,6 +362,11 @@ set(EXTCPPSRC
|
||||
#hard_reset
|
||||
external/hard_reset/main.cpp
|
||||
external/hard_reset/ui_hard_reset.cpp
|
||||
|
||||
#secplustx
|
||||
external/secplustx/main.cpp
|
||||
external/secplustx/ui_secplustx.cpp
|
||||
external/secplustx/secplustx.cpp
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -451,6 +456,7 @@ set(EXTAPPLIST
|
||||
two_tone_pager
|
||||
two_tone_rx
|
||||
hard_reset
|
||||
secplustx
|
||||
)
|
||||
|
||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
||||
|
||||
+7
@@ -110,6 +110,7 @@ MEMORY
|
||||
ram_external_app_two_tone_rx (rwx) : org = 0xAE050000, len = 32k
|
||||
ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k
|
||||
ram_external_app_hard_reset (rwx) : org = 0xAE070000, len = 32k
|
||||
ram_external_app_secplustx (rwx) : org = 0xAE080000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -635,4 +636,10 @@ SECTIONS
|
||||
KEEP(*(.external_app.app_hard_reset.application_information));
|
||||
*(*ui*external_app*hard_reset*);
|
||||
} > ram_external_app_hard_reset
|
||||
|
||||
.external_app_secplustx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_secplustx.application_information));
|
||||
*(*ui*external_app*secplustx*);
|
||||
} > ram_external_app_secplustx
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -280,9 +283,9 @@ void set_direction(const rf::Direction new_direction) {
|
||||
baseband_codec.set_mode((direction == rf::Direction::Transmit) ? max5864::Mode::Transmit : max5864::Mode::Receive);
|
||||
|
||||
if (direction == rf::Direction::Receive)
|
||||
led_rx.on();
|
||||
led_rx.setActive();
|
||||
else
|
||||
led_tx.on();
|
||||
led_tx.setActive();
|
||||
}
|
||||
|
||||
bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
@@ -390,11 +393,9 @@ void set_baseband_rate(const uint32_t rate) {
|
||||
}
|
||||
|
||||
void set_antenna_bias(const bool on) {
|
||||
/* Pull MOSFET gate low to turn on antenna bias. */
|
||||
#ifdef PRALINE
|
||||
// Praline: P2_12 = GPIO1[12], ANT_BIAS_EN_N (active LOW)
|
||||
LPC_GPIO->CLR[1] = on ? (1 << 12) : 0;
|
||||
LPC_GPIO->SET[1] = on ? 0 : (1 << 12);
|
||||
/* Praline: P2_12 = GPIO1[12], ANT_BIAS_EN_N (active LOW) */
|
||||
rf_path.set_ant_bias(on);
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
gpio_r9_not_ant_pwr.write(on ? 0 : 1);
|
||||
@@ -444,8 +445,8 @@ void disable() {
|
||||
first_if.disable();
|
||||
set_rf_amp(false);
|
||||
|
||||
led_rx.off();
|
||||
led_tx.off();
|
||||
led_rx.setInactive();
|
||||
led_tx.setInactive();
|
||||
}
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -627,4 +628,4 @@ uint32_t register_read(const size_t register_number) {
|
||||
|
||||
} /* namespace debug */
|
||||
|
||||
} /* namespace radio */
|
||||
} /* namespace radio */
|
||||
|
||||
@@ -46,14 +46,6 @@ struct PralineConfig {
|
||||
bool rf_amp_en;
|
||||
bool ant_bias_en_n; // Inverted: 0 = bias enabled
|
||||
|
||||
static void gpio_init() {
|
||||
gpio_tx_enable.output();
|
||||
gpio_mix_bypass.output();
|
||||
gpio_lpf_enable.output();
|
||||
gpio_rf_amp_enable.output();
|
||||
gpio_ant_bias_disable.output();
|
||||
}
|
||||
|
||||
void apply() const {
|
||||
gpio_tx_enable.write(tx_en);
|
||||
gpio_mix_bypass.write(mix_bypass); // Control RF path mixer
|
||||
@@ -231,7 +223,6 @@ constexpr Config get_config(
|
||||
|
||||
void Path::init() {
|
||||
#ifdef PRALINE
|
||||
PralineConfig::gpio_init();
|
||||
/* Set safe initial state: RX mode, mixer enabled, LPF on, amp off, no bias */
|
||||
PralineConfig config = {
|
||||
.tx_en = false,
|
||||
@@ -263,6 +254,15 @@ void Path::set_rf_amp(const bool new_rf_amp) {
|
||||
update();
|
||||
}
|
||||
|
||||
void Path::set_ant_bias(const bool new_ant_bias) {
|
||||
ant_bias = new_ant_bias;
|
||||
update();
|
||||
}
|
||||
|
||||
bool Path::get_ant_bias() const {
|
||||
return ant_bias;
|
||||
}
|
||||
|
||||
void Path::update() {
|
||||
/* 0 ^ 0 => 0 & 0 = 0 ^ 0 = 0 (no change)
|
||||
* 0 ^ 1 => 1 & 0 = 0 ^ 0 = 0 (ignore change to 1)
|
||||
@@ -301,8 +301,8 @@ void Path::update() {
|
||||
/* RF amp when amplification requested */
|
||||
config.rf_amp_en = rf_amp;
|
||||
|
||||
/* Antenna bias off by default */
|
||||
config.ant_bias_en_n = true;
|
||||
/* Antenna bias */
|
||||
config.ant_bias_en_n = !ant_bias;
|
||||
|
||||
config.apply();
|
||||
|
||||
|
||||
@@ -65,13 +65,15 @@ class Path {
|
||||
void set_direction(const Direction direction);
|
||||
void set_band(const Band band);
|
||||
void set_rf_amp(const bool rf_amp);
|
||||
|
||||
void set_ant_bias(const bool ant_bias);
|
||||
bool get_ant_bias() const;
|
||||
Band get_band() const { return _band; } //_band is used solely for debugging purposes.
|
||||
|
||||
private:
|
||||
Direction direction{Direction::Receive};
|
||||
Band band{Band::Mid};
|
||||
bool rf_amp{false};
|
||||
bool ant_bias{false};
|
||||
|
||||
void update();
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -73,6 +73,8 @@
|
||||
#include "file_path.hpp"
|
||||
#include "ff.h"
|
||||
|
||||
#include "i2cdev_max17055.hpp"
|
||||
|
||||
#include <locale>
|
||||
#include <codecvt>
|
||||
|
||||
@@ -334,10 +336,10 @@ void SystemStatusView::on_battery_data(const BatteryStateMessage* msg) {
|
||||
was_charging = msg->on_charger;
|
||||
|
||||
if (!pmem::ui_hide_numeric_battery()) {
|
||||
battery_text.set_battery(msg->valid_mask, msg->percent, msg->on_charger);
|
||||
battery_text.set_battery(msg->valid_mask, msg->percent, msg->on_charger, msg->battMayChanged);
|
||||
}
|
||||
if (!pmem::ui_hide_battery_icon()) {
|
||||
battery_icon.set_battery(msg->valid_mask, msg->percent, msg->on_charger);
|
||||
battery_icon.set_battery(msg->valid_mask, msg->percent, msg->on_charger, msg->battMayChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,6 +882,10 @@ void SystemMenuView::hackrf_mode(NavigationView& nav) {
|
||||
YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice) {
|
||||
i2cdev::I2cDev_MAX17055* dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
|
||||
if (dev) {
|
||||
dev->sleep_config(false); // don't enable sleep even if no i2c communication. so we won't lose any data. on reboot (like exit hackrf mode) we set it to true again.
|
||||
}
|
||||
EventDispatcher::request_stop();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -157,8 +157,7 @@ static void cmd_flash(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
// call nav with flash
|
||||
auto open_view = nav->push<ui::FlashUtilityView>();
|
||||
chprintf(chp, "Flashing started\r\n");
|
||||
chThdSleepMilliseconds(150); // to give display some time to paint the screen
|
||||
open_view->wait_till_loaded(); // also wait for first frame sync
|
||||
chThdSleepMilliseconds(150); // to give display some time to paint the screen
|
||||
if (!open_view->flash_firmware(path.native())) {
|
||||
chprintf(chp, "error\r\n");
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
@@ -776,6 +782,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 +813,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{};
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "platform_scu.h"
|
||||
#include "fixed_point.h"
|
||||
#include "clkin.h"
|
||||
#include "cpu_clock.h"
|
||||
#include "usb_type.h"
|
||||
#include <libopencm3/lpc43xx/cgu.h>
|
||||
#include <libopencm3/lpc43xx/ccu.h>
|
||||
@@ -51,6 +52,10 @@
|
||||
|
||||
#include "gpio_lpc.h"
|
||||
|
||||
/* Referenced by the hackrf common delay.c. The Mayhem firmware already runs
|
||||
* the CPU at 204MHz when this baseband image is started. */
|
||||
unsigned int cpu_clock_mhz = 204;
|
||||
|
||||
/* GPIO Output PinMux */
|
||||
static struct gpio gpio_led[] = {
|
||||
GPIO(2, 1),
|
||||
@@ -492,6 +497,9 @@ static void cpu_clock_pll1_max_speed(void) {
|
||||
reg_val |= CGU_BASE_M4_CLK_CLK_SEL(CGU_SRC_IRC) | CGU_BASE_M4_CLK_AUTOBLOCK(1);
|
||||
CGU_BASE_M4_CLK = reg_val;
|
||||
|
||||
/* CPU is now at 12MHz */
|
||||
cpu_clock_mhz = 12;
|
||||
|
||||
/* 2. Enable the crystal oscillator. */
|
||||
CGU_XTAL_OSC_CTRL &= ~CGU_XTAL_OSC_CTRL_ENABLE_MASK;
|
||||
|
||||
@@ -537,11 +545,17 @@ static void cpu_clock_pll1_max_speed(void) {
|
||||
reg_val |= CGU_BASE_M4_CLK_CLK_SEL(CGU_SRC_PLL1);
|
||||
CGU_BASE_M4_CLK = reg_val;
|
||||
|
||||
/* CPU is now at 102MHz */
|
||||
cpu_clock_mhz = 102;
|
||||
|
||||
/* 9. Wait 50us. */
|
||||
delay_us_at_mhz(50, 102);
|
||||
|
||||
/* 10. Set the PLL1 P-divider to direct output mode (DIRECT=1). */
|
||||
CGU_PLL1_CTRL |= CGU_PLL1_CTRL_DIRECT_MASK;
|
||||
|
||||
/* CPU is now at 204MHz */
|
||||
cpu_clock_mhz = 204;
|
||||
}
|
||||
|
||||
/* clock startup for LPC4320 configure PLL1 to max speed (204MHz).
|
||||
@@ -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)) {
|
||||
|
||||
@@ -26,11 +26,12 @@
|
||||
#include <libopencm3/lpc43xx/scu.h>
|
||||
#include <libopencm3/lpc43xx/rgu.h>
|
||||
#include <libopencm3/lpc43xx/wwdt.h>
|
||||
#include "delay.h"
|
||||
#include "string.h"
|
||||
|
||||
volatile bool usb_bulk_block_done = false;
|
||||
|
||||
void delay(uint32_t duration);
|
||||
|
||||
void usb_bulk_block_cb(void* user_data, unsigned int bytes_transferred) {
|
||||
usb_bulk_block_done = true;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,12 +33,10 @@
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
void boardInit(void);
|
||||
void boardInit(void);
|
||||
|
||||
void configure_pins_portapack(void);
|
||||
void configure_pins_portapack(void);
|
||||
|
||||
void vaa_power_on(void);
|
||||
void vaa_power_off(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
// ============================================================================
|
||||
static fpga_mode_t current_mode = FPGA_MODE_OFF;
|
||||
// Cached register values for debug reads (since reads may require mode switch)
|
||||
static uint8_t fpga_reg_cache[6] = {0, 0x01, 0x00, 0x00, 0x00, 0x00};
|
||||
static uint8_t fpga_reg_cache[7] = {0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
// Context structure for SPIFI-based reading
|
||||
struct spifi_fpga_read_ctx {
|
||||
@@ -198,34 +198,6 @@ static void ssp1_init_ice40(void) {
|
||||
SSP1_CR1_LOCAL = SSP_CR1_SSE;
|
||||
}
|
||||
|
||||
// Configure SSP1 pins via SCU
|
||||
static void configure_ssp1_pins(void) {
|
||||
// P1_3 = SSP1_MISO (function 5)
|
||||
MMIO32_LOCAL(SCU_SSP1_CIPO_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION5_LOCAL;
|
||||
// P1_4 = SSP1_MOSI (function 5)
|
||||
MMIO32_LOCAL(SCU_SSP1_COPI_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION5_LOCAL;
|
||||
// P1_19 = SSP1_SCK (function 1)
|
||||
MMIO32_LOCAL(SCU_SSP1_SCK_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION1_LOCAL;
|
||||
}
|
||||
|
||||
// Configure FPGA control pins via SCU and GPIO
|
||||
static void configure_fpga_control_pins(void) {
|
||||
// P5_2 = GPIO2[11] = CRESET (function 0, output)
|
||||
MMIO32_LOCAL(SCU_FPGA_CRESET_LOCAL) = SCU_GPIO_NOPULL_LOCAL | SCU_CONF_FUNCTION0_LOCAL;
|
||||
// P4_10 = GPIO5[14] = CDONE (function 4, input with pullup)
|
||||
MMIO32_LOCAL(SCU_FPGA_CDONE_LOCAL) = SCU_GPIO_PUP_LOCAL | SCU_CONF_FUNCTION4_LOCAL;
|
||||
// P5_1 = GPIO2[10] = SPI_CS (function 0, output)
|
||||
MMIO32_LOCAL(SCU_FPGA_SPI_CS_LOCAL) = SCU_GPIO_NOPULL_LOCAL | SCU_CONF_FUNCTION0_LOCAL;
|
||||
|
||||
// Set CRESET and SPI_CS as outputs (GPIO2[11] and GPIO2[10])
|
||||
GPIO_DIR(FPGA_CRESET_PORT) |= (1 << FPGA_CRESET_PIN) | (1 << FPGA_SPI_CS_PIN);
|
||||
// Clear both initially
|
||||
GPIO_CLR(FPGA_CRESET_PORT) = (1 << FPGA_CRESET_PIN) | (1 << FPGA_SPI_CS_PIN);
|
||||
|
||||
// CDONE is input (GPIO5[14])
|
||||
GPIO_DIR(FPGA_CDONE_PORT) &= ~(1 << FPGA_CDONE_PIN);
|
||||
}
|
||||
|
||||
// GPIO control helpers
|
||||
static void fpga_creset_low(void) {
|
||||
GPIO_CLR(FPGA_CRESET_PORT) = (1 << FPGA_CRESET_PIN);
|
||||
@@ -315,7 +287,7 @@ static void fpga_spi_write(uint8_t reg, uint8_t value) {
|
||||
// Public function to read FPGA register (callable from C++ application code)
|
||||
// Switches SPI mode, reads register, switches back
|
||||
uint8_t fpga_debug_register_read(uint8_t reg) {
|
||||
if (reg == 0 || reg > 5) return 0xFF;
|
||||
if (reg == 0 || reg > 6) return 0xFF;
|
||||
|
||||
uint8_t value;
|
||||
ssp1_set_mode_ice40();
|
||||
@@ -328,7 +300,7 @@ uint8_t fpga_debug_register_read(uint8_t reg) {
|
||||
|
||||
// Public function to write FPGA register (callable from C++ application code)
|
||||
void fpga_debug_register_write(uint8_t reg, uint8_t value) {
|
||||
if (reg == 0 || reg > 5) return;
|
||||
if (reg == 0 || reg > 6) return;
|
||||
|
||||
ssp1_set_mode_ice40();
|
||||
fpga_spi_write(reg, value);
|
||||
@@ -342,7 +314,7 @@ void fpga_debug_register_write(uint8_t reg, uint8_t value) {
|
||||
// ============================================================================
|
||||
|
||||
uint8_t fpga_register_read(uint8_t reg) {
|
||||
if (reg == 0 || reg > 5) return 0xFF;
|
||||
if (reg == 0 || reg > 6) return 0xFF;
|
||||
|
||||
ssp1_set_mode_ice40();
|
||||
uint8_t val = fpga_spi_read(reg);
|
||||
@@ -353,7 +325,7 @@ uint8_t fpga_register_read(uint8_t reg) {
|
||||
}
|
||||
|
||||
void fpga_register_write(uint8_t reg, uint8_t value) {
|
||||
if (reg == 0 || reg > 5) return;
|
||||
if (reg == 0 || reg > 6) return;
|
||||
|
||||
ssp1_set_mode_ice40();
|
||||
fpga_spi_write(reg, value);
|
||||
@@ -422,22 +394,22 @@ void fpga_rx_set_dc_adapt_rate(uint8_t rate) {
|
||||
/* TX Functions with mode assertion */
|
||||
void fpga_tx_set_nco_enable(bool enable) {
|
||||
if (current_mode != FPGA_MODE_TX) return;
|
||||
uint8_t val = fpga_register_read(FPGA_REG_SHARED_3);
|
||||
uint8_t val = fpga_register_read(FPGA_REG3_TX_NCO_CTRL);
|
||||
if (enable)
|
||||
val |= FPGA_TX_NCO_EN;
|
||||
else
|
||||
val &= ~FPGA_TX_NCO_EN;
|
||||
fpga_register_write(FPGA_REG_SHARED_3, val);
|
||||
fpga_register_write(FPGA_REG3_TX_NCO_CTRL, val);
|
||||
}
|
||||
|
||||
void fpga_tx_set_interpolation(uint8_t ratio) {
|
||||
if (current_mode != FPGA_MODE_TX) return;
|
||||
fpga_register_write(FPGA_REG_SHARED_4, ratio & FPGA_TX_INTERP_MASK);
|
||||
fpga_register_write(FPGA_REG_TX_INTERP, ratio & FPGA_TX_INTERP_MASK);
|
||||
}
|
||||
|
||||
void fpga_tx_set_phase_step(uint8_t step) {
|
||||
if (current_mode != FPGA_MODE_TX) return;
|
||||
fpga_register_write(FPGA_REG_SHARED_5, step);
|
||||
fpga_register_write(FPGA_REG_TX_PHASE_STEP, step);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -578,12 +550,6 @@ int fpga_bridge_init(uint8_t* mem_base) {
|
||||
CGU_BASE_SSP1_CLK = CGU_BASE_SSP1_CLK_AUTOBLOCK(1) |
|
||||
CGU_BASE_SSP1_CLK_CLK_SEL(CGU_SRC_PLL1);
|
||||
|
||||
// Configure SSP1 pins
|
||||
configure_ssp1_pins();
|
||||
|
||||
// Configure FPGA control pins
|
||||
configure_fpga_control_pins();
|
||||
|
||||
// Initialize SSP1 for iCE40 programming
|
||||
ssp1_init_ice40();
|
||||
|
||||
|
||||
@@ -16,17 +16,21 @@ extern "C" {
|
||||
|
||||
#ifdef PRALINE
|
||||
|
||||
/* FPGA Register Map Address 0x03 (Dual Purpose) */
|
||||
/* RX path (legacy PRALINE software map kept for compatibility) */
|
||||
#define FPGA_REG_RX_DIGITAL_GAIN 0x03 /* Digital Shift / scaling (RX Mode) */
|
||||
#define FPGA_REG_TX_CONTROL 0x03 /* NCO_EN and TX flags (TX Mode) */
|
||||
|
||||
/* FPGA Register Map Address 0x04 (Shared) */
|
||||
#define FPGA_REG_RX_DC_BLOCK_WIDTH 0x04 /* Notch filter cutoff (RX Mode) */
|
||||
#define FPGA_REG_TX_INTERP 0x04 /* Interpolation ratio (TX Mode) */
|
||||
|
||||
/* FPGA Register Map Address 0x05 (Shared) */
|
||||
#define FPGA_REG_RX_DC_ADAPT_RATE 0x05 /* Settle time/Integration (RX Mode) */
|
||||
#define FPGA_REG_TX_PHASE_STEP 0x05 /* NCO frequency step (TX Mode) */
|
||||
|
||||
/*
|
||||
* TX path must match the currently built PRALINE standard gateware in
|
||||
* hackrf/firmware/fpga/top/standard.py:
|
||||
* 0x04 tx_ctrl
|
||||
* 0x05 tx_intrp
|
||||
* 0x06 tx_pstep
|
||||
*/
|
||||
#define FPGA_REG_TX_CONTROL 0x04
|
||||
#define FPGA_REG_TX_INTERP 0x05
|
||||
#define FPGA_REG_TX_PHASE_STEP 0x06
|
||||
|
||||
/*
|
||||
* FPGA Operating Mode
|
||||
@@ -39,13 +43,21 @@ typedef enum {
|
||||
|
||||
/*
|
||||
* FPGA Register Addresses
|
||||
* Note: Registers 3-5 are dual-purpose (meaning depends on RX/TX mode)
|
||||
* NOTE:
|
||||
* The currently loaded PRALINE standard gateware uses:
|
||||
* 0x01 CTRL
|
||||
* 0x02 RX_DECIM
|
||||
* 0x03 RX_DIGITAL_GAIN (RX) / TX_NCO_CTRL (TX)
|
||||
* 0x04 TX_CTRL
|
||||
* 0x05 TX_INTRP
|
||||
* 0x06 TX_PSTEP
|
||||
*/
|
||||
#define FPGA_REG_CTRL 0x01 /* Control register */
|
||||
#define FPGA_REG_DECIM 0x02 /* Decimation (RX) / unused (TX) */
|
||||
#define FPGA_REG_SHARED_3 0x03 /* Dual-purpose register */
|
||||
#define FPGA_REG_SHARED_4 0x04 /* Dual-purpose register */
|
||||
#define FPGA_REG_SHARED_5 0x05 /* Dual-purpose register */
|
||||
#define FPGA_REG_DECIM 0x02 /* RX decimation */
|
||||
#define FPGA_REG_SHARED_3 0x03 /* Legacy shared register */
|
||||
#define FPGA_REG_SHARED_4 0x04 /* Legacy shared register */
|
||||
#define FPGA_REG_SHARED_5 0x05 /* Legacy shared register */
|
||||
#define FPGA_REG_SHARED_6 0x06 /* TX phase step */
|
||||
|
||||
/*
|
||||
* Register 1 (CTRL) Bit Definitions
|
||||
@@ -77,7 +89,7 @@ typedef enum {
|
||||
#define FPGA_RX_DC_WIDTH_MASK 0x07 /* Bits [2:0] */
|
||||
|
||||
/* TX Mode: Interpolation ratio */
|
||||
#define FPGA_REG4_TX_INTERP 0x04
|
||||
#define FPGA_REG4_TX_INTERP 0x05
|
||||
#define FPGA_TX_INTERP_MASK 0x07 /* Bits [2:0] */
|
||||
|
||||
/*
|
||||
@@ -88,7 +100,7 @@ typedef enum {
|
||||
#define FPGA_RX_DC_RATE_MASK 0xFF /* Bits [7:0] */
|
||||
|
||||
/* TX Mode: NCO phase step (frequency) */
|
||||
#define FPGA_REG5_TX_PHASE_STEP 0x05
|
||||
#define FPGA_REG5_TX_PHASE_STEP 0x06
|
||||
#define FPGA_TX_PHASE_STEP_MASK 0xFF /* Bits [7:0] */
|
||||
|
||||
/* Export default values so other methods can use them */
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
/* Driver exported functions. */
|
||||
/*===========================================================================*/
|
||||
|
||||
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
|
||||
|
||||
/**
|
||||
* @brief LPC43xx I/O ports configuration.
|
||||
* @details Ports 0 through 8.
|
||||
@@ -62,15 +60,15 @@
|
||||
*
|
||||
* @notapi
|
||||
*/
|
||||
void _pal_lld_init(const PALConfig *config) {
|
||||
for(size_t i=0; i<8; i++) {
|
||||
LPC_GPIO->PIN[i] = config->P[i].data;
|
||||
LPC_GPIO->DIR[i] = config->P[i].dir;
|
||||
}
|
||||
void _pal_lld_init(const PALConfig* config) {
|
||||
for (size_t i = 0; i < 8; i++) {
|
||||
LPC_GPIO->PIN[i] = config->P[i].data;
|
||||
LPC_GPIO->DIR[i] = config->P[i].dir;
|
||||
}
|
||||
|
||||
for(size_t i=0; i<ARRAY_SIZE(config->SCU); i++) {
|
||||
LPC_SCU->SFSP[config->SCU[i].port][config->SCU[i].pin] = config->SCU[i].config.word;
|
||||
}
|
||||
for (size_t i = 0; i < SCU_ARRAY_SIZE; i++) {
|
||||
LPC_SCU->SFSP[config->SCU[i].port][config->SCU[i].pin] = config->SCU[i].config.word;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,14 +85,14 @@ void _pal_lld_init(const PALConfig *config) {
|
||||
void _pal_lld_setgroupmode(ioportid_t port,
|
||||
ioportmask_t mask,
|
||||
iomode_t mode) {
|
||||
/* TODO: Handling pull-up, pull-down modes not implemented, as it would
|
||||
* require a map from GPIO to SCU pins. Not today.
|
||||
*/
|
||||
if( mode == PAL_MODE_OUTPUT_PUSHPULL ) {
|
||||
LPC_GPIO->DIR[port] |= mask;
|
||||
} else {
|
||||
LPC_GPIO->DIR[port] &= ~mask;
|
||||
}
|
||||
/* TODO: Handling pull-up, pull-down modes not implemented, as it would
|
||||
* require a map from GPIO to SCU pins. Not today.
|
||||
*/
|
||||
if (mode == PAL_MODE_OUTPUT_PUSHPULL) {
|
||||
LPC_GPIO->DIR[port] |= mask;
|
||||
} else {
|
||||
LPC_GPIO->DIR[port] &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* HAL_USE_PAL */
|
||||
|
||||
@@ -50,10 +50,18 @@
|
||||
* @note Implementations may extend this structure to contain more,
|
||||
* architecture dependent, fields.
|
||||
*/
|
||||
|
||||
#ifdef PRALINE
|
||||
#define SCU_ARRAY_SIZE 78
|
||||
#else
|
||||
#define SCU_ARRAY_SIZE 56
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
/** @brief GPIO setup data.*/
|
||||
gpio_setup_t P[8];
|
||||
scu_setup_t SCU[60];
|
||||
/** @brief GPIO setup data.*/
|
||||
gpio_setup_t P[8];
|
||||
scu_setup_t SCU[SCU_ARRAY_SIZE];
|
||||
|
||||
} PALConfig;
|
||||
|
||||
/**
|
||||
@@ -94,50 +102,50 @@ typedef uint8_t iopadid_t;
|
||||
/**
|
||||
* @brief GPIO0 port identifier.
|
||||
*/
|
||||
#define IOPORT1 0
|
||||
#define GPIO0 0
|
||||
#define IOPORT1 0
|
||||
#define GPIO0 0
|
||||
|
||||
/**
|
||||
* @brief GPIO1 port identifier.
|
||||
*/
|
||||
#define IOPORT2 1
|
||||
#define GPIO1 1
|
||||
#define IOPORT2 1
|
||||
#define GPIO1 1
|
||||
|
||||
/**
|
||||
* @brief GPIO1 port identifier.
|
||||
*/
|
||||
#define IOPORT3 2
|
||||
#define GPIO2 2
|
||||
#define IOPORT3 2
|
||||
#define GPIO2 2
|
||||
|
||||
/**
|
||||
* @brief GPIO1 port identifier.
|
||||
*/
|
||||
#define IOPORT4 3
|
||||
#define GPIO3 3
|
||||
#define IOPORT4 3
|
||||
#define GPIO3 3
|
||||
|
||||
/**
|
||||
* @brief GPIO1 port identifier.
|
||||
*/
|
||||
#define IOPORT5 4
|
||||
#define GPIO4 4
|
||||
#define IOPORT5 4
|
||||
#define GPIO4 4
|
||||
|
||||
/**
|
||||
* @brief GPIO1 port identifier.
|
||||
*/
|
||||
#define IOPORT6 5
|
||||
#define GPIO5 5
|
||||
#define IOPORT6 5
|
||||
#define GPIO5 5
|
||||
|
||||
/**
|
||||
* @brief GPIO1 port identifier.
|
||||
*/
|
||||
#define IOPORT7 6
|
||||
#define GPIO6 6
|
||||
#define IOPORT7 6
|
||||
#define GPIO6 6
|
||||
|
||||
/**
|
||||
* @brief GPIO1 port identifier.
|
||||
*/
|
||||
#define IOPORT8 7
|
||||
#define GPIO7 7
|
||||
#define IOPORT8 7
|
||||
#define GPIO7 7
|
||||
|
||||
/*===========================================================================*/
|
||||
/* Implementation, some of the following macros could be implemented as */
|
||||
@@ -237,7 +245,7 @@ typedef uint8_t iopadid_t;
|
||||
*
|
||||
* @notapi
|
||||
*/
|
||||
//#define pal_lld_readgroup(port, mask, offset) 0
|
||||
// #define pal_lld_readgroup(port, mask, offset) 0
|
||||
|
||||
/**
|
||||
* @brief Writes a group of bits.
|
||||
@@ -253,7 +261,7 @@ typedef uint8_t iopadid_t;
|
||||
*
|
||||
* @notapi
|
||||
*/
|
||||
//#define pal_lld_writegroup(port, mask, offset, bits) (void)bits
|
||||
// #define pal_lld_writegroup(port, mask, offset, bits) (void)bits
|
||||
|
||||
/**
|
||||
* @brief Pads group mode setup.
|
||||
@@ -268,8 +276,8 @@ typedef uint8_t iopadid_t;
|
||||
*
|
||||
* @notapi
|
||||
*/
|
||||
#define pal_lld_setgroupmode(port, mask, offset, mode) \
|
||||
_pal_lld_setgroupmode(port, mask << offset, mode)
|
||||
#define pal_lld_setgroupmode(port, mask, offset, mode) \
|
||||
_pal_lld_setgroupmode(port, mask << offset, mode)
|
||||
|
||||
/**
|
||||
* @brief Reads a logical state from an I/O pad.
|
||||
@@ -285,8 +293,8 @@ typedef uint8_t iopadid_t;
|
||||
*
|
||||
* @notapi
|
||||
*/
|
||||
#define pal_lld_readpad(port, pad) \
|
||||
(LPC_GPIO->B[((port) * 32) + (pad)])
|
||||
#define pal_lld_readpad(port, pad) \
|
||||
(LPC_GPIO->B[((port) * 32) + (pad)])
|
||||
|
||||
/**
|
||||
* @brief Writes a logical state on an output pad.
|
||||
@@ -303,8 +311,8 @@ typedef uint8_t iopadid_t;
|
||||
*
|
||||
* @notapi
|
||||
*/
|
||||
#define pal_lld_writepad(port, pad, bit) \
|
||||
((LPC_GPIO->B[((port) * 32) + (pad)]) = (bit))
|
||||
#define pal_lld_writepad(port, pad, bit) \
|
||||
((LPC_GPIO->B[((port) * 32) + (pad)]) = (bit))
|
||||
|
||||
/**
|
||||
* @brief Sets a pad logical state to @p PAL_HIGH.
|
||||
@@ -317,8 +325,8 @@ typedef uint8_t iopadid_t;
|
||||
*
|
||||
* @notapi
|
||||
*/
|
||||
#define pal_lld_setpad(port, pad) \
|
||||
(LPC_GPIO->SET[(port)] = 1 << (pad))
|
||||
#define pal_lld_setpad(port, pad) \
|
||||
(LPC_GPIO->SET[(port)] = 1 << (pad))
|
||||
|
||||
/**
|
||||
* @brief Clears a pad logical state to @p PAL_LOW.
|
||||
@@ -331,7 +339,7 @@ typedef uint8_t iopadid_t;
|
||||
*
|
||||
* @notapi
|
||||
*/
|
||||
#define pal_lld_clearpad(port, pad) \
|
||||
#define pal_lld_clearpad(port, pad) \
|
||||
(LPC_GPIO->CLR[(port)] = 1 << (pad))
|
||||
|
||||
/**
|
||||
@@ -345,8 +353,8 @@ typedef uint8_t iopadid_t;
|
||||
*
|
||||
* @notapi
|
||||
*/
|
||||
#define pal_lld_togglepad(port, pad) \
|
||||
(LPC_GPIO->NOT[(port)] = 1 << (pad))
|
||||
#define pal_lld_togglepad(port, pad) \
|
||||
(LPC_GPIO->NOT[(port)] = 1 << (pad))
|
||||
|
||||
/**
|
||||
* @brief Pad mode setup.
|
||||
@@ -362,7 +370,7 @@ typedef uint8_t iopadid_t;
|
||||
*
|
||||
* @notapi
|
||||
*/
|
||||
//#define pal_lld_setpadmode(port, pad, mode)
|
||||
// #define pal_lld_setpadmode(port, pad, mode)
|
||||
|
||||
#if !defined(__DOXYGEN__)
|
||||
extern const PALConfig pal_default_config;
|
||||
@@ -371,10 +379,10 @@ extern const PALConfig pal_default_config;
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
void _pal_lld_init(const PALConfig *config);
|
||||
void _pal_lld_setgroupmode(ioportid_t port,
|
||||
ioportmask_t mask,
|
||||
iomode_t mode);
|
||||
void _pal_lld_init(const PALConfig* config);
|
||||
void _pal_lld_setgroupmode(ioportid_t port,
|
||||
ioportmask_t mask,
|
||||
iomode_t mode);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -30,23 +30,31 @@ uint8_t BatteryManagement::calc_percent_voltage(uint16_t voltage) {
|
||||
}
|
||||
|
||||
// helper function to get data from ANY batt management ic
|
||||
void BatteryManagement::getBatteryInfo(uint8_t& valid_mask, uint8_t& percent, uint16_t& voltage, int32_t& current) {
|
||||
void BatteryManagement::getBatteryInfo(uint8_t& valid_mask, uint8_t& percent, uint16_t& voltage, int32_t& current, bool& battMayChanged) {
|
||||
auto dev = i2cdev::I2CDevManager::get_dev_by_model(I2CDEVMDL_MAX17055);
|
||||
battMayChanged = false;
|
||||
if (dev) {
|
||||
((i2cdev::I2cDev_MAX17055*)dev)->getBatteryInfo(valid_mask, percent, voltage, current);
|
||||
((i2cdev::I2cDev_MAX17055*)dev)->getBatteryInfo(valid_mask, percent, voltage, current, battMayChanged);
|
||||
return;
|
||||
}
|
||||
dev = i2cdev::I2CDevManager::get_dev_by_model(I2CDEVMDL_ADS1110);
|
||||
if (dev) {
|
||||
voltage = ((i2cdev::I2cDev_ADS1110*)dev)->readVoltage();
|
||||
percent = calc_percent_voltage(voltage);
|
||||
valid_mask = 1;
|
||||
valid_mask = BATT_VALID_VOLTAGE | BATT_VALID_PERCENT;
|
||||
return;
|
||||
}
|
||||
|
||||
valid_mask = 0;
|
||||
}
|
||||
|
||||
void BatteryManagement::reset_changed_flag() {
|
||||
auto dev = i2cdev::I2CDevManager::get_dev_by_model(I2CDEVMDL_MAX17055);
|
||||
if (dev) {
|
||||
((i2cdev::I2cDev_MAX17055*)dev)->resetChangedFlag();
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to get data from ANY batt management ic
|
||||
uint16_t BatteryManagement::get_cycles() {
|
||||
auto dev = i2cdev::I2CDevManager::get_dev_by_model(I2CDEVMDL_MAX17055);
|
||||
|
||||
@@ -46,10 +46,11 @@ class BatteryManagement {
|
||||
static void set_calc_override(bool override);
|
||||
static uint8_t calc_percent_voltage(uint16_t); // calculates battery percentage from the voltage
|
||||
static bool calcOverride; // if set to true, it'll override the battery percent calculation based on current voltage.
|
||||
static void getBatteryInfo(uint8_t& valid_mask, uint8_t& percent, uint16_t& voltage, int32_t& current);
|
||||
static void getBatteryInfo(uint8_t& valid_mask, uint8_t& percent, uint16_t& voltage, int32_t& current, bool& battMayChanged);
|
||||
static uint16_t get_cycles();
|
||||
static float get_tte();
|
||||
static float get_ttf();
|
||||
static void reset_changed_flag();
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
@@ -105,6 +105,7 @@ bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
|
||||
if (!((bmp_header.signature == 0x4D42) &&
|
||||
(bmp_header.planes == 1) &&
|
||||
(bmp_header.compression == 0 || bmp_header.compression == 3))) {
|
||||
bmpimage.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -114,6 +115,7 @@ bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
|
||||
byte_per_px = 1;
|
||||
// bmpimage.seek(sizeof(bmp_header_t));
|
||||
// bmpimage.read(color_palette, 1024);
|
||||
bmpimage.close();
|
||||
return false; // niy
|
||||
break;
|
||||
|
||||
@@ -121,6 +123,7 @@ bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
|
||||
byte_per_px = 2;
|
||||
type = 5;
|
||||
if (bmp_header.compression == 3) {
|
||||
bmpimage.close();
|
||||
return false;
|
||||
} // niy
|
||||
|
||||
@@ -135,6 +138,7 @@ bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
|
||||
break;
|
||||
default:
|
||||
// not supported
|
||||
bmpimage.close();
|
||||
return false;
|
||||
}
|
||||
byte_per_row = (bmp_header.width * byte_per_px + 3) & ~3;
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "gpio.hpp"
|
||||
|
||||
#include "platform.hpp"
|
||||
|
||||
using namespace gpio_control;
|
||||
|
||||
namespace power_control {
|
||||
|
||||
/* VAA powers:
|
||||
* MAX5864 analog section.
|
||||
* MAX2837 registers and other functions.
|
||||
* RFFC5072 analog section.
|
||||
*
|
||||
* Beware that power applied to pins of the MAX2837 may
|
||||
* show up on VAA and start powering other components on the
|
||||
* VAA net. So turn on VAA before driving pins from MCU to
|
||||
* MAX2837.
|
||||
*/
|
||||
|
||||
void vaa_power_on(void) {
|
||||
/* Very twitchy process for powering up VAA without glitching the 3.3V rail,
|
||||
* which can send the microcontroller into reset.
|
||||
*/
|
||||
#ifdef PRALINE
|
||||
/* P8_1 (GPIO4[1]) does not have MOTOCONPWM hardware routing.
|
||||
* Using software bit-banging (pseudo-PWM) for VAA soft-start.
|
||||
* VAA is active LOW (0 = ON).
|
||||
*/
|
||||
|
||||
/* Software soft-start loop to prevent brown-out */
|
||||
for (uint32_t i = 0; i < 1000; i++) {
|
||||
LPC_GPIO->W4[1] = 0; /* Turn ON briefly */
|
||||
LPC_GPIO->W4[1] = 1; /* Turn OFF briefly */
|
||||
}
|
||||
|
||||
/* Latch VAA to ON state (Active LOW) */
|
||||
VAA_en.setActive();
|
||||
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
/*
|
||||
* There is enough VCC->VAA leakage prior to VAA activation from IO pins on
|
||||
* HackRF One r9 that slowing down activation like this isn't necessary, but
|
||||
* we do it just in case a different start-up sequence in the future results
|
||||
* in less leakage.
|
||||
*/
|
||||
setup_pin(pin_setup_vaa_enablex_gpio_r9); // P6_10 GPIO3[ 6]: !VAA_ENABLE, 10K PU
|
||||
for (uint32_t i = 0; i < 1000; i++) {
|
||||
LPC_GPIO->W3[6] = 1;
|
||||
LPC_GPIO->W3[6] = 0;
|
||||
}
|
||||
} else {
|
||||
/* Configure and enable MOTOCONPWM peripheral clocks.
|
||||
* Assume IDIVC is running the post-bootloader configuration, outputting 96MHz derived from PLL1.
|
||||
*/
|
||||
base_clock_enable(&motocon_pwm_resources.base);
|
||||
branch_clock_enable(&motocon_pwm_resources.branch);
|
||||
peripheral_reset(&motocon_pwm_resources.reset);
|
||||
|
||||
/* Combination of pulse duration and duty cycle was arrived at empirically, to keep supply glitching
|
||||
* to +/- 0.15V.
|
||||
*/
|
||||
const uint32_t cycle_period = 256;
|
||||
uint32_t enable_period = 2;
|
||||
LPC_MCPWM->TC2 = 0;
|
||||
LPC_MCPWM->MAT2 = cycle_period - enable_period;
|
||||
LPC_MCPWM->LIM2 = cycle_period;
|
||||
|
||||
/* Switch !VAA_ENABLE pin from GPIO to MOTOCONPWM peripheral output, now that the peripheral is configured. */
|
||||
setup_pin(pin_setup_vaa_enablex_pwm); // P5_0 /GPIO2[ 9]/MCOB2: !VAA_ENABLE, 10K PU
|
||||
|
||||
/* Start the PWM operation. */
|
||||
LPC_MCPWM->CON_SET = (1 << 16);
|
||||
|
||||
/* Wait until VAA rises to approximately 90% of final voltage. */
|
||||
/* Timing assumes we're running immediately after the bootloader: 96 MHz from IRC+PLL1
|
||||
*/
|
||||
while (enable_period < cycle_period) {
|
||||
{
|
||||
volatile uint32_t delay = 2000;
|
||||
while (delay--);
|
||||
}
|
||||
enable_period <<= 1;
|
||||
LPC_MCPWM->MAT2 = cycle_period - enable_period;
|
||||
}
|
||||
|
||||
/* Hold !VAA_ENABLE active using a GPIO, so we can reclaim and shut down the MOTOCONPWM peripheral. */
|
||||
og_VAA_en.output();
|
||||
og_VAA_en.setActive();
|
||||
setup_pin(pin_setup_vaa_enablex_gpio_og); // P5_0 /GPIO2[ 9]/MCOB2: !VAA_ENABLE, 10K PU
|
||||
|
||||
peripheral_reset(&motocon_pwm_resources.reset);
|
||||
branch_clock_disable(&motocon_pwm_resources.branch);
|
||||
base_clock_disable(&motocon_pwm_resources.base);
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
void vaa_power_off(void) {
|
||||
/* TODO: There's a lot of other stuff that must be done to prevent
|
||||
* leakage from +3V3 into VAA.
|
||||
*/
|
||||
#ifdef PRALINE
|
||||
/* Safe state: OFF (VAA RF is active LOW, so Set = OFF) */
|
||||
VAA_en.setInactive();
|
||||
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
r9_VAA_en.setInactive(); // Turn OFF VAA for r9 P6_10
|
||||
} else {
|
||||
og_VAA_en.setInactive(); // Turn OFF VAA for OG P5_0
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef PRALINE
|
||||
|
||||
void aux_power_on(void) {
|
||||
// 3.3V Aux - P6_7 = GPIO5[15], Active LOW (Clear = ON)
|
||||
LPC_GPIO->CLR[5] = (1 << 15);
|
||||
}
|
||||
|
||||
void aux_power_off(void) {
|
||||
// 3.3V Aux - P6_7 = GPIO5[15], Active LOW (Set = OFF)
|
||||
LPC_GPIO->SET[5] = (1 << 15);
|
||||
}
|
||||
|
||||
#endif /* PRALINE */
|
||||
|
||||
void core_power_on(void) {
|
||||
#ifdef PRALINE
|
||||
// 1.2V FPGA - P8_7 = GPIO4[7], Active HIGH (Set = ON)
|
||||
en_1v2.setActive();
|
||||
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
r9_1v8_en.setActive();
|
||||
} else {
|
||||
og_1v8_en.setActive();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void core_power_off(void) {
|
||||
#ifdef PRALINE
|
||||
en_1v2.setInactive();
|
||||
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
r9_1v8_en.setInactive();
|
||||
} else {
|
||||
og_1v8_en.setInactive();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // namespace power_control
|
||||
+289
-12
@@ -22,10 +22,13 @@
|
||||
#ifndef __GPIO_H__
|
||||
#define __GPIO_H__
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "ch.h"
|
||||
#include "hal.h"
|
||||
#include "pal_lld.h"
|
||||
|
||||
struct PinConfig {
|
||||
const uint32_t mode;
|
||||
@@ -197,6 +200,12 @@ struct Pin {
|
||||
uint8_t _pin_pad;
|
||||
};
|
||||
|
||||
// Defines the logical polarity of the GPIO pin
|
||||
enum class Polarity {
|
||||
ActiveHigh, // High physical state means the function is ON (default)
|
||||
ActiveLow // Low physical state means the function is ON (inverted)
|
||||
};
|
||||
|
||||
struct GPIO {
|
||||
// GPIO() = delete;
|
||||
// GPIO(const GPIO& gpio) = delete;
|
||||
@@ -206,22 +215,15 @@ struct GPIO {
|
||||
const Pin& pin,
|
||||
const ioportid_t gpio_port,
|
||||
const iopadid_t gpio_pad,
|
||||
const uint16_t gpio_mode)
|
||||
const uint16_t gpio_mode,
|
||||
const Polarity polarity = Polarity::ActiveHigh) // Default parameter ensures backward compatibility
|
||||
: _pin{pin},
|
||||
_gpio_port{gpio_port},
|
||||
_gpio_pad{gpio_pad},
|
||||
_gpio_mode{gpio_mode} {
|
||||
_gpio_mode{gpio_mode},
|
||||
_polarity{polarity} {
|
||||
}
|
||||
/*
|
||||
constexpr GPIO(
|
||||
const GPIO& gpio
|
||||
) : _pin { gpio._pin },
|
||||
_gpio_port { gpio._gpio_port },
|
||||
_gpio_pad { gpio._gpio_pad },
|
||||
_gpio_mode { gpio._gpio_mode }
|
||||
{
|
||||
}
|
||||
*/
|
||||
|
||||
constexpr ioportid_t port() const {
|
||||
return _gpio_port;
|
||||
}
|
||||
@@ -234,6 +236,11 @@ struct GPIO {
|
||||
return _pin;
|
||||
}
|
||||
|
||||
// Returns the configured polarity of the pin
|
||||
constexpr Polarity polarity() const {
|
||||
return _polarity;
|
||||
}
|
||||
|
||||
void configure() const {
|
||||
_pin.mode(_gpio_mode);
|
||||
}
|
||||
@@ -242,6 +249,8 @@ struct GPIO {
|
||||
return _gpio_mode;
|
||||
}
|
||||
|
||||
// Physical Level Operations
|
||||
|
||||
void set() const {
|
||||
palSetPad(_gpio_port, _gpio_pad);
|
||||
}
|
||||
@@ -270,6 +279,39 @@ struct GPIO {
|
||||
return palReadPad(_gpio_port, _gpio_pad);
|
||||
}
|
||||
|
||||
// Turns the feature ON based on its polarity
|
||||
void setActive() const {
|
||||
if (_polarity == Polarity::ActiveHigh) {
|
||||
set();
|
||||
} else {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Turns the feature OFF based on its polarity
|
||||
void setInactive() const {
|
||||
if (_polarity == Polarity::ActiveHigh) {
|
||||
clear();
|
||||
} else {
|
||||
set();
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the logical state of the feature
|
||||
void setState(const bool active) const {
|
||||
if (active) {
|
||||
setActive();
|
||||
} else {
|
||||
setInactive();
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if the feature is logically active/enabled
|
||||
bool isEnabled() const {
|
||||
const bool physical_state = read();
|
||||
return (_polarity == Polarity::ActiveHigh) ? physical_state : !physical_state;
|
||||
}
|
||||
|
||||
bool operator!=(const GPIO& other) const {
|
||||
return (port() != other.port()) || (pad() != other.pad());
|
||||
}
|
||||
@@ -278,6 +320,241 @@ struct GPIO {
|
||||
const ioportid_t _gpio_port;
|
||||
const iopadid_t _gpio_pad;
|
||||
const uint16_t _gpio_mode;
|
||||
const Polarity _polarity;
|
||||
};
|
||||
|
||||
constexpr uint32_t boot_bit(const GPIO& pin, bool state) {
|
||||
return (state ? 1UL : 0UL) << pin.pad();
|
||||
}
|
||||
|
||||
inline void setup_pin(const scu_setup_t& pin_setup) {
|
||||
LPC_SCU->SFSP[pin_setup.port][pin_setup.pin] = pin_setup.config;
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
inline void setup_gpios(const std::array<gpio_setup_t, N>& pins_setup) {
|
||||
for (size_t i = 0; i < N; i++) {
|
||||
LPC_GPIO->PIN[i] |= pins_setup[i].data;
|
||||
LPC_GPIO->DIR[i] |= pins_setup[i].dir;
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
inline void setup_pins(const std::array<scu_setup_t, N>& pins_setup) {
|
||||
for (const auto& pin_setup : pins_setup) {
|
||||
setup_pin(pin_setup);
|
||||
}
|
||||
}
|
||||
|
||||
namespace gpio_control {
|
||||
|
||||
struct PinMap {
|
||||
uint8_t scu_port;
|
||||
uint8_t scu_pin;
|
||||
uint8_t gpio_port;
|
||||
uint8_t gpio_pad;
|
||||
uint8_t gpio_mode;
|
||||
};
|
||||
|
||||
#ifdef PRALINE
|
||||
constexpr PinMap map_p1_ctrl0{2, 10, 0, 14, 0}; // SCU: 2, 10 | GPIO: 0, 14
|
||||
constexpr PinMap map_p1_ctrl1{6, 8, 5, 16, 4};
|
||||
constexpr PinMap map_p1_ctrl2{6, 9, 3, 5, 0};
|
||||
|
||||
constexpr PinMap map_p2_ctrl0{8, 3, 7, 3, 0};
|
||||
constexpr PinMap map_p2_ctrl1{8, 4, 7, 4, 0};
|
||||
|
||||
constexpr PinMap map_trigger_out{2, 6, 5, 6, 4};
|
||||
constexpr PinMap map_trigger_in{13, 12, 6, 26, 4};
|
||||
|
||||
constexpr PinMap map_clkin_ctrl{1, 20, 0, 15, 0};
|
||||
|
||||
constexpr PinMap map_rf5072_mix_en{9, 0, 4, 12, 0};
|
||||
|
||||
constexpr PinMap map_aa_en{1, 14, 1, 7, 0}; // Anti-Aliasing filter
|
||||
|
||||
constexpr PinMap map_sgpio_4{9, 4, 5, 17, 6};
|
||||
constexpr PinMap map_sgpio_8{8, 0, 4, 0, 4};
|
||||
constexpr PinMap map_sgpio_9{9, 3, 4, 15, 6};
|
||||
constexpr PinMap map_sgpio_10{8, 2, 4, 2, 4};
|
||||
constexpr PinMap map_en_1v2{8, 7, 4, 7, 0};
|
||||
constexpr PinMap map_vregmode{4, 9, 5, 13, 4};
|
||||
constexpr PinMap map_VAA_en{8, 1, 4, 1, 0};
|
||||
constexpr PinMap map_led_mcu{8, 6, 4, 6, 0};
|
||||
#else
|
||||
constexpr PinMap map_sgpio_4{6, 3, 3, 2, 2};
|
||||
constexpr PinMap map_sgpio_8{9, 6, 4, 11, 6};
|
||||
constexpr PinMap map_sgpio_9{4, 3, 2, 3, 7};
|
||||
constexpr PinMap map_sgpio_10{1, 14, 1, 7, 6};
|
||||
constexpr PinMap map_sgpio_13{4, 8, 5, 12, 4};
|
||||
constexpr PinMap map_og_1v8_en{6, 10, 3, 6, 0};
|
||||
constexpr PinMap map_r9_1v8_en{5, 0, 2, 9, 0};
|
||||
constexpr PinMap map_vregmode{6, 11, 3, 7, 0};
|
||||
constexpr PinMap map_og_VAA_en{5, 0, 2, 9, 0};
|
||||
constexpr PinMap map_r9_VAA_en{6, 10, 3, 6, 0};
|
||||
#endif
|
||||
|
||||
constexpr PinMap map_sgpio_0{0, 0, 0, 0, 3};
|
||||
constexpr PinMap map_sgpio_1{0, 1, 0, 1, 3};
|
||||
constexpr PinMap map_sgpio_2{1, 15, 0, 2, 2};
|
||||
constexpr PinMap map_sgpio_3{1, 16, 0, 3, 2};
|
||||
constexpr PinMap map_sgpio_5{6, 6, 0, 5, 2};
|
||||
constexpr PinMap map_sgpio_6{2, 2, 5, 2, 0};
|
||||
constexpr PinMap map_sgpio_7{1, 0, 0, 4, 6};
|
||||
constexpr PinMap map_sgpio_11{1, 17, 0, 12, 6};
|
||||
constexpr PinMap map_sgpio_12{1, 18, 0, 13, 0};
|
||||
constexpr PinMap map_led_usb{4, 1, 2, 1, 0};
|
||||
constexpr PinMap map_led_rx{4, 2, 2, 2, 0};
|
||||
constexpr PinMap map_led_tx{6, 12, 2, 8, 0};
|
||||
#ifdef PRALINE
|
||||
|
||||
// P1 Multiplexer control pins
|
||||
constexpr Pin pin_p1_ctrl0{map_p1_ctrl0.scu_port, map_p1_ctrl0.scu_pin}; // SCU: P2_10
|
||||
constexpr GPIO p1_ctrl0{pin_p1_ctrl0, map_p1_ctrl0.gpio_port, map_p1_ctrl0.gpio_pad, map_p1_ctrl0.gpio_mode}; // GPIO[0]14
|
||||
|
||||
constexpr Pin pin_p1_ctrl1{map_p1_ctrl1.scu_port, map_p1_ctrl1.scu_pin}; // SCU: P6_8
|
||||
constexpr GPIO p1_ctrl1{pin_p1_ctrl1, map_p1_ctrl1.gpio_port, map_p1_ctrl1.gpio_pad, map_p1_ctrl1.gpio_mode}; // GPIO[5]16
|
||||
|
||||
constexpr Pin pin_p1_ctrl2{map_p1_ctrl2.scu_port, map_p1_ctrl2.scu_pin}; // SCU: P6_9
|
||||
constexpr GPIO p1_ctrl2{pin_p1_ctrl2, map_p1_ctrl2.gpio_port, map_p1_ctrl2.gpio_pad, map_p1_ctrl2.gpio_mode}; // GPIO[3]5
|
||||
|
||||
// P2 Multiplexer control pins
|
||||
constexpr Pin pin_p2_ctrl0{map_p2_ctrl0.scu_port, map_p2_ctrl0.scu_pin}; // SCU: PE_3
|
||||
constexpr GPIO p2_ctrl0{pin_p2_ctrl0, map_p2_ctrl0.gpio_port, map_p2_ctrl0.gpio_pad, map_p2_ctrl0.gpio_mode}; // GPIO[7]3
|
||||
|
||||
constexpr Pin pin_p2_ctrl1{map_p2_ctrl1.scu_port, map_p2_ctrl1.scu_pin}; // SCU: PE_4
|
||||
constexpr GPIO p2_ctrl1{pin_p2_ctrl1, map_p2_ctrl1.gpio_port, map_p2_ctrl1.gpio_pad, map_p2_ctrl1.gpio_mode}; // GPIO[7]4
|
||||
|
||||
// Trigger pins
|
||||
constexpr Pin trigger_out_pin{map_trigger_out.scu_port, map_trigger_out.scu_pin}; // SCU: P2_6
|
||||
constexpr GPIO trigger_out{trigger_out_pin, map_trigger_out.gpio_port, map_trigger_out.gpio_pad, map_trigger_out.gpio_mode}; // GPIO[5]6
|
||||
|
||||
constexpr Pin trigger_in_pin{map_trigger_in.scu_port, map_trigger_in.scu_pin}; // SCU: PD_12
|
||||
constexpr GPIO trigger_in{trigger_in_pin, map_trigger_in.gpio_port, map_trigger_in.gpio_pad, map_trigger_in.gpio_mode}; // GPIO[6]26
|
||||
|
||||
// Clock input control
|
||||
constexpr Pin pin_clkin_ctrl{map_clkin_ctrl.scu_port, map_clkin_ctrl.scu_pin}; // SCU: P1_20
|
||||
constexpr GPIO clkin_ctrl{pin_clkin_ctrl, map_clkin_ctrl.gpio_port, map_clkin_ctrl.gpio_pad, map_clkin_ctrl.gpio_mode}; // GPIO[0]15
|
||||
|
||||
// RF507x
|
||||
constexpr Pin pin_rf5072_mix_en{map_rf5072_mix_en.scu_port, map_rf5072_mix_en.scu_pin}; // SCU: P9_0
|
||||
constexpr GPIO rf5072_mix_en{pin_rf5072_mix_en, map_rf5072_mix_en.gpio_port, map_rf5072_mix_en.gpio_pad, map_rf5072_mix_en.gpio_mode}; // GPIO[4]12
|
||||
|
||||
// Anti-Aliasing filter
|
||||
constexpr Pin pin_aa_en{map_aa_en.scu_port, map_aa_en.scu_pin}; // SCU: P1_14
|
||||
constexpr GPIO aa_en{pin_aa_en, map_aa_en.gpio_port, map_aa_en.gpio_pad, map_aa_en.gpio_mode}; // GPIO[1]7
|
||||
|
||||
constexpr Pin pin_en_1v2{map_en_1v2.scu_port, map_en_1v2.scu_pin}; // SCU: P8_7
|
||||
constexpr GPIO en_1v2{pin_en_1v2, map_en_1v2.gpio_port, map_en_1v2.gpio_pad, map_en_1v2.gpio_mode}; // GPIO[4]7
|
||||
|
||||
constexpr Pin pin_VAA_en{map_VAA_en.scu_port, map_VAA_en.scu_pin}; // SCU: P8_1
|
||||
constexpr GPIO VAA_en{pin_VAA_en, map_VAA_en.gpio_port, map_VAA_en.gpio_pad, map_VAA_en.gpio_mode, Polarity::ActiveLow}; // GPIO[4]1
|
||||
|
||||
constexpr Pin pin_led_usb{map_led_usb.scu_port, map_led_usb.scu_pin}; // SCU: P4_1
|
||||
constexpr GPIO led_usb{pin_led_usb, map_led_usb.gpio_port, map_led_usb.gpio_pad, map_led_usb.gpio_mode, Polarity::ActiveLow}; // GPIO[2]1
|
||||
|
||||
constexpr Pin pin_led_rx{map_led_rx.scu_port, map_led_rx.scu_pin}; // SCU: P4_2
|
||||
constexpr GPIO led_rx{pin_led_rx, map_led_rx.gpio_port, map_led_rx.gpio_pad, map_led_rx.gpio_mode, Polarity::ActiveLow}; // GPIO[2]2
|
||||
|
||||
constexpr Pin pin_led_tx{map_led_tx.scu_port, map_led_tx.scu_pin}; // SCU: P6_12
|
||||
constexpr GPIO led_tx{pin_led_tx, map_led_tx.gpio_port, map_led_tx.gpio_pad, map_led_tx.gpio_mode, Polarity::ActiveLow}; // GPIO[2]8
|
||||
|
||||
constexpr Pin pin_led_mcu{map_led_mcu.scu_port, map_led_mcu.scu_pin}; // SCU: P8_6
|
||||
constexpr GPIO led_mcu{pin_led_mcu, map_led_mcu.gpio_port, map_led_mcu.gpio_pad, map_led_mcu.gpio_mode, Polarity::ActiveLow}; // GPIO[4]6
|
||||
#else
|
||||
constexpr Pin pin_sgpio_13{map_sgpio_13.scu_port, map_sgpio_13.scu_pin}; // SCU: P4_8
|
||||
constexpr GPIO sgpio_13{pin_sgpio_13, map_sgpio_13.gpio_port, map_sgpio_13.gpio_pad, map_sgpio_13.gpio_mode}; // GPIO[5]12
|
||||
|
||||
constexpr Pin pin_og_1v8_en{map_og_1v8_en.scu_port, map_og_1v8_en.scu_pin}; // SCU: P6_10
|
||||
constexpr GPIO og_1v8_en{pin_og_1v8_en, map_og_1v8_en.gpio_port, map_og_1v8_en.gpio_pad, map_og_1v8_en.gpio_mode}; // GPIO[3]6
|
||||
|
||||
constexpr Pin pin_r9_1v8_en{map_r9_1v8_en.scu_port, map_r9_1v8_en.scu_pin}; // SCU: P5_0
|
||||
constexpr GPIO r9_1v8_en{pin_r9_1v8_en, map_r9_1v8_en.gpio_port, map_r9_1v8_en.gpio_pad, map_r9_1v8_en.gpio_mode}; // GPIO[2]9
|
||||
|
||||
constexpr Pin pin_og_VAA_en{map_og_VAA_en.scu_port, map_og_VAA_en.scu_pin}; // SCU: P5_0
|
||||
constexpr GPIO og_VAA_en{pin_og_VAA_en, map_og_VAA_en.gpio_port, map_og_VAA_en.gpio_pad, map_og_VAA_en.gpio_mode, Polarity::ActiveLow}; // GPIO[2]9
|
||||
|
||||
constexpr Pin pin_r9_VAA_en{map_r9_VAA_en.scu_port, map_r9_VAA_en.scu_pin}; // SCU: P6_10
|
||||
constexpr GPIO r9_VAA_en{pin_r9_VAA_en, map_r9_VAA_en.gpio_port, map_r9_VAA_en.gpio_pad, map_r9_VAA_en.gpio_mode, Polarity::ActiveLow}; // GPIO[3]6
|
||||
|
||||
constexpr Pin pin_led_usb{map_led_usb.scu_port, map_led_usb.scu_pin}; // SCU: P4_1
|
||||
constexpr GPIO led_usb{pin_led_usb, map_led_usb.gpio_port, map_led_usb.gpio_pad, map_led_usb.gpio_mode}; // GPIO[2]1
|
||||
|
||||
constexpr Pin pin_led_rx{map_led_rx.scu_port, map_led_rx.scu_pin}; // SCU: P4_2
|
||||
constexpr GPIO led_rx{pin_led_rx, map_led_rx.gpio_port, map_led_rx.gpio_pad, map_led_rx.gpio_mode}; // GPIO[2]2
|
||||
|
||||
constexpr Pin pin_led_tx{map_led_tx.scu_port, map_led_tx.scu_pin}; // SCU: P6_12
|
||||
constexpr GPIO led_tx{pin_led_tx, map_led_tx.gpio_port, map_led_tx.gpio_pad, map_led_tx.gpio_mode}; // GPIO[2]8
|
||||
|
||||
static const motocon_pwm_resources_t motocon_pwm_resources = {
|
||||
.base = {.clk = &LPC_CGU->BASE_APB1_CLK, .stat = &LPC_CCU1->BASE_STAT, .stat_mask = (1 << 1)},
|
||||
.branch = {.cfg = &LPC_CCU1->CLK_APB1_MOTOCON_PWM_CFG, .stat = &LPC_CCU1->CLK_APB1_MOTOCON_PWM_STAT},
|
||||
.reset = {.output_index = 38},
|
||||
};
|
||||
|
||||
static const scu_setup_t pin_setup_vaa_enablex_pwm = {5, 0, scu_config_normal_drive_t{.mode = 1, .epd = 0, .epun = 1, .ehs = 0, .ezi = 0, .zif = 0}};
|
||||
static const scu_setup_t pin_setup_vaa_enablex_gpio_og = {map_og_VAA_en.scu_port, map_og_VAA_en.scu_pin, scu_config_normal_drive_t{.mode = map_og_VAA_en.gpio_mode, .epd = 0, .epun = 1, .ehs = 0, .ezi = 0, .zif = 0}};
|
||||
static const scu_setup_t pin_setup_vaa_enablex_gpio_r9 = {map_r9_VAA_en.scu_port, map_r9_VAA_en.scu_pin, scu_config_normal_drive_t{.mode = map_r9_VAA_en.gpio_mode, .epd = 0, .epun = 1, .ehs = 0, .ezi = 0, .zif = 0}};
|
||||
|
||||
#endif
|
||||
|
||||
// SGPIO
|
||||
constexpr Pin pin_sgpio_0{map_sgpio_0.scu_port, map_sgpio_0.scu_pin}; // SCU: P0_0
|
||||
constexpr GPIO sgpio_0{pin_sgpio_0, map_sgpio_0.gpio_port, map_sgpio_0.gpio_pad, map_sgpio_0.gpio_mode}; // GPIO[0]0
|
||||
|
||||
constexpr Pin pin_sgpio_1{map_sgpio_1.scu_port, map_sgpio_1.scu_pin}; // SCU: P0_1
|
||||
constexpr GPIO sgpio_1{pin_sgpio_1, map_sgpio_1.gpio_port, map_sgpio_1.gpio_pad, map_sgpio_1.gpio_mode}; // GPIO[0]1
|
||||
|
||||
constexpr Pin pin_sgpio_2{map_sgpio_2.scu_port, map_sgpio_2.scu_pin}; // SCU: P1_15
|
||||
constexpr GPIO sgpio_2{pin_sgpio_2, map_sgpio_2.gpio_port, map_sgpio_2.gpio_pad, map_sgpio_2.gpio_mode}; // GPIO[0]2
|
||||
|
||||
constexpr Pin pin_sgpio_3{map_sgpio_3.scu_port, map_sgpio_3.scu_pin}; // SCU: P1_16
|
||||
constexpr GPIO sgpio_3{pin_sgpio_3, map_sgpio_3.gpio_port, map_sgpio_3.gpio_pad, map_sgpio_3.gpio_mode}; // GPIO[0]3
|
||||
|
||||
constexpr Pin pin_sgpio_4{map_sgpio_4.scu_port, map_sgpio_4.scu_pin}; // SCU: P6_3, PRALINE: P9_4
|
||||
constexpr GPIO sgpio_4{pin_sgpio_4, map_sgpio_4.gpio_port, map_sgpio_4.gpio_pad, map_sgpio_4.gpio_mode}; // GPIO[6]3 PRALINE: GPIO[5]17
|
||||
|
||||
constexpr Pin pin_sgpio_5{map_sgpio_5.scu_port, map_sgpio_5.scu_pin}; // SCU: P6_6
|
||||
constexpr GPIO sgpio_5{pin_sgpio_5, map_sgpio_5.gpio_port, map_sgpio_5.gpio_pad, map_sgpio_5.gpio_mode}; // GPIO[0]5
|
||||
|
||||
constexpr Pin pin_sgpio_6{map_sgpio_6.scu_port, map_sgpio_6.scu_pin}; // SCU: P2_2
|
||||
constexpr GPIO sgpio_6{pin_sgpio_6, map_sgpio_6.gpio_port, map_sgpio_6.gpio_pad, map_sgpio_6.gpio_mode}; // GPIO[5]2
|
||||
|
||||
constexpr Pin pin_sgpio_7{map_sgpio_7.scu_port, map_sgpio_7.scu_pin}; // SCU: P1_0
|
||||
constexpr GPIO sgpio_7{pin_sgpio_7, map_sgpio_7.gpio_port, map_sgpio_7.gpio_pad, map_sgpio_7.gpio_mode}; // GPIO[0]4
|
||||
|
||||
constexpr Pin pin_sgpio_8{map_sgpio_8.scu_port, map_sgpio_8.scu_pin}; // SCU: P9_6, PRALINE: P8_0
|
||||
constexpr GPIO sgpio_8{pin_sgpio_8, map_sgpio_8.gpio_port, map_sgpio_8.gpio_pad, map_sgpio_8.gpio_mode}; // GPIO[4]11, PRALINE: GPIO[4]0
|
||||
|
||||
constexpr Pin pin_sgpio_9{map_sgpio_9.scu_port, map_sgpio_9.scu_pin}; // SCU: P4_3, PRALINE: P9_3
|
||||
constexpr GPIO sgpio_9{pin_sgpio_9, map_sgpio_9.gpio_port, map_sgpio_9.gpio_pad, map_sgpio_9.gpio_mode}; // GPIO[2]3, PRALINE: GPIO[4]12
|
||||
|
||||
constexpr Pin pin_sgpio_10{map_sgpio_10.scu_port, map_sgpio_10.scu_pin}; // SCU: P1_14, PRALINE: P8_2
|
||||
constexpr GPIO sgpio_10{pin_sgpio_10, map_sgpio_10.gpio_port, map_sgpio_10.gpio_pad, map_sgpio_10.gpio_mode}; // GPIO[1]7, PRALINE: GPIO[4]2
|
||||
|
||||
constexpr Pin pin_sgpio_11{map_sgpio_11.scu_port, map_sgpio_11.scu_pin}; // SCU: P1_17
|
||||
constexpr GPIO sgpio_11{pin_sgpio_11, map_sgpio_11.gpio_port, map_sgpio_11.gpio_pad, map_sgpio_11.gpio_mode}; // GPIO[0]17
|
||||
|
||||
constexpr Pin pin_sgpio_12{map_sgpio_12.scu_port, map_sgpio_12.scu_pin}; // SCU: P1_18
|
||||
constexpr GPIO sgpio_12{pin_sgpio_12, map_sgpio_12.gpio_port, map_sgpio_12.gpio_pad, map_sgpio_12.gpio_mode}; // GPIO[0]13
|
||||
|
||||
constexpr Pin pin_vregmode{map_vregmode.scu_port, map_vregmode.scu_pin}; // SCU: P6_11, PRALINE: P4_9
|
||||
constexpr GPIO vregmode{pin_vregmode, map_vregmode.gpio_port, map_vregmode.gpio_pad, map_vregmode.gpio_mode}; // GPIO[3]7, PRALINE: GPIO[5]13
|
||||
} // namespace gpio_control
|
||||
|
||||
namespace power_control {
|
||||
|
||||
void vaa_power_on(void);
|
||||
void vaa_power_off(void);
|
||||
|
||||
#ifdef PRALINE
|
||||
void aux_power_on(void);
|
||||
void aux_power_off(void);
|
||||
#endif
|
||||
|
||||
void core_power_on(void);
|
||||
void core_power_off(void);
|
||||
|
||||
} // namespace power_control
|
||||
|
||||
#endif /*__GPIO_H__*/
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#define __HACKRF_GPIO_H__
|
||||
|
||||
#include "pins.hpp"
|
||||
#include "led.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
@@ -34,27 +33,11 @@ namespace one {
|
||||
|
||||
/* GPIO */
|
||||
|
||||
constexpr GPIO gpio_led_usb = gpio[GPIO2_1];
|
||||
constexpr GPIO gpio_led_rx = gpio[GPIO2_2];
|
||||
constexpr GPIO gpio_led_tx = gpio[GPIO2_8];
|
||||
|
||||
constexpr GPIO gpio_og_1v8_enable = gpio[GPIO3_6];
|
||||
constexpr GPIO gpio_r9_1v8_enable = gpio[GPIO2_9];
|
||||
constexpr GPIO gpio_vregmode = gpio[GPIO3_7];
|
||||
#ifdef PRALINE
|
||||
// PRALINE uses different power control pins
|
||||
constexpr GPIO gpio_og_vaa_disable = gpio[GPIO4_1]; // PRALINE VAA disable (P8_1)
|
||||
constexpr GPIO gpio_r9_vaa_disable = gpio[GPIO4_1]; // PRALINE VAA disable (P8_1)
|
||||
#else
|
||||
constexpr GPIO gpio_og_vaa_disable = gpio[GPIO2_9];
|
||||
constexpr GPIO gpio_r9_vaa_disable = gpio[GPIO3_6];
|
||||
#endif
|
||||
|
||||
constexpr GPIO gpio_rx_mix_bp = gpio[GPIO2_12];
|
||||
constexpr GPIO gpio_tx_mix_bp = gpio[GPIO2_11];
|
||||
#ifdef PRALINE
|
||||
constexpr GPIO gpio_mix_bypass = gpio[GPIO3_2]; // P6_3: PRALINE RF path mixer bypass inverted
|
||||
constexpr GPIO gpio_mix_en_n_r1_0 = gpio[GPIO5_6]; // P2_6: R1.0 board mixer bypass
|
||||
constexpr GPIO gpio_mix_bypass = gpio[GPIO3_2]; // P6_3: PRALINE RF path mixer bypass inverted
|
||||
// constexpr GPIO gpio_mix_en_n_r1_0 = gpio[GPIO5_6]; // P2_6: R1.0 board mixer bypass
|
||||
#else
|
||||
constexpr GPIO gpio_mix_bypass = gpio[GPIO5_16];
|
||||
#endif
|
||||
@@ -70,7 +53,6 @@ constexpr GPIO gpio_hp = gpio[GPIO2_0];
|
||||
constexpr GPIO gpio_rx_amp = gpio[GPIO1_11];
|
||||
constexpr GPIO gpio_tx_amp = gpio[GPIO2_15];
|
||||
constexpr GPIO gpio_amp_bypass = gpio[GPIO0_14];
|
||||
constexpr GPIO gpio_not_rx_amp_pwr = gpio[GPIO1_12];
|
||||
constexpr GPIO gpio_not_tx_amp_pwr = gpio[GPIO3_5];
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -80,6 +62,7 @@ constexpr GPIO gpio_rffc5072_resetx = gpio[GPIO2_14]; // P5_5: LPC43xx controls
|
||||
#else
|
||||
constexpr GPIO gpio_rffc5072_select = gpio[GPIO2_13];
|
||||
constexpr GPIO gpio_rffc5072_resetx = gpio[GPIO2_14];
|
||||
constexpr GPIO gpio_not_rx_amp_pwr = gpio[GPIO1_12];
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -98,11 +81,11 @@ constexpr GPIO gpio_max283x_select = gpio[GPIO0_15];
|
||||
|
||||
#ifdef PRALINE
|
||||
// PRALINE uses MAX2831 transceiver with different control pins
|
||||
constexpr GPIO gpio_max283x_enable = gpio[GPIO7_1]; // MAX2831 ENABLE (PE_1)
|
||||
constexpr GPIO gpio_max2831_enable = gpio[GPIO7_1]; // Alias
|
||||
constexpr GPIO gpio_max283x_enable = gpio[GPIO7_1]; // MAX2831 ENABLE (PE_1)
|
||||
// constexpr GPIO gpio_max2831_enable = gpio[GPIO7_1]; // Alias
|
||||
constexpr GPIO gpio_max2831_rx_enable = gpio[GPIO7_2]; // MAX2831 RX_ENABLE (PE_2)
|
||||
constexpr GPIO gpio_max2831_rxhp = gpio[GPIO6_29]; // MAX2831 RXHP (PD_15)
|
||||
constexpr GPIO gpio_max2831_ld = gpio[GPIO4_11]; // MAX2831 Lock Detect (P9_6)
|
||||
// constexpr GPIO gpio_max2831_rxhp = gpio[GPIO6_29]; // MAX2831 RXHP (PD_15)
|
||||
constexpr GPIO gpio_max2831_ld = gpio[GPIO4_11]; // MAX2831 Lock Detect (P9_6)
|
||||
// Legacy aliases for code compatibility
|
||||
constexpr GPIO gpio_max2837_rxenable = gpio[GPIO7_2];
|
||||
constexpr GPIO gpio_max2837_txenable = gpio[GPIO7_2]; // MAX2831 uses single RX/TX control
|
||||
@@ -116,7 +99,6 @@ constexpr GPIO gpio_max2839_rxtx = gpio[GPIO2_5];
|
||||
|
||||
#ifdef PRALINE
|
||||
constexpr GPIO gpio_max5864_select = gpio[GPIO6_30]; // PD_16: PRALINE MAX5864 CS
|
||||
constexpr GPIO gpio_fpga_select = gpio[GPIO2_10]; // FPGA SPI CS (P5_1)
|
||||
#else
|
||||
constexpr GPIO gpio_max5864_select = gpio[GPIO2_7];
|
||||
#endif
|
||||
@@ -125,36 +107,16 @@ constexpr GPIO gpio_q_invert = gpio[GPIO0_13];
|
||||
|
||||
#ifdef PRALINE
|
||||
// PRALINE power control
|
||||
constexpr GPIO gpio_vaa_disable = gpio[GPIO4_1]; // VAA disable (P8_1)
|
||||
constexpr GPIO gpio_1v2_enable = gpio[GPIO4_7]; // 1V2 enable (P8_7)
|
||||
constexpr GPIO gpio_3v3aux_disable = gpio[GPIO5_15]; // 3V3 aux disable (P6_7)
|
||||
constexpr GPIO gpio_vbus_enable = gpio[GPIO8_4]; // VBUS_IN_EN P8_4 ->LOW
|
||||
constexpr GPIO gpio_vin_enable = gpio[GPIO8_5]; // VIN_IN_EN P8_5 ->HIGH
|
||||
constexpr GPIO gpio_vaa_disable = gpio[GPIO4_1]; // VAA disable (P8_1)
|
||||
|
||||
// PRALINE RF path control
|
||||
constexpr GPIO gpio_tx_enable = gpio[GPIO3_4]; // TX enable (P6_5)
|
||||
// constexpr GPIO gpio_mix_enable_n = gpio[GPIO3_2]; // Mixer enable inverted (P6_3)
|
||||
constexpr GPIO gpio_tx_enable = gpio[GPIO3_4]; // TX enable (P6_5)
|
||||
constexpr GPIO gpio_lpf_enable = gpio[GPIO4_8]; // LPF enable (PA_1)
|
||||
constexpr GPIO gpio_rf_amp_enable = gpio[GPIO4_9]; // RF amp enable (PA_2)
|
||||
constexpr GPIO gpio_ant_bias_disable = gpio[GPIO1_12]; // Antenna bias disable (P2_12)
|
||||
|
||||
// PRALINE mixer lock detect (gpio_max2831_ld defined above at line 93)
|
||||
constexpr GPIO gpio_rffc5072_ld = gpio[GPIO6_25]; // Mixer lock detect (PD_11)
|
||||
// constexpr GPIO gpio_pps_out = gpio[GPIO5_5]; // PPS output (P2_5)
|
||||
|
||||
// PRALINE LED4
|
||||
constexpr GPIO gpio_led4 = gpio[GPIO4_6]; // LED4 (P8_6)
|
||||
|
||||
// PRALINE dual port control
|
||||
constexpr GPIO gpio_p1_ctrl0 = gpio[GPIO0_14]; // P1 control 0 (P2_10)
|
||||
constexpr GPIO gpio_p1_ctrl1 = gpio[GPIO5_16]; // P1 control 1 (P6_8)
|
||||
constexpr GPIO gpio_p1_ctrl2 = gpio[GPIO3_5]; // P1 control 2 (P6_9)
|
||||
constexpr GPIO gpio_p2_ctrl0 = gpio[GPIO7_3]; // P2 control 0 (PE_3)
|
||||
constexpr GPIO gpio_p2_ctrl1 = gpio[GPIO7_4]; // P2 control 1 (PE_4)
|
||||
constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15]; // CLKIN control (P1_20)
|
||||
|
||||
// PRALINE trigger/sync I/O
|
||||
constexpr GPIO gpio_trigger_in = gpio[GPIO6_26]; // Trigger input (PD_12)
|
||||
constexpr GPIO gpio_trigger_out = gpio[GPIO5_6]; // Trigger output (P2_6)
|
||||
constexpr GPIO gpio_pps_out = gpio[GPIO5_5]; // PPS output (P2_5)
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -175,12 +137,6 @@ constexpr GPIO gpio_r9_clkout_en = gpio[GPIO0_9];
|
||||
constexpr GPIO gpio_r9_mcu_clk_en = gpio[GPIO0_8];
|
||||
constexpr GPIO gpio_r9_not_ant_pwr = gpio[GPIO2_4];
|
||||
|
||||
/* LEDs */
|
||||
|
||||
constexpr LED led_usb{gpio_led_usb};
|
||||
constexpr LED led_rx{gpio_led_rx};
|
||||
constexpr LED led_tx{gpio_led_tx};
|
||||
|
||||
} /* namespace one */
|
||||
} /* namespace hackrf */
|
||||
|
||||
|
||||
@@ -54,6 +54,12 @@ constexpr ClockFrequency max283x_reference_f = 40000000U;
|
||||
constexpr ClockFrequency mcu_clkin_og_f = 40000000U;
|
||||
constexpr ClockFrequency mcu_clkin_r9_f = 10000000U;
|
||||
|
||||
#ifdef PRALINE
|
||||
constexpr int8_t default_tx_gain_db = 16;
|
||||
#else
|
||||
constexpr int8_t default_tx_gain_db = 35;
|
||||
#endif
|
||||
|
||||
constexpr uint8_t si5351_i2c_address = 0x60;
|
||||
|
||||
/* Clock Generator */
|
||||
|
||||
@@ -57,7 +57,7 @@ void I2cDev_ADS1110::update() {
|
||||
uint16_t voltage = readVoltage();
|
||||
uint8_t batteryPercentage = battery::BatteryManagement::calc_percent_voltage(voltage);
|
||||
// send local message
|
||||
BatteryStateMessage msg{1, batteryPercentage, false, voltage};
|
||||
BatteryStateMessage msg{1, batteryPercentage, false, voltage, false};
|
||||
EventDispatcher::send_message(msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,10 @@ Boston, MA 02110-1301, USA.
|
||||
#include "i2cdev_max17055.hpp"
|
||||
#include "battery.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
namespace i2cdev {
|
||||
|
||||
@@ -189,13 +189,36 @@ void I2cDev_MAX17055::update() {
|
||||
uint8_t batteryPercentage = 102;
|
||||
uint16_t voltage = 0;
|
||||
int32_t current = 0;
|
||||
getBatteryInfo(validity, batteryPercentage, voltage, current);
|
||||
bool battChanged = false;
|
||||
|
||||
getBatteryInfo(validity, batteryPercentage, voltage, current, battChanged);
|
||||
if (validity == 0) {
|
||||
// no valid data, don't send message
|
||||
return;
|
||||
}
|
||||
// send local message
|
||||
BatteryStateMessage msg{validity, batteryPercentage, current >= 25, voltage};
|
||||
BatteryStateMessage msg{validity, batteryPercentage, current >= 25, voltage, battChanged};
|
||||
EventDispatcher::send_message(msg);
|
||||
}
|
||||
|
||||
void I2cDev_MAX17055::sleep_config(bool enable_sleep) {
|
||||
uint16_t config1 = read_register(0x1D);
|
||||
if (enable_sleep) {
|
||||
// A COMMSH bit (bit 6)
|
||||
config1 |= 0x0040;
|
||||
// ShdnTimer (0x3F):
|
||||
// THR = 0 -> ~45 sec (def, 0x0000)
|
||||
// THR = 1 -> ~90 sec (0x1000)
|
||||
// THR = 2 -> ~180 sec (0x2000)
|
||||
write_register(0x3F, 0x2000);
|
||||
} else {
|
||||
// A COMMSH bit (bit 6)
|
||||
config1 &= ~0x0040;
|
||||
}
|
||||
// write config register
|
||||
write_register(0x1D, config1);
|
||||
}
|
||||
|
||||
bool I2cDev_MAX17055::init(uint8_t addr_) {
|
||||
if (addr_ != I2CDEV_MAX17055_ADDR_1) return false;
|
||||
addr = addr_;
|
||||
@@ -205,10 +228,11 @@ bool I2cDev_MAX17055::init(uint8_t addr_) {
|
||||
bool return_status = true;
|
||||
if (needsInitialization()) {
|
||||
// First-time or POR initialization
|
||||
was_por = true;
|
||||
return_status = full_reset_and_init();
|
||||
}
|
||||
partialInit(); // If you always want hibernation disabled
|
||||
// statusClear(); // I am not sure if this should be here or not (Clear all bits in the Status register (0x00))
|
||||
chThdSleepMilliseconds(300); // wait for adc to fully wake up!
|
||||
partialInit();
|
||||
return return_status;
|
||||
}
|
||||
return false;
|
||||
@@ -253,7 +277,7 @@ bool I2cDev_MAX17055::initialize_custom_parameters() {
|
||||
if (!write_register(0x1E, 0x03C0)) return false; // IChgTerm
|
||||
if (!write_register(0x3A, 0x9661)) return false; // VEmpty
|
||||
if (!write_register(0x60, 0x0090)) return false; // Unknown register
|
||||
if (!write_register(0x46, ((designcap / 32) * 44138) * designcap)) return false; // dPAcc
|
||||
if (!write_register(0x46, ((designcap / 32) * 44138) / designcap)) return false; // dPAcc = dQAcc * 44138 / DesignCap (Maxim EZ config)
|
||||
if (!write_register(0xDB, 0x8000)) return false; // ModelCfg --we should wait till it loads here. While (ReadRegister(0xDB)&0x8000) Wait(10);//do not continue until ModelCFG.Refresh == 0
|
||||
if (!write_register(0x40, 0x0001)) return false; // Set user mem to 1
|
||||
return true;
|
||||
@@ -284,17 +308,18 @@ bool I2cDev_MAX17055::clear_por() {
|
||||
}
|
||||
|
||||
bool I2cDev_MAX17055::needsInitialization() {
|
||||
uint16_t UserMem1 = read_register(0x40);
|
||||
|
||||
if (UserMem1 == 0) {
|
||||
return true;
|
||||
uint16_t status = read_register(0x00);
|
||||
if (status == 0xFFFF) {
|
||||
chThdSleepMilliseconds(15);
|
||||
status = read_register(0x00);
|
||||
}
|
||||
return false;
|
||||
return (status & 0x0002) != 0;
|
||||
}
|
||||
|
||||
void I2cDev_MAX17055::partialInit() {
|
||||
// Only update necessary volatile settings
|
||||
setHibCFG(0x0000); // If you always want hibernation disabled
|
||||
setHibCFG(0x0000); // If you always want hibernation disabled. this is a lower resolution mode, when the ic is on, and measuring, but on lower freq. depends un tha current (mA).
|
||||
sleep_config(false); // shut down the comm
|
||||
// Add any other volatile settings that need updating
|
||||
}
|
||||
|
||||
@@ -312,7 +337,12 @@ bool I2cDev_MAX17055::detect() {
|
||||
if (dev_name == 0x4010) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// try again, we we just woke up.
|
||||
chThdSleepMilliseconds(5);
|
||||
dev_name = read_register(0x21);
|
||||
if (dev_name == 0x4010) {
|
||||
return true;
|
||||
}
|
||||
// If DevName doesn't match, try reading Status register as a fallback
|
||||
uint16_t status = read_register(0x00);
|
||||
if (status != 0xFFFF && status != 0x0000) {
|
||||
@@ -354,14 +384,35 @@ bool I2cDev_MAX17055::write_register(const uint8_t reg, const uint16_t value) {
|
||||
return success;
|
||||
}
|
||||
|
||||
void I2cDev_MAX17055::getBatteryInfo(uint8_t& valid_mask, uint8_t& batteryPercentage, uint16_t& voltage, int32_t& current) {
|
||||
void I2cDev_MAX17055::getBatteryInfo(uint8_t& valid_mask, uint8_t& batteryPercentage, uint16_t& voltage, int32_t& current, bool& battMayChanged) {
|
||||
// Read Status Register
|
||||
uint16_t status = read_register(0x00);
|
||||
battMayChanged = false;
|
||||
voltage = averageMVoltage();
|
||||
if ((status == 0 && voltage == 0) || (status == 0x0002 && voltage == 3600) || (status == 0x0002 && voltage == 0)) {
|
||||
valid_mask = 0;
|
||||
return;
|
||||
}
|
||||
if (status == 0xFFFF) {
|
||||
valid_mask = 0;
|
||||
return;
|
||||
}
|
||||
bool requires_reset = false;
|
||||
|
||||
if (((status & 0xFF) >> 1) & 0x01) { // POR FLAG
|
||||
requires_reset = true;
|
||||
was_por = true;
|
||||
}
|
||||
// Execute the reset if either flag was tripped
|
||||
if (requires_reset) {
|
||||
reInit(); // reinit the ic. that takes time, so we'll return
|
||||
valid_mask = 0;
|
||||
return;
|
||||
}
|
||||
#ifdef MAX17055_REPORT_POR_FLAG
|
||||
battMayChanged = was_por && portapack::persistent_memory::battery_replaceable(); // only signal a change if the battery is replaceable. othervise do it silently
|
||||
#endif
|
||||
|
||||
batteryPercentage = stateOfCharge();
|
||||
current = instantCurrent();
|
||||
valid_mask = 31; // BATT_VALID_VOLTAGE + CURRENT + PERCENT + BATT_VALID_CYCLES + BATT_VALID_TTEF
|
||||
@@ -551,15 +602,6 @@ bool I2cDev_MAX17055::setModelCfg(const uint8_t _Model_ID) {
|
||||
}
|
||||
|
||||
bool I2cDev_MAX17055::setHibCFG(const uint16_t _Config) {
|
||||
uint16_t config1_reg = read_register(0x1D);
|
||||
|
||||
// (SHDN: 0x80) and (COMMSH: 0x40) Otherwise it will go into sleep mode after 45 seconds.
|
||||
config1_reg &= ~(0x0080 | 0x0040);
|
||||
|
||||
if (!write_register(0x1D, config1_reg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return write_register(0xBA, _Config);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
#include "i2cdevmanager.hpp"
|
||||
|
||||
// if this define is set, the UI will show the battery has changed (POR flag is set) until the settings are saved (not ic reinited, but settings applyed by user!)
|
||||
#define MAX17055_REPORT_POR_FLAG 1
|
||||
|
||||
#define MAX17055_POR 0
|
||||
#define MAX17055_IMin 1
|
||||
#define MAX17055_IMax 2
|
||||
@@ -139,7 +142,7 @@
|
||||
// Tmn, Vmx, Vmn, Imx, and Imn bits of the Status register
|
||||
// (00h) are not disabled.
|
||||
#ifndef MAX17055_Aen
|
||||
#define MAX17055_Aen 1
|
||||
#define MAX17055_Aen 0
|
||||
#endif
|
||||
|
||||
// This allows the host to control the bias of the thermistor switch or
|
||||
@@ -175,7 +178,7 @@
|
||||
|
||||
// Set to 1 and set ETHRM or FTHRM to 1 to enable temperature measurements selected by Config.TSel.
|
||||
#ifndef MAX17055_Ten
|
||||
#define MAX17055_Ten 1
|
||||
#define MAX17055_Ten 0
|
||||
#endif
|
||||
|
||||
// Set to 1 to enable device shutdown when the IC is mounted host side and the battery is removed.
|
||||
@@ -231,7 +234,7 @@
|
||||
// Set this bit to 1 to enable temperature based alerts.
|
||||
// Write this bit to 0 to disable temperature alerts. This bit is set to 1 at power-up.
|
||||
#ifndef MAX17055_TAIrtEN
|
||||
#define MAX17055_TAIrtEN 1
|
||||
#define MAX17055_TAIrtEN 0
|
||||
#endif
|
||||
|
||||
// Set this bit to 1 to enable alert output with the Status.dSOCi bit function.
|
||||
@@ -283,14 +286,20 @@ class I2cDev_MAX17055 : public I2cDev {
|
||||
void update() override;
|
||||
bool detect();
|
||||
|
||||
void getBatteryInfo(uint8_t& valid_mask, uint8_t& batteryPercentage, uint16_t& voltage, int32_t& current);
|
||||
void getBatteryInfo(uint8_t& valid_mask, uint8_t& batteryPercentage, uint16_t& voltage, int32_t& current, bool& battMayChanged);
|
||||
bool reset_learned();
|
||||
|
||||
float getValue(const char* entityName);
|
||||
uint16_t averageMVoltage(void);
|
||||
int32_t instantCurrent(void);
|
||||
uint16_t stateOfCharge(void);
|
||||
bool reInit(); // call when battery parameters changed from ui. don't call if not needed, or the battery is not changed!!!
|
||||
bool reInit(); // call when battery parameters changed from ui. don't call if not needed, or the battery is not changed!!!
|
||||
bool getIsBattChanged() { return statusControl(MAX17055_POR); } // true if the ic thinks we have a new battery
|
||||
void resetChangedFlag() {
|
||||
clear_por();
|
||||
was_por = false;
|
||||
} // reset the flag
|
||||
void sleep_config(bool enable_sleep); // if true, the ic can sleep after 3 minutes of inactivity (over i2c). can be waken up on any i2c communication (first packet may be dropped)
|
||||
private:
|
||||
const RegisterEntry* findEntry(const char* name) const;
|
||||
|
||||
@@ -323,6 +332,8 @@ class I2cDev_MAX17055 : public I2cDev {
|
||||
bool setModelCfg(const uint8_t _Model_ID);
|
||||
bool setHibCFG(const uint16_t _Config);
|
||||
void config(void);
|
||||
|
||||
bool was_por = false;
|
||||
};
|
||||
|
||||
} /* namespace i2cdev */
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __LED_H__
|
||||
#define __LED_H__
|
||||
|
||||
#include "gpio.hpp"
|
||||
|
||||
struct LED {
|
||||
constexpr LED(const GPIO gpio)
|
||||
: _gpio{gpio} {
|
||||
}
|
||||
|
||||
void setup() const {
|
||||
#ifdef PRALINE
|
||||
/* PRALINE LEDs are active-low (GPIO LOW = LED ON) */
|
||||
_gpio.set(); /* Start with LED OFF (HIGH) */
|
||||
#else
|
||||
_gpio.clear();
|
||||
#endif
|
||||
_gpio.output();
|
||||
_gpio.configure();
|
||||
}
|
||||
|
||||
void on() const {
|
||||
#ifdef PRALINE
|
||||
_gpio.clear(); /* LOW = ON for PRALINE */
|
||||
#else
|
||||
_gpio.set();
|
||||
#endif
|
||||
}
|
||||
|
||||
void off() const {
|
||||
#ifdef PRALINE
|
||||
_gpio.set(); /* HIGH = OFF for PRALINE */
|
||||
#else
|
||||
_gpio.clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
void toggle() const {
|
||||
_gpio.toggle();
|
||||
}
|
||||
|
||||
void write(const bool value) const {
|
||||
#ifdef PRALINE
|
||||
_gpio.write(!value); /* Invert for PRALINE */
|
||||
#else
|
||||
_gpio.write(value);
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
const GPIO _gpio;
|
||||
};
|
||||
|
||||
#endif /*__LED_H__*/
|
||||
@@ -1214,7 +1214,7 @@ class SSTVRXConfigureMessage : public Message {
|
||||
public:
|
||||
constexpr SSTVRXConfigureMessage(
|
||||
const uint8_t code)
|
||||
: Message{id : ID::SSTVRXConfigure},
|
||||
: Message{ID::SSTVRXConfigure},
|
||||
code(code) {
|
||||
}
|
||||
|
||||
@@ -1610,17 +1610,20 @@ class BatteryStateMessage : public Message {
|
||||
uint8_t valid_mask,
|
||||
uint8_t percent,
|
||||
bool on_charger,
|
||||
uint16_t voltage)
|
||||
uint16_t voltage,
|
||||
bool battMayChanged)
|
||||
: Message{ID::BatteryStateData},
|
||||
valid_mask{valid_mask},
|
||||
percent{percent},
|
||||
on_charger{on_charger},
|
||||
voltage{voltage} {
|
||||
voltage{voltage},
|
||||
battMayChanged{battMayChanged} {
|
||||
}
|
||||
uint8_t valid_mask = 0;
|
||||
uint8_t percent = 0;
|
||||
bool on_charger = false;
|
||||
uint16_t voltage = 0; // mV
|
||||
bool battMayChanged = false;
|
||||
};
|
||||
|
||||
class ProtoViewDataMessage : public Message {
|
||||
|
||||
@@ -337,8 +337,8 @@ enum GPIOs {
|
||||
GPIO4_14,
|
||||
// Power control and LED4
|
||||
GPIO4_1,
|
||||
GPIO8_4,
|
||||
GPIO8_5,
|
||||
GPIO4_4, // VBUS_IN_EN
|
||||
GPIO4_5, // VIN_IN_EN
|
||||
GPIO4_6,
|
||||
GPIO4_7,
|
||||
// RF path control
|
||||
@@ -451,8 +451,8 @@ constexpr GPIO gpio[] = {
|
||||
#ifdef PRALINE
|
||||
[GPIO4_14] = {pins[P9_2], 4, 14, 0}, // RFFC5072 mixer data
|
||||
[GPIO4_1] = {pins[P8_1], 4, 1, 0}, // VAA disable
|
||||
[GPIO8_4] = {pins[P8_4], 8, 4, 0}, // VBUS_IN_EN
|
||||
[GPIO8_5] = {pins[P8_5], 8, 5, 0}, // VIN_IN_EN
|
||||
[GPIO4_4] = {pins[P8_4], 4, 4, 0}, // VBUS_IN_EN
|
||||
[GPIO4_5] = {pins[P8_5], 4, 5, 0}, // VIN_IN_EN
|
||||
[GPIO4_6] = {pins[P8_6], 4, 6, 0}, // LED4
|
||||
[GPIO4_7] = {pins[P8_7], 4, 7, 0}, // 1V2 enable
|
||||
[GPIO4_8] = {pins[PA_1], 4, 8, 0}, // LPF enable
|
||||
@@ -470,8 +470,8 @@ constexpr GPIO gpio[] = {
|
||||
// Placeholder entries for non-PRALINE builds (use P0_0 as dummy)
|
||||
[GPIO4_14] = {pins[P0_0], 4, 14, 0},
|
||||
[GPIO4_1] = {pins[P0_0], 4, 1, 0},
|
||||
[GPIO8_4] = {pins[P0_0], 8, 4, 0},
|
||||
[GPIO8_5] = {pins[P0_0], 8, 5, 0},
|
||||
[GPIO4_4] = {pins[P0_0], 4, 4, 0},
|
||||
[GPIO4_5] = {pins[P0_0], 4, 5, 0},
|
||||
[GPIO4_6] = {pins[P0_0], 4, 6, 0},
|
||||
[GPIO4_7] = {pins[P0_0], 4, 7, 0},
|
||||
[GPIO4_8] = {pins[P0_0], 4, 8, 0},
|
||||
|
||||
@@ -212,7 +212,7 @@ struct data_t {
|
||||
bool updown_frequency_tx_correction;
|
||||
bool lcd_normally_black : 1;
|
||||
bool encoder_dial_direction : 1; // false = normal, true = reverse
|
||||
bool UNUSED_6 : 1;
|
||||
bool battery_replaceable : 1;
|
||||
bool UNUSED_7 : 1;
|
||||
|
||||
// up/down converter offset
|
||||
@@ -290,7 +290,7 @@ struct data_t {
|
||||
updown_frequency_tx_correction(false),
|
||||
lcd_normally_black(false),
|
||||
encoder_dial_direction(false),
|
||||
UNUSED_6(false),
|
||||
battery_replaceable(false),
|
||||
UNUSED_7(false),
|
||||
|
||||
converter_frequency_offset(0),
|
||||
@@ -446,6 +446,7 @@ void defaults() {
|
||||
set_config_tx_gain_max_db(47);
|
||||
|
||||
set_battery_cap_mah(0);
|
||||
set_battery_replaceable(false);
|
||||
}
|
||||
|
||||
void init() {
|
||||
@@ -1258,6 +1259,14 @@ void set_battery_cap_mah(uint16_t mah) {
|
||||
}
|
||||
}
|
||||
|
||||
void set_battery_replaceable(bool v) {
|
||||
data->battery_replaceable = v;
|
||||
}
|
||||
|
||||
bool battery_replaceable() {
|
||||
return data->battery_replaceable;
|
||||
}
|
||||
|
||||
uint32_t battery_cap_mah() {
|
||||
if (battery_cap_valid()) {
|
||||
return data->misc_config.batt_cap_mah;
|
||||
|
||||
@@ -395,6 +395,8 @@ uint32_t pmem_calculated_checksum(void);
|
||||
void set_battery_cap_mah(uint16_t mah); // 0 is not known; use assumed default based on device type/build config
|
||||
uint32_t battery_cap_mah();
|
||||
bool battery_cap_valid();
|
||||
void set_battery_replaceable(bool v);
|
||||
bool battery_replaceable();
|
||||
|
||||
size_t data_size();
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
#ifndef __SINE_TABLE_I8_H__
|
||||
#define __SINE_TABLE_I8_H__
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
static const int8_t sine_table_i8[256] = {
|
||||
static constexpr int8_t sine_table_i8[256] = {
|
||||
0, 2, 5, 8, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45,
|
||||
48, 51, 54, 57, 59, 62, 65, 67, 70, 73, 75, 78, 80, 83, 85, 87,
|
||||
90, 92, 94, 96, 98, 100, 102, 104, 105, 107, 109, 110, 112, 113, 115, 116,
|
||||
|
||||
+16
-16
@@ -26,6 +26,9 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
using Coord = int16_t;
|
||||
using Dim = int16_t;
|
||||
|
||||
// Positioning helpers PER CHARACTERS (8*16)
|
||||
// EACH parameters must be used az CHAR position, not PX coordinates. So If you wanna use the 8,16 coordinates (that is the second character in X and second character in Y you must use UI_POS_X(1) UI_POS_Y(1) (since we count from 0)
|
||||
|
||||
@@ -36,31 +39,31 @@ namespace ui {
|
||||
// default font width
|
||||
#define UI_POS_DEFAULT_WIDTH 8
|
||||
// px position of the linenum-th character (Y)
|
||||
#define UI_POS_Y(linenum) ((int)((linenum) * UI_POS_DEFAULT_HEIGHT))
|
||||
#define UI_POS_Y(linenum) ((Coord)((linenum) * UI_POS_DEFAULT_HEIGHT))
|
||||
// px position of the linenum-th character from the bottom of the screen (Y) (please calculate the +1 line top-bar to it too if that is visible!)
|
||||
#define UI_POS_Y_BOTTOM(linenum) ((int)(screen_height - (linenum) * UI_POS_DEFAULT_HEIGHT))
|
||||
#define UI_POS_Y_BOTTOM(linenum) ((Coord)(screen_height - (int)((linenum) * UI_POS_DEFAULT_HEIGHT)))
|
||||
// px position of the linenum-th character from the left of the screen (X)
|
||||
#define UI_POS_X(charnum) ((int)((charnum) * UI_POS_DEFAULT_WIDTH))
|
||||
#define UI_POS_X(charnum) ((Coord)((charnum) * UI_POS_DEFAULT_WIDTH))
|
||||
// px position of the linenum-th character from the right of the screen (X)
|
||||
#define UI_POS_X_RIGHT(charnum) ((int)(screen_width - ((charnum) * UI_POS_DEFAULT_WIDTH)))
|
||||
#define UI_POS_X_RIGHT(charnum) ((Coord)(screen_width - (int)((charnum) * UI_POS_DEFAULT_WIDTH)))
|
||||
// px position of the left character from the center of the screen (X) (for N character wide string)
|
||||
#define UI_POS_X_CENTER(charnum) ((int)((screen_width / 2) - ((charnum) * UI_POS_DEFAULT_WIDTH / 2)))
|
||||
// px position of the currcol in a table with colnum number of columns, where one coloumn is charnum characters wide maximum
|
||||
#define UI_POS_X_TABLE(colnum, currcol) ((currcol) * (screen_width / (colnum)))
|
||||
#define UI_POS_X_CENTER(charnum) ((Coord)((screen_width / 2) - (int)((charnum) * UI_POS_DEFAULT_WIDTH / 2)))
|
||||
// px position of the currcol in a table with colnum number of columns, where one column is charnum characters wide maximum
|
||||
#define UI_POS_X_TABLE(colnum, currcol) ((Coord)((currcol) * (screen_width / (colnum))))
|
||||
// px width of N characters
|
||||
#define UI_POS_WIDTH(charnum) ((int)((charnum) * UI_POS_DEFAULT_WIDTH))
|
||||
#define UI_POS_WIDTH(charnum) ((Dim)((charnum) * UI_POS_DEFAULT_WIDTH))
|
||||
// px width of the screen
|
||||
#define UI_POS_MAXWIDTH (screen_width)
|
||||
// px height of N line
|
||||
#define UI_POS_HEIGHT(linecount) ((int)((linecount) * UI_POS_DEFAULT_HEIGHT))
|
||||
#define UI_POS_HEIGHT(linecount) ((Dim)((linecount) * UI_POS_DEFAULT_HEIGHT))
|
||||
// px height of the screen's percent
|
||||
#define UI_POS_HEIGHT_PERCENT(percent) ((int)(screen_height * (percent) / 100))
|
||||
#define UI_POS_HEIGHT_PERCENT(percent) ((Dim)(screen_height * (percent) / 100))
|
||||
// remaining px from the linenum-th line to the bottom of the screen. (please calculate the +1 line top-bar to it too if that is visible!)
|
||||
#define UI_POS_HEIGHT_REMAINING(linenum) ((int)(screen_height - ((linenum) * UI_POS_DEFAULT_HEIGHT)))
|
||||
#define UI_POS_HEIGHT_REMAINING(linenum) ((Dim)(screen_height - (int)((linenum) * UI_POS_DEFAULT_HEIGHT)))
|
||||
// remaining px from the charnum-th character to the right of the screen
|
||||
#define UI_POS_WIDTH_REMAINING(charnum) ((int)(screen_width - ((charnum) * UI_POS_DEFAULT_WIDTH)))
|
||||
#define UI_POS_WIDTH_REMAINING(charnum) ((Dim)(screen_width - (int)((charnum) * UI_POS_DEFAULT_WIDTH)))
|
||||
// px width of the screen's percent
|
||||
#define UI_POS_WIDTH_PERCENT(percent) ((int)(screen_width * (percent) / 100))
|
||||
#define UI_POS_WIDTH_PERCENT(percent) ((Dim)(screen_width * (percent) / 100))
|
||||
// px width of the screen
|
||||
#define UI_POS_MAXHEIGHT (screen_height)
|
||||
|
||||
@@ -85,9 +88,6 @@ namespace ui {
|
||||
|
||||
#define DEG_TO_RAD(d) (d * (2 * pi) / 360.0)
|
||||
|
||||
using Coord = int16_t;
|
||||
using Dim = int16_t;
|
||||
|
||||
extern uint16_t screen_width;
|
||||
extern uint16_t screen_height;
|
||||
|
||||
|
||||
@@ -206,7 +206,11 @@ void Widget::set_style(const Style* new_style) {
|
||||
}
|
||||
|
||||
const Style& Widget::style() const {
|
||||
return style_ ? *style_ : parent()->style();
|
||||
for (const Widget* curr = this; curr; curr = curr->parent()) {
|
||||
if (curr->style_) return *curr->style_;
|
||||
}
|
||||
// default style should always be set by SystemView
|
||||
return *style_;
|
||||
}
|
||||
|
||||
void Widget::drawn(bool v) {
|
||||
@@ -2184,7 +2188,7 @@ bool TextField::on_touch(TouchEvent event) {
|
||||
|
||||
BatteryIcon::BatteryIcon(Rect parent_rect, uint8_t percent)
|
||||
: Widget(parent_rect) {
|
||||
this->set_battery(percent <= 100 ? 1 : 0, percent, false);
|
||||
this->set_battery(percent <= 100 ? 1 : 0, percent, false, false);
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
@@ -2195,11 +2199,13 @@ void BatteryIcon::getWidgetName(std::string& result) {
|
||||
result = "Battery percent";
|
||||
}
|
||||
|
||||
void BatteryIcon::set_battery(uint8_t valid_mask, uint8_t percentage, bool charge) {
|
||||
if (charge == charge_ && percent_ == percentage && valid_ == valid_mask) return;
|
||||
void BatteryIcon::set_battery(uint8_t valid_mask, uint8_t percentage, bool charge, bool batt_changed) {
|
||||
if (charge == charge_ && percent_ == percentage && valid_ == valid_mask && batt_changed == batt_changed_) return;
|
||||
percent_ = percentage;
|
||||
charge_ = charge;
|
||||
valid_ = valid_mask;
|
||||
batt_changed_ = batt_changed;
|
||||
|
||||
if ((valid_mask & battery::BatteryManagement::BATT_VALID_VOLTAGE) != battery::BatteryManagement::BATT_VALID_VOLTAGE) percent_ = 102; // to indicate error
|
||||
set_dirty();
|
||||
}
|
||||
@@ -2227,6 +2233,7 @@ void BatteryIcon::paint(Painter& painter) {
|
||||
ui::Rect rect = screen_rect(); // 10, 1 * 16
|
||||
painter.fill_rectangle(rect, has_focus() || highlighted() ? Theme::getInstance()->fg_light->foreground : Theme::getInstance()->bg_dark->background); // clear
|
||||
ui::Color battColor = (charge_) ? Theme::getInstance()->fg_blue->foreground : Theme::getInstance()->fg_green->foreground;
|
||||
if (batt_changed_) battColor = Theme::getInstance()->fg_orange->foreground;
|
||||
// batt body:
|
||||
painter.draw_vline({rect.left() + 1, rect.top() + 2}, rect.height() - 4, battColor);
|
||||
painter.draw_vline({rect.right() - 2, rect.top() + 2}, rect.height() - 4, battColor);
|
||||
@@ -2251,6 +2258,7 @@ void BatteryIcon::paint(Painter& painter) {
|
||||
else
|
||||
battColor = Theme::getInstance()->fg_red->foreground;
|
||||
}
|
||||
if (batt_changed_) battColor = Theme::getInstance()->fg_orange->foreground;
|
||||
// fill the bars
|
||||
for (int y = pp; y < ppx; y++) {
|
||||
painter.draw_hline({rect.left() + 2, rect.top() + 3 + y}, rect.width() - 4, battColor);
|
||||
@@ -2261,7 +2269,7 @@ void BatteryIcon::paint(Painter& painter) {
|
||||
|
||||
BatteryTextField::BatteryTextField(Rect parent_rect, uint8_t percent)
|
||||
: Widget(parent_rect) {
|
||||
this->set_battery(percent <= 100 ? 1 : 0, percent, false);
|
||||
this->set_battery(percent <= 100 ? 1 : 0, percent, false, false);
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
@@ -2275,8 +2283,11 @@ void BatteryTextField::paint(Painter& painter) {
|
||||
xdelta = 5;
|
||||
else if (txt_batt.length() == 2)
|
||||
xdelta = 2;
|
||||
std::string post = " %";
|
||||
if (charge_) post = "+%";
|
||||
if (batt_changed_) post = "!%";
|
||||
painter.draw_string({rect.left() + xdelta, rect.top()}, font::fixed_5x8, Theme::getInstance()->bg_dark->foreground, bg, txt_batt);
|
||||
painter.draw_string({rect.left(), rect.top() + 8}, font::fixed_5x8, Theme::getInstance()->bg_dark->foreground, bg, (charge_) ? "+%" : " %");
|
||||
painter.draw_string({rect.left(), rect.top() + 8}, font::fixed_5x8, Theme::getInstance()->bg_dark->foreground, bg, post);
|
||||
}
|
||||
|
||||
void BatteryTextField::getAccessibilityText(std::string& result) {
|
||||
@@ -2286,11 +2297,12 @@ void BatteryTextField::getWidgetName(std::string& result) {
|
||||
result = "Battery percent";
|
||||
}
|
||||
|
||||
void BatteryTextField::set_battery(uint8_t valid_mask, uint8_t percentage, bool charge) {
|
||||
if (charge == charge_ && percent_ == percentage && valid_ == valid_mask) return;
|
||||
void BatteryTextField::set_battery(uint8_t valid_mask, uint8_t percentage, bool charge, bool batt_changed) {
|
||||
if (charge == charge_ && percent_ == percentage && valid_ == valid_mask && batt_changed == batt_changed_) return;
|
||||
charge_ = charge;
|
||||
percent_ = percentage;
|
||||
valid_ = valid_mask;
|
||||
batt_changed_ = batt_changed;
|
||||
if ((valid_mask & battery::BatteryManagement::BATT_VALID_VOLTAGE) != battery::BatteryManagement::BATT_VALID_VOLTAGE) percent_ = 102; // to indicate error
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
@@ -808,7 +808,7 @@ class BatteryTextField : public Widget {
|
||||
BatteryTextField(Rect parent_rect, uint8_t percent);
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
void set_battery(uint8_t valid_mask, uint8_t percentage, bool charge);
|
||||
void set_battery(uint8_t valid_mask, uint8_t percentage, bool charge, bool batt_changed);
|
||||
void set_text(std::string_view value);
|
||||
|
||||
bool on_key(KeyEvent key) override;
|
||||
@@ -821,6 +821,7 @@ class BatteryTextField : public Widget {
|
||||
uint8_t percent_{102};
|
||||
uint8_t valid_{0};
|
||||
bool charge_{false};
|
||||
bool batt_changed_{false};
|
||||
|
||||
Style style{
|
||||
.font = font::fixed_5x8,
|
||||
@@ -835,7 +836,7 @@ class BatteryIcon : public Widget {
|
||||
|
||||
BatteryIcon(Rect parent_rect, uint8_t percent);
|
||||
void paint(Painter& painter) override;
|
||||
void set_battery(uint8_t valid_mask, uint8_t percentage, bool charge);
|
||||
void set_battery(uint8_t valid_mask, uint8_t percentage, bool charge, bool batt_changed);
|
||||
|
||||
bool on_key(KeyEvent key) override;
|
||||
bool on_touch(TouchEvent event) override;
|
||||
@@ -847,6 +848,7 @@ class BatteryIcon : public Widget {
|
||||
uint8_t percent_{102};
|
||||
uint8_t valid_{0};
|
||||
bool charge_{false};
|
||||
bool batt_changed_{false};
|
||||
};
|
||||
|
||||
class NumberField : public Widget {
|
||||
|
||||
@@ -201,15 +201,11 @@ void Widget::set_style(const Style* new_style) {
|
||||
}
|
||||
|
||||
const Style& Widget::style() const {
|
||||
if (style_ != nullptr)
|
||||
return *style_;
|
||||
else {
|
||||
auto p = parent();
|
||||
if (p == nullptr)
|
||||
// TODO: debug
|
||||
while (true);
|
||||
return p->style();
|
||||
for (const Widget* curr = this; curr; curr = curr->parent()) {
|
||||
if (curr->style_) return *curr->style_;
|
||||
}
|
||||
// default style should always be set by SystemView
|
||||
return *style_;
|
||||
}
|
||||
|
||||
void Widget::drawn(bool v) {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "doctest.h"
|
||||
#include "convert.hpp"
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
|
||||
@@ -24,4 +24,4 @@
|
||||
# external app address ranges below must match those in linker file "external.ld"
|
||||
maximum_application_size = 32*1024
|
||||
external_apps_address_start = 0xADB00000
|
||||
external_apps_address_end = 0xAE060000
|
||||
external_apps_address_end = 0xAE090000
|
||||
|
||||
+1
-1
Submodule hackrf updated: f526ddf4eb...d371476c03
@@ -0,0 +1,647 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
DEFAULT_TOOLCHAIN_URL="https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/RC2.1/gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2?revision=6e63531f-8cb1-40b9-bbfc-8a57cdfc01b4&la=en&hash=F761343D43A0587E8AC0925B723C04DBFB848339"
|
||||
|
||||
mbt_log() {
|
||||
printf "mbt: %s\n" "$*"
|
||||
}
|
||||
|
||||
mbt_error() {
|
||||
printf "mbt: error: %s\n" "$*" >&2
|
||||
}
|
||||
|
||||
mbt_die() {
|
||||
mbt_error "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
mbt_command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
mbt_default_jobs() {
|
||||
local jobs
|
||||
|
||||
jobs="$(getconf _NPROCESSORS_ONLN 2>/dev/null || true)"
|
||||
if [[ -z "$jobs" ]] && mbt_command_exists nproc; then
|
||||
jobs="$(nproc)"
|
||||
fi
|
||||
if [[ -z "$jobs" ]]; then
|
||||
jobs=1
|
||||
fi
|
||||
|
||||
printf "%s" "$jobs"
|
||||
}
|
||||
|
||||
mbt_detect_distro() {
|
||||
if mbt_command_exists apt-get; then
|
||||
printf "debian"
|
||||
return 0
|
||||
fi
|
||||
if mbt_command_exists pacman; then
|
||||
printf "arch"
|
||||
return 0
|
||||
fi
|
||||
printf "unknown"
|
||||
}
|
||||
|
||||
mbt_run_privileged() {
|
||||
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
|
||||
"$@"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! mbt_command_exists sudo; then
|
||||
mbt_die "sudo is required for dependency installation. Re-run as root or install sudo."
|
||||
fi
|
||||
|
||||
sudo "$@"
|
||||
}
|
||||
|
||||
mbt_install_deps() {
|
||||
local distro
|
||||
distro="$(mbt_detect_distro)"
|
||||
|
||||
case "$distro" in
|
||||
debian)
|
||||
mbt_log "detected Debian-like environment; installing required packages."
|
||||
mbt_run_privileged apt-get update
|
||||
mbt_run_privileged apt-get install -y --no-install-recommends \
|
||||
git tar wget dfu-util cmake make python3 bzip2 lz4 curl hackrf \
|
||||
python3-setuptools python3-yaml binutils python3-setuptools ccache ninja-build
|
||||
;;
|
||||
arch)
|
||||
mbt_log "detected Arch-like environment; installing required packages."
|
||||
mbt_run_privileged pacman -Sy --needed --noconfirm \
|
||||
git tar wget dfu-util cmake make python3 bzip2 lz4 curl hackrf \
|
||||
python-distutils-extra python-setuptools python-pip python-yaml \
|
||||
ninja ccache binutils
|
||||
;;
|
||||
*)
|
||||
mbt_die "unsupported distribution. Install dependencies manually (see wiki on github), or you can send PR to support for your distro. Detected distro: $distro"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
mbt_sync_submodules() {
|
||||
local jobs
|
||||
jobs="$(mbt_default_jobs)"
|
||||
mbt_log "syncing submodules."
|
||||
git -C "$SCRIPT_PATH" submodule update --init --recursive --jobs "$((jobs * 2))"
|
||||
}
|
||||
|
||||
mbt_enable_toolchain_env() {
|
||||
export MBT_ROOT="$SCRIPT_PATH"
|
||||
export MBT_TOOLCHAIN_DIR="${MBT_TOOLCHAIN_DIR:-$SCRIPT_PATH/armbin}"
|
||||
export MBT_TOOLCHAIN_URL="${MBT_TOOLCHAIN_URL:-$DEFAULT_TOOLCHAIN_URL}"
|
||||
export MBT_EXPECTED_GCC_VERSION="${MBT_EXPECTED_GCC_VERSION:-9.2.1}"
|
||||
|
||||
if [[ -z "${MBT_FORCE_TOOLCHAIN+x}" ]]; then
|
||||
MBT_FORCE_TOOLCHAIN="${FORCE_TOOLCHAIN:-0}"
|
||||
fi
|
||||
export MBT_FORCE_TOOLCHAIN
|
||||
|
||||
if [[ -z "${MBT_NO_TOOLCHAIN_DOWNLOAD+x}" ]]; then
|
||||
MBT_NO_TOOLCHAIN_DOWNLOAD="${NO_TOOLCHAIN_DOWNLOAD:-0}"
|
||||
fi
|
||||
export MBT_NO_TOOLCHAIN_DOWNLOAD
|
||||
export MBT_VERBOSE="${MBT_VERBOSE:-}"
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. "$SCRIPT_PATH/scripts/toolchain/mbtenv.sh"
|
||||
}
|
||||
|
||||
mbt_use_existing_toolchain_env() {
|
||||
local repo_toolchain_bin="$SCRIPT_PATH/armbin/bin"
|
||||
|
||||
if [[ ! -d "$repo_toolchain_bin" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case ":$PATH:" in
|
||||
*":$repo_toolchain_bin:"*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
export PATH="$repo_toolchain_bin:$PATH"
|
||||
}
|
||||
|
||||
mbt_normalize_device() {
|
||||
local raw
|
||||
raw="$(printf "%s" "$1" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
case "$raw" in
|
||||
1|one|hackrf|hackrf-one)
|
||||
printf "hackrf-one"
|
||||
;;
|
||||
2|pro|hackrf-pro|praline)
|
||||
printf "hackrf-pro"
|
||||
;;
|
||||
3|portarf|porta-rf|porta_rf)
|
||||
printf "portarf"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
mbt_prompt_device() {
|
||||
local choice normalized
|
||||
|
||||
cat >&2 <<'EOF'
|
||||
Select device type:
|
||||
1) HackRF One
|
||||
2) HackRF Pro (PRALINE)
|
||||
3) PortaRF
|
||||
EOF
|
||||
|
||||
while true; do
|
||||
printf "Device [1-3]: " >&2
|
||||
if ! IFS= read -r choice; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
normalized="$(mbt_normalize_device "$choice" 2>/dev/null || true)"
|
||||
if [[ -n "$normalized" ]]; then
|
||||
printf "%s" "$normalized"
|
||||
return 0
|
||||
fi
|
||||
mbt_error "invalid device selection: $choice"
|
||||
done
|
||||
}
|
||||
|
||||
mbt_print_help() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./mbt One-step build flow (installs deps, syncs submodules, downloads toolchain, asks for device, builds)
|
||||
./mbt build [options] Developer build flow
|
||||
./mbt deps Install distro dependencies only
|
||||
./mbt toolchain [--force-toolchain]
|
||||
Download/setup ARM toolchain only
|
||||
./mbt doctor Validate host setup and report gaps
|
||||
./mbt shell [options] Open a shell with toolchain exported for this session
|
||||
|
||||
Build options:
|
||||
-d, --device <name> Device: hackrf-one | hackrf-pro | portarf
|
||||
--ccache / --no-ccache Toggle USE_CCACHE (default: on)
|
||||
--with-deps / --no-deps Install Linux dependencies before build
|
||||
--with-submodule-sync Update git submodules before build (default)
|
||||
--no-submodule-sync Skip submodule sync
|
||||
--with-toolchain Ensure toolchain exists and matches expected version (default)
|
||||
--no-toolchain Skip toolchain checks/downloads and use existing PATH (prepends ./armbin/bin if present)
|
||||
--force-toolchain Re-download and reinstall toolchain
|
||||
-B, --build-dir <path> Build directory (default: ./build)
|
||||
-G, --generator <name> CMake generator (default: Ninja)
|
||||
-j, --jobs <n> Build parallelism (default: host CPU count)
|
||||
--target <name> Build a specific target
|
||||
--clean Remove build directory, run libopencm3 clean, and exit
|
||||
--cmake-arg <arg> Extra CMake configure arg (repeatable)
|
||||
--build-arg <arg> Extra cmake --build arg (repeatable)
|
||||
-y, --non-interactive Fail instead of prompting when required input is missing
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
./mbt
|
||||
./mbt build --device hackrf-one
|
||||
./mbt build --device hackrf-pro --no-submodule-sync
|
||||
./mbt build --device portarf --no-toolchain --no-deps
|
||||
./mbt build --device hackrf-one --cmake-arg=-DVERSION_STRING=my-build
|
||||
EOF
|
||||
}
|
||||
|
||||
mbt_doctor() {
|
||||
local distro missing=0 toolchain_cc toolchain_version cmd
|
||||
local -a required_cmds
|
||||
|
||||
distro="$(mbt_detect_distro)"
|
||||
mbt_log "doctor report for $SCRIPT_PATH"
|
||||
mbt_log "distro family: $distro"
|
||||
|
||||
required_cmds=(git tar wget dfu-util cmake python3 bzip2 lz4 curl ccache ninja)
|
||||
if [[ "$distro" == "arch" ]]; then
|
||||
required_cmds+=(make)
|
||||
fi
|
||||
|
||||
for cmd in "${required_cmds[@]}"; do
|
||||
if mbt_command_exists "$cmd"; then
|
||||
printf " [ok] %s\n" "$cmd"
|
||||
else
|
||||
printf " [missing] %s\n" "$cmd"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
|
||||
toolchain_cc="$SCRIPT_PATH/armbin/bin/arm-none-eabi-gcc"
|
||||
if [[ -x "$toolchain_cc" ]]; then
|
||||
toolchain_version="$("$toolchain_cc" -dumpfullversion 2>/dev/null || "$toolchain_cc" -dumpversion 2>/dev/null || true)"
|
||||
printf " [ok] arm-none-eabi-gcc %s (%s)\n" "$toolchain_version" "$toolchain_cc"
|
||||
if [[ "$toolchain_version" != "9.2.1" ]]; then
|
||||
printf " [warn] expected arm-none-eabi-gcc 9.2.1\n"
|
||||
fi
|
||||
else
|
||||
if mbt_command_exists arm-none-eabi-gcc; then
|
||||
toolchain_cc="arm-none-eabi-gcc"
|
||||
toolchain_version="$("$toolchain_cc" -dumpfullversion 2>/dev/null || "$toolchain_cc" -dumpversion 2>/dev/null || true)"
|
||||
printf " [ok] arm-none-eabi-gcc %s (%s)\n" "$toolchain_version" "$toolchain_cc"
|
||||
if [[ "$toolchain_version" != "9.2.1" ]]; then
|
||||
printf " [warn] expected arm-none-eabi-gcc 9.2.1\n"
|
||||
fi
|
||||
else
|
||||
printf " [missing] %s\n" "$toolchain_cc"
|
||||
missing=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if git -C "$SCRIPT_PATH" submodule status --recursive | grep -q '^-'; then
|
||||
printf " [warn] some submodules are not initialized (run ./mbt build --with-submodule-sync --device hackrf-one)\n"
|
||||
else
|
||||
printf " [ok] submodules initialized\n"
|
||||
fi
|
||||
|
||||
if [[ "$missing" -ne 0 ]]; then
|
||||
mbt_error "doctor found missing prerequisites."
|
||||
return 1
|
||||
fi
|
||||
|
||||
mbt_log "doctor passed."
|
||||
return 0
|
||||
}
|
||||
|
||||
mbt_validate_jobs() {
|
||||
[[ "$1" =~ ^[0-9]+$ ]] && [[ "$1" -gt 0 ]]
|
||||
}
|
||||
|
||||
mbt_detect_cached_device() {
|
||||
local build_dir="$1"
|
||||
local cache_file="$build_dir/CMakeCache.txt"
|
||||
local board flash_limit
|
||||
|
||||
if [[ ! -f "$cache_file" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
board="$(grep -E '^BOARD:' "$cache_file" | head -n 1 | cut -d= -f2- || true)"
|
||||
flash_limit="$(grep -E '^FLASH_MB_LIMIT_SIZE:' "$cache_file" | head -n 1 | cut -d= -f2- || true)"
|
||||
|
||||
if [[ "$board" == "PRALINE" ]]; then
|
||||
printf "hackrf-pro"
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$flash_limit" in
|
||||
2|2.0)
|
||||
printf "portarf"
|
||||
return 0
|
||||
;;
|
||||
1|1.0)
|
||||
printf "hackrf-one"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
printf "unknown"
|
||||
return 0
|
||||
}
|
||||
|
||||
mbt_clean() {
|
||||
local build_dir="$1"
|
||||
local libopencm3_dir="$SCRIPT_PATH/hackrf/firmware/libopencm3"
|
||||
|
||||
mbt_log "cleaning build directory: $build_dir"
|
||||
if ! rm -rf "$build_dir"; then
|
||||
mbt_error "failed to delete build directory. Try running ./mbt --clean as root."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -d "$libopencm3_dir" ]]; then
|
||||
mbt_log "cleaning libopencm3: $libopencm3_dir"
|
||||
make -C "$libopencm3_dir" clean
|
||||
else
|
||||
mbt_error "libopencm3 directory not found: $libopencm3_dir"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
mbt_report_build_error() {
|
||||
printf '%s\n' 'Build error happened. Try to fix your code, or run ./mbt --clean and try again' >&2
|
||||
}
|
||||
|
||||
mbt_build() {
|
||||
local build_dir="$1"
|
||||
local generator="$2"
|
||||
local use_ccache="$3"
|
||||
local device="$4"
|
||||
local jobs="$5"
|
||||
local target="$6"
|
||||
local clean_build="$7"
|
||||
shift 7
|
||||
if [[ "$#" -lt 2 ]]; then
|
||||
mbt_die "internal error: mbt_build expected extra arg array names"
|
||||
fi
|
||||
|
||||
local -n cmake_extra_args_ref="$1"
|
||||
local -a cmake_extra_args=("${cmake_extra_args_ref[@]}")
|
||||
shift
|
||||
local -n build_extra_args_ref="$1"
|
||||
local -a build_extra_args=("${build_extra_args_ref[@]}")
|
||||
local -a cmake_args build_args device_args
|
||||
local marker_file="$build_dir/.mbt_device"
|
||||
local previous_device=""
|
||||
|
||||
if [[ "$clean_build" -eq 1 ]]; then
|
||||
mbt_clean "$build_dir"
|
||||
return 0
|
||||
else
|
||||
if [[ -f "$marker_file" ]]; then
|
||||
previous_device="$(<"$marker_file")"
|
||||
elif [[ -f "$build_dir/CMakeCache.txt" ]]; then
|
||||
previous_device="$(mbt_detect_cached_device "$build_dir" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [[ -n "$previous_device" ]] && [[ "$previous_device" != "$device" ]]; then
|
||||
mbt_log "device changed ($previous_device -> $device), cleaning build directory: $build_dir"
|
||||
rm -rf "$build_dir"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$build_dir"
|
||||
|
||||
device_args=()
|
||||
case "$device" in
|
||||
hackrf-one)
|
||||
device_args+=()
|
||||
;;
|
||||
hackrf-pro)
|
||||
device_args+=("-DBOARD=PRALINE")
|
||||
;;
|
||||
portarf)
|
||||
device_args+=("-DFLASH_MB_SIZE=2" "-DFLASH_MB_LIMIT_SIZE=2")
|
||||
;;
|
||||
*)
|
||||
mbt_die "unsupported device preset: $device"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$generator" == "Ninja" ]] && ! mbt_command_exists ninja; then
|
||||
if mbt_command_exists make; then
|
||||
mbt_log "ninja not found; falling back to Unix Makefiles."
|
||||
generator="Unix Makefiles"
|
||||
else
|
||||
mbt_die "ninja not found and make is unavailable. Install dependencies or set a working generator."
|
||||
fi
|
||||
fi
|
||||
|
||||
cmake_args=(
|
||||
-S "$SCRIPT_PATH"
|
||||
-B "$build_dir"
|
||||
-G "$generator"
|
||||
"-DUSE_CCACHE=$use_ccache"
|
||||
)
|
||||
cmake_args+=("${device_args[@]}")
|
||||
cmake_args+=("${cmake_extra_args[@]}")
|
||||
|
||||
mbt_log "configuring build for $device in $build_dir"
|
||||
if ! cmake "${cmake_args[@]}"; then
|
||||
mbt_report_build_error
|
||||
return 1
|
||||
fi
|
||||
printf "%s\n" "$device" > "$marker_file"
|
||||
|
||||
build_args=(--build "$build_dir" --parallel "$jobs")
|
||||
if [[ -n "$target" ]]; then
|
||||
build_args+=(--target "$target")
|
||||
fi
|
||||
build_args+=("${build_extra_args[@]}")
|
||||
|
||||
mbt_log "building..."
|
||||
if ! cmake "${build_args[@]}"; then
|
||||
mbt_report_build_error
|
||||
return 1
|
||||
fi
|
||||
mbt_log "build complete. Outputs are in $build_dir."
|
||||
mbt_log "The bare binary (for the hackrf_spiflash host tool), excluding external apps, is located at $build_dir/firmware/portapack-mayhem-firmware.bin"
|
||||
mbt_log "The full tar.gz package (for MayhemHub and the Flash Utility OTA app), including external apps and the firmware file, is located at $build_dir/firmware/portapack-mayhem_OCI.ppfw.tar"
|
||||
mbt_log "If you have the dev's SD card switcher hardware, the fast flash script is at $build_dir/flash.py"
|
||||
# have to hard code the name here cuz it's too expensive to read cmake for just a file name.
|
||||
}
|
||||
|
||||
main() {
|
||||
local command="build"
|
||||
local quickstart=0
|
||||
local device=""
|
||||
local use_ccache="ON"
|
||||
local with_deps=0
|
||||
local with_submodule_sync=1
|
||||
local with_toolchain=1
|
||||
local build_dir="$SCRIPT_PATH/build"
|
||||
local generator="Ninja"
|
||||
local jobs
|
||||
local target=""
|
||||
local clean_build=0
|
||||
local non_interactive=0
|
||||
local -a cmake_extra_args=()
|
||||
local -a build_extra_args=()
|
||||
|
||||
FORCE_TOOLCHAIN=0
|
||||
jobs="$(mbt_default_jobs)"
|
||||
|
||||
if [[ "$#" -eq 0 ]]; then
|
||||
quickstart=1
|
||||
with_deps=1
|
||||
fi
|
||||
|
||||
if [[ "$#" -gt 0 ]]; then
|
||||
case "$1" in
|
||||
build|deps|toolchain|doctor|shell)
|
||||
command="$1"
|
||||
shift
|
||||
;;
|
||||
-h|--help|help)
|
||||
mbt_print_help
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-d|--device)
|
||||
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
|
||||
device="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ccache)
|
||||
use_ccache="ON"
|
||||
shift
|
||||
;;
|
||||
--no-ccache)
|
||||
use_ccache="OFF"
|
||||
shift
|
||||
;;
|
||||
--with-deps)
|
||||
with_deps=1
|
||||
shift
|
||||
;;
|
||||
--no-deps)
|
||||
with_deps=0
|
||||
shift
|
||||
;;
|
||||
--with-submodule-sync)
|
||||
with_submodule_sync=1
|
||||
shift
|
||||
;;
|
||||
--no-submodule-sync)
|
||||
with_submodule_sync=0
|
||||
shift
|
||||
;;
|
||||
--with-toolchain)
|
||||
with_toolchain=1
|
||||
shift
|
||||
;;
|
||||
--no-toolchain)
|
||||
with_toolchain=0
|
||||
shift
|
||||
;;
|
||||
--force-toolchain)
|
||||
FORCE_TOOLCHAIN=1
|
||||
with_toolchain=1
|
||||
shift
|
||||
;;
|
||||
-B|--build-dir)
|
||||
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
|
||||
build_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
-G|--generator)
|
||||
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
|
||||
generator="$2"
|
||||
shift 2
|
||||
;;
|
||||
-j|--jobs)
|
||||
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
|
||||
jobs="$2"
|
||||
shift 2
|
||||
;;
|
||||
--target)
|
||||
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
|
||||
target="$2"
|
||||
shift 2
|
||||
;;
|
||||
--clean)
|
||||
clean_build=1
|
||||
shift
|
||||
;;
|
||||
--cmake-arg)
|
||||
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
|
||||
cmake_extra_args+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--cmake-arg=*)
|
||||
cmake_extra_args+=("${1#*=}")
|
||||
shift
|
||||
;;
|
||||
--build-arg)
|
||||
[[ "$#" -ge 2 ]] || mbt_die "$1 requires a value"
|
||||
build_extra_args+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--build-arg=*)
|
||||
build_extra_args+=("${1#*=}")
|
||||
shift
|
||||
;;
|
||||
-y|--non-interactive)
|
||||
non_interactive=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
mbt_print_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
mbt_die "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! mbt_validate_jobs "$jobs"; then
|
||||
mbt_die "invalid jobs value: $jobs"
|
||||
fi
|
||||
|
||||
case "$command" in
|
||||
deps)
|
||||
mbt_install_deps
|
||||
exit 0
|
||||
;;
|
||||
toolchain)
|
||||
NO_TOOLCHAIN_DOWNLOAD=0
|
||||
mbt_enable_toolchain_env
|
||||
mbt_log "toolchain ready: $SCRIPT_PATH/armbin"
|
||||
exit 0
|
||||
;;
|
||||
doctor)
|
||||
mbt_doctor
|
||||
exit $?
|
||||
;;
|
||||
shell)
|
||||
if [[ "$with_toolchain" -eq 1 ]]; then
|
||||
mbt_enable_toolchain_env
|
||||
else
|
||||
mbt_use_existing_toolchain_env
|
||||
fi
|
||||
mbt_log "spawning shell with toolchain exported in this session. Use 'exit' to leave."
|
||||
exec "${SHELL:-/bin/bash}" -i
|
||||
;;
|
||||
build)
|
||||
;;
|
||||
*)
|
||||
mbt_die "unsupported command: $command"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$clean_build" -eq 1 ]]; then
|
||||
mbt_clean "$build_dir"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$with_deps" -eq 1 ]]; then
|
||||
mbt_install_deps
|
||||
fi
|
||||
|
||||
if [[ "$with_submodule_sync" -eq 1 ]]; then
|
||||
mbt_sync_submodules
|
||||
fi
|
||||
|
||||
if [[ "$with_toolchain" -eq 1 ]]; then
|
||||
mbt_enable_toolchain_env
|
||||
else
|
||||
mbt_use_existing_toolchain_env
|
||||
fi
|
||||
|
||||
if [[ -n "$device" ]]; then
|
||||
device="$(mbt_normalize_device "$device" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "$device" ]]; then
|
||||
if [[ "$non_interactive" -eq 1 ]]; then
|
||||
mbt_die "--device is required when --non-interactive is set"
|
||||
fi
|
||||
if [[ ! -t 0 ]]; then
|
||||
mbt_die "missing --device and no interactive terminal available"
|
||||
fi
|
||||
device="$(mbt_prompt_device)" || mbt_die "failed to read device selection"
|
||||
fi
|
||||
|
||||
if [[ "$quickstart" -eq 1 ]]; then
|
||||
mbt_log "quickstart mode enabled."
|
||||
fi
|
||||
|
||||
mbt_build "$build_dir" "$generator" "$use_ccache" "$device" "$jobs" "$target" "$clean_build" \
|
||||
cmake_extra_args[@] build_extra_args[@]
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+209
@@ -0,0 +1,209 @@
|
||||
#!/bin/sh
|
||||
|
||||
# shellcheck disable=SC2039
|
||||
|
||||
MBT_ROOT="${MBT_ROOT:-$(pwd -P)}"
|
||||
MBT_VERBOSE="${MBT_VERBOSE:-}"
|
||||
MBT_TOOLCHAIN_DIR="${MBT_TOOLCHAIN_DIR:-$MBT_ROOT/armbin}"
|
||||
MBT_TOOLCHAIN_URL="${MBT_TOOLCHAIN_URL:-https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/RC2.1/gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2?revision=6e63531f-8cb1-40b9-bbfc-8a57cdfc01b4&la=en&hash=F761343D43A0587E8AC0925B723C04DBFB848339}"
|
||||
MBT_EXPECTED_GCC_VERSION="${MBT_EXPECTED_GCC_VERSION:-9.2.1}"
|
||||
MBT_FORCE_TOOLCHAIN="${MBT_FORCE_TOOLCHAIN:-0}"
|
||||
MBT_NO_TOOLCHAIN_DOWNLOAD="${MBT_NO_TOOLCHAIN_DOWNLOAD:-0}"
|
||||
MBT_TOOLCHAIN_BIN="$MBT_TOOLCHAIN_DIR/bin"
|
||||
MBT_TOOLCHAIN_CC="$MBT_TOOLCHAIN_BIN/arm-none-eabi-gcc"
|
||||
MBT_TMP_ARCHIVE=""
|
||||
MBT_DOWNLOADER=""
|
||||
|
||||
mbtenv_log() {
|
||||
printf "mbt: %s\n" "$*"
|
||||
}
|
||||
|
||||
mbtenv_verbose() {
|
||||
if [ -n "$MBT_VERBOSE" ]; then
|
||||
mbtenv_log "$*"
|
||||
fi
|
||||
}
|
||||
|
||||
mbtenv_command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
mbtenv_cleanup_tmp() {
|
||||
if [ -n "$MBT_TMP_ARCHIVE" ] && [ -f "$MBT_TMP_ARCHIVE" ]; then
|
||||
rm -f "$MBT_TMP_ARCHIVE"
|
||||
fi
|
||||
}
|
||||
|
||||
mbtenv_check_linux() {
|
||||
if [ "$(uname -s)" != "Linux" ]; then
|
||||
mbtenv_log "error: mbt currently supports Linux only."
|
||||
return 1
|
||||
fi
|
||||
|
||||
arch="$(uname -m)"
|
||||
case "$arch" in
|
||||
x86_64|amd64)
|
||||
# Supported architecture for the default x86_64 Linux toolchain archive.
|
||||
;;
|
||||
*)
|
||||
mbtenv_log "error: unsupported architecture '$arch'. mbt currently supports x86_64 Linux only for the bundled toolchain."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
mbtenv_choose_downloader() {
|
||||
if mbtenv_command_exists curl; then
|
||||
MBT_DOWNLOADER="curl"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if mbtenv_command_exists wget; then
|
||||
MBT_DOWNLOADER="wget"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mbtenv_log "error: neither curl nor wget found in PATH."
|
||||
return 1
|
||||
}
|
||||
|
||||
mbtenv_download_archive() {
|
||||
if [ "$MBT_DOWNLOADER" = "curl" ]; then
|
||||
curl --fail --location --progress-bar --output "$1" "$2"
|
||||
return $?
|
||||
fi
|
||||
|
||||
wget --show-progress --progress=bar:force -O "$1" "$2"
|
||||
}
|
||||
|
||||
mbtenv_get_installed_version() {
|
||||
if [ ! -x "$MBT_TOOLCHAIN_CC" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
"$MBT_TOOLCHAIN_CC" -dumpfullversion 2>/dev/null || "$MBT_TOOLCHAIN_CC" -dumpversion 2>/dev/null
|
||||
}
|
||||
|
||||
mbtenv_toolchain_is_expected() {
|
||||
INSTALLED_VERSION="$(mbtenv_get_installed_version 2>/dev/null || true)"
|
||||
if [ "$INSTALLED_VERSION" = "$MBT_EXPECTED_GCC_VERSION" ]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
mbtenv_download_and_unpack_toolchain() {
|
||||
mbtenv_choose_downloader || return 1
|
||||
|
||||
MBT_TMP_ARCHIVE="$(mktemp /tmp/mbt-toolchain-XXXXXX.tar.bz2)"
|
||||
trap mbtenv_cleanup_tmp EXIT INT TERM
|
||||
|
||||
mbtenv_log "downloading ARM GNU toolchain..."
|
||||
mbtenv_download_archive "$MBT_TMP_ARCHIVE" "$MBT_TOOLCHAIN_URL" || {
|
||||
mbtenv_log "error: failed to download toolchain from ARM."
|
||||
return 1
|
||||
}
|
||||
|
||||
mbtenv_log "putting toolchain into $MBT_TOOLCHAIN_DIR"
|
||||
if [ -d "$MBT_TOOLCHAIN_DIR" ] && [ ! -f "$MBT_TOOLCHAIN_DIR/bin/arm-none-eabi-gcc-9.2.1" ]; then
|
||||
mbtenv_log "Sorry, i can't verify if the toolchain dir is legit to delete. it either could be becasue you failed to download the full toolchain last time, or the enviroment var were pointed to something else, which is not safe for me to remove. please check if it's safe to remove $MBT_TOOLCHAIN_DIR and manually remove $MBT_TOOLCHAIN_DIR"
|
||||
return 1
|
||||
fi
|
||||
rm -rf "$MBT_TOOLCHAIN_DIR"
|
||||
mkdir -p "$MBT_TOOLCHAIN_DIR"
|
||||
tar --strip-components=1 -xjf "$MBT_TMP_ARCHIVE" -C "$MBT_TOOLCHAIN_DIR" || {
|
||||
mbtenv_log "error: failed to extract toolchain archive."
|
||||
return 1
|
||||
}
|
||||
|
||||
mbtenv_cleanup_tmp
|
||||
trap - EXIT INT TERM
|
||||
MBT_TMP_ARCHIVE=""
|
||||
return 0
|
||||
}
|
||||
|
||||
mbtenv_ensure_toolchain() {
|
||||
if mbtenv_toolchain_is_expected && [ "$MBT_FORCE_TOOLCHAIN" != "1" ]; then
|
||||
mbtenv_verbose "toolchain already installed at $MBT_TOOLCHAIN_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$MBT_NO_TOOLCHAIN_DOWNLOAD" = "1" ]; then
|
||||
if [ ! -x "$MBT_TOOLCHAIN_CC" ]; then
|
||||
mbtenv_log "error: arm-none-eabi-gcc not found in $MBT_TOOLCHAIN_BIN"
|
||||
mbtenv_log "hint: run ./mbt toolchain once, or remove --no-toolchain."
|
||||
return 1
|
||||
fi
|
||||
|
||||
INSTALLED_VERSION="$(mbtenv_get_installed_version 2>/dev/null || true)"
|
||||
mbtenv_log "error: found arm-none-eabi-gcc $INSTALLED_VERSION, expected $MBT_EXPECTED_GCC_VERSION"
|
||||
mbtenv_log "hint: run ./mbt toolchain once, or remove --no-toolchain."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -x "$MBT_TOOLCHAIN_CC" ]; then
|
||||
INSTALLED_VERSION="$(mbtenv_get_installed_version 2>/dev/null || true)"
|
||||
mbtenv_log "toolchain version mismatch (found $INSTALLED_VERSION, expected $MBT_EXPECTED_GCC_VERSION)"
|
||||
mbtenv_log "reinstalling toolchain..."
|
||||
fi
|
||||
|
||||
mbtenv_download_and_unpack_toolchain || return 1
|
||||
|
||||
if ! mbtenv_toolchain_is_expected; then
|
||||
INSTALLED_VERSION="$(mbtenv_get_installed_version 2>/dev/null || true)"
|
||||
mbtenv_log "error: installed toolchain version check failed (found $INSTALLED_VERSION)."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
mbtenv_activate_path() {
|
||||
if [ ! -d "$MBT_TOOLCHAIN_BIN" ]; then
|
||||
mbtenv_log "error: missing toolchain bin directory: $MBT_TOOLCHAIN_BIN"
|
||||
return 1
|
||||
fi
|
||||
|
||||
case ":$PATH:" in
|
||||
*":$MBT_TOOLCHAIN_BIN:"*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "${MBT_SAVED_PATH+x}" ]; then
|
||||
MBT_SAVED_PATH="$PATH"
|
||||
export MBT_SAVED_PATH
|
||||
fi
|
||||
|
||||
PATH="$MBT_TOOLCHAIN_BIN:$PATH"
|
||||
export PATH
|
||||
return 0
|
||||
}
|
||||
|
||||
mbtenv_restore() {
|
||||
if [ -n "${MBT_SAVED_PATH+x}" ]; then
|
||||
PATH="$MBT_SAVED_PATH"
|
||||
export PATH
|
||||
unset MBT_SAVED_PATH
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
mbtenv_main() {
|
||||
mbtenv_check_linux || return 1
|
||||
|
||||
if [ "${1:-}" = "--restore" ]; then
|
||||
mbtenv_restore
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "${MBT_NOENV:-0}" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
mbtenv_ensure_toolchain || return 1
|
||||
mbtenv_activate_path || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
mbtenv_main "${1:-}"
|
||||
@@ -58,3 +58,21 @@ f=1258000000,d=FPV Ch 6
|
||||
f=1280000000,d=FPV Ch 7
|
||||
f=1320000000,d=FPV Ch 8
|
||||
f=1360000000,d=FPV Ch 9
|
||||
|
||||
# 3.1GHz & 3.3GHz Analog FPV Channels
|
||||
f=3330000000,d=FPV Ch A-1
|
||||
f=3350000000,d=FPV Ch A-2
|
||||
f=3370000000,d=FPV Ch A-3
|
||||
f=3390000000,d=FPV Ch A-4
|
||||
f=3410000000,d=FPV Ch A-5
|
||||
f=3430000000,d=FPV Ch A-6
|
||||
f=3450000000,d=FPV Ch A-7
|
||||
f=3470000000,d=FPV Ch A-8
|
||||
f=3170000000,d=FPV Ch B-1
|
||||
f=3190000000,d=FPV Ch B-2
|
||||
f=3210000000,d=FPV Ch B-3
|
||||
f=3230000000,d=FPV Ch B-4
|
||||
f=3250000000,d=FPV Ch B-5
|
||||
f=3270000000,d=FPV Ch B-6
|
||||
f=3290000000,d=FPV Ch B-7
|
||||
f=3310000000,d=FPV Ch B-8
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# INI,END,DESCRIPTION
|
||||
# (Freq range in MHz, min separation 3MHz)
|
||||
# (Description up to 20 char)
|
||||
# (fields comma delimiter)
|
||||
# 433 MHz BAND
|
||||
423,443,433 BAND
|
||||
@@ -0,0 +1,6 @@
|
||||
# INI,END,DESCRIPTION
|
||||
# (Freq range in MHz, min separation 3MHz)
|
||||
# (Description up to 20 char)
|
||||
# (fields comma delimiter)
|
||||
# AUS UHF CB
|
||||
476,478,AUS UHF CB
|
||||
@@ -0,0 +1,10 @@
|
||||
# INI,END,DESCRIPTION
|
||||
# (Freq range in MHz, min separation 3MHz)
|
||||
# (Description up to 20 char)
|
||||
# (fields comma delimiter)
|
||||
# AVIATION
|
||||
108,118,AVIATION 108 NAVAIDS
|
||||
118,137,AVIATION 118 VOICE
|
||||
960,1215,AVIATION 960
|
||||
1089,1091,ADS-B 1090
|
||||
128,137,ACARS
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user