Compare commits

..

5 Commits

Author SHA1 Message Date
Kyle Reed a6a1483083 Improved trimming (#1458)
* Add threshold UI
* WIP Better trimming
* Rewrite mostly done WIP
* WIP - trim idea
* WIP threshold trimming
* WIP with new design
* Cleanup
2023-09-23 21:56:37 +02:00
Kyle Reed ef03f020ce IQ Trim app and Capture auto-trim (#1456)
* Add IQ Trim app WIP
* Add 'trim' checkbox to capture
* Implement trimming
* Finish auto-trim
* Stray //
2023-09-21 17:43:10 +02:00
Kyle Reed 070eb4003e Remove proc pocsag & add keep/drop filtering (#1453)
* Remove proc_pocsag from build, use new proc in app
* Add support for keep/drop filtering
2023-09-19 11:46:30 +02:00
Kyle Reed fca373d936 Add Remote App & UI updates. (#1451)
* Alpha order sub-menus
* WIP Getting Remote types outlined
* WIP building UI
* WIP adding RemoteButton control
* WIP Fix build
* WIP Basic editing support
* Border on the active button
* Make TxView2 sane
* Add easier RGB color creation from uint32
* Center some button icons
* WIP Remote - main UI
* WIP main UI mostly working, can send
* Add 'join' utility
* WIP save/load
* Pre-alloc buttons to prevent focus dangling
* Alpha order settings/debug pages
* Add UI for picking capture and set frequency
* WIP Getting really close now
* Fix path for init name
* Some fit & finish
2023-09-18 23:22:46 +02:00
Kyle Reed 537cf2e79b Fix console scroll - comment on how scrolling works (#1448)
* Force console scroll area to be multiple of line height. Tons of comments.
* Resize POCSAG console to fill height.
* Make scoll behavior comment clearer
2023-09-14 20:20:40 +02:00
44 changed files with 2134 additions and 455 deletions
+30 -28
View File
@@ -182,6 +182,7 @@ set(CPPSRC
io_convert.cpp
io_file.cpp
io_wave.cpp
iq_trim.cpp
irq_controls.cpp
irq_lcd_frame.cpp
irq_rtc.cpp
@@ -238,43 +239,64 @@ set(CPPSRC
ui/ui_textentry.cpp
ui/ui_tone_key.cpp
ui/ui_transmitter.cpp
apps/acars_app.cpp
apps/ais_app.cpp
apps/analog_audio_app.cpp
apps/analog_tv_app.cpp
apps/capture_app.cpp
apps/ert_app.cpp
apps/gps_sim_app.cpp
apps/lge_app.cpp
apps/lge_app.cpp
apps/pocsag_app.cpp
# apps/replay_app.cpp
apps/soundboard_app.cpp
apps/tpms_app.cpp
apps/tpms_app.cpp
apps/ui_about_simple.cpp
apps/ui_adsb_rx.cpp
apps/ui_adsb_tx.cpp
apps/ui_afsk_rx.cpp
apps/ui_aprs_rx.cpp
apps/ui_btle_rx.cpp
apps/ui_nrf_rx.cpp
apps/ui_aprs_tx.cpp
apps/ui_bht_tx.cpp
apps/ui_dfu_menu.cpp
apps/ui_btle_rx.cpp
apps/ui_coasterp.cpp
apps/ui_debug.cpp
apps/ui_dfu_menu.cpp
apps/ui_encoders.cpp
apps/ui_fileman.cpp
apps/ui_flash_utility.cpp
apps/ui_sd_over_usb.cpp
apps/ui_freqman.cpp
apps/ui_iq_trim.cpp
apps/ui_jammer.cpp
#apps/ui_keyfob.cpp
# apps/ui_keyfob.cpp
apps/ui_lcr.cpp
apps/lge_app.cpp
apps/ui_level.cpp
apps/ui_looking_glass_app.cpp
apps/ui_mictx.cpp
apps/ui_modemsetup.cpp
apps/ui_morse.cpp
apps/ui_nrf_rx.cpp
# apps/ui_nuoptix.cpp
apps/ui_playlist.cpp
apps/ui_pocsag_tx.cpp
apps/ui_rds.cpp
apps/ui_recon_settings.cpp
apps/ui_recon.cpp
apps/ui_remote.cpp
apps/ui_scanner.cpp
apps/ui_search.cpp
apps/ui_sd_over_usb.cpp
apps/ui_sd_wipe.cpp
apps/ui_search.cpp
apps/ui_settings.cpp
apps/ui_siggen.cpp
apps/ui_sonde.cpp
apps/ui_sstvtx.cpp
apps/ui_spectrum_painter_image.cpp
apps/ui_spectrum_painter_text.cpp
apps/ui_spectrum_painter.cpp
apps/ui_ss_viewer.cpp
apps/ui_sstvtx.cpp
# apps/ui_test.cpp
apps/ui_text_editor.cpp
apps/ui_tone_search.cpp
@@ -282,26 +304,6 @@ set(CPPSRC
apps/ui_touchtunes.cpp
apps/ui_view_wav.cpp
apps/ui_whipcalc.cpp
apps/acars_app.cpp
apps/ais_app.cpp
apps/analog_audio_app.cpp
apps/analog_tv_app.cpp
apps/capture_app.cpp
apps/ert_app.cpp
apps/lge_app.cpp
apps/pocsag_app.cpp
# apps/replay_app.cpp
apps/ui_playlist.cpp
apps/gps_sim_app.cpp
apps/soundboard_app.cpp
apps/ui_recon.cpp
apps/ui_recon_settings.cpp
apps/ui_level.cpp
apps/tpms_app.cpp
apps/tpms_app.cpp
apps/ui_spectrum_painter.cpp
apps/ui_spectrum_painter_image.cpp
apps/ui_spectrum_painter_text.cpp
protocols/aprs.cpp
protocols/ax25.cpp
protocols/bht.cpp
+6 -1
View File
@@ -44,6 +44,7 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
&field_vga,
&option_bandwidth,
&option_format,
&check_trim,
&record_view,
&waterfall,
});
@@ -59,6 +60,10 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
record_view.set_file_type((RecordView::FileType)file_type);
};
check_trim.on_select = [this](Checkbox&, bool v) {
record_view.set_auto_trim(v);
};
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
option_bandwidth.on_change = [this](size_t, uint32_t bandwidth) {
/* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
@@ -92,7 +97,7 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
};
receiver_model.enable();
option_bandwidth.set_by_value(500000); // better by_value than by option_bandwidth.set_selected_index(4), Preselected default option 500kHz.
option_bandwidth.set_by_value(500000);
record_view.on_error = [&nav](std::string message) {
nav.display_modal("Error", message);
@@ -92,6 +92,12 @@ class CaptureAppView : public View {
{{"C16", RecordView::FileType::RawS16},
{"C8", RecordView::FileType::RawS8}}};
Checkbox check_trim{
{23 * 8, 1 * 16},
4,
"Trim",
/*small*/ true};
RecordView record_view{
{0 * 8, 2 * 16, 30 * 8, 1 * 16},
u"BBD_????.*",
+2 -7
View File
@@ -24,9 +24,6 @@
#ifndef __GPS_SIM_APP_HPP__
#define __GPS_SIM_APP_HPP__
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui_widget.hpp"
@@ -109,10 +106,8 @@ class GpsSimAppView : public View {
nav_};
TransmitterView2 tx_view{
// new handling of NumberField field_rfgain, NumberField field_rfamp
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Used in Replay / GPS Simul / Playlist App)
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
};
{11 * 8, 2 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{21 * 8, 2 * 16},
+54 -23
View File
@@ -54,35 +54,32 @@ POCSAGSettingsView::POCSAGSettingsView(
POCSAGSettings& settings)
: settings_{settings} {
add_children(
{&check_beta,
{&labels,
&check_log,
&check_log_raw,
&check_small_font,
&check_hide_bad,
&check_hide_addr_only,
&check_ignore,
&field_ignore,
&opt_filter_mode,
&field_filter_address,
&button_save});
check_beta.set_value(settings_.use_new_proc);
check_log.set_value(settings_.enable_logging);
check_log_raw.set_value(settings_.enable_raw_log);
check_small_font.set_value(settings_.enable_small_font);
check_hide_bad.set_value(settings_.hide_bad_data);
check_hide_addr_only.set_value(settings_.hide_addr_only);
check_ignore.set_value(settings_.enable_ignore);
field_ignore.set_value(settings_.address_to_ignore);
opt_filter_mode.set_by_value(settings_.filter_mode);
field_filter_address.set_value(settings_.filter_address);
button_save.on_select = [this, &nav](Button&) {
settings_.use_new_proc = check_beta.value();
settings_.enable_logging = check_log.value();
settings_.enable_raw_log = check_log_raw.value();
settings_.enable_small_font = check_small_font.value();
settings_.hide_bad_data = check_hide_bad.value();
settings_.hide_addr_only = check_hide_addr_only.value();
settings_.enable_ignore = check_ignore.value();
settings_.address_to_ignore = field_ignore.to_integer();
settings_.filter_mode = opt_filter_mode.selected_index_value();
settings_.filter_address = field_filter_address.to_integer();
nav.pop();
};
@@ -90,10 +87,7 @@ POCSAGSettingsView::POCSAGSettingsView(
POCSAGAppView::POCSAGAppView(NavigationView& nav)
: nav_{nav} {
if (settings_.use_new_proc)
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
else
baseband::run_image(portapack::spi_flash::image_tag_pocsag);
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
add_children(
{&rssi,
@@ -109,14 +103,16 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
&widget_baud,
&widget_bits,
&widget_frames,
&button_ignore_last,
&button_filter_last,
&button_config,
&console});
// No app settings, use fallbacks from pmem.
if (!app_settings_.loaded()) {
settings_.address_to_ignore = pmem::pocsag_ignore_address();
settings_.enable_ignore = settings_.address_to_ignore > 0;
settings_.filter_address = pmem::pocsag_ignore_address();
settings_.filter_mode = (settings_.filter_address == 0)
? FILTER_NONE
: FILTER_DROP;
}
if (!app_settings_.radio_loaded()) {
field_frequency.set_value(initial_target_frequency);
@@ -129,9 +125,11 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
receiver_model.set_squelch_level(v);
};
button_ignore_last.on_select = [this](Button&) {
settings_.enable_ignore = true;
settings_.address_to_ignore = last_address;
button_filter_last.on_select = [this](Button&) {
if (settings_.filter_mode == FILTER_NONE)
settings_.filter_mode = FILTER_DROP;
settings_.filter_address = last_address;
refresh_ui();
};
button_config.on_select = [this](Button&) {
@@ -155,15 +153,48 @@ POCSAGAppView::~POCSAGAppView() {
baseband::shutdown();
// Save pmem settings.
pmem::set_pocsag_ignore_address(settings_.address_to_ignore);
pmem::set_pocsag_ignore_address(settings_.filter_address);
pmem::set_pocsag_last_address(last_address); // For POCSAG TX.
}
void POCSAGAppView::refresh_ui() {
// Set console font style.
console.set_style(
settings_.enable_small_font
? &Styles::white_small
: &Styles::white);
// Update filter button text.
std::string btn_text = "Filter Last";
switch (settings_.filter_mode) {
case FILTER_DROP:
btn_text = "Ignore Last";
break;
case FILTER_KEEP:
btn_text = "Keep Last";
break;
case FILTER_NONE:
default:
btn_text = "Filter Last";
break;
}
button_filter_last.set_text(btn_text);
}
bool POCSAGAppView::ignore_address(uint32_t address) const {
switch (settings_.filter_mode) {
case FILTER_DROP:
return address == settings_.filter_address;
case FILTER_KEEP:
return address != settings_.filter_address;
case FILTER_NONE:
default:
return false;
}
}
void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefix) {
@@ -176,8 +207,8 @@ void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefi
return;
}
// Ignored address.
if (ignore() && pocsag_state.address == settings_.address_to_ignore) {
// Ignore address? TODO: could filter earlier.
if (ignore_address(pocsag_state.address)) {
console.write("\n" STR_COLOR_CYAN + prefix + " Ignored: " + to_string_dec_uint(pocsag_state.address));
last_address = pocsag_state.address;
return;
+27 -21
View File
@@ -114,15 +114,20 @@ class FrameIndicator : public Widget {
bool has_sync_ = false;
};
enum POCSAGFilter : uint8_t {
FILTER_NONE,
FILTER_DROP,
FILTER_KEEP
};
struct POCSAGSettings {
bool enable_small_font = false;
bool enable_logging = false;
bool enable_raw_log = false;
bool enable_ignore = false;
bool hide_bad_data = false;
bool hide_addr_only = false;
uint32_t address_to_ignore = 0;
bool use_new_proc = false;
uint8_t filter_mode = false;
uint32_t filter_address = 0;
};
class POCSAGSettingsView : public View {
@@ -135,11 +140,10 @@ class POCSAGSettingsView : public View {
private:
POCSAGSettings& settings_;
Checkbox check_beta{
{0 * 8 + 2, 18 * 16 - 4},
6,
"Beta",
true /*small*/};
Labels labels{
{{2 * 8, 12 * 16}, "Filter Mode:", Color::light_grey()},
{{2 * 8, 13 * 16}, "Filter Addr:", Color::light_grey()},
};
Checkbox check_log{
{2 * 8, 2 * 16},
@@ -166,13 +170,15 @@ class POCSAGSettingsView : public View {
22,
"Hide Addr Only"};
Checkbox check_ignore{
{2 * 8, 12 * 16},
22,
"Enable Ignored Address"};
OptionsField opt_filter_mode{
{15 * 8, 12 * 16},
4,
{{"None", FILTER_NONE},
{"Drop", FILTER_DROP},
{"Keep", FILTER_KEEP}}};
SymField field_ignore{
{7 * 8, 13 * 16 + 8},
SymField field_filter_address{
{15 * 8, 13 * 16},
7,
SymField::Type::Dec,
true /*explicit_edit*/};
@@ -194,7 +200,6 @@ class POCSAGAppView : public View {
static constexpr uint32_t initial_target_frequency = 466'175'000;
bool logging() const { return settings_.enable_logging; };
bool logging_raw() const { return settings_.enable_raw_log; };
bool ignore() const { return settings_.enable_ignore; };
bool hide_bad_data() const { return settings_.hide_bad_data; };
bool hide_addr_only() const { return settings_.hide_addr_only; };
@@ -210,14 +215,14 @@ class POCSAGAppView : public View {
{"small_font"sv, &settings_.enable_small_font},
{"enable_logging"sv, &settings_.enable_logging},
{"enable_raw_log"sv, &settings_.enable_raw_log},
{"enable_ignore"sv, &settings_.enable_ignore},
{"address_to_ignore"sv, &settings_.address_to_ignore},
{"filter_mode"sv, &settings_.filter_mode},
{"filter_address"sv, &settings_.filter_address},
{"hide_bad_data"sv, &settings_.hide_bad_data},
{"hide_addr_only"sv, &settings_.hide_addr_only},
{"use_new_proc"sv, &settings_.use_new_proc},
}};
void refresh_ui();
bool ignore_address(uint32_t address) const;
void handle_decoded(Timestamp timestamp, const std::string& prefix);
void on_packet(const POCSAGPacketMessage* message);
void on_stats(const POCSAGStatsMessage* stats);
@@ -273,16 +278,17 @@ class POCSAGAppView : public View {
BaudIndicator widget_baud{
{8 * 9 + 1, 1 * 16 + 2}};
Button button_ignore_last{
Button button_filter_last{
{10 * 8, 1 * 16, 12 * 8, 20},
"Ignore Last"};
"Filter Last"};
Button button_config{
{22 * 8, 1 * 16, 8 * 8, 20},
"Config"};
// 54 == status bar (16) + top controls (2 * 16 + 6).
Console console{
{0, 2 * 16 + 6, screen_width, screen_height - 56}};
{0, 2 * 16 + 6, screen_width, screen_height - 54}};
MessageHandlerRegistration message_handler_packet{
Message::ID::POCSAGPacket,
-3
View File
@@ -203,9 +203,6 @@ ReplayAppView::ReplayAppView(
ReplayAppView::~ReplayAppView() {
transmitter_model.disable();
display.fill_rectangle({0, 0, 240, 320}, Color::black()); // Solving sometimes visible bottom waterfall artifacts, clearing all LCD pixels.
chThdSleepMilliseconds(40); // (that happened sometimes if we interrupt the waterfall play at the beggining of the play around 25% and exit )
baseband::shutdown();
}
+2 -8
View File
@@ -23,9 +23,6 @@
#ifndef __REPLAY_APP_HPP__
#define __REPLAY_APP_HPP__
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui_widget.hpp"
@@ -99,16 +96,13 @@ class ReplayAppView : public View {
ProgressBar progressbar{
{18 * 8, 1 * 16, 12 * 8, 16}};
// TODO: Does this need to be a frequency field at all?
TxFrequencyField field_frequency{
{0 * 8, 2 * 16},
nav_};
TransmitterView2 tx_view{
// new handling of NumberField field_rfgain, NumberField field_rfamp
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Uused in Repay / GPS Simul / Playlist App)
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
};
{11 * 8, 2 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{21 * 8, 2 * 16},
+3 -3
View File
@@ -94,15 +94,15 @@ class AFSKRxView : public View {
false};
Text text_debug{
{0 * 8, 12 + 2 * 16, 240, 16},
{0 * 8, 12 + 2 * 16, screen_width, 16},
"DEBUG"};
Button button_modem_setup{
{240 - 12 * 8, 1 * 16, 96, 24},
{screen_width - 12 * 8, 1 * 16, 96, 24},
"Modem setup"};
Console console{
{0, 4 * 16, 240, 240}};
{0, 4 * 16, 240, screen_width}};
void on_data_afsk(const AFSKDataMessage& message);
+6 -6
View File
@@ -398,17 +398,17 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
//{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }}, // temporarily disabled to conserve ROM space
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
{"Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugMemoryView>(); }},
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
//{ "Radio State", ui::Color::white(), nullptr, [&nav](){ nav.push<NotImplementedView>(); } },
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [&nav]() { nav.push<SDCardDebugView>(); }},
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
{"Temperature", ui::Color::dark_cyan(), &bitmap_icon_temperature, [&nav]() { nav.push<TemperatureView>(); }},
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
{"Touch Test", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugScreenTest>(); }},
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
//{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }}, // temporarily disabled to conserve ROM space
});
set_max_rows(2); // allow wider buttons
}
+6 -1
View File
@@ -27,6 +27,7 @@
#include <algorithm>
#include "ui_fileman.hpp"
#include "ui_playlist.hpp"
#include "ui_remote.hpp"
#include "ui_ss_viewer.hpp"
#include "ui_text_editor.hpp"
#include "string_format.hpp"
@@ -44,6 +45,7 @@ static const fs::path c16_ext{u".C16"};
static const fs::path cxx_ext{u".C*"};
static const fs::path png_ext{u".PNG"};
static const fs::path bmp_ext{u".BMP"};
static const fs::path rem_ext{u".REM"};
} // namespace ui
namespace {
@@ -525,6 +527,9 @@ bool FileManagerView::handle_file_open() {
nav_.push<SplashViewer>(path);
reload_current();
return true;
} else if (path_iequal(rem_ext, ext)) {
nav_.push<RemoteView>(path);
return true;
}
return false;
@@ -629,7 +634,7 @@ FileManagerView::FileManagerView(
button_open_notepad.on_select = [this]() {
if (selected_is_valid() && !get_selected_entry().is_directory) {
auto path = get_selected_full_path();
nav_.replace<TextEditorView>(path);
nav_.push<TextEditorView>(path);
} else
nav_.display_modal("Open in Notepad", "Can't open that in Notepad.");
};
+4 -2
View File
@@ -77,7 +77,8 @@ class FileManBaseView : public View {
{u".C8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".C16", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".WAV", &bitmap_icon_file_wav, ui::Color::dark_magenta()},
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // PPL is the file extension for playlist app
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // Playlist/Replay
{u".REM", &bitmap_icon_remote, ui::Color::orange()}, // Remote
{u"", &bitmap_icon_file, ui::Color::light_grey()} // NB: Must be last.
};
@@ -269,7 +270,8 @@ class FileManagerView : public FileManBaseView {
{4 * 8, 34 * 8, 4 * 8, 32},
{},
&bitmap_icon_options_datetime,
Color::orange()};
Color::orange(),
/*vcenter*/ true};
};
} /* namespace ui */
+194
View File
@@ -0,0 +1,194 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* 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_iq_trim.hpp"
#include "complex.hpp"
#include "portapack.hpp"
#include "ui_fileman.hpp"
using namespace portapack;
namespace fs = std::filesystem;
namespace ui {
IQTrimView::IQTrimView(NavigationView& nav)
: nav_{nav} {
add_children({
&labels,
&field_path,
&field_start,
&field_end,
&text_samples,
&text_max,
&field_cutoff,
&button_trim,
});
field_path.on_select = [this](TextField&) {
auto open_view = nav_.push<FileLoadView>(".C*");
open_view->push_dir(u"CAPTURES");
open_view->on_changed = [this](fs::path path) {
path_ = std::move(path);
profile_capture();
compute_range();
refresh_ui();
};
};
text_samples.set_style(&Styles::light_grey);
text_max.set_style(&Styles::light_grey);
field_start.on_change = [this](int32_t v) {
if (field_end.value() < v)
field_end.set_value(v, false);
set_dirty();
};
field_end.on_change = [this](int32_t v) {
if (field_start.value() > v)
field_start.set_value(v, false);
set_dirty();
};
field_cutoff.set_value(7); // 7% of max is a good default.
field_cutoff.on_change = [this](int32_t) {
compute_range();
refresh_ui();
};
button_trim.on_select = [this](Button&) {
if (trim_capture()) {
profile_capture();
compute_range();
refresh_ui();
}
};
}
void IQTrimView::paint(Painter& painter) {
if (info_) {
uint32_t power_cutoff = field_cutoff.value() * static_cast<uint64_t>(info_->max_power) / 100;
// Draw power buckets.
for (size_t i = 0; i < power_buckets_.size(); ++i) {
auto power = power_buckets_[i].power;
uint8_t amp = 0;
if (power > power_cutoff && info_->max_power > 0)
amp = (255ULL * power) / info_->max_power;
painter.draw_vline(
pos_lines + Point{(int)i, 0},
height_lines,
Color(amp, amp, amp));
}
// Draw trim range edges.
int start_x = screen_width * field_start.value() / info_->sample_count;
int end_x = screen_width * field_end.value() / info_->sample_count;
painter.draw_vline(
pos_lines + Point{start_x, 0},
height_lines,
Color::dark_green());
painter.draw_vline(
pos_lines + Point{end_x, 0},
height_lines,
Color::dark_red());
}
}
void IQTrimView::focus() {
field_path.focus();
}
void IQTrimView::refresh_ui() {
field_path.set_text(path_.filename().string());
text_samples.set(to_string_dec_uint(info_->sample_count));
text_max.set(to_string_dec_uint(info_->max_power));
set_dirty();
}
void IQTrimView::update_range_controls(iq::TrimRange trim_range) {
auto max_range = info_ ? info_->sample_count : 0;
auto step = info_ ? info_->sample_count / screen_width : 0;
field_start.set_range(0, max_range);
field_start.set_step(step);
field_end.set_range(0, max_range);
field_end.set_step(step);
field_start.set_value(trim_range.start_sample, false);
field_end.set_value(trim_range.end_sample, false);
}
void IQTrimView::profile_capture() {
power_buckets_ = {};
iq::PowerBuckets buckets{
.p = power_buckets_.data(),
.size = power_buckets_.size()};
progress_ui.show_reading();
info_ = iq::profile_capture(path_, buckets);
progress_ui.clear();
}
void IQTrimView::compute_range() {
if (!info_)
return;
iq::PowerBuckets buckets{
.p = power_buckets_.data(),
.size = power_buckets_.size()};
auto trim_range = iq::compute_trim_range(*info_, buckets, field_cutoff.value());
update_range_controls(trim_range);
}
bool IQTrimView::trim_capture() {
if (!info_) {
nav_.display_modal("Error", "Open a file first.");
return false;
}
bool trimmed = false;
iq::TrimRange trim_range{
static_cast<uint32_t>(field_start.value()),
static_cast<uint32_t>(field_end.value()),
info_->sample_size};
if (trim_range.start_sample >= trim_range.end_sample) {
nav_.display_modal("Error", "Invalid trimming range.");
return false;
}
progress_ui.show_trimming();
trimmed = iq::trim_capture_with_range(path_, trim_range, progress_ui.get_callback());
progress_ui.clear();
if (!trimmed)
nav_.display_modal("Error", "Trimming failed.");
return trimmed;
}
} // namespace ui
+152
View File
@@ -0,0 +1,152 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* 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_IQ_TRIM_H__
#define __UI_IQ_TRIM_H__
#include "file.hpp"
#include "iq_trim.hpp"
#include "optional.hpp"
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_styles.hpp"
#include "ui_widget.hpp"
#include <array>
namespace ui {
/* Helper for drawing progress related to IQ trimming. */
/* TODO: Ideally this would all be done on second thread. */
class TrimProgressUI {
public:
void show_reading() {
clear();
p.draw_string({6 * 8, 5 * 16}, Styles::yellow, "Reading Capture...");
}
void show_trimming() {
clear();
p.draw_string({5 * 8, 5 * 16}, Styles::yellow, "Trimming Capture...");
}
void show_progress(uint8_t percent) {
auto width = percent * screen_width / 100;
p.draw_hline({0, 6 * 16 + 2}, width, Color::yellow());
}
void clear() {
p.fill_rectangle({0 * 8, 4 * 16, screen_width, 3 * 16}, Color::black());
}
auto get_callback() {
return [this](uint8_t percent) { show_progress(percent); };
}
private:
Painter p{};
};
class IQTrimView : public View {
public:
IQTrimView(NavigationView& nav);
std::string title() const override { return "IQ Trim"; }
void paint(Painter& painter) override;
void focus() override;
private:
/* Update controls with latest values. */
void refresh_ui();
/* Update the start/end controls with trim range info. */
void update_range_controls(iq::TrimRange trim_range);
/* Collect capture info and samples to draw the UI. */
void profile_capture();
/* Determine the start and end buckets based on the cutoff. */
void compute_range();
/* Trims the capture file based on the settings. */
bool trim_capture();
NavigationView& nav_;
std::filesystem::path path_{};
Optional<iq::CaptureInfo> info_{};
std::array<iq::PowerBuckets::Bucket, screen_width> power_buckets_{};
TrimProgressUI progress_ui{};
Labels labels{
{{0 * 8, 0 * 16}, "Capture File:", Color::light_grey()},
{{0 * 8, 6 * 16}, "Start :", Color::light_grey()},
{{0 * 8, 7 * 16}, "End :", Color::light_grey()},
{{0 * 8, 8 * 16}, "Samples:", Color::light_grey()},
{{0 * 8, 9 * 16}, "Max Pwr:", Color::light_grey()},
{{0 * 8, 10 * 16}, "Cutoff :", Color::light_grey()},
{{12 * 8, 10 * 16}, "%", Color::light_grey()},
};
TextField field_path{
{0 * 8, 1 * 16, 30 * 8, 1 * 16},
"Open File..."};
Point pos_lines{0 * 8, 4 * 16};
Dim height_lines{2 * 16};
NumberField field_start{
{9 * 8, 6 * 16},
10,
{0, 0},
1,
' '};
NumberField field_end{
{9 * 8, 7 * 16},
10,
{0, 0},
1,
' '};
Text text_samples{
{9 * 8, 8 * 16, 10 * 8, 1 * 16},
"0"};
Text text_max{
{9 * 8, 9 * 16, 10 * 8, 1 * 16},
"0"};
NumberField field_cutoff{
{9 * 8, 10 * 16},
3,
{1, 100},
1,
' '};
Button button_trim{
{20 * 8, 16 * 16, 8 * 8, 2 * 16},
"Trim"};
};
} /* namespace ui */
#endif /*__UI_IQ_TRIM_H__*/
+1 -1
View File
@@ -633,7 +633,7 @@ MicTXView::MicTXView(
MicTXView::~MicTXView() {
audio::input::stop();
transmitter_model.set_target_frequency(tx_frequency); // Save Tx frequency instead of Rx. Or maybe we need some "System Wide" changes to seperate Tx and Rx frequency.
transmitter_model.set_target_frequency(tx_frequency);
transmitter_model.disable();
if (rx_enabled) // Also turn off audio rx if enabled
rxaudio(false);
+2 -7
View File
@@ -25,9 +25,6 @@
#ifndef __UI_MICTX_H__
#define __UI_MICTX_H__
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui.hpp"
@@ -219,10 +216,8 @@ class MicTXView : public View {
' '};
TransmitterView2 tx_view{
// new handling of NumberField field_rfgain, NumberField field_rfamp
// 3*8, 2*8, SHORT_UI // x(columns), y (line) position. (used in Replay / GPS Simul / Playlist App's)
3 * 8, 2 * 8, NORMAL_UI // x(columns), y (line) position. (used in Mic App)
};
{3 * 8, 5 * 8},
/*short_ui*/ false};
OptionsField options_mode{
{24 * 8, 5 * 8},
@@ -386,6 +386,7 @@ PlaylistView::PlaylistView(
&waterfall,
});
ensure_directory(u"PLAYLIST");
waterfall.show_audio_spectrum_view(false);
field_frequency.set_value(100'000'000);
+5 -10
View File
@@ -21,9 +21,6 @@
* Boston, MA 02110-1301, USA.
*/
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "bitmap.hpp"
#include "file.hpp"
@@ -63,7 +60,7 @@ class PlaylistView : public View {
// More header == less spectrum view.
static constexpr ui::Dim header_height = 6 * 16;
static constexpr uint32_t baseband_bandwidth = 2500000;
static constexpr uint32_t baseband_bandwidth = 2'500'000;
struct playlist_entry {
std::filesystem::path path{};
@@ -118,14 +115,11 @@ class PlaylistView : public View {
Text text_sample_rate{
{10 * 8, 1 * 16, 7 * 8, 16}};
/*v making there's 1px line (instead of two) between two progress bars,
* by letting 1px overlapped.
* So, since they overlapped 1px, they are visually same, and looks better.
*/
ProgressBar progressbar_track{
{18 * 8, 1 * 16, 12 * 8, 8 + 1}};
// (-1) to overlap with progressbar_track so there's
// only 1 pixel between them instead of 2.
ProgressBar progressbar_transmit{
{18 * 8, 3 * 8 - 1, 12 * 8, 8}};
@@ -135,7 +129,8 @@ class PlaylistView : public View {
// TODO: delay duration field.
TransmitterView2 tx_view{
9 * 8, 1 * 8, SHORT_UI};
{11 * 8, 2 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{21 * 8, 2 * 16},
+598 -14
View File
@@ -1,6 +1,5 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2018 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -22,30 +21,615 @@
#include "ui_remote.hpp"
#include "baseband_api.hpp"
#include "convert.hpp"
#include "file_reader.hpp"
#include "io_convert.hpp"
#include "irq_controls.hpp"
#include "oversample.hpp"
#include "string_format.hpp"
#include "ui_fileman.hpp"
#include "ui_receiver.hpp"
#include "ui_textentry.hpp"
#include "utility.hpp"
using namespace portapack;
namespace fs = std::filesystem;
namespace ui {
void RemoteView::focus() {
button.focus();
static constexpr uint8_t text_edit_max = 30;
/* RemoteEntryModel **************************************/
std::string RemoteEntryModel::to_string() const {
return join(',', {path.string(),
name,
to_string_dec_uint(icon),
to_string_dec_uint(bg_color),
to_string_dec_uint(fg_color),
to_string_dec_uint(metadata.center_frequency),
to_string_dec_uint(metadata.sample_rate)});
}
RemoteView::~RemoteView() {
// transmitter_model.disable();
// baseband::shutdown();
Optional<RemoteEntryModel> RemoteEntryModel::parse(std::string_view line) {
auto cols = split_string(line, ',');
if (cols.size() < 7)
return {};
RemoteEntryModel entry{};
entry.path = cols[0];
entry.name = std::string{cols[1]};
parse_int(cols[2], entry.icon);
parse_int(cols[3], entry.bg_color);
parse_int(cols[4], entry.fg_color);
parse_int(cols[5], entry.metadata.center_frequency);
parse_int(cols[6], entry.metadata.sample_rate);
return entry;
}
RemoteView::RemoteView(
NavigationView& nav) {
add_children({&labels,
&button});
/* RemoteModel *******************************************/
button.on_select = [this, &nav](Button&) {
nav.pop();
bool RemoteModel::delete_entry(const RemoteEntryModel* entry) {
// NB: expecting 'entry' to be a pointer to an entry in vector.
auto it = std::find_if(
entries.begin(), entries.end(),
[entry](auto& item) { return entry == &item; });
if (it == entries.end())
return false;
entries.erase(it);
return true;
}
bool RemoteModel::load(const std::filesystem::path& path) {
File f;
auto error = f.open(path);
if (error)
return false;
entries.clear();
bool first = true;
auto reader = FileLineReader(f);
for (const auto& line : reader) {
if (line.length() == 0 || line[0] == '#')
continue; // Empty or comment line.
// First line is the "name" field.
if (first) {
name = trim(line);
first = false;
continue;
}
// All the other lines are button entries.
auto entry = RemoteEntryModel::parse(line);
if (entry)
entries.push_back(*std::move(entry));
}
return true;
}
bool RemoteModel::save(const std::filesystem::path& path) {
File f;
auto error = f.create(path);
if (error)
return false;
f.write_line(name);
for (auto& entry : entries)
f.write_line(entry.to_string());
return true;
}
/* RemoteButton ******************************************/
RemoteButton::RemoteButton(Rect parent_rect, RemoteEntryModel* entry)
: NewButton{parent_rect, {}, nullptr},
entry_{nullptr} {
set_entry(entry);
// Forward to on_select2 -- this isn't ideal, but works for now.
on_select = [this]() {
if (on_select2)
on_select2(*this);
};
}
void RemoteButton::on_focus() {
// Enable long press on "Select".
SwitchesState config;
config[toUType(Switch::Sel)] = true;
set_switches_long_press_config(config);
}
void RemoteButton::on_blur() {
// Reset long press.
SwitchesState config{};
set_switches_long_press_config(config);
}
bool RemoteButton::on_key(KeyEvent key) {
if (key == KeyEvent::Select) {
if (key_is_long_pressed(key) && on_long_select) {
on_long_select(*this);
return true;
}
if (on_select2) {
on_select2(*this);
return true;
}
}
return false;
}
void RemoteButton::paint(Painter& painter) {
NewButton::paint(painter);
// Add a border on the highlighted button.
if (has_focus() || highlighted()) {
auto r = screen_rect();
painter.draw_rectangle(r, Color::white());
auto p = r.location() + Point{1, 1};
auto s = Size{r.size().width() - 2, r.size().height() - 2};
painter.draw_rectangle({p, s}, Color::light_grey());
}
};
RemoteEntryModel* RemoteButton::entry() {
return entry_;
}
void RemoteButton::set_entry(RemoteEntryModel* entry) {
entry_ = entry;
set_focusable(entry_ != nullptr);
hidden(entry_ == nullptr);
if (entry_) {
set_text(entry_->name);
set_bitmap(RemoteIcons::get(entry_->icon));
}
set_dirty();
}
Style RemoteButton::paint_style() {
if (!entry_)
return style();
MutableStyle s{style()};
s.foreground = RemoteColors::get(entry_->fg_color);
s.background = RemoteColors::get(entry_->bg_color);
if (has_focus() || highlighted())
s.invert();
// It's kind of a hack to set 'color_' here, but the base
// class' paint logic is kind of convoluted but isn't worth
// the extra bytes to copy and paste a paint override.
color_ = s.foreground;
return s;
}
/* RemoteEntryEditView ***********************************/
RemoteEntryEditView::RemoteEntryEditView(
NavigationView& nav,
RemoteEntryModel& entry)
: entry_{entry} {
add_children({
&labels,
&field_name,
&field_path,
&field_freq,
&text_rate,
&field_icon_index,
&field_fg_color_index,
&field_bg_color_index,
&button_preview,
&button_delete,
&button_done,
});
// TODO: It's time to make field bindings and clean this mess up.
field_name.on_change = [this](TextField& tf) {
entry_.name = tf.get_text();
button_preview.set_text(entry_.name);
};
field_name.on_select = [this, &nav](TextField& tf) {
temp_buffer_ = tf.get_text();
text_prompt(nav, temp_buffer_, text_edit_max,
[this, &tf](std::string& str) {
tf.set_text(str);
});
};
field_name.set_text(entry_.name);
field_path.on_select = [this, &nav](TextField&) {
auto open_view = nav.push<FileLoadView>(".C*");
open_view->push_dir(u"CAPTURES");
open_view->on_changed = [this](fs::path path) {
load_path(std::move(path));
refresh_ui();
};
};
field_freq.on_edit = [this, &nav]() {
auto freq_view = nav.push<FrequencyKeypadView>(entry_.metadata.center_frequency);
freq_view->on_changed = [this](rf::Frequency f) {
entry_.metadata.center_frequency = f;
field_freq.set_value(f);
};
};
field_icon_index.on_change = [this](int32_t v) {
entry_.icon = v;
button_preview.set_bitmap(RemoteIcons::get(v));
};
field_icon_index.set_value(entry.icon);
field_fg_color_index.on_change = [this](int32_t v) {
entry_.fg_color = v;
button_preview.set_color(RemoteColors::get(v));
};
field_fg_color_index.set_value(entry_.fg_color);
field_bg_color_index.on_change = [this](int32_t v) {
entry_.bg_color = v;
button_preview.set_dirty();
};
field_bg_color_index.set_value(entry_.bg_color);
button_delete.on_select = [this, &nav]() {
nav.display_modal(
"Delete?", " Delete this button?", YESNO,
[this, &nav](bool choice) {
if (choice) {
if (on_delete)
on_delete(entry_);
// Exit the edit view upon delete.
nav.set_on_pop([&nav]() { nav.pop(); });
}
});
};
button_done.on_select = [&nav](Button&) {
nav.pop();
};
refresh_ui();
}
void RemoteEntryEditView::focus() {
button_done.focus();
}
void RemoteEntryEditView::refresh_ui() {
field_path.set_text(entry_.path.filename().string());
field_freq.set_value(entry_.metadata.center_frequency);
text_rate.set(unit_auto_scale(entry_.metadata.sample_rate, 3, 0) + "Hz");
}
void RemoteEntryEditView::load_path(std::filesystem::path&& path) {
// Read metafile if it exists.
auto metadata_path = get_metadata_path(path);
auto metadata = read_metadata_file(metadata_path);
entry_.path = std::move(path);
// Use metadata if found, otherwise fallback to the TX frequency.
if (metadata)
entry_.metadata = *metadata;
else
entry_.metadata = {transmitter_model.target_frequency(), 500'000};
}
/* RemoteView ********************************************/
RemoteView::RemoteView(
NavigationView& nav)
: nav_{nav} {
baseband::run_image(portapack::spi_flash::image_tag_replay);
add_children({
&field_title,
&tx_view,
&check_loop,
&field_filename,
&button_add,
&button_new,
&button_open,
&waterfall,
});
create_buttons();
field_title.on_select = [this, &nav](TextField&) {
temp_buffer_ = model_.name;
text_prompt(nav_, temp_buffer_, text_edit_max, [this](std::string& new_name) {
model_.name = new_name;
refresh_ui();
set_needs_save();
});
};
field_filename.on_select = [this, &nav](TextField&) {
temp_buffer_ = remote_path_.stem().string();
text_prompt(nav_, temp_buffer_, text_edit_max, [this](std::string& new_name) {
rename_remote(new_name);
refresh_ui();
});
};
button_add.on_select = [this]() { add_button(); };
button_new.on_select = [this]() { new_remote(); };
button_open.on_select = [this]() { open_remote(); };
// Fill in the area between the remote buttons and bottom UI with waterfall.
Dim waterfall_top = buttons_top_.y() + button_area_height;
Dim waterfall_bottom = button_add.parent_rect().top();
Dim waterfall_height = waterfall_bottom - waterfall_top;
waterfall.set_parent_rect({0, waterfall_top, screen_width, waterfall_height});
ensure_directory(u"REMOTES");
// Load the previously loaded remote if exists.
if (!load_remote(settings_.remote_path))
init_remote();
refresh_ui();
}
RemoteView::RemoteView(NavigationView& nav, fs::path path)
: RemoteView(nav) {
load_remote(std::move(path));
refresh_ui();
}
RemoteView::~RemoteView() {
stop();
baseband::shutdown();
save_remote(/*show_error*/ false);
}
void RemoteView::focus() {
if (model_.entries.empty())
button_add.focus();
else
buttons_[0]->focus();
}
void RemoteView::create_buttons() {
// Handler callbacks.
auto handle_send = [this](RemoteButton& btn) {
if (btn.entry()->path.empty())
// No path set? Go to edit mode instead.
edit_button(btn);
else if (is_sending() && &btn == current_btn_)
// Pressed the same button again? Stop.
stop();
else
// Start sending.
send_button(btn);
};
auto handle_edit = [this](RemoteButton& btn) {
edit_button(btn);
};
// Create and add RemoteButtons for the whole grid.
for (size_t i = 0; i < max_buttons; ++i) {
Coord x = i % button_cols;
Coord y = i / button_cols;
Point pos = Point{x * button_width, y * button_height} + buttons_top_;
auto btn = std::make_unique<RemoteButton>(
Rect{pos, {button_width, button_height}},
nullptr);
btn->on_select2 = handle_send;
btn->on_long_select = handle_edit;
add_child(btn.get());
buttons_.push_back(std::move(btn));
}
}
void RemoteView::reset_buttons() {
// Whever the model's entries instance is invalidated,
// all the pointers in the buttons will end up dangling.
// TODO: This is pretty lame. Could maybe static alloc?
for (auto& btn : buttons_)
btn->set_entry(nullptr);
}
void RemoteView::refresh_ui() {
field_title.set_text(model_.name);
field_filename.set_text(remote_path_.stem().string());
// Update buttons from the model.
for (size_t i = 0; i < buttons_.size(); ++i) {
if (i < model_.entries.size())
buttons_[i]->set_entry(&model_.entries[i]);
else
buttons_[i]->set_entry(nullptr);
}
}
void RemoteView::add_button() {
if (model_.entries.size() >= max_buttons)
return;
// Don't let replay thread read the model while editing.
stop();
model_.entries.push_back({{}, "<EMPTY>", 0, 3, 1});
reset_buttons();
refresh_ui();
set_needs_save();
}
void RemoteView::edit_button(RemoteButton& btn) {
// Don't let replay thread read the model while editing.
stop();
auto edit_view = nav_.push<RemoteEntryEditView>(*btn.entry());
nav_.set_on_pop([this]() {
refresh_ui();
set_needs_save();
});
edit_view->on_delete = [this](RemoteEntryModel& to_delete) {
model_.delete_entry(&to_delete);
reset_buttons();
};
}
void RemoteView::send_button(RemoteButton& btn) {
// TODO: If this is called while is_sending() == true,
// it just stops and doesn't start the new button?
// Reset everything to prepare to send a file.
stop();
current_btn_ = &btn; // Stash for looping.
// Open the sample file to send.
auto reader = std::make_unique<FileConvertReader>();
auto error = reader->open(btn.entry()->path);
if (error) {
show_error("Can't open file:\n" + btn.entry()->path.stem().string());
return;
}
// Update the sample rate in proc_replay baseband.
baseband::set_sample_rate(
btn.entry()->metadata.sample_rate,
get_oversample_rate(btn.entry()->metadata.sample_rate));
// ReplayThread starts immediately on construction; must be set before creating.
transmitter_model.set_target_frequency(btn.entry()->metadata.center_frequency);
transmitter_model.set_sampling_rate(get_actual_sample_rate(btn.entry()->metadata.sample_rate));
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
transmitter_model.enable();
// ReplayThread reads the file and sends to the baseband.
replay_thread_ = std::make_unique<ReplayThread>(
std::move(reader),
/* read_size */ 0x4000,
/* buffer_count */ 3,
&ready_signal_,
[](uint32_t return_code) {
ReplayThreadDoneMessage message{return_code};
EventDispatcher::send_message(message);
});
}
void RemoteView::stop() {
// This terminates the underlying chThread.
replay_thread_.reset();
transmitter_model.disable();
ready_signal_ = false;
}
void RemoteView::new_remote() {
save_remote();
init_remote();
refresh_ui();
// View needs to redraw to hide old buttons.
set_dirty();
}
void RemoteView::open_remote() {
auto open_view = nav_.push<FileLoadView>(".REM");
open_view->push_dir(u"REMOTES");
open_view->on_changed = [this](fs::path path) {
save_remote();
load_remote(std::move(path));
refresh_ui();
;
};
}
void RemoteView::init_remote() {
model_ = {"<Unnamed Remote>", {}};
reset_buttons();
set_remote_path(next_filename_matching_pattern(u"/REMOTES/REMOTE_????.REM"));
set_needs_save(false);
if (remote_path_.empty())
show_error("Couldn't make new remote file.");
}
bool RemoteView::load_remote(fs::path&& path) {
set_remote_path(std::move(path));
set_needs_save(false);
reset_buttons();
return model_.load(remote_path_);
}
void RemoteView::save_remote(bool show_errors) {
if (!needs_save_)
return;
bool ok = model_.save(remote_path_);
if (!ok && show_errors)
show_error("Save failed for:\n" + remote_path_.stem().string());
set_needs_save(false);
}
void RemoteView::rename_remote(const std::string& new_name) {
auto folder = remote_path_.parent_path();
auto ext = remote_path_.extension();
auto new_path = folder / new_name + ext;
if (file_exists(new_path)) {
show_error("Remote " + new_name + " already exists");
return;
}
// Rename file if there is one.
if (fs::file_exists(remote_path_))
rename_file(remote_path_, new_path);
set_remote_path(std::move(new_path));
}
void RemoteView::handle_replay_thread_done(uint32_t return_code) {
if (return_code == ReplayThread::END_OF_FILE) {
if (check_loop.value() && current_btn_) {
send_button(*current_btn_);
return;
}
}
/*
// TODO: This can happen when stopping an in-progress Tx.
if (return_code == ReplayThread::READ_ERROR)
show_error("Bad capture file.");
*/
stop();
}
void RemoteView::set_remote_path(fs::path&& path) {
// Unfortunately, have to keep these two in sync because
// settings doesn't know about fs::path.
remote_path_ = std::move(path);
settings_.remote_path = remote_path_.string();
}
void RemoteView::show_error(const std::string& msg) const {
nav_.display_modal("Error", msg);
}
} /* namespace ui */
+348 -21
View File
@@ -1,6 +1,5 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2018 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -21,42 +20,370 @@
*/
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_spectrum.hpp"
#include "ui_transmitter.hpp"
#include "transmitter_model.hpp"
#include "app_settings.hpp"
#include "baseband_api.hpp"
#include "bitmap.hpp"
#include "file.hpp"
#include "metadata_file.hpp"
#include "optional.hpp"
#include "radio_state.hpp"
#include "replay_thread.hpp"
#include <algorithm>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace ui {
/* Maps icon index to bitmap. */
class RemoteIcons {
public:
static constexpr const Bitmap* get(uint8_t index) {
if (index >= std::size(bitmaps_))
return bitmaps_[0];
return bitmaps_[index];
}
static constexpr size_t size() {
return std::size(bitmaps_);
}
private:
// NB: Icons need to be 16x16 or they won't fit corrently.
static constexpr std::array<const Bitmap*, 25> bitmaps_{
nullptr,
&bitmap_icon_fox,
&bitmap_icon_adsb,
&bitmap_icon_ais,
&bitmap_icon_aprs,
&bitmap_icon_btle,
&bitmap_icon_burger,
&bitmap_icon_camera,
&bitmap_icon_cwgen,
&bitmap_icon_dmr,
&bitmap_icon_file_image,
&bitmap_icon_lge,
&bitmap_icon_looking,
&bitmap_icon_memory,
&bitmap_icon_morse,
&bitmap_icon_nrf,
&bitmap_icon_notepad,
&bitmap_icon_rds,
&bitmap_icon_remote,
&bitmap_icon_setup,
&bitmap_icon_sleep,
&bitmap_icon_sonde,
&bitmap_icon_stealth,
&bitmap_icon_tetra,
&bitmap_icon_temperature};
};
// TODO: Use RGB colors instead?
/* Maps color index to color. */
class RemoteColors {
public:
static constexpr Color get(uint8_t index) {
if (index >= std::size(colors_))
return colors_[0];
return colors_[index];
}
static constexpr size_t size() {
return std::size(colors_);
}
private:
static constexpr std::array<Color, 21> colors_{
Color::black(), // 0
Color::white(), // 1
Color::darker_grey(), // 2
Color::dark_grey(), // 3
Color::grey(), // 4
Color::light_grey(), // 5
Color::red(), // 6
Color::orange(), // 7
Color::yellow(), // 8
Color::green(), // 9
Color::blue(), // 10
Color::cyan(), // 11
Color::magenta(), // 12
Color::dark_red(), // 13
Color::dark_orange(), // 14
Color::dark_yellow(), // 15
Color::dark_green(), // 16
Color::dark_blue(), // 17
Color::dark_cyan(), // 18
Color::dark_magenta(), // 19
Color::purple()}; // 20
};
/* Data model for a remote entry. */
struct RemoteEntryModel {
std::filesystem::path path{};
std::string name{};
uint8_t icon = 0;
uint8_t bg_color = 0;
uint8_t fg_color = 0;
capture_metadata metadata{};
// TODO: start/end position for trimming.
std::string to_string() const;
static Optional<RemoteEntryModel> parse(std::string_view line);
};
/* Data model for a remote. */
struct RemoteModel {
std::string name{};
std::vector<RemoteEntryModel> entries{};
bool delete_entry(const RemoteEntryModel* entry);
bool load(const std::filesystem::path& path);
bool save(const std::filesystem::path& path);
};
/* Button for the remote UI. */
class RemoteButton : public NewButton {
public:
std::function<void(RemoteButton&)> on_select2{};
std::function<void(RemoteButton&)> on_long_select{};
RemoteButton(Rect parent_rect, RemoteEntryModel* entry);
RemoteButton(const RemoteButton&) = delete;
RemoteButton& operator=(const RemoteButton&) = delete;
void on_focus() override;
void on_blur() override;
bool on_key(KeyEvent key) override;
void paint(Painter& painter) override;
RemoteEntryModel* entry();
void set_entry(RemoteEntryModel* entry);
protected:
Style paint_style() override;
private:
// Hide because it's not used.
using NewButton::on_select;
RemoteEntryModel* entry_;
};
/* Settings container for remote. */
struct RemoteSettings {
std::string remote_path{};
};
/* View for editing a remote entry button. */
class RemoteEntryEditView : public View {
public:
std::function<void(RemoteEntryModel&)> on_delete{};
RemoteEntryEditView(NavigationView& nav, RemoteEntryModel& entry);
std::string title() const override { return "Edit Button"; };
void focus() override;
private:
RemoteEntryModel& entry_;
std::string temp_buffer_{};
void refresh_ui();
void load_path(std::filesystem::path&& path);
Labels labels{
{{2 * 8, 1 * 16}, "Name:", Color::light_grey()},
{{2 * 8, 2 * 16}, "Path:", Color::light_grey()},
{{2 * 8, 3 * 16}, "Freq:", Color::light_grey()},
{{17 * 8, 3 * 16}, "MHz", Color::light_grey()},
{{2 * 8, 4 * 16}, "Rate:", Color::light_grey()},
{{2 * 8, 5 * 16}, "Icon:", Color::light_grey()},
{{2 * 8, 6 * 16}, "FG Color:", Color::light_grey()},
{{2 * 8, 7 * 16}, "BG Color:", Color::light_grey()},
{{8 * 8, 9 * 16}, "Button preview", Color::light_grey()},
};
TextField field_name{{8 * 8, 1 * 16, 20 * 8, 1 * 16}, {}};
TextField field_path{{8 * 8, 2 * 16, 20 * 8, 1 * 16}, {}};
FrequencyField field_freq{{8 * 8, 3 * 16}};
Text text_rate{{8 * 8, 4 * 16, 20 * 8, 1 * 16}, {}};
NumberField field_icon_index{
{8 * 8, 5 * 16},
2,
{0, RemoteIcons::size() - 1},
/*step*/ 1,
/*fill*/ ' ',
/*loop*/ true};
NumberField field_fg_color_index{
{11 * 8, 6 * 16},
2,
{0, RemoteColors::size() - 1},
/*step*/ 1,
/*fill*/ ' ',
/*loop*/ true};
NumberField field_bg_color_index{
{11 * 8, 7 * 16},
2,
{0, RemoteColors::size() - 1},
/*step*/ 1,
/*fill*/ ' ',
/*loop*/ true};
RemoteButton button_preview{
{10 * 8, 11 * 16 - 8, 10 * 8, 50},
&entry_};
NewButton button_delete{
{2 * 8, 16 * 16, 4 * 8, 2 * 16},
{},
&bitmap_icon_trash,
Color::red()};
Button button_done{{11 * 8, 16 * 16, 8 * 8, 2 * 16}, "Done"};
};
/* App that allows for buttons to be bound to captures for playback. */
class RemoteView : public View {
public:
RemoteView(NavigationView& nav);
RemoteView(NavigationView& nav, std::filesystem::path path);
~RemoteView();
RemoteView(const RemoteView&) = delete;
RemoteView& operator=(const RemoteView&) = delete;
std::string title() const override { return "Remote"; };
void focus() override;
std::string title() const override { return "Custom remote"; };
private:
/*enum tx_modes {
IDLE = 0,
SINGLE,
SCAN
};
/* Creates the dynamic buttons. */
void create_buttons();
/* Resets all the pointers to null entries. */
void reset_buttons();
void refresh_ui();
tx_modes tx_mode = IDLE;
void add_button();
void edit_button(RemoteButton& btn);
void send_button(RemoteButton& btn);
void stop();
void new_remote();
void open_remote();
void init_remote();
bool load_remote(std::filesystem::path&& path);
void save_remote(bool show_errors = true);
void rename_remote(const std::string& new_name);
void handle_replay_thread_done(uint32_t return_code);
void set_needs_save(bool v = true) { needs_save_ = v; }
void set_remote_path(std::filesystem::path&& path);
bool is_sending() const { return replay_thread_ != nullptr; }
void show_error(const std::string& msg) const;
struct remote_layout_t {
Point position;
std::string text;
};
static constexpr Dim button_rows = 4;
static constexpr Dim button_cols = 3;
static constexpr uint8_t max_buttons = button_rows * button_cols;
static constexpr Dim button_area_height = 200;
static constexpr Dim button_width = screen_width / button_cols;
static constexpr Dim button_height = button_area_height / button_rows;
const std::array<remote_layout_t, 32> remote_layout { };*/
// This value is mysterious... why?
static constexpr uint32_t baseband_bandwidth = 2'500'000;
Labels labels{
{{1 * 8, 0}, "Work in progress...", Color::light_grey()}};
NavigationView& nav_;
RxRadioState radio_state_{};
Button button{
{60, 64, 120, 32},
"Exit"};
// Settings
RemoteSettings settings_{};
app_settings::SettingsManager app_settings_{
"tx_remote"sv,
app_settings::Mode::TX,
{
{"remote_path"sv, &settings_.remote_path},
}};
RemoteModel model_{};
bool needs_save_ = false;
std::string temp_buffer_{};
std::filesystem::path remote_path_{};
RemoteButton* current_btn_{};
const Point buttons_top_{0, 20};
std::vector<std::unique_ptr<RemoteButton>> buttons_{};
std::unique_ptr<ReplayThread> replay_thread_{};
bool ready_signal_{}; // Used to signal ReplayThread ready.
TextField field_title{
{0 * 8, 0 * 16 + 2, 30 * 8, 1 * 16},
{}};
TransmitterView2 tx_view{
{0 * 8, 17 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{10 * 8, 17 * 16},
4,
"Loop",
/*small*/ true};
TextField field_filename{
{0 * 8, 18 * 16, 17 * 8, 1 * 16},
{}};
NewButton button_add{
{17 * 8 + 4, 17 * 16, 4 * 8, 2 * 16},
"",
&bitmap_icon_add,
Color::orange(),
/*vcenter*/ true};
NewButton button_new{
{22 * 8, 17 * 16, 4 * 8, 2 * 16},
"",
&bitmap_icon_new_file,
Color::dark_blue(),
/*vcenter*/ true};
NewButton button_open{
{26 * 8, 17 * 16, 4 * 8, 2 * 16},
"",
&bitmap_icon_load,
Color::dark_blue(),
/*vcenter*/ true};
spectrum::WaterfallView waterfall{};
MessageHandlerRegistration message_handler_replay_thread_error{
Message::ID::ReplayThreadDone,
[this](const Message* p) {
auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
handle_replay_thread_done(message.return_code);
}};
MessageHandlerRegistration message_handler_fifo_signal{
Message::ID::RequestSignal,
[this](const Message* p) {
auto message = static_cast<const RequestSignalMessage*>(p);
if (message->signal == RequestSignalMessage::Signal::FillRequest) {
ready_signal_ = true;
}
}};
};
} /* namespace ui */
+8 -8
View File
@@ -654,17 +654,17 @@ SettingsMenuView::SettingsMenuView(NavigationView& nav) {
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
{"App Settings", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetAppSettingsView>(); }},
{"Audio", ui::Color::dark_cyan(), &bitmap_icon_speaker, [&nav]() { nav.push<SetAudioView>(); }},
{"Calibration", ui::Color::dark_cyan(), &bitmap_icon_options_touch, [&nav]() { nav.push<TouchCalibrationView>(); }},
{"Converter", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetConverterSettingsView>(); }},
{"Date/Time", ui::Color::dark_cyan(), &bitmap_icon_options_datetime, [&nav]() { nav.push<SetDateTimeView>(); }},
{"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetEncoderDialView>(); }},
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetFrequencyCorrectionView>(); }},
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<SetPersistentMemoryView>(); }},
{"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push<SetQRCodeView>(); }},
{"Radio", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetRadioView>(); }},
{"User Interface", ui::Color::dark_cyan(), &bitmap_icon_options_ui, [&nav]() { nav.push<SetUIView>(); }},
{"Date/Time", ui::Color::dark_cyan(), &bitmap_icon_options_datetime, [&nav]() { nav.push<SetDateTimeView>(); }},
{"Calibration", ui::Color::dark_cyan(), &bitmap_icon_options_touch, [&nav]() { nav.push<TouchCalibrationView>(); }},
{"App Settings", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetAppSettingsView>(); }},
{"Converter", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetConverterSettingsView>(); }},
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetFrequencyCorrectionView>(); }},
{"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push<SetQRCodeView>(); }},
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<SetPersistentMemoryView>(); }},
{"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetEncoderDialView>(); }},
});
set_max_rows(2); // allow wider buttons
}
+2 -1
View File
@@ -261,7 +261,8 @@ class TextEditorView : public View {
{26 * 8, 34 * 8, 4 * 8, 4 * 8},
{},
&bitmap_icon_controls,
Color::dark_grey()};
Color::dark_grey(),
/*vcenter*/ true};
Text text_position{
{0 * 8, 34 * 8, 26 * 8, 2 * 8},
+1 -2
View File
@@ -292,8 +292,7 @@ std::filesystem::filesystem_error rename_file(
std::filesystem::filesystem_error copy_file(
const std::filesystem::path& file_path,
const std::filesystem::path& dest_path) {
// 512 seems to be the largest block size FatFS likes.
constexpr size_t buffer_size = 512;
constexpr size_t buffer_size = std::filesystem::max_file_block_size;
uint8_t buffer[buffer_size];
File src;
File dst;
+4
View File
@@ -173,6 +173,9 @@ uint8_t capture_file_sample_size(const path& filename);
using file_status = BYTE;
/* The largest block that can be read/written to a file. */
constexpr uint16_t max_file_block_size = 512;
static_assert(sizeof(path::value_type) == 2, "sizeof(std::filesystem::path::value_type) != 2");
static_assert(sizeof(path::value_type) == sizeof(TCHAR), "FatFs TCHAR size != std::filesystem::path::value_type");
@@ -298,6 +301,7 @@ static_assert(sizeof(FIL::err) == 1, "FatFs FIL::err size not expected.");
#define FR_BAD_SEEK (0x102)
#define FR_UNEXPECTED (0x103)
/* NOTE: sizeof(File) == 556 bytes because of the FIL's buf member. */
class File {
public:
using Size = uint64_t;
+195
View File
@@ -0,0 +1,195 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* 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 "iq_trim.hpp"
#include <memory>
#include "string_format.hpp"
namespace fs = std::filesystem;
namespace iq {
/* Trimming helpers based on the sample type (complex8 or complex16). */
template <typename T>
uint32_t power(T value) {
auto real = value.real();
auto imag = value.imag();
return (real * real) + (imag * imag);
}
/* Collects capture file metadata and sample power buckets. */
template <typename T>
Optional<CaptureInfo> profile_capture(
const std::filesystem::path& path,
PowerBuckets& buckets,
uint8_t samples_per_bucket) {
File f;
auto error = f.open(path);
if (error)
return {};
CaptureInfo info{
.file_size = f.size(),
.sample_count = f.size() / sizeof(T),
.sample_size = sizeof(T),
.max_power = 0};
auto profile_samples = buckets.size * samples_per_bucket;
auto sample_interval = info.sample_count / profile_samples;
uint32_t bucket_width = std::max(1ULL, info.sample_count / buckets.size);
uint64_t sample_index = 0;
T value{};
while (true) {
f.seek(sample_index * info.sample_size);
auto result = f.read(&value, info.sample_size);
if (!result) return {}; // Read failed.
if (*result != info.sample_size)
break; // EOF
auto mag_squared = power(value);
if (mag_squared > info.max_power)
info.max_power = mag_squared;
auto bucket_index = sample_index / bucket_width;
buckets.add(bucket_index, mag_squared);
sample_index += sample_interval;
}
return info;
}
Optional<CaptureInfo> profile_capture(
const fs::path& path,
PowerBuckets& buckets,
uint8_t samples_per_bucket) {
auto sample_size = fs::capture_file_sample_size(path);
switch (sample_size) {
case sizeof(complex16_t):
return profile_capture<complex16_t>(path, buckets, samples_per_bucket);
case sizeof(complex8_t):
return profile_capture<complex8_t>(path, buckets, samples_per_bucket);
default:
return {};
};
}
TrimRange compute_trim_range(
CaptureInfo info,
const PowerBuckets& buckets,
uint8_t cutoff_percent) {
bool has_start = false;
uint8_t start_bucket = 0;
uint8_t end_bucket = 0;
uint32_t power_cutoff = cutoff_percent * static_cast<uint64_t>(info.max_power) / 100;
for (size_t i = 0; i < buckets.size; ++i) {
auto power = buckets.p[i].power;
if (power > power_cutoff) {
if (has_start)
end_bucket = i;
else {
start_bucket = i;
end_bucket = i;
has_start = true;
}
}
}
// End should be the first bucket after the last with signal.
// This makes the math downstream simpler. NB: may be > buckets.size.
end_bucket++;
auto samples_per_bucket = info.sample_count / buckets.size;
return {
start_bucket * samples_per_bucket,
end_bucket * samples_per_bucket,
info.sample_size};
}
bool trim_capture_with_range(
const fs::path& path,
TrimRange range,
const std::function<void(uint8_t)>& on_progress) {
constexpr size_t buffer_size = std::filesystem::max_file_block_size;
uint8_t buffer[buffer_size];
auto temp_path = path + u"-tmp";
// end_sample is the first sample to _not_ include.
auto start_byte = range.start_sample * range.sample_size;
auto end_byte = (range.end_sample * range.sample_size);
auto length = end_byte - start_byte;
// 'File' is 556 bytes! Heap alloc to avoid overflowing the stack.
auto src = std::make_unique<File>();
auto dst = std::make_unique<File>();
auto error = src->open(path);
if (error) return false;
error = dst->create(temp_path);
if (error) return false;
src->seek(start_byte);
auto processed = 0UL;
auto next_report = 0UL;
auto report_interval = length / 20UL;
while (true) {
auto result = src->read(buffer, buffer_size);
if (result.is_error()) return false;
auto remaining = length - processed;
auto to_write = std::min(remaining, *result);
result = dst->write(buffer, to_write);
if (result.is_error()) return false;
processed += *result;
if (*result < buffer_size || processed >= length)
break;
if (processed >= next_report) {
on_progress(100 * processed / length);
next_report += report_interval;
}
}
// Close files before renaming/deleting.
src.reset();
dst.reset();
// Delete original and overwrite with temp file.
delete_file(path);
rename_file(temp_path, path);
return true;
}
} // namespace iq
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* 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 __IQ_TRIM_H__
#define __IQ_TRIM_H__
#include "complex.hpp"
#include "file.hpp"
#include "optional.hpp"
#include <functional>
#include <limits>
namespace iq {
/* Information about a capture. */
struct CaptureInfo {
uint64_t file_size;
uint64_t sample_count;
uint8_t sample_size;
uint32_t max_power;
};
/* Holds sample average power by bucket. */
struct PowerBuckets {
struct Bucket {
uint32_t power = 0;
uint8_t count = 0;
};
Bucket* p = nullptr;
const size_t size = 0;
/* Add the power to the bucket average at index. */
void add(size_t index, uint32_t power) {
if (index < size) {
auto& b = p[index];
auto avg = static_cast<uint64_t>(b.power) * b.count;
b.count++;
b.power = (power + avg) / b.count;
}
}
};
/* Data needed to trim a capture by sample range.
* end_sample is the sample *after* the last to keep. */
struct TrimRange {
uint64_t start_sample;
uint64_t end_sample;
uint8_t sample_size;
};
/* Collects capture file metadata and samples power buckets. */
Optional<CaptureInfo> profile_capture(
const std::filesystem::path& path,
PowerBuckets& buckets,
uint8_t samples_per_bucket = 10);
/* Computes the trimming range given profiling info.
* Cutoff percent is a number 1-100. */
TrimRange compute_trim_range(
CaptureInfo info,
const PowerBuckets& buckets,
uint8_t cutoff_percent);
/* Trims the capture file with the specified range. */
bool trim_capture_with_range(
const std::filesystem::path& path,
TrimRange range,
const std::function<void(uint8_t)>& on_progress);
} // namespace iq
#endif /*__IQ_TRIM_H__*/
+2 -2
View File
@@ -311,10 +311,10 @@ std::string to_string_file_size(uint32_t file_size) {
return to_string_dec_uint(file_size) + suffix[suffix_index];
}
std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precision) {
std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision) {
const uint32_t powers_of_ten[5] = {1, 10, 100, 1000, 10000};
std::string string{""};
uint32_t prefix_index = base_nano;
uint32_t prefix_index = base_unit;
double integer_part;
double fractional_part;
+3 -1
View File
@@ -76,7 +76,9 @@ std::string to_string_FAT_timestamp(const FATTimestamp& timestamp);
// Gets a human readable file size string.
std::string to_string_file_size(uint32_t file_size);
std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precision);
/* Scales 'n' to be a value less than 1000. 'base_unit' is the index of the unit from
* 'unit_prefix' that 'n' is in initially. 3 is the index of the '1s' unit. */
std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision);
double get_decimals(double num, int16_t mult, bool round = false);
std::string trim(std::string_view str); // Remove whitespace at ends.
+2 -1
View File
@@ -82,7 +82,8 @@ class AlphanumView : public TextEntryView {
{192, 214, screen_width / 5, 38},
{},
&bitmap_icon_shift,
Color::dark_grey()};
Color::dark_grey(),
/*vcenter*/ true};
Labels labels{
{{1 * 8, 33 * 8}, "Raw:", Color::light_grey()},
+68 -82
View File
@@ -32,8 +32,25 @@
using namespace portapack;
#define POWER_THRESHOLD_HIGH 47
#define POWER_THRESHOLD_MED 38
#define POWER_THRESHOLD_LOW 17
namespace ui {
/* Gets a style indicating total TX gain level. */
static const Style* get_style_for_gain(uint8_t tot_gain) {
if (tot_gain > POWER_THRESHOLD_HIGH) return &Styles::red;
if (tot_gain > POWER_THRESHOLD_MED)
return &Styles::orange;
if (tot_gain > POWER_THRESHOLD_LOW)
return &Styles::yellow;
return nullptr; // Uses default.
}
/* TransmitterView *******************************************************/
void TransmitterView::paint(Painter& painter) {
@@ -73,21 +90,13 @@ void TransmitterView::on_tx_amp_changed(bool rf_amp) {
}
void TransmitterView::update_gainlevel_styles() {
const Style* new_style_ptr = NULL;
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
auto style = get_style_for_gain(tot_gain);
if (tot_gain > POWER_THRESHOLD_HIGH) {
new_style_ptr = &style_power_high;
} else if (tot_gain > POWER_THRESHOLD_MED) {
new_style_ptr = &style_power_med;
} else if (tot_gain > POWER_THRESHOLD_LOW) {
new_style_ptr = &style_power_low;
}
field_gain.set_style(new_style_ptr);
text_gain.set_style(new_style_ptr);
field_amp.set_style(new_style_ptr);
text_amp.set_style(new_style_ptr);
field_gain.set_style(style);
text_gain.set_style(style);
field_amp.set_style(style);
text_amp.set_style(style);
}
void TransmitterView::set_transmitting(const bool transmitting) {
@@ -186,89 +195,66 @@ TransmitterView::TransmitterView(
}
TransmitterView::~TransmitterView() {
// TODO: Does this make sense? Seems wrong to have
// what's basically a widget control the radio.
audio::output::stop();
transmitter_model.disable();
baseband::shutdown();
}
/* TransmitterView2 *******************************************************/
/* Simpler transmitter view that only renders TX Gain and Amp.
* There are two modes, NORMAL_UI and SHORT_UI. SHORT_UI abbreviates control labels. */
void TransmitterView2::paint(Painter&) {
// All widgets paint themselves. Don't let base paint.
}
void TransmitterView2::on_tx_gain_changed(int32_t tx_gain) {
transmitter_model.set_tx_gain(tx_gain);
update_gainlevel_styles();
}
TransmitterView2::TransmitterView2(Point pos, bool short_ui) {
// There are two modes, short and !short
// Short: "G:XX A:YY"
// !Short: "Gain:XX Amp:YY"
Dim width = short_ui ? (9 * 8) : (14 * 8);
set_parent_rect({pos, {width, 16}});
add_children({
&text_labels,
&field_gain,
&field_amp,
});
// Set up controls depending UI mode.
text_labels.set(short_ui ? "G: A:" : "Gain: Amp:");
text_labels.set_parent_rect(
short_ui
? Rect{0 * 8, 0 * 16, 7 * 8, 1 * 16}
: Rect{0 * 8, 0 * 16, 12 * 8, 1 * 16});
field_gain.set_parent_rect(
short_ui
? Rect{2 * 8, 0 * 16, 2 * 8, 1 * 16}
: Rect{5 * 8, 0 * 16, 2 * 8, 1 * 16});
field_amp.set_parent_rect(
short_ui
? Rect{7 * 8, 0 * 16, 2 * 8, 1 * 16}
: Rect{12 * 8, 0 * 16, 2 * 8, 1 * 16});
field_gain.set_value(transmitter_model.tx_gain());
field_gain.on_change = [this](uint32_t tx_gain) {
transmitter_model.set_tx_gain(tx_gain);
update_gainlevel_styles();
};
field_amp.set_value(transmitter_model.rf_amp() ? 14 : 0);
field_amp.on_change = [this](uint32_t rf_amp) {
transmitter_model.set_rf_amp(rf_amp > 0);
update_gainlevel_styles();
};
void TransmitterView2::on_tx_amp_changed(bool rf_amp) {
transmitter_model.set_rf_amp(rf_amp);
update_gainlevel_styles();
}
void TransmitterView2::update_gainlevel_styles() {
const Style* new_style_ptr = NULL;
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
auto style = get_style_for_gain(tot_gain);
if (tot_gain > POWER_THRESHOLD_HIGH) {
new_style_ptr = &style_power_high;
} else if (tot_gain > POWER_THRESHOLD_MED) {
new_style_ptr = &style_power_med;
} else if (tot_gain > POWER_THRESHOLD_LOW) {
new_style_ptr = &style_power_low;
}
field_gain.set_style(new_style_ptr);
text_gain_amp.set_style(new_style_ptr);
field_amp.set_style(new_style_ptr);
field_gain_short_UI.set_style(new_style_ptr);
text_gain_amp_short_UI.set_style(new_style_ptr);
field_amp_short_UI.set_style(new_style_ptr);
}
void TransmitterView2::on_show() {
field_gain.set_value(transmitter_model.tx_gain());
field_amp.set_value(transmitter_model.rf_amp() ? 14 : 0);
field_gain_short_UI.set_value(transmitter_model.tx_gain());
field_amp_short_UI.set_value(transmitter_model.rf_amp() ? 14 : 0);
update_gainlevel_styles();
}
TransmitterView2::TransmitterView2(const Coord x, const Coord y, bool short_UI) {
set_parent_rect({x, y, 20 * 8, 1 * 8});
add_children({
&(short_UI ? text_gain_amp_short_UI : text_gain_amp),
&(short_UI ? field_gain_short_UI : field_gain),
&(short_UI ? field_amp_short_UI : field_amp),
});
if (short_UI) {
field_gain_short_UI.on_change = [this](uint32_t tx_gain) {
on_tx_gain_changed(tx_gain);
};
field_amp_short_UI.on_change = [this](uint32_t rf_amp) {
on_tx_amp_changed((bool)rf_amp);
};
} else {
field_gain.on_change = [this](uint32_t tx_gain) {
on_tx_gain_changed(tx_gain);
};
field_amp.on_change = [this](uint32_t rf_amp) {
on_tx_amp_changed((bool)rf_amp);
};
}
}
TransmitterView2::~TransmitterView2() {
audio::output::stop();
transmitter_model.disable();
baseband::shutdown();
text_labels.set_style(style);
field_gain.set_style(style);
field_amp.set_style(style);
}
} /* namespace ui */
+12 -47
View File
@@ -26,9 +26,9 @@
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_painter.hpp"
#include "ui_receiver.hpp"
#include "ui_styles.hpp"
#include "ui_widget.hpp"
#include "ui_receiver.hpp"
#include "rf_path.hpp"
@@ -37,10 +37,6 @@
#include <algorithm>
#include <functional>
#define POWER_THRESHOLD_HIGH 47
#define POWER_THRESHOLD_MED 38
#define POWER_THRESHOLD_LOW 17
namespace ui {
class TXGainField : public NumberField {
@@ -74,11 +70,8 @@ class TransmitterView : public View {
private:
const Style& style_start = Styles::green;
const Style style_stop = Styles::red;
const Style style_locked = Styles::dark_grey;
const Style style_power_low = Styles::yellow;
const Style style_power_med = Styles::orange;
const Style style_power_high = Styles::red;
const Style& style_stop = Styles::red;
const Style& style_locked = Styles::dark_grey;
bool lock_{false};
bool transmitting_{false};
@@ -134,60 +127,32 @@ class TransmitterView : public View {
void update_gainlevel_styles(void);
};
/* Simpler transmitter view that only renders TX Gain and Amp.
* When short_UI is set it abbreviates control labels. */
class TransmitterView2 : public View {
public:
TransmitterView2(const Coord x, const Coord y, bool short_UI);
~TransmitterView2();
void on_show() override;
void paint(Painter& painter) override;
TransmitterView2(Point pos, bool short_ui);
private:
const Style& style_power_low = Styles::yellow;
const Style& style_power_med = Styles::orange;
const Style& style_power_high = Styles::red;
Text text_gain_amp{
{0, 3 * 8, 5 * 8, 1 * 16},
"Gain: Amp:"};
Text text_labels{
{}, // Set in ctor.
{}};
NumberField field_gain{
{5 * 8, 3 * 8},
{}, // Set in ctor.
2,
{max2837::tx::gain_db_range.minimum, max2837::tx::gain_db_range.maximum},
max2837::tx::gain_db_step,
' '};
NumberField field_amp{
{12 * 8, 3 * 8},
{}, // Set in ctor.
2,
{0, 14},
14,
' '};
Text text_gain_amp_short_UI{
{0, (3 * 8), 5 * 8, 1 * 16},
"Gain A:"};
NumberField field_gain_short_UI{
{(4 * 8) + 2, 3 * 8},
2,
{max2837::tx::gain_db_range.minimum, max2837::tx::gain_db_range.maximum},
max2837::tx::gain_db_step,
' '};
NumberField field_amp_short_UI{
{(9 * 8) - 2, 3 * 8},
2,
{0, 14},
14,
' '};
void on_tx_gain_changed(int32_t tx_gain);
void on_tx_amp_changed(bool rf_amp);
void update_gainlevel_styles(void);
void update_gainlevel_styles();
};
} /* namespace ui */
+83 -88
View File
@@ -24,60 +24,62 @@
// #include "modules.h"
#include "portapack.hpp"
#include "event_m0.hpp"
#include "bmp_splash.hpp"
#include "bmp_modal_warning.hpp"
#include "bmp_splash.hpp"
#include "event_m0.hpp"
#include "portapack_persistent_memory.hpp"
#include "portapack_shared_memory.hpp"
#include "portapack.hpp"
#include "ui_about_simple.hpp"
#include "ui_adsb_rx.hpp"
#include "ui_adsb_tx.hpp"
#include "ui_afsk_rx.hpp"
#include "ui_aprs_rx.hpp"
#include "ui_btle_rx.hpp"
#include "ui_nrf_rx.hpp"
#include "ui_aprs_tx.hpp"
#include "ui_bht_tx.hpp"
#include "ui_btle_rx.hpp"
#include "ui_coasterp.hpp"
#include "ui_debug.hpp"
#include "ui_encoders.hpp"
#include "ui_fileman.hpp"
#include "ui_flash_utility.hpp"
#include "ui_font_fixed_8x16.hpp"
#include "ui_freqman.hpp"
#include "ui_iq_trim.hpp"
#include "ui_jammer.hpp"
// #include "ui_keyfob.hpp"
#include "ui_lcr.hpp"
#include "ui_level.hpp"
#include "ui_looking_glass_app.hpp"
#include "ui_mictx.hpp"
#include "ui_morse.hpp"
#include "ui_nrf_rx.hpp"
// #include "ui_numbers.hpp"
// #include "ui_nuoptix.hpp"
// #include "ui_playdead.hpp"
#include "ui_playlist.hpp"
#include "ui_pocsag_tx.hpp"
#include "ui_rds.hpp"
#include "ui_recon.hpp"
#include "ui_remote.hpp"
#include "ui_scanner.hpp"
#include "ui_search.hpp"
#include "ui_recon.hpp"
#include "ui_level.hpp"
#include "ui_sd_over_usb.hpp"
#include "ui_sd_wipe.hpp"
#include "ui_search.hpp"
#include "ui_settings.hpp"
#include "ui_siggen.hpp"
#include "ui_sonde.hpp"
#include "ui_spectrum_painter.hpp"
#include "ui_ss_viewer.hpp"
#include "ui_sstvtx.hpp"
#include "ui_styles.hpp"
// #include "ui_test.hpp"
#include "ui_text_editor.hpp"
#include "ui_tone_search.hpp"
#include "ui_touchtunes.hpp"
#include "ui_playlist.hpp"
#include "ui_view_wav.hpp"
#include "ui_whipcalc.hpp"
#include "ui_flash_utility.hpp"
#include "ui_sd_over_usb.hpp"
#include "ui_spectrum_painter.hpp"
#include "ui_ss_viewer.hpp"
// #include "acars_app.hpp"
#include "ais_app.hpp"
@@ -85,15 +87,14 @@
#include "analog_tv_app.hpp"
#include "capture_app.hpp"
#include "ert_app.hpp"
#include "gps_sim_app.hpp"
#include "lge_app.hpp"
#include "pocsag_app.hpp"
#include "replay_app.hpp"
#include "gps_sim_app.hpp"
#include "soundboard_app.hpp"
#include "tpms_app.hpp"
#include "core_control.hpp"
#include "ui_looking_glass_app.hpp"
#include "file.hpp"
#include "png_writer.hpp"
@@ -302,17 +303,18 @@ void SystemStatusView::on_converter() {
void SystemStatusView::on_bias_tee() {
if (!portapack::get_antenna_bias()) {
nav_.display_modal("Bias voltage",
"Enable DC voltage on\nantenna connector?",
YESNO,
[this](bool v) {
if (v) {
portapack::set_antenna_bias(true);
receiver_model.set_antenna_bias();
transmitter_model.set_antenna_bias();
refresh();
}
});
nav_.display_modal(
"Bias voltage",
"Enable DC voltage on\nantenna connector?",
YESNO,
[this](bool v) {
if (v) {
portapack::set_antenna_bias(true);
receiver_model.set_antenna_bias();
transmitter_model.set_antenna_bias();
refresh();
}
});
} else {
portapack::set_antenna_bias(false);
receiver_model.set_antenna_bias();
@@ -498,30 +500,28 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) {
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
// {"ACARS", Color::yellow(), &bitmap_icon_adsb, [&nav](){ nav.push<ACARSAppView>(); }},
{"ADS-B", Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBRxView>(); }},
//{ "ACARS", Color::yellow(), &bitmap_icon_adsb, [&nav](){ nav.push<ACARSAppView>(); }},
{"AIS Boats", Color::green(), &bitmap_icon_ais, [&nav]() { nav.push<AISAppView>(); }},
{"AFSK", Color::yellow(), &bitmap_icon_modem, [&nav]() { nav.push<AFSKRxView>(); }},
{"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push<BTLERxView>(); }},
{"NRF", Color::yellow(), &bitmap_icon_nrf, [&nav]() { nav.push<NRFRxView>(); }},
{"Audio", Color::green(), &bitmap_icon_speaker, [&nav]() { nav.push<AnalogAudioView>(); }},
{"AIS Boats", Color::green(), &bitmap_icon_ais, [&nav]() { nav.push<AISAppView>(); }},
{"Analog TV", Color::yellow(), &bitmap_icon_sstv, [&nav]() { nav.push<AnalogTvView>(); }},
{"APRS", Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSRXView>(); }},
{"Audio", Color::green(), &bitmap_icon_speaker, [&nav]() { nav.push<AnalogAudioView>(); }},
{"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push<BTLERxView>(); }},
{"ERT Meter", Color::green(), &bitmap_icon_ert, [&nav]() { nav.push<ERTAppView>(); }},
{"Level", Color::green(), &bitmap_icon_options_radio, [&nav]() { nav.push<LevelView>(); }},
{"NRF", Color::yellow(), &bitmap_icon_nrf, [&nav]() { nav.push<NRFRxView>(); }},
{"POCSAG", Color::green(), &bitmap_icon_pocsag, [&nav]() { nav.push<POCSAGAppView>(); }},
{"Radiosnde", Color::green(), &bitmap_icon_sonde, [&nav]() { nav.push<SondeView>(); }},
{"TPMS Cars", Color::green(), &bitmap_icon_tpms, [&nav]() { nav.push<TPMSAppView>(); }},
{"Recon", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<ReconView>(); }},
{"Level", Color::green(), &bitmap_icon_options_radio, [&nav]() { nav.push<LevelView>(); }},
{"APRS", Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSRXView>(); }}
/*
{ "DMR", Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push<NotImplementedView>(); } },
{ "SIGFOX", Color::dark_grey(), &bitmap_icon_fox, [&nav](){ nav.push<NotImplementedView>(); } }, // SIGFRXView
{ "LoRa", Color::dark_grey(), &bitmap_icon_lora, [&nav](){ nav.push<NotImplementedView>(); } },
{ "SSTV", Color::dark_grey(), &bitmap_icon_sstv, [&nav](){ nav.push<NotImplementedView>(); } },
{ "TETRA", Color::dark_grey(), &bitmap_icon_tetra, [&nav](){ nav.push<NotImplementedView>(); } },*/
{"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push<SearchView>(); }},
{"TPMS Cars", Color::green(), &bitmap_icon_tpms, [&nav]() { nav.push<TPMSAppView>(); }},
// {"DMR", Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push<NotImplementedView>(); }},
// {"SIGFOX", Color::dark_grey(), &bitmap_icon_fox, [&nav](){ nav.push<NotImplementedView>(); }},
// {"LoRa", Color::dark_grey(), &bitmap_icon_lora, [&nav](){ nav.push<NotImplementedView>(); }},
// {"SSTV", Color::dark_grey(), &bitmap_icon_sstv, [&nav](){ nav.push<NotImplementedView>(); }},
// {"TETRA", Color::dark_grey(), &bitmap_icon_tetra, [&nav](){ nav.push<NotImplementedView>(); }},
});
// set_highlighted(0); // Default selection is "Audio"
}
/* TransmittersMenuView **************************************************/
@@ -531,26 +531,24 @@ TransmittersMenuView::TransmittersMenuView(NavigationView& nav) {
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
{"ADS-B [S]", ui::Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBTxView>(); }},
{"ADS-B", ui::Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBTxView>(); }},
{"APRS", ui::Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSTXView>(); }},
{"BHT Xy/EP", ui::Color::green(), &bitmap_icon_bht, [&nav]() { nav.push<BHTView>(); }},
{"BurgerPgr", ui::Color::yellow(), &bitmap_icon_burger, [&nav]() { nav.push<CoasterPagerView>(); }},
{"GPS Sim", ui::Color::green(), &bitmap_icon_gps_sim, [&nav]() { nav.push<GpsSimAppView>(); }},
{"Jammer", ui::Color::green(), &bitmap_icon_jammer, [&nav]() { nav.push<JammerView>(); }},
//{ "Key fob", ui::Color::orange(), &bitmap_icon_keyfob, [&nav](){ nav.push<KeyfobView>(); } },
{"LGE tool", ui::Color::yellow(), &bitmap_icon_lge, [&nav]() { nav.push<LGEView>(); }},
// { "Key fob", ui::Color::orange(), &bitmap_icon_keyfob, [&nav](){ nav.push<KeyfobView>(); }},
{"LGE", ui::Color::yellow(), &bitmap_icon_lge, [&nav]() { nav.push<LGEView>(); }},
{"Morse", ui::Color::green(), &bitmap_icon_morse, [&nav]() { nav.push<MorseView>(); }},
{"BurgerPgr", ui::Color::yellow(), &bitmap_icon_burger, [&nav]() { nav.push<CoasterPagerView>(); }},
//{ "Nuoptix DTMF", ui::Color::green(), &bitmap_icon_nuoptix, [&nav](){ nav.push<NuoptixView>(); } },
// { "Nuoptix DTMF", ui::Color::green(), &bitmap_icon_nuoptix, [&nav](){ nav.push<NuoptixView>(); }},
{"OOK", ui::Color::yellow(), &bitmap_icon_remote, [&nav]() { nav.push<EncodersView>(); }},
{"POCSAG", ui::Color::green(), &bitmap_icon_pocsag, [&nav]() { nav.push<POCSAGTXView>(); }},
{"RDS", ui::Color::green(), &bitmap_icon_rds, [&nav]() { nav.push<RDSView>(); }},
{"Soundbrd", ui::Color::green(), &bitmap_icon_soundboard, [&nav]() { nav.push<SoundBoardView>(); }},
{"S.Painter", ui::Color::orange(), &bitmap_icon_paint, [&nav]() { nav.push<SpectrumPainterView>(); }},
{"SSTV", ui::Color::green(), &bitmap_icon_sstv, [&nav]() { nav.push<SSTVTXView>(); }},
{"TEDI/LCR", ui::Color::yellow(), &bitmap_icon_lcr, [&nav]() { nav.push<LCRView>(); }},
{"TouchTune", ui::Color::green(), &bitmap_icon_touchtunes, [&nav]() { nav.push<TouchTunesView>(); }},
//{"Playlist", ui::Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<PlaylistView>(); }},
{"S.Painter", ui::Color::orange(), &bitmap_icon_paint, [&nav]() { nav.push<SpectrumPainterView>(); }},
//{ "Remote", ui::Color::dark_grey(), &bitmap_icon_remote, [&nav](){ nav.push<RemoteView>(); } },
});
}
@@ -561,41 +559,47 @@ UtilitiesMenuView::UtilitiesMenuView(NavigationView& nav) {
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
//{ "Test app", Color::dark_grey(), nullptr, [&nav](){ nav.push<TestView>(); } },
{"Freq. manager", Color::green(), &bitmap_icon_freqman, [&nav]() { nav.push<FrequencyManagerView>(); }},
{"File manager", Color::green(), &bitmap_icon_dir, [&nav]() { nav.push<FileManagerView>(); }},
{"Antenna Length", Color::green(), &bitmap_icon_tools_antenna, [&nav]() { nav.push<WhipCalcView>(); }},
{"File Manager", Color::green(), &bitmap_icon_dir, [&nav]() { nav.push<FileManagerView>(); }},
{"Freq. Manager", Color::green(), &bitmap_icon_freqman, [&nav]() { nav.push<FrequencyManagerView>(); }},
{"Notepad", Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<TextEditorView>(); }},
{"Signal gen", Color::green(), &bitmap_icon_cwgen, [&nav]() { nav.push<SigGenView>(); }},
//{ "Tone search", Color::dark_grey(), nullptr, [&nav](){ nav.push<ToneSearchView>(); } },
{"Wav viewer", Color::yellow(), &bitmap_icon_soundboard, [&nav]() { nav.push<ViewWavView>(); }},
{"Antenna length", Color::green(), &bitmap_icon_tools_antenna, [&nav]() { nav.push<WhipCalcView>(); }},
{"IQ Trim", Color::orange(), &bitmap_icon_cut, [&nav]() { nav.push<IQTrimView>(); }},
{"SD Over USB", Color::yellow(), &bitmap_icon_hackrf, [&nav]() { nav.push<SdOverUsbView>(); }},
{"Signal Gen", Color::green(), &bitmap_icon_cwgen, [&nav]() { nav.push<SigGenView>(); }},
// {"Test App", Color::dark_grey(), nullptr, [&nav](){ nav.push<TestView>(); }},
// {"Tone Search", Color::dark_grey(), nullptr, [&nav](){ nav.push<ToneSearchView>(); }},
{"Wav View", Color::yellow(), &bitmap_icon_soundboard, [&nav]() { nav.push<ViewWavView>(); }},
{"Wipe SD card", Color::red(), &bitmap_icon_tools_wipesd, [&nav]() { nav.push<WipeSDView>(); }},
// Dangerous apps.
{"Flash Utility", Color::red(), &bitmap_icon_temperature, [&nav]() { nav.push<FlashUtilityView>(); }},
{"SD over USB", Color::yellow(), &bitmap_icon_hackrf, [&nav]() { nav.push<SdOverUsbView>(); }},
{"Wipe SD card", Color::red(), &bitmap_icon_tools_wipesd, [&nav]() { nav.push<WipeSDView>(); }},
});
set_max_rows(2); // allow wider buttons
}
/* SystemMenuView ********************************************************/
void SystemMenuView::hackrf_mode(NavigationView& nav) {
nav.push<ModalMessageView>("HackRF mode", " This mode enables HackRF\n functionality. To return,\n press the reset button.\n\n Switch to HackRF mode?", YESNO,
[this](bool choice) {
if (choice) {
EventDispatcher::request_stop();
}
});
nav.push<ModalMessageView>(
"HackRF mode",
" This mode enables HackRF\n functionality. To return,\n press the reset button.\n\n Switch to HackRF mode?",
YESNO,
[this](bool choice) {
if (choice) {
EventDispatcher::request_stop();
}
});
}
SystemMenuView::SystemMenuView(NavigationView& nav) {
add_items({
//{ "Play dead", Color::red(), &bitmap_icon_playdead, [&nav](){ nav.push<PlayDeadView>(); } },
// {"Play dead", Color::red(), &bitmap_icon_playdead, [&nav]() { nav.push<PlayDeadView>(); }},
{"Receive", Color::cyan(), &bitmap_icon_receivers, [&nav]() { nav.push<ReceiversMenuView>(); }},
{"Transmit", Color::cyan(), &bitmap_icon_transmit, [&nav]() { nav.push<TransmittersMenuView>(); }},
{"Capture", Color::red(), &bitmap_icon_capture, [&nav]() { nav.push<CaptureAppView>(); }},
{"Replay", Color::green(), &bitmap_icon_replay, [&nav]() { nav.push<PlaylistView>(); }},
{"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push<SearchView>(); }},
{"Remote", ui::Color::green(), &bitmap_icon_remote, [&nav]() { nav.push<RemoteView>(); }},
{"Scanner", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<ScannerView>(); }},
{"Microphone", Color::green(), &bitmap_icon_microphone, [&nav]() { nav.push<MicTXView>(); }},
{"Looking Glass", Color::green(), &bitmap_icon_looking, [&nav]() { nav.push<GlassView>(); }},
@@ -603,11 +607,11 @@ SystemMenuView::SystemMenuView(NavigationView& nav) {
{"Settings", Color::cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SettingsMenuView>(); }},
{"Debug", Color::light_grey(), &bitmap_icon_debug, [&nav]() { nav.push<DebugMenuView>(); }},
{"HackRF", Color::cyan(), &bitmap_icon_hackrf, [this, &nav]() { hackrf_mode(nav); }},
//{ "About", Color::cyan(), nullptr, [&nav](){ nav.push<AboutView>(); } }
// {"About", Color::cyan(), nullptr, [&nav]() { nav.push<AboutView>(); }},
});
set_max_rows(2); // allow wider buttons
set_arrow_enabled(false);
// set_highlighted(1); // Startup selection
}
/* SystemView ************************************************************/
@@ -623,19 +627,22 @@ SystemView::SystemView(
constexpr Dim info_view_height = 16;
add_child(&status_view);
status_view.set_parent_rect({{0, 0},
{parent_rect.width(), status_view_height}});
status_view.set_parent_rect(
{{0, 0},
{parent_rect.width(), status_view_height}});
status_view.on_back = [this]() {
this->navigation_view.pop();
};
add_child(&navigation_view);
navigation_view.set_parent_rect({{0, status_view_height},
{parent_rect.width(), static_cast<Dim>(parent_rect.height() - status_view_height)}});
navigation_view.set_parent_rect(
{{0, status_view_height},
{parent_rect.width(), static_cast<Dim>(parent_rect.height() - status_view_height)}});
add_child(&info_view);
info_view.set_parent_rect({{0, 19 * 16},
{parent_rect.width(), info_view_height}});
info_view.set_parent_rect(
{{0, 19 * 16},
{parent_rect.width(), info_view_height}});
navigation_view.on_view_changed = [this](const View& new_view) {
if (!this->navigation_view.is_top()) {
@@ -651,14 +658,6 @@ SystemView::SystemView(
this->status_view.set_dirty();
};
// pmem::set_playdead_sequence(0x8D1);
// Initial view
/*if ((pmem::playing_dead() == 0x5920C1DF) || // Enable code
(pmem::ui_config() & 16)) { // Login option
navigation_view.push<PlayDeadView>();
} else {*/
navigation_view.push<SystemMenuView>();
if (pmem::config_splash()) {
@@ -667,10 +666,6 @@ SystemView::SystemView(
status_view.set_back_enabled(false);
status_view.set_title_image_enabled(true);
status_view.set_dirty();
// else
// navigation_view.push<SystemMenuView>();
//}
}
Context& SystemView::context() const {
@@ -812,7 +807,7 @@ void ModalMessageView::paint(Painter& painter) {
portapack::display.drawBMP({100, 48}, modal_warning_bmp, false);
// Terrible...
// Break on lines.
while ((pos = message_.find("\n", start)) != std::string::npos) {
painter.draw_string(
{1 * 8, (Coord)(120 + (i * 16))},
+33 -1
View File
@@ -36,6 +36,7 @@ using namespace portapack;
#include "utility.hpp"
#include <cstdint>
#include <vector>
namespace ui {
@@ -163,6 +164,7 @@ void RecordView::start() {
text_record_filename.set("");
text_record_dropped.set("");
trim_path = {};
if (sampling_rate == 0) {
return;
@@ -217,7 +219,8 @@ void RecordView::start() {
}
auto p = std::make_unique<FileConvertWriter>();
auto create_error = p->create(base_path.replace_extension((file_type == FileType::RawS8) ? u".C8" : u".C16"));
trim_path = base_path.replace_extension((file_type == FileType::RawS8) ? u".C8" : u".C16");
auto create_error = p->create(trim_path);
if (create_error.is_valid()) {
handle_error(create_error.value());
} else {
@@ -257,6 +260,7 @@ void RecordView::stop() {
if (is_active()) {
capture_thread.reset();
button_record.set_bitmap(&bitmap_record);
trim_capture();
}
update_status_display();
@@ -299,6 +303,34 @@ void RecordView::update_status_display() {
}
}
void RecordView::trim_capture() {
using bucket_t = iq::PowerBuckets::Bucket;
if (file_type != FileType::WAV && auto_trim && !trim_path.empty()) {
// Need to heap alloc the buckets in this case. The large static buffer overflows the stack.
std::vector<bucket_t> buckets(size_t(255), bucket_t{});
;
iq::PowerBuckets power_buckets{
.p = &buckets[0],
.size = buckets.size()};
trim_ui.show_reading();
auto info = iq::profile_capture(trim_path, power_buckets);
if (info) {
// 7% - decent trimming without being too aggressive.
auto trim_range = iq::compute_trim_range(*info, power_buckets, 7);
trim_ui.show_trimming();
iq::trim_capture_with_range(trim_path, trim_range, trim_ui.get_callback());
}
trim_ui.clear();
}
trim_path = {};
}
void RecordView::handle_capture_thread_done(const File::Error error) {
stop();
if (error.code()) {
+10 -3
View File
@@ -22,12 +22,13 @@
#ifndef __UI_RECORD_VIEW_H__
#define __UI_RECORD_VIEW_H__
#include "ui_iq_trim.hpp"
#include "ui_widget.hpp"
#include "capture_thread.hpp"
#include "signal.hpp"
#include "bitmap.hpp"
#include "capture_thread.hpp"
#include "iq_trim.hpp"
#include "signal.hpp"
#include <cstddef>
#include <string>
@@ -63,6 +64,7 @@ class RecordView : public View {
uint32_t set_sampling_rate(uint32_t new_sampling_rate);
void set_file_type(const FileType v) { file_type = v; }
void set_auto_trim(bool v) { auto_trim = v; }
void start();
void stop();
@@ -78,6 +80,7 @@ class RecordView : public View {
void on_tick_second();
void update_status_display();
void trim_capture();
void handle_capture_thread_done(const File::Error error);
void handle_error(const File::Error error);
@@ -98,6 +101,10 @@ class RecordView : public View {
uint32_t sampling_rate{0};
SignalToken signal_token_tick_second{};
bool auto_trim = false;
std::filesystem::path trim_path{};
TrimProgressUI trim_ui{};
Rectangle rect_background{
Color::black()};
+6 -6
View File
@@ -437,12 +437,12 @@ set(MODE_CPPSRC
)
DeclareTargets(POOK ook)
### POCSAG RX
set(MODE_CPPSRC
proc_pocsag.cpp
)
DeclareTargets(PPOC pocsag)
#### POCSAG RX
#
#set(MODE_CPPSRC
# proc_pocsag.cpp
#)
#DeclareTargets(PPOC pocsag)
### POCSAG2 RX
+6 -7
View File
@@ -405,13 +405,12 @@ void ILI9341::drawBMP(const ui::Point p, const uint8_t* bitmap, const bool trans
}
}
/*
Draw BMP from SD card.
Currently supported formats:
16bpp ARGB, RGB565
24bpp RGB
32bpp ARGB
*/
/* Draw BMP from SD card.
* Currently supported formats:
* 16bpp ARGB, RGB565
* 24bpp RGB
* 32bpp ARGB
*/
bool ILI9341::drawBMP2(const ui::Point p, const std::filesystem::path& file) {
File bmpimage;
size_t file_pos = 0;
+43 -5
View File
@@ -90,14 +90,52 @@ class ILI9341 {
const ui::Color foreground,
const ui::Color background);
/*** Scrolling ***
* Scrolling support is implemented in the ILI9341 driver. Basically a region
* of the screen is set up to act as a circular buffer. The VSA (vertical scroll
* address) is the line that defines the "start" of the circular buffer. In our
* case, the driver is set up for "bottom-up" scrolling. In this mode, drawing
* starts at the bottom of the scroll region and draws the buffer upward.
* However, the whole display's address space is inverted (the screen is actually
* upside down in the PortaPack) so this bottom-up drawing appears to be top-down.
*
* What this means is that the line pointed to by VSA will be drawn at the top
* of the scroll region and the line at VSA - 1 (wrapped) will be the bottom.
* Consider the following screen buffers and VSA pointers.
*
* Buffer: Display: Buffer: Display:
* VSA > A A A C NB: VSA points to the "Top"
* B B B D of the rendered output.
* C C VSA > C A
* D D D B
*/
/* Sets the top and bottom lines of the scrolling region. */
void scroll_set_area(const ui::Coord top_y, const ui::Coord bottom_y);
ui::Coord scroll_set_position(const ui::Coord position);
ui::Coord scroll(const int32_t delta);
ui::Coord scroll_area_y(const ui::Coord y) const;
void scroll_disable();
constexpr ui::Dim width() const { return 240; }
constexpr ui::Dim height() const { return 320; }
/* Sets VSA. This an offset from top_y not a screen coordinate. */
ui::Coord scroll_set_position(const ui::Coord position);
/* Relative adjustment to VSA. Positive value will cause output
* to scoll "down", negative values will cause the output to scroll "up". */
ui::Coord scroll(const int32_t delta);
/* Gets a screen coordinate from a scroll region offset. Values are wrapped
* so the offset is wrapped within the bounds of the scroll region. The
* specified offset is applied to VSA and then offset by the scroll region
* top to produce a screen coordinate.
* Consider the following screen buffers and VSA pointer. Note the offsets.
*
* VSA > A = +0 A = +2
* B = +1 B = +3 NB: As before, VSA is always the "top"
* C = +2 VSA > C = +0 and line at VSA - 1 will be the
* D = +3 D = +1 "bottom" or the max 'y' offset.
*/
ui::Coord scroll_area_y(const ui::Coord y) const;
constexpr ui::Dim width() const { return ui::screen_width; }
constexpr ui::Dim height() const { return ui::screen_height; }
constexpr ui::Rect screen_rect() const { return {0, 0, width(), height()}; }
void draw_pixels(const ui::Rect r, const ui::Color* const colors, const size_t count);
+8
View File
@@ -97,6 +97,14 @@ struct Color {
return (v ^ 0xffff);
}
/* Converts a 32-bit color into a 16-bit color.
* High byte is ignored. */
static constexpr Color RGB(uint32_t rgb) {
return {static_cast<uint8_t>((rgb >> 16) & 0xff),
static_cast<uint8_t>((rgb >> 8) & 0xff),
static_cast<uint8_t>(rgb & 0xff)};
}
static constexpr Color black() {
return {0, 0, 0};
}
+69 -43
View File
@@ -494,7 +494,9 @@ void BigFrequency::paint(Painter& painter) {
const auto rect = screen_rect();
// Erase
painter.fill_rectangle({{0, rect.location().y()}, {240, 52}}, ui::Color::black());
painter.fill_rectangle(
{{0, rect.location().y()}, {screen_width, 52}},
ui::Color::black());
// Prepare digits
if (!_frequency) {
@@ -612,10 +614,10 @@ void Console::write(std::string message) {
if (!hidden() && visible()) {
const Style& s = style();
const Font& font = s.font;
const auto rect = screen_rect();
auto rect = screen_rect();
ui::Color pen_color = s.foreground;
for (const auto c : message) {
for (auto c : message) {
if (escape) {
if (c < std::size(term_colors))
pen_color = term_colors[(uint8_t)c];
@@ -628,12 +630,13 @@ void Console::write(std::string message) {
} else if (c == '\x1B') {
escape = true;
} else {
const auto glyph = font.glyph(c);
const auto advance = glyph.advance();
if ((pos.x() + advance.x()) > rect.width()) {
auto glyph = font.glyph(c);
auto advance = glyph.advance();
// Would drawing next character be off the end? Newline.
if ((pos.x() + advance.x()) > rect.width())
crlf();
}
const Point pos_glyph{
Point pos_glyph{
rect.left() + pos.x(),
display.scroll_area_y(pos.y())};
display.draw_glyph(pos_glyph, glyph, pen_color, s.background);
@@ -649,7 +652,6 @@ void Console::write(std::string message) {
void Console::writeln(std::string message) {
write(message + "\n");
// crlf();
}
void Console::paint(Painter&) {
@@ -659,15 +661,23 @@ void Console::paint(Painter&) {
void Console::on_show() {
enable_scrolling(true);
clear();
// visible = true;
}
bool Console::scrolling_enabled = false;
void Console::enable_scrolling(bool enable) {
if (enable) {
const auto screen_r = screen_rect();
display.scroll_set_area(screen_r.top(), screen_r.bottom());
auto sr = screen_rect();
auto line_height = style().font.line_height();
// Count full lines that can fit in console's rectangle.
auto max_lines = sr.height() / line_height; // NB: int division to floor.
// The scroll area must be a multiple of the line_height
// or some lines will end up vertically truncated.
scroll_height = max_lines * line_height;
display.scroll_set_area(sr.top(), sr.top() + scroll_height);
display.scroll_set_position(0);
scrolling_enabled = true;
} else {
@@ -678,28 +688,37 @@ void Console::enable_scrolling(bool enable) {
void Console::on_hide() {
/* TODO: Clear region to eliminate brief flash of content at un-shifted
* position?
*/
* position? */
enable_scrolling(false);
// visible = false;
}
void Console::crlf() {
if (hidden() || !visible()) return;
const Style& s = style();
const auto sr = screen_rect();
const auto line_height = s.font.line_height();
pos = {0, pos.y() + line_height};
const int32_t y_excess = pos.y() + line_height - sr.height();
if (y_excess > 0) {
if (!scrolling_enabled) {
enable_scrolling(true);
}
display.scroll(-y_excess);
pos = {pos.x(), pos.y() - y_excess};
const auto& s = style();
auto sr = screen_rect();
auto line_height = s.font.line_height();
const Rect dirty{sr.left(), display.scroll_area_y(pos.y()), sr.width(), line_height};
// Advance to the next line (\n) position and "carriage return" x to 0.
pos = {0, pos.y() + line_height};
if (pos.y() >= scroll_height) {
// Line is past off the "bottom", need to scroll.
if (!scrolling_enabled)
enable_scrolling(true);
// See the notes in lcd_ili9341.hpp about how scrolling works.
// The gist is that VSA will be moved to scroll the "top" off the
// screen. The drawing code uses 'scroll_area_y' to get the actual
// screen coordinate based on VSA. The "bottom" line is *always*
// at 'VSA + ((max_lines - 1) * line_height)' and so is constant.
pos = {0, scroll_height - line_height};
// Scroll off the "top" line.
display.scroll(-line_height);
// Clear the new line at the "bottom".
Rect dirty{sr.left(), display.scroll_area_y(pos.y()), sr.width(), line_height};
display.fill_rectangle(dirty, s.background);
}
}
@@ -1120,9 +1139,9 @@ NewButton::NewButton(
Color color,
bool vertical_center)
: Widget{parent_rect},
color_{color},
text_{text},
bitmap_{bitmap},
color_{color},
vertical_center_{vertical_center} {
set_focusable(true);
}
@@ -1163,18 +1182,8 @@ void NewButton::paint(Painter& painter) {
if (!bitmap_ && text_.empty())
return;
Color bg, fg;
const auto r = screen_rect();
if (has_focus() || highlighted()) {
bg = style().foreground;
fg = Color::black();
} else {
bg = Color::grey();
fg = style().foreground;
}
const Style paint_style = {style().font, bg, fg};
const Style style = paint_style();
painter.draw_rectangle({r.location(), {r.width(), 1}}, Color::light_grey());
painter.draw_rectangle({r.left(), r.top() + r.height() - 1, r.width(), 1}, Color::dark_grey());
@@ -1182,7 +1191,7 @@ void NewButton::paint(Painter& painter) {
painter.fill_rectangle(
{r.left(), r.top() + 1, r.width() - 1, r.height() - 2},
paint_style.background);
style.background);
int y = r.top();
if (bitmap_) {
@@ -1194,18 +1203,32 @@ void NewButton::paint(Painter& painter) {
bmp_pos,
*bitmap_,
color_,
bg);
style.background);
}
if (!text_.empty()) {
const auto label_r = paint_style.font.size_of(text_);
const auto label_r = style.font.size_of(text_);
painter.draw_string(
{r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
paint_style,
style,
text_);
}
}
Style NewButton::paint_style() {
MutableStyle s{style()};
if (has_focus() || highlighted()) {
s.background = style().foreground;
s.foreground = Color::black();
} else {
s.background = Color::grey();
s.foreground = style().foreground;
}
return s;
}
void NewButton::on_focus() {
if (on_highlight)
on_highlight(*this);
@@ -1964,6 +1987,9 @@ void SymField::set_value(uint64_t value) {
value_[i] = uint_to_char(temp, radix);
v /= radix;
}
if (on_change)
on_change(*this);
}
void SymField::set_value(std::string_view value) {
+5 -2
View File
@@ -332,8 +332,8 @@ class Console : public Widget {
void on_hide() override;
private:
// bool visible = false;
Point pos{0, 0};
Dim scroll_height = 0;
std::string buffer{};
static bool scrolling_enabled;
@@ -479,10 +479,13 @@ class NewButton : public Widget {
void paint(Painter& painter) override;
protected:
virtual Style paint_style();
Color color_;
private:
std::string text_;
const Bitmap* bitmap_;
Color color_;
bool vertical_center_{false};
};
+22
View File
@@ -213,3 +213,25 @@ static constexpr uint32_t gcd_top(const uint32_t u, const uint32_t v) {
uint32_t gcd(const uint32_t u, const uint32_t v) {
return gcd_top(u, v);
}
std::string join(char c, std::initializer_list<std::string_view> strings) {
std::string result;
size_t total_size = strings.size();
for (auto s : strings)
total_size += s.size();
result.reserve(total_size);
bool first = true;
for (auto s : strings) {
if (!first)
result += c;
else
first = false;
result += s;
}
return result;
}
+4
View File
@@ -27,7 +27,9 @@
#include <complex>
#include <cstdint>
#include <cstddef>
#include <initializer_list>
#include <memory>
#include <string_view>
#include <type_traits>
#define LOCATE_IN_RAM __attribute__((section(".ramtext")))
@@ -213,4 +215,6 @@ struct range_t {
}
};
std::string join(char c, std::initializer_list<std::string_view> strings);
#endif /*__UTILITY_H__*/
+1
View File
@@ -47,6 +47,7 @@ add_executable(application_test EXCLUDE_FROM_ALL
${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp
${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp
${PROJECT_SOURCE_DIR}/../../common/utility.cpp
# Dependencies
${PROJECT_SOURCE_DIR}/../../application/file.cpp
@@ -89,4 +89,11 @@ TEST_CASE("to_byte_array returns correct size and values.") {
CHECK_EQ(arr4[1], 0xCD);
CHECK_EQ(arr4[2], 0x12);
CHECK_EQ(arr4[3], 0x34);
}
TEST_CASE("join will join strings") {
CHECK_EQ(join(',', {}), "");
CHECK_EQ(join(',', {"a"}), "a");
CHECK_EQ(join('-', {"a", "b"}), "a-b");
CHECK_EQ(join(',', {"a", "b", "c"}), "a,b,c");
}