Compare commits

...

13 Commits

Author SHA1 Message Date
Tommaso Ventafridda 20f28c8331 add dark theme (#2695) 2025-06-13 18:34:20 +02:00
Tommaso Ventafridda ea38a0fe48 Add 1ms hop option to hopper app + 0ms (freeze UI) (#2692) 2025-06-12 11:54:37 +02:00
RocketGod fb2e576b34 Super secret dont look (#2690)
* Add new jammer modes

Overview

This PR enhances the PortaPack Jammer app by introducing eight new signal types, ported from my Flipper Zero RF Jammer app (https://github.com/RocketGod-git/flipper-zero-rf-jammer). These modes expand the app's capability to disrupt a wide range of RF communication protocols, from analog radios to modern digital systems. The implementation preserves the original app structure, resolves namespace conflicts, and ensures compatibility with the Mayhem firmware.

New Modes

The following modes have been added to the options_type in ui_jammer.hpp, with corresponding signal generation in proc_jammer.cpp:

Noise: Generates broadband white noise to interfere with analog and digital signals (e.g., Wi-Fi, Bluetooth, key fobs). Highly effective for overwhelming receivers across a frequency range.

Sine: Produces a continuous, unmodulated sine wave to jam narrowband receivers, ideal for analog FM/AM radios or telemetry systems.

Square: Emits a harmonic-rich square wave, disrupting digital protocols (e.g., OOK, ASK) and systems sensitive to sharp transitions, such as remote keyless entry.

Sawtooth (Experimental): Generates a sawtooth wave with a unique harmonic profile, useful for testing interference against PWM-based or niche analog systems.

Triangle (Experimental): Creates a triangle wave with minimal harmonics, suitable for exploratory jamming of narrowband systems or receiver linearity testing.

Chirp: Outputs a rapid frequency-sweeping chirp signal, effective against frequency-hopping and spread-spectrum systems (e.g., some Wi-Fi, Bluetooth, or military radios).

Gauss: Generates Gaussian noise to mimic natural interference, targeting digital systems like GPS or data links by degrading signal-to-noise ratios.

Brute (Experimental): Transmits a constant maximum-amplitude signal to saturate simple receiver front-ends, useful for brute-force jamming of basic analog devices.

* Super secret

* You gotta get (Get) that (That) dirt off your shoulder
2025-06-10 13:36:26 -07:00
Tommaso Ventafridda 1d79e30d4b Add all jammer modes in hopper app (#2691)
Added all modes that jammer app supports in hopper app.
2025-06-10 06:54:59 -07:00
Totoo be372e12bc Externalize widget (#2688) 2025-06-09 12:52:40 +02:00
Totoo 00853f526a Gfx widget and Radio (#2685)
* widgetize
* gfx and Radio improvement
* format + handle not wfm visual states
* wf or gf
2025-06-07 11:43:07 +02:00
Petro Danylevskyi 37ca7a601c Rename looking glass preset for clarity and consistency (#2686) 2025-06-07 11:27:25 +02:00
RocketGod b50d18eafc Radio app improvements (#2680) 2025-06-05 21:25:09 +02:00
Totoo 1070d951e6 80mhz jammer range (#2682)
Looks great 😎🤘🚀
2025-06-05 07:08:03 -07:00
Totoo a980fe21ac removed some std stuff only used here (#2681) 2025-06-05 11:32:26 +02:00
Alien 9e96715176 Added ability to enter custom tone values in Morse app (#2679)
* Added ability to enter custom tone values in Morse app

Added the ability to type in a custom tone value in the morse TX app (issue#2582)

*Click on the tone field to open a keyboard for entering in a desired value between 100hz - 9999hz.

*Maintains original step value of 20 when scrolling the rotary wheel.

* Update ui_morse.cpp

Replaced std::to_string with to_string_dec_uint

* Moved tone_input_buffer init to in-class
2025-06-05 05:21:44 +00:00
Brumi-2021 fecfe8b8fc Clean_LCD_beat_in_NOAA_Rx_App (#2678) 2025-06-03 21:10:58 +02:00
zix dfdd52c667 trivial apps folder movement (#2677) 2025-06-03 12:13:44 +02:00
27 changed files with 1125 additions and 234 deletions
@@ -143,7 +143,7 @@ class WFMAMAptOptionsView : public View {
};
OptionsField options_config{
{3 * 8, 0 * 16},
19, // Max option char length "80k-NOAA Apt LPF-2K" , example.
16, // Max option char length "80k-NOAA Apt LPF" , example.
{
// Using common messages from freqman_ui.cpp
}};
@@ -34,6 +34,7 @@ constexpr std::string_view mayhem_information_list[] = {
"RedFox-Fr,nemanjan00,",
"MichalLeonBorsuk,",
"MatiasFernandez,Giorgiofox",
"TommasoVentafridda",
" ",
"#Havoc:",
"jboone,furrtek,eried,argilo,",
@@ -954,6 +954,7 @@ class SetThemeView : public View {
{"Aqua", 2},
{"Green", 3},
{"Red", 4},
{"Dark", 5},
},
true};
+1 -1
View File
@@ -38,7 +38,7 @@ __attribute__((section(".external_app.app_fmradio.application_information"), use
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "FM Radio",
/*.app_name = */ "Radio",
/*.bitmap_data = */ {
0x00,
0x00,
+168 -54
View File
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* Copyright (C) 2025 RocketGod
*
* This file is part of PortaPack.
*
@@ -26,6 +27,7 @@
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "portapack_persistent_memory.hpp"
#include "oversample.hpp"
using namespace portapack;
using namespace modems;
@@ -33,20 +35,118 @@ using namespace ui;
namespace ui::external_app::fmradio {
#include "external/ui_grapheq.cpi"
void FmRadioView::focus() {
field_frequency.focus();
}
void FmRadioView::show_hide_gfx(bool show) {
gr.hidden(!show);
gr.set_paused(!show);
waveform.set_paused(show);
btn_fav_0.hidden(show);
btn_fav_1.hidden(show);
btn_fav_2.hidden(show);
btn_fav_3.hidden(show);
btn_fav_4.hidden(show);
btn_fav_5.hidden(show);
btn_fav_6.hidden(show);
btn_fav_7.hidden(show);
btn_fav_8.hidden(show);
btn_fav_9.hidden(show);
txt_save_help.hidden(show);
btn_fav_save.hidden(show);
field_bw.hidden(show);
field_modulation.hidden(show);
text_mode_label.hidden(show);
set_dirty();
}
void FmRadioView::change_mode(int32_t mod) {
field_bw.on_change = [this](size_t n, OptionsField::value_t) { (void)n; };
audio::output::stop();
receiver_model.disable();
baseband::shutdown();
audio_spectrum_update = false; // Reset spectrum update flag
std::fill(audio_spectrum, audio_spectrum + 128, 0); // Clear spectrum buffer
waveform.set_dirty();
receiver_mode = static_cast<ReceiverModel::Mode>(mod);
bool is_ssb = (mod == static_cast<int32_t>(ReceiverModel::Mode::AMAudio) &&
(field_modulation.selected_index() == 3 || field_modulation.selected_index() == 4));
switch (mod) {
case static_cast<int32_t>(ReceiverModel::Mode::AMAudio):
audio_sampling_rate = audio::Rate::Hz_24000; // Increased to 24 kHz for better AM/SSB audio
freqman_set_bandwidth_option(0, field_bw); // AM_MODULATION
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
receiver_mode = ReceiverModel::Mode::AMAudio;
field_bw.set_by_value(0); // DSB default
receiver_model.set_modulation(receiver_mode);
if (is_ssb) {
receiver_model.set_am_configuration(field_modulation.selected_index() == 3 ? 1 : 2); // 1=USB, 2=LSB
} else {
receiver_model.set_am_configuration(0); // DSB
}
field_bw.on_change = [this](size_t index, OptionsField::value_t n) {
radio_bw = index;
receiver_model.set_am_configuration(n);
};
show_hide_gfx(false);
break;
case static_cast<int32_t>(ReceiverModel::Mode::NarrowbandFMAudio):
audio_sampling_rate = audio::Rate::Hz_24000;
freqman_set_bandwidth_option(1, field_bw); // NFM_MODULATION
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
receiver_mode = ReceiverModel::Mode::NarrowbandFMAudio;
field_bw.set_by_value(2); // 16k default
receiver_model.set_nbfm_configuration(field_bw.selected_index_value());
field_bw.on_change = [this](size_t index, OptionsField::value_t n) {
radio_bw = index;
receiver_model.set_nbfm_configuration(n);
};
show_hide_gfx(false);
break;
case static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio):
audio_sampling_rate = audio::Rate::Hz_48000;
freqman_set_bandwidth_option(2, field_bw); // WFM_MODULATION
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
receiver_mode = ReceiverModel::Mode::WidebandFMAudio;
field_bw.set_by_value(0); // 200k default
receiver_model.set_wfm_configuration(field_bw.selected_index_value());
field_bw.on_change = [this](size_t index, OptionsField::value_t n) {
radio_bw = index;
receiver_model.set_wfm_configuration(n);
};
break;
default:
break;
}
receiver_model.set_modulation(receiver_mode);
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
audio::set_rate(audio_sampling_rate);
audio::output::start();
receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack
receiver_model.enable();
}
FmRadioView::FmRadioView(NavigationView& nav)
: nav_{nav} {
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
add_children({&rssi,
&field_rf_amp,
add_children({&field_rf_amp,
&field_lna,
&field_vga,
&field_volume,
&field_frequency,
&field_bw,
&text_mode_label,
&field_modulation,
&btn_fav_save,
&txt_save_help,
&btn_fav_0,
@@ -60,12 +160,16 @@ FmRadioView::FmRadioView(NavigationView& nav)
&btn_fav_8,
&btn_fav_9,
&audio,
&waveform});
&waveform,
&rssi,
&gr});
txt_save_help.set_focusable(false);
txt_save_help.visible(false);
for (uint8_t i = 0; i < 12; ++i) {
if (freq_fav_list[i] == 0) {
freq_fav_list[i] = 87000000;
if (freq_fav_list[i].frequency == 0) {
freq_fav_list[i].frequency = 87000000;
freq_fav_list[i].modulation = static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio);
}
}
@@ -73,42 +177,20 @@ FmRadioView::FmRadioView(NavigationView& nav)
field_frequency.set_value(87000000);
}
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
field_frequency.set_step(25000);
receiver_model.enable();
audio::output::start();
change_mode(static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio));
field_modulation.set_by_value(static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio));
btn_fav_0.on_select = [this](Button&) {
on_btn_clicked(0);
};
btn_fav_1.on_select = [this](Button&) {
on_btn_clicked(1);
};
btn_fav_2.on_select = [this](Button&) {
on_btn_clicked(2);
};
btn_fav_3.on_select = [this](Button&) {
on_btn_clicked(3);
};
btn_fav_4.on_select = [this](Button&) {
on_btn_clicked(4);
};
btn_fav_5.on_select = [this](Button&) {
on_btn_clicked(5);
};
btn_fav_6.on_select = [this](Button&) {
on_btn_clicked(6);
};
btn_fav_7.on_select = [this](Button&) {
on_btn_clicked(7);
};
btn_fav_8.on_select = [this](Button&) {
on_btn_clicked(8);
};
btn_fav_9.on_select = [this](Button&) {
on_btn_clicked(9);
};
btn_fav_0.on_select = [this](Button&) { on_btn_clicked(0); };
btn_fav_1.on_select = [this](Button&) { on_btn_clicked(1); };
btn_fav_2.on_select = [this](Button&) { on_btn_clicked(2); };
btn_fav_3.on_select = [this](Button&) { on_btn_clicked(3); };
btn_fav_4.on_select = [this](Button&) { on_btn_clicked(4); };
btn_fav_5.on_select = [this](Button&) { on_btn_clicked(5); };
btn_fav_6.on_select = [this](Button&) { on_btn_clicked(6); };
btn_fav_7.on_select = [this](Button&) { on_btn_clicked(7); };
btn_fav_8.on_select = [this](Button&) { on_btn_clicked(8); };
btn_fav_9.on_select = [this](Button&) { on_btn_clicked(9); };
btn_fav_save.on_select = [this](Button&) {
save_fav = !save_fav;
@@ -117,20 +199,44 @@ FmRadioView::FmRadioView(NavigationView& nav)
txt_save_help.set_dirty();
};
field_modulation.on_change = [this](size_t index, int32_t mod) {
change_mode(mod);
if (index == 3 || index == 4) { // USB or LSB
receiver_model.set_am_configuration(index == 3 ? 1 : 2); // 1=USB, 2=LSB
}
};
waveform.on_select = [this](Waveform&) {
if (receiver_mode != ReceiverModel::Mode::WidebandFMAudio) { // only there is spectrum message
return;
}
show_hide_gfx(!btn_fav_0.hidden());
};
gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color);
gr.on_select = [this](GraphEq&) {
current_theme = (current_theme + 1) % themes.size();
gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color);
gr.set_paused(false);
};
update_fav_btn_texts();
show_hide_gfx(false);
}
void FmRadioView::on_btn_clicked(uint8_t i) {
if (save_fav) {
save_fav = false;
freq_fav_list[i] = field_frequency.value();
freq_fav_list[i].frequency = field_frequency.value();
freq_fav_list[i].modulation = field_modulation.selected_index_value();
freq_fav_list[i].bandwidth = radio_bw;
update_fav_btn_texts();
txt_save_help.visible(save_fav);
txt_save_help.set_text("");
txt_save_help.set_dirty();
return;
}
field_frequency.set_value(freq_fav_list[i]);
field_frequency.set_value(freq_fav_list[i].frequency);
field_modulation.set_by_value(freq_fav_list[i].modulation);
change_mode(freq_fav_list[i].modulation);
}
std::string FmRadioView::to_nice_freq(rf::Frequency freq) {
@@ -141,16 +247,16 @@ std::string FmRadioView::to_nice_freq(rf::Frequency freq) {
}
void FmRadioView::update_fav_btn_texts() {
btn_fav_0.set_text(to_nice_freq(freq_fav_list[0]));
btn_fav_1.set_text(to_nice_freq(freq_fav_list[1]));
btn_fav_2.set_text(to_nice_freq(freq_fav_list[2]));
btn_fav_3.set_text(to_nice_freq(freq_fav_list[3]));
btn_fav_4.set_text(to_nice_freq(freq_fav_list[4]));
btn_fav_5.set_text(to_nice_freq(freq_fav_list[5]));
btn_fav_6.set_text(to_nice_freq(freq_fav_list[6]));
btn_fav_7.set_text(to_nice_freq(freq_fav_list[7]));
btn_fav_8.set_text(to_nice_freq(freq_fav_list[8]));
btn_fav_9.set_text(to_nice_freq(freq_fav_list[9]));
btn_fav_0.set_text(to_nice_freq(freq_fav_list[0].frequency));
btn_fav_1.set_text(to_nice_freq(freq_fav_list[1].frequency));
btn_fav_2.set_text(to_nice_freq(freq_fav_list[2].frequency));
btn_fav_3.set_text(to_nice_freq(freq_fav_list[3].frequency));
btn_fav_4.set_text(to_nice_freq(freq_fav_list[4].frequency));
btn_fav_5.set_text(to_nice_freq(freq_fav_list[5].frequency));
btn_fav_6.set_text(to_nice_freq(freq_fav_list[6].frequency));
btn_fav_7.set_text(to_nice_freq(freq_fav_list[7].frequency));
btn_fav_8.set_text(to_nice_freq(freq_fav_list[8].frequency));
btn_fav_9.set_text(to_nice_freq(freq_fav_list[9].frequency));
}
FmRadioView::~FmRadioView() {
@@ -160,9 +266,17 @@ FmRadioView::~FmRadioView() {
}
void FmRadioView::on_audio_spectrum() {
for (size_t i = 0; i < audio_spectrum_data->db.size(); i++)
audio_spectrum[i] = ((int16_t)audio_spectrum_data->db[i] - 127) * 256;
waveform.set_dirty();
if (gr.visible() && audio_spectrum_data) gr.update_audio_spectrum(*audio_spectrum_data);
if (audio_spectrum_data && audio_spectrum_data->db.size() <= 128) {
for (size_t i = 0; i < audio_spectrum_data->db.size(); ++i) {
audio_spectrum[i] = ((int16_t)audio_spectrum_data->db[i] - 127) * 256;
}
waveform.set_dirty();
} else {
// Fallback: Clear waveform if no valid data
std::fill(audio_spectrum, audio_spectrum + 128, 0);
waveform.set_dirty();
}
}
} // namespace ui::external_app::fmradio
+107 -16
View File
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* Copyright (C) 2025 RocketGod
*
* This file is part of PortaPack.
*
@@ -38,11 +39,16 @@
#include "radio_state.hpp"
#include "log_file.hpp"
#include "utility.hpp"
#include "audio.hpp"
#include "freqman_db.hpp"
#include "ui_freqman.hpp"
using namespace ui;
namespace ui::external_app::fmradio {
#include "external/ui_grapheq.hpp"
#define FMR_BTNGRID_TOP 60
class FmRadioView : public View {
@@ -54,31 +60,65 @@ class FmRadioView : public View {
void focus() override;
std::string title() const override { return "FM radio"; };
std::string title() const override { return "Radio"; };
private:
NavigationView& nav_;
RxRadioState radio_state_{};
int16_t audio_spectrum[128]{0};
bool audio_spectrum_update = false;
ReceiverModel::Mode receiver_mode = ReceiverModel::Mode::WidebandFMAudio;
AudioSpectrum* audio_spectrum_data{nullptr};
rf::Frequency freq_fav_list[12] = {0};
struct Favorite {
rf::Frequency frequency = 0;
int32_t modulation = static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio);
uint8_t bandwidth = 0;
};
Favorite freq_fav_list[12];
audio::Rate audio_sampling_rate = audio::Rate::Hz_48000;
uint8_t radio_bw = 0;
uint32_t current_theme{0};
app_settings::SettingsManager settings_{
"rx_fmradio",
app_settings::Mode::RX,
{{"favlist0"sv, &freq_fav_list[0]},
{"favlist1"sv, &freq_fav_list[1]},
{"favlist2"sv, &freq_fav_list[2]},
{"favlist3"sv, &freq_fav_list[3]},
{"favlist4"sv, &freq_fav_list[4]},
{"favlist5"sv, &freq_fav_list[5]},
{"favlist6"sv, &freq_fav_list[6]},
{"favlist7"sv, &freq_fav_list[7]},
{"favlist8"sv, &freq_fav_list[8]},
{"favlist9"sv, &freq_fav_list[9]},
{"favlist10"sv, &freq_fav_list[10]},
{"favlist11"sv, &freq_fav_list[11]}}};
{{"favlist0_freq"sv, &freq_fav_list[0].frequency},
{"favlist1_freq"sv, &freq_fav_list[1].frequency},
{"favlist2_freq"sv, &freq_fav_list[2].frequency},
{"favlist3_freq"sv, &freq_fav_list[3].frequency},
{"favlist4_freq"sv, &freq_fav_list[4].frequency},
{"favlist5_freq"sv, &freq_fav_list[5].frequency},
{"favlist6_freq"sv, &freq_fav_list[6].frequency},
{"favlist7_freq"sv, &freq_fav_list[7].frequency},
{"favlist8_freq"sv, &freq_fav_list[8].frequency},
{"favlist9_freq"sv, &freq_fav_list[9].frequency},
{"favlist10_freq"sv, &freq_fav_list[10].frequency},
{"favlist11_freq"sv, &freq_fav_list[11].frequency},
{"favlist0_mod"sv, &freq_fav_list[0].modulation},
{"favlist1_mod"sv, &freq_fav_list[1].modulation},
{"favlist2_mod"sv, &freq_fav_list[2].modulation},
{"favlist3_mod"sv, &freq_fav_list[3].modulation},
{"favlist4_mod"sv, &freq_fav_list[4].modulation},
{"favlist5_mod"sv, &freq_fav_list[5].modulation},
{"favlist6_mod"sv, &freq_fav_list[6].modulation},
{"favlist7_mod"sv, &freq_fav_list[7].modulation},
{"favlist8_mod"sv, &freq_fav_list[8].modulation},
{"favlist9_mod"sv, &freq_fav_list[9].modulation},
{"favlist10_mod"sv, &freq_fav_list[10].modulation},
{"favlist11_mod"sv, &freq_fav_list[11].modulation},
{"favlist0_bw"sv, &freq_fav_list[0].bandwidth},
{"favlist1_bw"sv, &freq_fav_list[1].bandwidth},
{"favlist2_bw"sv, &freq_fav_list[2].bandwidth},
{"favlist3_bw"sv, &freq_fav_list[3].bandwidth},
{"favlist4_bw"sv, &freq_fav_list[4].bandwidth},
{"favlist5_bw"sv, &freq_fav_list[5].bandwidth},
{"favlist6_bw"sv, &freq_fav_list[6].bandwidth},
{"favlist7_bw"sv, &freq_fav_list[7].bandwidth},
{"favlist8_bw"sv, &freq_fav_list[8].bandwidth},
{"favlist9_bw"sv, &freq_fav_list[9].bandwidth},
{"favlist10_bw"sv, &freq_fav_list[10].bandwidth},
{"favlist11_bw"sv, &freq_fav_list[11].bandwidth},
{"radio_bw"sv, &radio_bw},
{"theme"sv, &current_theme}}};
RFAmpField field_rf_amp{
{13 * 8, 0 * 16}};
@@ -95,6 +135,24 @@ class FmRadioView : public View {
{0 * 8, 0 * 16},
nav_};
OptionsField field_bw{
{10 * 8, FMR_BTNGRID_TOP + 6 * 34},
6,
{}};
Text text_mode_label{
{20 * 8, FMR_BTNGRID_TOP + 6 * 34, 5 * 8, 1 * 28},
"MODE:"};
OptionsField field_modulation{
{26 * 8, FMR_BTNGRID_TOP + 6 * 34},
4,
{{"AM", static_cast<int32_t>(ReceiverModel::Mode::AMAudio)},
{"NFM", static_cast<int32_t>(ReceiverModel::Mode::NarrowbandFMAudio)},
{"WFM", static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio)},
{"USB", static_cast<int32_t>(ReceiverModel::Mode::AMAudio)},
{"LSB", static_cast<int32_t>(ReceiverModel::Mode::AMAudio)}}};
TextField txt_save_help{
{2, FMR_BTNGRID_TOP + 6 * 34 - 20, 12 * 8, 16},
" "};
@@ -103,7 +161,7 @@ class FmRadioView : public View {
{21 * 8, 10, 6 * 8, 4}};
Waveform waveform{
{0, 20, screen_width, 2 * 16},
{0, 20, UI_POS_MAXWIDTH, 2 * 16},
audio_spectrum,
128,
0,
@@ -111,6 +169,8 @@ class FmRadioView : public View {
Theme::getInstance()->bg_darkest->foreground,
true};
GraphEq gr{{2, FMR_BTNGRID_TOP, UI_POS_MAXWIDTH - 4, UI_POS_MAXHEIGHT - FMR_BTNGRID_TOP}, true};
Button btn_fav_0{{2, FMR_BTNGRID_TOP + 0 * 34, 10 * 8, 28}, "---"};
Button btn_fav_1{{2 + 15 * 8, FMR_BTNGRID_TOP + 0 * 34, 10 * 8, 28}, "---"};
Button btn_fav_2{{2, FMR_BTNGRID_TOP + 1 * 34, 10 * 8, 28}, "---"};
@@ -124,10 +184,41 @@ class FmRadioView : public View {
Button btn_fav_save{{2, FMR_BTNGRID_TOP + 6 * 34, 7 * 8, 1 * 28}, "Save"};
bool save_fav = false;
void on_btn_clicked(uint8_t i);
void update_fav_btn_texts();
std::string to_nice_freq(rf::Frequency freq);
void on_audio_spectrum();
void change_mode(int32_t mod);
void show_hide_gfx(bool show);
struct ColorTheme {
Color base_color;
Color peak_color;
};
const std::array<ColorTheme, 20> themes{
ColorTheme{Color(255, 0, 255), Color(255, 255, 255)},
ColorTheme{Color(0, 255, 0), Color(255, 0, 0)},
ColorTheme{Color(0, 0, 255), Color(255, 255, 0)},
ColorTheme{Color(255, 128, 0), Color(255, 0, 128)},
ColorTheme{Color(128, 0, 255), Color(0, 255, 255)},
ColorTheme{Color(255, 255, 0), Color(0, 255, 128)},
ColorTheme{Color(255, 0, 0), Color(0, 128, 255)},
ColorTheme{Color(0, 255, 128), Color(255, 128, 255)},
ColorTheme{Color(128, 128, 128), Color(255, 255, 255)},
ColorTheme{Color(255, 64, 0), Color(0, 255, 64)},
ColorTheme{Color(0, 128, 128), Color(255, 192, 0)},
ColorTheme{Color(0, 255, 0), Color(0, 128, 0)},
ColorTheme{Color(32, 64, 32), Color(0, 255, 0)},
ColorTheme{Color(64, 0, 128), Color(255, 0, 255)},
ColorTheme{Color(0, 64, 0), Color(0, 255, 128)},
ColorTheme{Color(255, 255, 255), Color(0, 0, 255)},
ColorTheme{Color(128, 0, 0), Color(255, 128, 0)},
ColorTheme{Color(0, 128, 255), Color(255, 255, 128)},
ColorTheme{Color(64, 64, 64), Color(255, 0, 0)},
ColorTheme{Color(255, 192, 0), Color(0, 64, 128)}};
MessageHandlerRegistration message_handler_audio_spectrum{
Message::ID::AudioSpectrum,
+6 -104
View File
@@ -20,10 +20,12 @@ using namespace portapack;
namespace ui::external_app::gfxeq {
#include "external/ui_grapheq.cpi"
gfxEQView::gfxEQView(NavigationView& nav)
: nav_{nav}, bar_heights(NUM_BARS, 0), prev_bar_heights(NUM_BARS, 0) {
: nav_{nav} {
add_children({&button_frequency, &field_rf_amp, &field_lna, &field_vga,
&button_mood, &field_volume});
&button_mood, &field_volume, &gr});
audio::output::stop();
receiver_model.disable();
@@ -69,6 +71,7 @@ gfxEQView::gfxEQView(NavigationView& nav)
};
button_mood.on_select = [this](Button&) { this->cycle_theme(); };
gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color);
}
// needed to answer usb serial frequency set
@@ -87,110 +90,9 @@ void gfxEQView::focus() {
button_frequency.focus();
}
void gfxEQView::on_show() {
needs_background_redraw = true;
}
void gfxEQView::on_hide() {
needs_background_redraw = true;
}
void gfxEQView::update_audio_spectrum(const AudioSpectrum& spectrum) {
const float bin_frequency_size = 48000.0f / 128;
for (int bar = 0; bar < NUM_BARS; bar++) {
float start_freq = FREQUENCY_BANDS[bar];
float end_freq = FREQUENCY_BANDS[bar + 1];
int start_bin = std::max(1, (int)(start_freq / bin_frequency_size));
int end_bin = std::min(127, (int)(end_freq / bin_frequency_size));
if (start_bin >= end_bin) {
end_bin = start_bin + 1;
}
float total_energy = 0;
int bin_count = 0;
for (int bin = start_bin; bin <= end_bin; bin++) {
total_energy += spectrum.db[bin];
bin_count++;
}
float avg_db = bin_count > 0 ? (total_energy / bin_count) : 0;
// Manually boost highs for better visual balance
float treble_boost = 1.0f;
if (bar == 10)
treble_boost = 1.7f;
else if (bar >= 9)
treble_boost = 1.3f;
else if (bar >= 7)
treble_boost = 1.3f;
// Mid emphasis for a V-shape effect
float mid_boost = 1.0f;
if (bar == 4 || bar == 5 || bar == 6) mid_boost = 1.2f;
float amplified_db = avg_db * treble_boost * mid_boost;
if (amplified_db > 255) amplified_db = 255;
float band_scale = 1.0f;
int target_height = (amplified_db * RENDER_HEIGHT * band_scale) / 255;
if (target_height > RENDER_HEIGHT) {
target_height = RENDER_HEIGHT;
}
// Adjusted to look nice to my eyes
float rise_speed = 0.8f;
float fall_speed = 1.0f;
if (target_height > bar_heights[bar]) {
bar_heights[bar] = bar_heights[bar] * (1.0f - rise_speed) + target_height * rise_speed;
} else {
bar_heights[bar] = bar_heights[bar] * (1.0f - fall_speed) + target_height * fall_speed;
}
}
}
void gfxEQView::render_equalizer(Painter& painter) {
const int num_segments = RENDER_HEIGHT / SEGMENT_HEIGHT;
const ColorTheme& theme = themes[current_theme];
for (int bar = 0; bar < NUM_BARS; bar++) {
int x = HORIZONTAL_OFFSET + bar * (BAR_WIDTH + BAR_SPACING);
int active_segments = (bar_heights[bar] * num_segments) / RENDER_HEIGHT;
if (prev_bar_heights[bar] > active_segments) {
int clear_height = (prev_bar_heights[bar] - active_segments) * SEGMENT_HEIGHT;
int clear_y = screen_height - prev_bar_heights[bar] * SEGMENT_HEIGHT;
painter.fill_rectangle({x, clear_y, BAR_WIDTH, clear_height}, Color(0, 0, 0));
}
for (int seg = 0; seg < active_segments; seg++) {
int y = screen_height - (seg + 1) * SEGMENT_HEIGHT;
if (y < header_height) break;
Color segment_color = (seg >= active_segments - 2 && seg < active_segments) ? theme.peak_color : theme.base_color;
painter.fill_rectangle({x, y, BAR_WIDTH, SEGMENT_HEIGHT - 1}, segment_color);
}
prev_bar_heights[bar] = active_segments;
}
}
void gfxEQView::paint(Painter& painter) {
if (needs_background_redraw) {
painter.fill_rectangle({0, header_height, screen_width, RENDER_HEIGHT}, Color(0, 0, 0));
needs_background_redraw = false;
}
render_equalizer(painter);
}
void gfxEQView::cycle_theme() {
current_theme = (current_theme + 1) % themes.size();
gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color);
}
} // namespace ui::external_app::gfxeq
+5 -33
View File
@@ -22,6 +22,8 @@
namespace ui::external_app::gfxeq {
#include "external/ui_grapheq.hpp"
class gfxEQView : public View {
public:
gfxEQView(NavigationView& nav);
@@ -32,45 +34,17 @@ class gfxEQView : public View {
void focus() override;
std::string title() const override { return "gfxEQ"; }
void on_show() override;
void on_hide() override;
void paint(Painter& painter) override;
void on_freqchg(int64_t freq);
private:
static constexpr ui::Dim header_height = 2 * 16;
static constexpr int RENDER_HEIGHT = 288;
static constexpr int NUM_BARS = 11;
static constexpr int BAR_SPACING = 2;
int BAR_WIDTH = (screen_width - (BAR_SPACING * (NUM_BARS - 1))) / NUM_BARS;
static constexpr int HORIZONTAL_OFFSET = 2;
static constexpr int SEGMENT_HEIGHT = 10;
static constexpr std::array<int, NUM_BARS + 1> FREQUENCY_BANDS = {
375, // Bass warmth and low rumble (e.g., deep basslines, kick drum body)
750, // Upper bass punch (e.g., bass guitar punch, kick drum attack)
1500, // Lower midrange fullness (e.g., warmth in vocals, guitar body)
2250, // Midrange clarity (e.g., vocal presence, snare crack)
3375, // Upper midrange bite (e.g., instrument definition, vocal articulation)
4875, // Presence and edge (e.g., guitar bite, vocal sibilance start)
6750, // Lower brilliance (e.g., cymbal shimmer, vocal clarity)
9375, // Brilliance and air (e.g., hi-hat crispness, breathy vocals)
13125, // High treble sparkle (e.g., subtle overtones, synth shimmer)
16875, // Upper treble airiness (e.g., faint harmonics, room ambiance)
20625, // Top-end sheen (e.g., ultra-high harmonics, noise floor)
24375 // Extreme treble limit (e.g., inaudible overtones, signal cutoff, static)
};
struct ColorTheme {
Color base_color;
Color peak_color;
};
NavigationView& nav_;
bool needs_background_redraw{false};
std::vector<int> bar_heights;
std::vector<int> prev_bar_heights;
uint32_t current_theme{0};
const std::array<ColorTheme, 20> themes{
ColorTheme{Color(255, 0, 255), Color(255, 255, 255)},
@@ -100,6 +74,7 @@ class gfxEQView : public View {
VGAGainField field_vga{{18 * 8, 0 * 16}};
Button button_mood{{21 * 8, 0, 6 * 8, 16}, "MOOD"};
AudioVolumeField field_volume{{screen_width - 2 * 8, 0 * 16}};
GraphEq gr{{2, UI_POS_DEFAULT_HEIGHT, UI_POS_MAXWIDTH - 4, UI_POS_HEIGHT_REMAINING(2)}, false};
rf::Frequency frequency_value{93100000};
@@ -111,16 +86,13 @@ class gfxEQView : public View {
{{"theme", &current_theme},
{"frequency", &frequency_value}}};
void update_audio_spectrum(const AudioSpectrum& spectrum);
void render_equalizer(Painter& painter);
void cycle_theme();
MessageHandlerRegistration message_handler_audio_spectrum{
Message::ID::AudioSpectrum,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const AudioSpectrumMessage*>(p);
this->update_audio_spectrum(*message.data);
this->set_dirty();
this->gr.update_audio_spectrum(*message.data);
}};
MessageHandlerRegistration message_handler_freqchg{
+20 -5
View File
@@ -78,7 +78,7 @@ void HopperView::set_hopper_channel(uint32_t i, uint32_t width, uint64_t center,
hopper_channels[i].enabled = true;
hopper_channels[i].width = (width * 0xFFFFFFULL) / 1536000;
hopper_channels[i].center = center;
hopper_channels[i].duration = 30720 * duration;
hopper_channels[i].duration = duration ? 3072 * duration : 10;
}
void HopperView::start_tx() {
@@ -274,7 +274,7 @@ HopperView::HopperView(
options_type.set_selected_index(3); // Rand CW
options_speed.set_selected_index(3); // 10kHz
options_hop.set_selected_index(1); // 50ms
options_hop.set_selected_index(3); // 50ms
button_transmit.set_style(&style_val);
field_timetx.set_value(30);
@@ -327,10 +327,25 @@ HopperView::HopperView(
};
button_transmit.on_select = [this](Button&) {
if (jamming || cooling)
if (jamming || cooling) {
stop_tx();
else
start_tx();
} else {
// if hop speed is 0, alert the user that this will cause a freeze on UI
if (options_hop.selected_index_value() == 0) {
nav_.display_modal(
"Warning", "Hopping set to 0ms (fastest).\n\nTHIS WILL FREEZE THE HACKRF,\npress RESET button to stop\n\nAre you sure?", YESNO, [this](bool choice) {
if (choice) {
// Wait for UI update before the freeze
chThdSleepMilliseconds(50);
start_tx();
}
},
TRUE);
} else {
// if hop speed is not 0, just start the transmission
start_tx();
}
}
};
menu_freq_list.on_left = [this]() {
+17 -8
View File
@@ -143,6 +143,13 @@ class HopperView : public View {
{"FM tone", 1},
{"CW sweep", 2},
{"Rand CW", 3},
{"Sine", 4},
{"Square", 5},
{"Sawtooth", 6},
{"Triangle", 7},
{"Chirp", 8},
{"Gauss", 9},
{"Brute", 10},
}};
Text text_range_number{
@@ -163,14 +170,16 @@ class HopperView : public View {
OptionsField options_hop{
{7 * 8, 27 * 8},
5,
{{"10ms ", 1},
{"50ms ", 5},
{"100ms", 10},
{"1s ", 100},
{"2s ", 200},
{"5s ", 500},
{"10s ", 1000}}};
6,
{{"0ms !!", 0},
{"1ms ", 1},
{"10ms ", 10},
{"50ms ", 50},
{"100ms", 100},
{"1s ", 1000},
{"2s ", 2000},
{"5s ", 5000},
{"10s ", 10000}}};
NumberField field_timetx{
{7 * 8, 29 * 8},
+29
View File
@@ -98,6 +98,31 @@ static msg_t loopthread_fn(void* arg) {
return 0;
}
void MorseView::on_set_tone(NavigationView& nav) {
tone_input_buffer = to_string_dec_uint(tone);
text_prompt(nav, tone_input_buffer, 4, ENTER_KEYBOARD_MODE_DIGITS, [this](std::string& buffer) {
bool is_digit_only = true;
for (size_t i = 0; i < tone_input_buffer.size(); ++i) {
if (tone_input_buffer[i] < '0' || tone_input_buffer[i] > '9') {
is_digit_only = false;
break;
}
}
if (!buffer.empty() && is_digit_only) {
int new_tone = atoi(buffer.c_str());
if (new_tone >= 100 && new_tone <= 9999) {
tone = new_tone;
field_tone.set_value(tone);
update_tx_duration();
} else {
nav_.display_modal("Out of range", "Tone must be between 100 and 9999 Hz");
}
} else {
nav_.display_modal("Invalid input", "Please enter digits only");
}
});
}
void MorseView::on_set_text(NavigationView& nav) {
text_prompt(nav, buffer, 28, ENTER_KEYBOARD_MODE_ALPHA);
}
@@ -250,6 +275,10 @@ MorseView::MorseView(
tone = value;
};
field_tone.on_select = [this, &nav](NumberField&) {
this->on_set_tone(nav);
};
button_message.on_select = [this, &nav](Button&) {
this->on_set_text(nav);
};
+2
View File
@@ -72,6 +72,7 @@ class MorseView : public View {
NavigationView& nav_;
std::string message{};
uint32_t time_units{0};
std::string tone_input_buffer{}; // Holds the tone value while the text prompt is open
TxRadioState radio_state_{
0 /* frequency */,
@@ -100,6 +101,7 @@ class MorseView : public View {
bool start_tx();
void update_tx_duration();
void on_set_tone(NavigationView& nav);
void on_set_text(NavigationView& nav);
void set_foxhunt(size_t i);
+1 -1
View File
@@ -74,7 +74,7 @@ __attribute__((section(".external_app.app_random_password.application_informatio
0x01,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::RX,
/*.menu_location = */ app_location_t::UTILITIES,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_afsk_rx */ {'P', 'A', 'F', 'R'},
+3 -3
View File
@@ -108,9 +108,9 @@ options_t freqman_bandwidths[6] = {
},
{
// WFMAM for NOAA satellites, 137 Mhz band
{"80k-NOAA Apt LPF-2K", 0},
{"38k-NOAA Apt LPF-2K", 1},
{"38k-NOAA Apt BPF-2K", 2}, // Symetrical BPF centred to the 2k4 AM subcarrier, BW = 2KHz
{"80k-NOAA Apt LPF", 0}, // Captured RF IQ filtered BW 80K, APT baseband filtered with Low Pass Filter 4k5 fc -3dB
{"38k-NOAA Apt LPF", 1}, // Captured RF IQ filtered BW 38K, APT baseband filtered with Low Pass Filter 4k5 fc -3dB
{"38k-NOAA Apt BPF", 2}, // Captured RF IQ filtered BW 38K, APT baseband filtered BPF centred to the 2k4 AM subcarrier, BW = 2KHz
},
};
+136 -1
View File
@@ -1,4 +1,3 @@
#include "theme.hpp"
namespace ui {
@@ -25,6 +24,9 @@ void Theme::SetTheme(ThemeId theme) {
case Red:
current = new ThemeRed();
break;
case Dark:
current = new ThemeDark();
break;
case DefaultGrey:
default:
current = new ThemeDefault();
@@ -725,4 +727,137 @@ ThemeRed::ThemeRed() {
bg_table_header = new Color{205, 30, 0};
}
ThemeDark::ThemeDark() {
bg_lightest = new Style{
.font = font::fixed_8x16,
.background = {32, 32, 32},
.foreground = Color::white(),
};
bg_lightest_small = new Style{
.font = font::fixed_5x8,
.background = {32, 32, 32},
.foreground = Color::white(),
};
bg_light = new Style{
.font = font::fixed_8x16,
.background = {24, 24, 24},
.foreground = Color::white(),
};
bg_medium = new Style{
.font = font::fixed_8x16,
.background = {16, 16, 16},
.foreground = Color::white(),
};
bg_dark = new Style{
.font = font::fixed_8x16,
.background = {8, 8, 8},
.foreground = Color::white(),
};
bg_darker = new Style{
.font = font::fixed_8x16,
.background = {4, 4, 4},
.foreground = Color::white(),
};
bg_darkest = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::white(),
};
bg_darkest_small = new Style{
.font = font::fixed_5x8,
.background = Color::black(),
.foreground = Color::white(),
};
bg_important_small = new Style{
.font = font::fixed_5x8,
.background = {64, 64, 64},
.foreground = Color::white(),
};
error_dark = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::red(),
};
warning_dark = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::yellow(),
};
ok_dark = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::green(),
};
fg_dark = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = {96, 96, 96},
};
fg_medium = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = {128, 128, 128},
};
fg_light = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::white(),
};
fg_red = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::red(),
};
fg_green = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::green(),
};
fg_yellow = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::yellow(),
};
fg_orange = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::orange(),
};
fg_blue = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::blue(),
};
fg_cyan = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::cyan(),
};
fg_darkcyan = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::dark_cyan(),
};
fg_magenta = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::magenta(),
};
option_active = new Style{
.font = font::fixed_8x16,
.background = {64, 64, 64},
.foreground = Color::white(),
};
status_active = new Color{0, 255, 0};
bg_table_header = new Color{48, 48, 48};
}
} // namespace ui
+6
View File
@@ -93,6 +93,11 @@ class ThemeRed : public ThemeTemplate {
ThemeRed();
};
class ThemeDark : public ThemeTemplate {
public:
ThemeDark();
};
class Theme {
public:
enum ThemeId {
@@ -101,6 +106,7 @@ class Theme {
Aqua = 2,
Green = 3,
Red = 4,
Dark = 5,
MAX
};
static ThemeTemplate* getInstance();
+43
View File
@@ -0,0 +1,43 @@
Copyright (C) 2025 HTotoo
# External UI elements
## Read carefully
These widgets are only used in external apps!
The concept is, we move these widgets out of FW space, and compile them with external apps. To all separately. This is multiple include, but since we compile the widget under different namespaces (and those namespaces will be under the external_app namespace) these all will be removed from the base firmware.
This way we can free up some spaces, and still reuse the same widgets in different apps easily.
## How to create external widget
You create a **hpp** file indet the ui/external folder, and include everything you need for your widget as you normally wanted to do.
**Don't forget the ifdef guards!**
**Never use any namespace for your class there!** The namespace of the actual ext app will be used where you include this.
Then create a **cpi** file, where you include your hpp file, and create the implementation of your widget class. Still, don't use namespace.
You won't need to include these files in any cmake file! (see usage, why)
## How to use these widgets
This is important, so follow the rules carefully. If it works, doesn't mean, you did it good!
In your external app's hpp file, you need to include the widget's hpp file.
**But be careful**, you must include it under the ext app's namespace, before your first class. For examlpe:
namespace ui::external_app::gfxeq {
#include "external/ui_grapheq.hpp"
class gfxEQView : public View {
Then you must include the cpi file in the external app's cpp file. Again, it must be under the ext app's namespace!
using namespace portapack;
namespace ui::external_app::gfxeq {
#include "external/ui_grapheq.cpi"
gfxEQView::gfxEQView(NavigationView& nav)
This must be done under each ext app that uses the same widget.
This way, the widget will be compiled multiple times under the external_app namespace, but those will be removed from the base. Each app will be able to use it, easy to handle, easy to create.
+221
View File
@@ -0,0 +1,221 @@
/*
* Copyright (C) 2025 RocketGod
* Copyright (C) 2025 HTotoo
*
* 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_grapheq.hpp"
/* GraphEq *************************************************************/
GraphEq::GraphEq(
Rect parent_rect,
bool clickable)
: Widget{parent_rect},
clickable_{clickable},
bar_heights(NUM_BARS, 0),
prev_bar_heights(NUM_BARS, 0) {
if (clickable) {
set_focusable(true);
// previous_data.resize(length_, 0);
}
}
void GraphEq::set_parent_rect(const Rect new_parent_rect) {
Widget::set_parent_rect(new_parent_rect);
calculate_params();
}
void GraphEq::calculate_params() {
y_top = screen_rect().top();
RENDER_HEIGHT = parent_rect().height();
BAR_WIDTH = (parent_rect().width() - (BAR_SPACING * (NUM_BARS - 1))) / NUM_BARS;
HORIZONTAL_OFFSET = screen_rect().left();
}
bool GraphEq::is_paused() const {
return paused_;
}
void GraphEq::set_paused(bool paused) {
paused_ = paused;
needs_background_redraw = true;
set_dirty();
}
bool GraphEq::is_clickable() const {
return clickable_;
}
void GraphEq::getAccessibilityText(std::string& result) {
result = paused_ ? "paused GraphEq" : "GraphEq";
}
void GraphEq::getWidgetName(std::string& result) {
result = "GraphEq";
}
bool GraphEq::on_key(const KeyEvent key) {
if (!clickable_) return false;
if (key == KeyEvent::Select) {
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
}
return false;
}
bool GraphEq::on_keyboard(const KeyboardEvent key) {
if (!clickable_) return false;
if (key == 32 || key == 10) {
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
}
return false;
}
bool GraphEq::on_touch(const TouchEvent event) {
if (!clickable_) return false;
switch (event.type) {
case TouchEvent::Type::Start:
focus();
return true;
case TouchEvent::Type::End:
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
default:
return false;
}
}
void GraphEq::set_theme(Color base_color_, Color peak_color_) {
base_color = base_color_;
peak_color = peak_color_;
set_dirty();
}
void GraphEq::update_audio_spectrum(const AudioSpectrum& spectrum) {
const float bin_frequency_size = 48000.0f / 128;
for (int bar = 0; bar < NUM_BARS; bar++) {
float start_freq = FREQUENCY_BANDS[bar];
float end_freq = FREQUENCY_BANDS[bar + 1];
int start_bin = std::max(1, (int)(start_freq / bin_frequency_size));
int end_bin = std::min(127, (int)(end_freq / bin_frequency_size));
if (start_bin >= end_bin) {
end_bin = start_bin + 1;
}
float total_energy = 0;
int bin_count = 0;
for (int bin = start_bin; bin <= end_bin; bin++) {
total_energy += spectrum.db[bin];
bin_count++;
}
float avg_db = bin_count > 0 ? (total_energy / bin_count) : 0;
// Manually boost highs for better visual balance
float treble_boost = 1.0f;
if (bar == 10)
treble_boost = 1.7f;
else if (bar >= 9)
treble_boost = 1.3f;
else if (bar >= 7)
treble_boost = 1.3f;
// Mid emphasis for a V-shape effect
float mid_boost = 1.0f;
if (bar == 4 || bar == 5 || bar == 6) mid_boost = 1.2f;
float amplified_db = avg_db * treble_boost * mid_boost;
if (amplified_db > 255) amplified_db = 255;
float band_scale = 1.0f;
int target_height = (amplified_db * RENDER_HEIGHT * band_scale) / 255;
if (target_height > RENDER_HEIGHT) {
target_height = RENDER_HEIGHT;
}
// Adjusted to look nice to my eyes
float rise_speed = 0.8f;
float fall_speed = 1.0f;
if (target_height > bar_heights[bar]) {
bar_heights[bar] = bar_heights[bar] * (1.0f - rise_speed) + target_height * rise_speed;
} else {
bar_heights[bar] = bar_heights[bar] * (1.0f - fall_speed) + target_height * fall_speed;
}
}
set_dirty();
}
void GraphEq::paint(Painter& painter) {
if (!visible()) return;
if (!is_calculated) { // calc positions first
calculate_params();
is_calculated = true;
}
if (needs_background_redraw) {
painter.fill_rectangle(screen_rect(), Theme::getInstance()->bg_darkest->background);
needs_background_redraw = false;
}
if (paused_) {
return;
}
const int num_segments = RENDER_HEIGHT / SEGMENT_HEIGHT;
uint16_t bottom = screen_rect().bottom();
for (int bar = 0; bar < NUM_BARS; bar++) {
int x = HORIZONTAL_OFFSET + bar * (BAR_WIDTH + BAR_SPACING);
int active_segments = (bar_heights[bar] * num_segments) / RENDER_HEIGHT;
if (prev_bar_heights[bar] > active_segments) {
int clear_height = (prev_bar_heights[bar] - active_segments) * SEGMENT_HEIGHT;
int clear_y = bottom - prev_bar_heights[bar] * SEGMENT_HEIGHT;
painter.fill_rectangle({x, clear_y, BAR_WIDTH, clear_height}, Theme::getInstance()->bg_darkest->background);
}
for (int seg = 0; seg < active_segments; seg++) {
int y = bottom - (seg + 1) * SEGMENT_HEIGHT;
if (y < y_top) break;
Color segment_color = (seg >= active_segments - 2 && seg < active_segments) ? peak_color : base_color;
painter.fill_rectangle({x, y, BAR_WIDTH, SEGMENT_HEIGHT - 1}, segment_color);
}
prev_bar_heights[bar] = active_segments;
}
}
+88
View File
@@ -0,0 +1,88 @@
/*
* Copyright (C) 2025 RocketGod
* Copyright (C) 2025 HTotoo
*
* 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_GRAPHEQ_H__
#define __UI_GRAPHEQ_H__
#include "ui_widget.hpp"
class GraphEq : public Widget {
public:
std::function<void(GraphEq&)> on_select{};
GraphEq(Rect parent_rect, bool clickable = false);
GraphEq(const GraphEq&) = delete;
GraphEq(GraphEq&&) = delete;
GraphEq& operator=(const GraphEq&) = delete;
GraphEq& operator=(GraphEq&&) = delete;
bool is_paused() const;
void set_paused(bool paused);
bool is_clickable() const;
void paint(Painter& painter) override;
bool on_key(const KeyEvent key) override;
bool on_touch(const TouchEvent event) override;
bool on_keyboard(const KeyboardEvent event) override;
void set_parent_rect(const Rect new_parent_rect) override;
void getAccessibilityText(std::string& result) override;
void getWidgetName(std::string& result) override;
void update_audio_spectrum(const AudioSpectrum& spectrum);
void set_theme(Color base_color, Color peak_color);
private:
bool is_calculated{false};
bool paused_{false};
bool clickable_{false};
bool needs_background_redraw{true}; // Redraw background only when needed.
Color base_color = Color(255, 0, 255);
Color peak_color = Color(255, 255, 255);
std::vector<ui::Dim> bar_heights;
std::vector<ui::Dim> prev_bar_heights;
ui::Dim y_top = 2 * 16;
ui::Dim RENDER_HEIGHT = 288;
ui::Dim BAR_WIDTH = 20;
ui::Dim HORIZONTAL_OFFSET = 2;
static const int NUM_BARS = 11;
static const int BAR_SPACING = 2;
static const int SEGMENT_HEIGHT = 10;
static constexpr std::array<int16_t, NUM_BARS + 1> FREQUENCY_BANDS = {
375, // Bass warmth and low rumble (e.g., deep basslines, kick drum body)
750, // Upper bass punch (e.g., bass guitar punch, kick drum attack)
1500, // Lower midrange fullness (e.g., warmth in vocals, guitar body)
2250, // Midrange clarity (e.g., vocal presence, snare crack)
3375, // Upper midrange bite (e.g., instrument definition, vocal articulation)
4875, // Presence and edge (e.g., guitar bite, vocal sibilance start)
6750, // Lower brilliance (e.g., cymbal shimmer, vocal clarity)
9375, // Brilliance and air (e.g., hi-hat crispness, breathy vocals)
13125, // High treble sparkle (e.g., subtle overtones, synth shimmer)
16875, // Upper treble airiness (e.g., faint harmonics, room ambiance)
20625, // Top-end sheen (e.g., ultra-high harmonics, noise floor)
24375 // Extreme treble limit (e.g., inaudible overtones, signal cutoff, static)
};
void calculate_params(); // re calculate some parameters based on parent_rect()
};
#endif /*__UI_GRAPHEQ_H__*/
+1 -1
View File
@@ -157,7 +157,7 @@ void NoaaAptRx::configure(const NoaaAptRxConfigureMessage& message) {
channel_filter_high_f = taps_38k_wfmam_decim_1.high_frequency_normalized * decim_1_input_fs;
channel_filter_transition = taps_38k_wfmam_decim_1.transition_normalized * decim_1_input_fs;
demod.configure(demod_input_fs, 17000);
audio_filter.configure(taps_64_lp_1875_2166.taps);
audio_filter.configure(taps_64_bpf_2k4_bw_2k.taps);
audio_output.configure(apt_audio_12k_notch_2k4_config, apt_audio_12k_lpf_2000hz_config);
// channel_spectrum.set_decimation_factor(1);
update_params();
+1 -1
View File
@@ -1498,7 +1498,7 @@ constexpr fir_taps_real<64> taps_64_lp_1875_2166{
* -> 12kHz int16_t output, gain of 1.0 (I think).
*/
constexpr fir_taps_real<64> taps_64_bpf_2k4_bw_2k{
.low_frequency_normalized = -0.1875f, // not updated, this is just for LPF , waterfall GUI, we are not using in HPF NOAA app.
.low_frequency_normalized = -0.1875f, // not updated, this is just for LPF , waterfall GUI, we are not using in BPF NOAA app.
.high_frequency_normalized = 0.1875f, // not used GUI in NOAA App.
.transition_normalized = 0.03f, // not used GUI in NOAA app.
.taps = {{-45, -29, 32, 63, 0, -125, -181, -81, 61,
+1 -1
View File
@@ -25,7 +25,7 @@
#define __JAMMER_H__
#define JAMMER_CH_WIDTH 1000000
#define JAMMER_MAX_CH 24
#define JAMMER_MAX_CH 80
namespace jammer {
+1 -1
View File
@@ -69,7 +69,7 @@ struct SharedMemory {
union {
ToneData tones_data;
struct {
JammerChannel jammer_channels[24];
JammerChannel jammer_channels[80];
HopperChannel hopper_channels[24];
} dummy_seperate;
uint8_t data[512];
+2
View File
@@ -57,6 +57,8 @@ namespace ui {
#define UI_POS_HEIGHT_REMAINING(linenum) ((int)(screen_height - ((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)))
// px width of the screen
#define UI_POS_MAXHEIGHT (screen_height)
// Escape sequences for colored text; second character is index into term_colors[]
#define STR_COLOR_BLACK "\x1B\x00"
+200 -2
View File
@@ -1,6 +1,8 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2025 RocketGod
* Copyright (C) 2025 HTotoo
*
* This file is part of PortaPack.
*
@@ -2667,7 +2669,6 @@ bool Waveform::is_clickable() const {
}
void Waveform::getAccessibilityText(std::string& result) {
// no idea what this is in use in any places, but others have it
result = paused_ ? "paused waveform" : "waveform";
}
@@ -2689,7 +2690,6 @@ bool Waveform::on_key(const KeyEvent key) {
}
bool Waveform::on_keyboard(const KeyboardEvent key) {
// no idea what this is for, but others have it
if (!clickable_) return false;
if (key == 32 || key == 10) {
@@ -2822,6 +2822,204 @@ void Waveform::paint(Painter& painter) {
}
}
/* GraphEq *************************************************************/
GraphEq::GraphEq(
Rect parent_rect,
bool clickable)
: Widget{parent_rect},
clickable_{clickable},
bar_heights(NUM_BARS, 0),
prev_bar_heights(NUM_BARS, 0) {
if (clickable) {
set_focusable(true);
// previous_data.resize(length_, 0);
}
}
void GraphEq::set_parent_rect(const Rect new_parent_rect) {
Widget::set_parent_rect(new_parent_rect);
calculate_params();
}
void GraphEq::calculate_params() {
y_top = screen_rect().top();
RENDER_HEIGHT = parent_rect().height();
BAR_WIDTH = (parent_rect().width() - (BAR_SPACING * (NUM_BARS - 1))) / NUM_BARS;
HORIZONTAL_OFFSET = screen_rect().left();
}
bool GraphEq::is_paused() const {
return paused_;
}
void GraphEq::set_paused(bool paused) {
paused_ = paused;
needs_background_redraw = true;
set_dirty();
}
bool GraphEq::is_clickable() const {
return clickable_;
}
void GraphEq::getAccessibilityText(std::string& result) {
result = paused_ ? "paused GraphEq" : "GraphEq";
}
void GraphEq::getWidgetName(std::string& result) {
result = "GraphEq";
}
bool GraphEq::on_key(const KeyEvent key) {
if (!clickable_) return false;
if (key == KeyEvent::Select) {
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
}
return false;
}
bool GraphEq::on_keyboard(const KeyboardEvent key) {
if (!clickable_) return false;
if (key == 32 || key == 10) {
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
}
return false;
}
bool GraphEq::on_touch(const TouchEvent event) {
if (!clickable_) return false;
switch (event.type) {
case TouchEvent::Type::Start:
focus();
return true;
case TouchEvent::Type::End:
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
default:
return false;
}
}
void GraphEq::set_theme(Color base_color_, Color peak_color_) {
base_color = base_color_;
peak_color = peak_color_;
set_dirty();
}
void GraphEq::update_audio_spectrum(const AudioSpectrum& spectrum) {
const float bin_frequency_size = 48000.0f / 128;
for (int bar = 0; bar < NUM_BARS; bar++) {
float start_freq = FREQUENCY_BANDS[bar];
float end_freq = FREQUENCY_BANDS[bar + 1];
int start_bin = std::max(1, (int)(start_freq / bin_frequency_size));
int end_bin = std::min(127, (int)(end_freq / bin_frequency_size));
if (start_bin >= end_bin) {
end_bin = start_bin + 1;
}
float total_energy = 0;
int bin_count = 0;
for (int bin = start_bin; bin <= end_bin; bin++) {
total_energy += spectrum.db[bin];
bin_count++;
}
float avg_db = bin_count > 0 ? (total_energy / bin_count) : 0;
// Manually boost highs for better visual balance
float treble_boost = 1.0f;
if (bar == 10)
treble_boost = 1.7f;
else if (bar >= 9)
treble_boost = 1.3f;
else if (bar >= 7)
treble_boost = 1.3f;
// Mid emphasis for a V-shape effect
float mid_boost = 1.0f;
if (bar == 4 || bar == 5 || bar == 6) mid_boost = 1.2f;
float amplified_db = avg_db * treble_boost * mid_boost;
if (amplified_db > 255) amplified_db = 255;
float band_scale = 1.0f;
int target_height = (amplified_db * RENDER_HEIGHT * band_scale) / 255;
if (target_height > RENDER_HEIGHT) {
target_height = RENDER_HEIGHT;
}
// Adjusted to look nice to my eyes
float rise_speed = 0.8f;
float fall_speed = 1.0f;
if (target_height > bar_heights[bar]) {
bar_heights[bar] = bar_heights[bar] * (1.0f - rise_speed) + target_height * rise_speed;
} else {
bar_heights[bar] = bar_heights[bar] * (1.0f - fall_speed) + target_height * fall_speed;
}
}
set_dirty();
}
void GraphEq::paint(Painter& painter) {
if (!visible()) return;
if (!is_calculated) { // calc positions first
calculate_params();
is_calculated = true;
}
if (needs_background_redraw) {
painter.fill_rectangle(screen_rect(), Theme::getInstance()->bg_darkest->background);
needs_background_redraw = false;
}
if (paused_) {
return;
}
const int num_segments = RENDER_HEIGHT / SEGMENT_HEIGHT;
uint16_t bottom = screen_rect().bottom();
for (int bar = 0; bar < NUM_BARS; bar++) {
int x = HORIZONTAL_OFFSET + bar * (BAR_WIDTH + BAR_SPACING);
int active_segments = (bar_heights[bar] * num_segments) / RENDER_HEIGHT;
if (prev_bar_heights[bar] > active_segments) {
int clear_height = (prev_bar_heights[bar] - active_segments) * SEGMENT_HEIGHT;
int clear_y = bottom - prev_bar_heights[bar] * SEGMENT_HEIGHT;
painter.fill_rectangle({x, clear_y, BAR_WIDTH, clear_height}, Theme::getInstance()->bg_darkest->background);
}
for (int seg = 0; seg < active_segments; seg++) {
int y = bottom - (seg + 1) * SEGMENT_HEIGHT;
if (y < y_top) break;
Color segment_color = (seg >= active_segments - 2 && seg < active_segments) ? peak_color : base_color;
painter.fill_rectangle({x, y, BAR_WIDTH, SEGMENT_HEIGHT - 1}, segment_color);
}
prev_bar_heights[bar] = active_segments;
}
}
/* VuMeter **************************************************************/
VuMeter::VuMeter(
+62
View File
@@ -1,6 +1,8 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2025 RocketGod
* Copyright (C) 2025 HTotoo
*
* This file is part of PortaPack.
*
@@ -1015,6 +1017,66 @@ class Waveform : public Widget {
bool if_ever_painted_pause{false}; // for prevent the "hidden" label keeps painting and being expensive
};
class GraphEq : public Widget {
public:
std::function<void(GraphEq&)> on_select{};
GraphEq(Rect parent_rect, bool clickable = false);
GraphEq(const GraphEq&) = delete;
GraphEq(GraphEq&&) = delete;
GraphEq& operator=(const GraphEq&) = delete;
GraphEq& operator=(GraphEq&&) = delete;
bool is_paused() const;
void set_paused(bool paused);
bool is_clickable() const;
void paint(Painter& painter) override;
bool on_key(const KeyEvent key) override;
bool on_touch(const TouchEvent event) override;
bool on_keyboard(const KeyboardEvent event) override;
void set_parent_rect(const Rect new_parent_rect) override;
void getAccessibilityText(std::string& result) override;
void getWidgetName(std::string& result) override;
void update_audio_spectrum(const AudioSpectrum& spectrum);
void set_theme(Color base_color, Color peak_color);
private:
bool is_calculated{false};
bool paused_{false};
bool clickable_{false};
bool needs_background_redraw{true}; // Redraw background only when needed.
Color base_color = Color(255, 0, 255);
Color peak_color = Color(255, 255, 255);
std::vector<ui::Dim> bar_heights;
std::vector<ui::Dim> prev_bar_heights;
ui::Dim y_top = 2 * 16;
ui::Dim RENDER_HEIGHT = 288;
ui::Dim BAR_WIDTH = 20;
ui::Dim HORIZONTAL_OFFSET = 2;
static const int NUM_BARS = 11;
static const int BAR_SPACING = 2;
static const int SEGMENT_HEIGHT = 10;
static constexpr std::array<int16_t, NUM_BARS + 1> FREQUENCY_BANDS = {
375, // Bass warmth and low rumble (e.g., deep basslines, kick drum body)
750, // Upper bass punch (e.g., bass guitar punch, kick drum attack)
1500, // Lower midrange fullness (e.g., warmth in vocals, guitar body)
2250, // Midrange clarity (e.g., vocal presence, snare crack)
3375, // Upper midrange bite (e.g., instrument definition, vocal articulation)
4875, // Presence and edge (e.g., guitar bite, vocal sibilance start)
6750, // Lower brilliance (e.g., cymbal shimmer, vocal clarity)
9375, // Brilliance and air (e.g., hi-hat crispness, breathy vocals)
13125, // High treble sparkle (e.g., subtle overtones, synth shimmer)
16875, // Upper treble airiness (e.g., faint harmonics, room ambiance)
20625, // Top-end sheen (e.g., ultra-high harmonics, noise floor)
24375 // Extreme treble limit (e.g., inaudible overtones, signal cutoff, static)
};
void calculate_params(); // re calculate some parameters based on parent_rect()
};
class VuMeter : public Widget {
public:
VuMeter(Rect parent_rect, uint32_t LEDs, bool show_max);
+1 -1
View File
@@ -14,7 +14,7 @@
87,108,FM BROADCAST USA
76,108,FM BROADCAST BRAZIL
76,95,FM BROADCAST JAPAN
65,74,FM BROADCAST RUSSIA
65,74,FM OIRT EAST EU
# CITIZENS BAND RADIO
26,28,CB RADIO
# COMMON PUBLIC SERVICE BANDS