Compare commits

...

41 Commits

Author SHA1 Message Date
jLynx e2ad0a1b1a Stable 1.7.4 release (#1340)
* Update version.txt

* Update past_version.txt
2023-08-02 10:24:23 +12:00
Mark Thompson 96cdb2e9e0 Keyboard tweaks (#1338)
* Disallow 1-time shift in digits/symbols mode

* Slightly wider "123" button and smaller OK button

* Slightly wider "123" button and smaller OK button
2023-08-01 14:25:26 -05:00
Mark Thompson 002ef720ee Fixed waterfall frequency scale in Replay app (#1337)
* Fix waterfall frequency scale in Replay

* Fix waterfall frequency scale in Replay
2023-08-01 20:20:19 +02:00
Kyle Reed 2214533894 Keyboard Shift Mode (#1333)
* Add "shift" concept to keyboard

* Better shift colors

* Better keybaord layout for symbols

* Start ShiftLocked for consistency with previous UX.

* Fix number layout

* PR and test-drive feedback
2023-07-31 21:44:05 -07:00
Mark Thompson 06b7a0419e Fixed Select button responsiveness when updating frequency field during heavy CPU activity (e.g. WFM Audio) (#1335)
* Resolve button responsiveness

* Resolve button responsiveness

* Clang

* Clang

* Clang try again

* Removed unnecessary lines

* Address review comments

* Add comments per reviewer suggestion

* Clang test

* Clang retry
2023-07-31 23:27:15 -05:00
Kyle Reed d24ff7b3bc Oversample capturing for low bandwidths (#1332)
* draft_low_bit_rate_solution_Capture_App

* second_draft_dynamic_decim

* Add support for Oversample Rate to capture.

---------

Co-authored-by: Brumi-2021 <ea3hqj@gmail.com>
2023-07-31 17:46:07 +02:00
Mark Thompson a24b3ad3de Correct estimated capture time in C8 format (#1330)
* Correct capture time remaining for C8 format

* Removed unsupported RawS32 type

* Clang
2023-07-31 08:02:11 -07:00
Mark Thompson 91c6e3fc30 Display error message when trying to delete non-empty directory (#1321)
* Non-empty directory check

* Non-empty directory check

* Display error when attempting to delete non-empty directory

* Clang

Delete white-space that the friggin editor added

* is_empty_directory

* is_empty_directory

* is_empty_directory

* Now need to check if it's a directory first
2023-07-30 15:46:59 -05:00
Kyle Reed 411f6c0a34 Progress bar for Notepad IO (#1322) 2023-07-30 09:36:57 +02:00
Kyle Reed 0a3aa706ef Scanner cleanup (#1320)
* FreqmanDB in Scanner

* Format

* Add comment on squelch(0)
2023-07-29 15:36:00 -07:00
Kyle Reed 5ca74db2f9 Fix output file config in recon (#1319) 2023-07-29 07:16:49 +02:00
Kyle Reed f24523c2f1 Add button to prompt rename with a timestamp. (#1315) 2023-07-28 18:53:16 -07:00
Mark Thompson e7c5a862da Revert encoder sensitivity change (#1314) 2023-07-27 16:45:39 +02:00
Mark Thompson b27c738b69 XOR cursor support in Notepad (#1311)
* XOR cursor support in Notepad

* XOR cursor support

* XOR cursor support

* Revert change

* Use static buffer

* Use static buffer

* Clang
2023-07-27 09:14:02 -05:00
jLynx 8c565bbf15 Allow changelog to run without a token (#1312)
* Allow code to run without a token

* Reverted submodule change by mistake
2023-07-27 14:53:56 +12:00
Mark Thompson 37aa9c046f Gps default frequency (#1309)
* Default frequency

* Default frequency

* Use sample rate from .ini file if present

* Eliminate unneeded sampling_rate variable

* Eliminate unneeded sampling_rate variable
2023-07-26 13:52:27 -05:00
Kyle Reed 195a6224a0 Freq field tweaks (#1306)
* UX improvements

* Prevent wrapping
2023-07-25 08:06:13 -05:00
Mark Thompson ea238f4988 Improve Select button response when Long Press is enabled during CPU-intensive apps (WFM) (#1304)
* Long press

* Long-press improvement during CPU-intensive apps

* Long-press improvement during CPU-intensive apps
2023-07-24 20:45:24 -07:00
Kyle Reed 3514a9a608 Digit Mode for frequency field (#1298)
* Remove 'auto' step mode

* Support per-digit edits on the freq field.

* Swizzle instead of raw accessor

* Fix debug ui after swizzle
2023-07-24 18:09:22 +02:00
Mark Thompson e2bca9aebb Restore missing line (#1302) 2023-07-24 10:12:35 -05:00
Mark Thompson 6ae164e59b Show scale on map (#1296)
* Add scale to geomap

* Add scale to geomap
2023-07-24 14:52:30 +02:00
Mark Thompson e6ad5efbb7 Touch Test tweak & Scanner default squelch (#1295)
* Leave nav bar visible at start & change seed
* Change default Scanner squelch
2023-07-23 21:06:19 +02:00
Kyle Reed 7bd370b5bc Fix baseband thread init order bug for all procs. (#1293) 2023-07-23 08:54:17 +02:00
Mark Thompson 828eb67a52 Remove experimental support for Replaying C32 files (#1294) 2023-07-23 08:52:23 +02:00
Mark Thompson c4df2e66be A Simple Touchscreen Test App (Debug) (#1292)
* Touchscreen test

* Touchscreen test

* Touchscreen test

* Clang

* Moved some Debug menu icons
2023-07-22 16:49:28 -05:00
Kyle Reed 47e95c0c47 Workaround for Capture startup hang (#1285)
* Attempt to fix Capture startup hang

* Pump baseband_queue on M4 startup

* Synchronization experiment

* Moved SpectrumCapture member, better hang detection for M0

* Prevent execute from working on members until class has been initialized.

* Formatting

* Remove workaround.

* Rebase on next
2023-07-22 10:06:55 -07:00
gullradriel 3b5890d0aa fix timer and sd settings issue (#1291) 2023-07-22 16:30:02 +02:00
Mark Thompson bee2dc17af Fixes to C8 capture support (#1290)
* Use divide vs shift in C16->C8 and C32->C16 conversion
* Color cyan for .C32 files
2023-07-22 16:12:35 +02:00
Mark Thompson d6b0173e7a C8 capture support (#1286)
* C8 conversion

* C8 conversion

* C8 support

* C8 support

* C8 support

* C8 support

* Don't auto-convert GPS C8 files

* C8 support

* C8 support

* C8 support

* Remove hang workaround (different PR)

* Comment change

* Clang

* Clang

* Clang

* Merged change from PR #1287

* C8 support

* C8 support

* Improve bandwidth display

* Merged minor optimization from PR 1289

* Merge change from PR 1289

* Use complex types for C8/C16 conversion

* C8 support

* C8 support

* C8 support

* C8 support

* Roll back changes

* Roll back C8 changes

* C8 support

* C8 support

* C8 support

* C8 support

* C8 support

* Don't transmit samples past EOF

* Don't transmit samples past EOF

* Clang

* Clang attempt

* Clang attempt

* C8 support

* Clang
2023-07-22 02:20:56 -05:00
Mark Thompson 8eafe27955 Minor Replay & GPS baseband optimizations (#1289)
* No need to scale progress now that widget is fixed
* Use memcpy vs byte copy, elim progress scaling, correct comments
* Minor optimization in C16->C8 conversion
* Only copy bytes actually read from file
2023-07-21 18:50:00 +02:00
Mark Thompson 2af95456a2 Fix ProgressBar widget overflow (#1288) 2023-07-21 00:06:37 -05:00
Mark Thompson 8ce48dbcf6 Interim Capture hang workaround (#1287)
* Temporary hang workaround

* Clang found a trailing space in a comment :-)
2023-07-20 23:04:54 +02:00
gullradriel 92072b4225 removed redundant call (#1282)
Co-authored-by: GullCode <gullradriel@hotmail.com>
2023-07-17 15:31:36 -05:00
gullradriel 4e823dd3a3 Recon adjustements (#1280)
* Fix offset by 1 error in button_remove, added back load_freqfile(true) in restart
* removed badly duplicated load freqman file
2023-07-17 22:19:39 +02:00
gullradriel 53d9cecdaf Encoder default setting (#1281)
* fix default encoder value to normal
* use enum name instead of hardcoded value
2023-07-17 22:16:40 +02:00
Mark Thompson 680f1f49ff Resolve compile error (#1279) 2023-07-17 21:31:25 +02:00
Kyle Reed 6574272ca8 Freqman improvements (#1276)
* Show .1 MHz in pretty freqman string

* Refactor load to user FreqmanDB

* Recon file parsing cleanup

* use strtol for parse_int

* recon file cleanup

* Fix bugs in Recon changes

* PR feedback

---------

Co-authored-by: kallanreed <kallanreed@noreply.github.com>
2023-07-17 11:43:37 -07:00
Brumi-2021 f5c4aa2be2 Correct_RXBW_char_7_spaces_in_Mic_App (#1278)
* Correct_RXBW_char_7_spaces_in_Mic_App

* format_issues_Mic_app

* using_freqman_functions_and_solving_other_defaults
2023-07-17 09:29:24 -05:00
Mark Thompson 2498861003 Increased number of encoder dial sensitivity setting levels (#1275)
* Encoder dial sensitivity 0-4

* Workaround for worn encoders & more sensitivity levels

* Workaround for worn encoders & more sensitivity levels

* Encoder sensitivity levels 0-4

* Encoder sensitivity levels 0-4

* Fix comment

* Clang

* Clang

* Change default encoder sensitivity in pmem

* Set default encoder sensitivity

* Set default encoder sensitivity

* Revert widget type for encoder sensitivity

* Revert widget type for encoder dial sensitivity

* Revert worn encoder workaround (didn't work)

* 5 levels of encoder dial sensitivity

* 5 levels of encoder dial sensitivity
2023-07-16 17:05:27 -05:00
Kyle Reed 25923e82a4 New Freqman edit UI (#1272)
* WIP new edit UI

* Fix textfield highlight

* WIP new edit

* Wrap up first pass of freqman edit

* Fix indexing of options

---------

Co-authored-by: kallanreed <kallanreed@noreply.github.com>
2023-07-14 18:46:39 -07:00
Mark Thompson 61cb57e48d Added to_string_rounded_freq() function (#1271)
* Added to_string_rounded_freq function

* Added to_string_rounded_freq function

* Clang

* Clang try #2

* Use constexpr for pow10 array

* Additional string_format test cases

* Specify std=c++17 for g++ versions that default to an older standard

* Corrected string_format test cases
2023-07-14 13:38:02 -05:00
129 changed files with 2716 additions and 1371 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ def print_nightly_changelog():
def handle_get_request(path, offset=None):
headers = {"Authorization": f"Bearer {token}"}
headers = {} if token == None else {"Authorization": f"Bearer {token}"}
params = {"since": offset}
url_base = f"https://api.github.com/repos/{repo_owner}/{repo_name}/"
response = requests.get(url_base + path, headers=headers, params=params)
+1 -1
View File
@@ -1 +1 @@
v1.7.2
v1.7.3
+1 -1
View File
@@ -1 +1 @@
v1.7.3
v1.7.4
+1
View File
@@ -179,6 +179,7 @@ set(CPPSRC
file.cpp
freqman_db.cpp
freqman.cpp
io_convert.cpp
io_file.cpp
io_wave.cpp
irq_controls.cpp
@@ -25,10 +25,10 @@
#include "audio.hpp"
#include "baseband_api.hpp"
#include "file.hpp"
#include "freqman.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
#include "string_format.hpp"
#include "ui_freqman.hpp"
#include "utility.hpp"
using namespace portapack;
@@ -48,9 +48,9 @@ class AMOptionsView : public View {
OptionsField options_config{
{3 * 8, 0 * 16},
6, // number of blanking characters
6, // Max option length
{
// using common messages from freqman.cpp
// Using common messages from freqman_ui.cpp
}};
};
@@ -65,9 +65,9 @@ class NBFMOptionsView : public View {
};
OptionsField options_config{
{3 * 8, 0 * 16},
4,
3, // Max option length
{
// using common messages from freqman.cpp
// Using common messages from freqman_ui.cpp
}};
Text text_squelch{
@@ -93,9 +93,9 @@ class WFMOptionsView : public View {
};
OptionsField options_config{
{3 * 8, 0 * 16},
4,
4, // Max option length
{
// using common messages from freqman.cpp
// Using common messages from freqman_ui.cpp
}};
};
+29 -22
View File
@@ -23,6 +23,7 @@
#include "capture_app.hpp"
#include "baseband_api.hpp"
#include "portapack.hpp"
#include "ui_freqman.hpp"
using namespace portapack;
@@ -30,7 +31,6 @@ namespace ui {
CaptureAppView::CaptureAppView(NavigationView& nav)
: nav_{nav} {
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
baseband::run_image(portapack::spi_flash::image_tag_capture);
add_children({
@@ -43,6 +43,7 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
&field_lna,
&field_vga,
&option_bandwidth,
&option_format,
&record_view,
&waterfall,
});
@@ -53,25 +54,36 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
this->field_frequency.set_step(v);
};
option_bandwidth.on_change = [this](size_t, uint32_t base_rate) {
sampling_rate = 8 * base_rate; // Decimation by 8 done on baseband side
/* base_rate is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card */
waterfall.on_hide();
/* Set up proper anti aliasing BPF bandwith in MAX2837 before ADC sampling according to the new added BW Options. */
auto anti_alias_baseband_bandwidth_filter = filter_bandwidth_for_sampling_rate(sampling_rate);
record_view.set_sampling_rate(sampling_rate);
receiver_model.set_sampling_rate(sampling_rate);
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
waterfall.on_show();
option_format.set_selected_index(0); // Default to C16
option_format.on_change = [this](size_t, uint32_t file_type) {
record_view.set_file_type((RecordView::FileType)file_type);
};
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
option_bandwidth.on_change = [this](size_t, uint32_t base_rate) {
/* base_rate is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card */
// For lower bandwidths, (12k5, 16k, 20k), increase the oversample rate to get a higher sample rate.
OversampleRate oversample_rate = base_rate >= 25'000 ? OversampleRate::Rate8x : OversampleRate::Rate16x;
// HackRF suggests a minimum sample rate of 2M.
// Oversampling helps get to higher sample rates when recording lower bandwidths.
uint32_t sampling_rate = toUType(oversample_rate) * base_rate;
// Set up proper anti aliasing BPF bandwidth in MAX2837 before ADC sampling according to the new added BW Options.
auto anti_alias_baseband_bandwidth_filter = filter_bandwidth_for_sampling_rate(sampling_rate);
waterfall.stop();
record_view.set_sampling_rate(sampling_rate, oversample_rate); // NB: Actually updates the baseband.
receiver_model.set_sampling_rate(sampling_rate);
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
waterfall.start();
};
option_bandwidth.set_selected_index(7); // Preselected default option 500kHz.
receiver_model.enable();
option_bandwidth.set_selected_index(7); // Preselected default option 500kHz.
record_view.on_error = [&nav](std::string message) {
nav.display_modal("Error", message);
@@ -83,11 +95,6 @@ CaptureAppView::~CaptureAppView() {
baseband::shutdown();
}
void CaptureAppView::on_hide() {
waterfall.on_hide();
View::on_hide();
}
void CaptureAppView::set_parent_rect(const Rect new_parent_rect) {
View::set_parent_rect(new_parent_rect);
+7 -4
View File
@@ -31,7 +31,6 @@
#include "ui_spectrum.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "freqman.hpp"
namespace ui {
@@ -40,7 +39,6 @@ class CaptureAppView : public View {
CaptureAppView(NavigationView& nav);
~CaptureAppView();
void on_hide() override;
void focus() override;
void set_parent_rect(const Rect new_parent_rect) override;
@@ -55,10 +53,9 @@ class CaptureAppView : public View {
"rx_capture", app_settings::Mode::RX,
app_settings::Options::UseGlobalTargetFrequency};
uint32_t sampling_rate = 0;
Labels labels{
{{0 * 8, 1 * 16}, "Rate:", Color::light_grey()},
{{11 * 8, 1 * 16}, "Format:", Color::light_grey()},
};
RSSI rssi{
@@ -88,6 +85,12 @@ class CaptureAppView : public View {
5,
{}};
OptionsField option_format{
{18 * 8, 1 * 16},
3,
{{"C16", RecordView::FileType::RawS16},
{"C8", RecordView::FileType::RawS8}}};
RecordView record_view{
{0 * 8, 2 * 16, 30 * 8, 1 * 16},
u"BBD_????.*",
+9 -7
View File
@@ -63,17 +63,15 @@ void GpsSimAppView::on_file_changed(const fs::path& new_file_path) {
if (metadata) {
field_frequency.set_value(metadata->center_frequency);
sample_rate = metadata->sample_rate;
} else {
sample_rate = 500000;
transmitter_model.set_sampling_rate(metadata->sample_rate);
}
// UI Fixup.
text_sample_rate.set(unit_auto_scale(sample_rate, 3, 1) + "Hz");
progressbar.set_max(file_size / 1024);
text_sample_rate.set(unit_auto_scale(transmitter_model.sampling_rate(), 3, 1) + "Hz");
progressbar.set_max(file_size);
text_filename.set(truncate(file_path.filename().string(), 12));
auto duration = ms_duration(file_size, sample_rate, 2);
auto duration = ms_duration(file_size, transmitter_model.sampling_rate(), 2);
text_duration.set(to_string_time_ms(duration));
button_play.focus();
@@ -129,7 +127,6 @@ void GpsSimAppView::start() {
});
}
transmitter_model.set_sampling_rate(sample_rate);
transmitter_model.enable();
}
@@ -176,6 +173,11 @@ GpsSimAppView::GpsSimAppView(
&waterfall,
});
if (!settings_.loaded()) {
field_frequency.set_value(initial_target_frequency);
transmitter_model.set_sampling_rate(2600000);
}
field_frequency.set_step(5000);
button_play.on_select = [this](ImageButton&) {
+2 -1
View File
@@ -54,6 +54,8 @@ class GpsSimAppView : public View {
std::string title() const override { return "GPS Sim TX"; };
private:
static constexpr uint32_t initial_target_frequency = 1575420000;
NavigationView& nav_;
RxRadioState radio_state_{
3000000 /* bandwidth */,
@@ -64,7 +66,6 @@ class GpsSimAppView : public View {
static constexpr ui::Dim header_height = 3 * 16;
uint32_t sample_rate = 0;
int32_t tx_gain{47};
bool rf_amp{true}; // aux private var to store temporal, same as Replay App rf_amp user selection.
static constexpr uint32_t baseband_bandwidth = 3000000; // filter bandwidth
+6 -3
View File
@@ -26,6 +26,8 @@
#include "ui_fileman.hpp"
#include "io_file.hpp"
#include "io_convert.hpp"
#include "baseband_api.hpp"
#include "metadata_file.hpp"
#include "portapack.hpp"
@@ -75,7 +77,8 @@ void ReplayAppView::on_file_changed(const fs::path& new_file_path) {
progressbar.set_max(file_size);
text_filename.set(truncate(file_path.filename().string(), 12));
auto duration = ms_duration(file_size, sample_rate, 4);
uint8_t sample_size = capture_file_sample_size(current()->path);
auto duration = ms_duration(file_size, sample_rate, sample_size);
text_duration.set(to_string_time_ms(duration));
button_play.focus();
@@ -110,7 +113,7 @@ void ReplayAppView::start() {
std::unique_ptr<stream::Reader> reader;
auto p = std::make_unique<FileReader>();
auto p = std::make_unique<FileConvertReader>();
auto open_error = p->open(file_path);
if (open_error.is_valid()) {
file_error();
@@ -191,7 +194,7 @@ ReplayAppView::ReplayAppView(
};
button_open.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".C16");
auto open_view = nav.push<FileLoadView>(".C*");
open_view->on_changed = [this](fs::path new_file_path) {
on_file_changed(new_file_path);
};
+57 -6
View File
@@ -251,7 +251,7 @@ void ControlsSwitchesWidget::on_show() {
bool ControlsSwitchesWidget::on_key(const KeyEvent key) {
key_event_mask = 1 << toUType(key);
long_press_key_event_mask = switch_long_press_occurred((size_t)key) ? key_event_mask : 0;
long_press_key_event_mask = key_is_long_pressed(key) ? key_event_mask : 0;
return true;
}
@@ -264,9 +264,9 @@ void ControlsSwitchesWidget::paint(Painter& painter) {
{32, 64, 16, 16}, // Down
{32, 0, 16, 16}, // Up
{32, 32, 16, 16}, // Select
{96, 0, 16, 16}, // Dfu
{16, 96, 16, 16}, // Encoder phase 0
{48, 96, 16, 16}, // Encoder phase 1
{96, 0, 16, 16}, // Dfu
{96, 64, 16, 16}, // Touch
}};
@@ -283,9 +283,9 @@ void ControlsSwitchesWidget::paint(Painter& painter) {
{32 + 1, 64 + 1, 16 - 2, 16 - 2}, // Down
{32 + 1, 0 + 1, 16 - 2, 16 - 2}, // Up
{32 + 1, 32 + 1, 16 - 2, 16 - 2}, // Select
{96 + 1, 0 + 1, 16 - 2, 16 - 2}, // Dfu
{16 + 1, 96 + 1, 16 - 2, 16 - 2}, // Encoder phase 0
{48 + 1, 96 + 1, 16 - 2, 16 - 2}, // Encoder phase 1
{96 + 1, 0 + 1, 16 - 2, 16 - 2}, // Dfu
}};
auto switches_raw = control::debug::switches();
@@ -354,13 +354,13 @@ DebugControlsView::DebugControlsView(NavigationView& nav) {
});
button_done.on_select = [&nav](Button&) {
switches_long_press_enable(0);
set_switches_long_press_config(0);
nav.pop();
};
options_switches_mode.on_change = [this](size_t, OptionsField::value_t v) {
(void)v;
switches_long_press_enable(options_switches_mode.selected_index_value());
set_switches_long_press_config(options_switches_mode.selected_index_value());
};
}
@@ -403,9 +403,10 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
{"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>(); }},
{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }},
{"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>(); }},
});
set_max_rows(2); // allow wider buttons
}
@@ -501,6 +502,56 @@ DebugFontsView::DebugFontsView(NavigationView& nav)
set_focusable(true);
}
/* DebugScreenTest ****************************************************/
DebugScreenTest::DebugScreenTest(NavigationView& nav)
: nav_{nav} {
set_focusable(true);
}
bool DebugScreenTest::on_key(const KeyEvent key) {
Painter painter;
switch (key) {
case KeyEvent::Select:
nav_.pop();
break;
case KeyEvent::Down:
painter.fill_rectangle({0, 0, screen_width, screen_height}, semirand());
break;
case KeyEvent::Left:
pen_color = semirand();
break;
default:
break;
}
return true;
}
bool DebugScreenTest::on_encoder(EncoderEvent delta) {
pen_size = clip<int32_t>(pen_size + delta, 1, screen_width);
return true;
}
bool DebugScreenTest::on_touch(const TouchEvent event) {
Painter painter;
pen_pos = event.point;
painter.fill_rectangle({pen_pos.x() - pen_size / 2, pen_pos.y() - pen_size / 2, pen_size, pen_size}, pen_color);
return true;
}
uint16_t DebugScreenTest::semirand() {
static uint64_t seed{0x0102030405060708};
seed = seed * 137;
seed = (seed >> 1) | ((seed & 0x01) << 63);
return (uint16_t)seed;
}
void DebugScreenTest::paint(Painter& painter) {
painter.fill_rectangle({0, 16, screen_width, screen_height - 16}, Color::white());
painter.draw_string({10 * 8, screen_height / 2}, Styles::white, "Use Stylus");
pen_color = semirand();
}
/* DebugLCRView *******************************************************/
/*DebugLCRView::DebugLCRView(NavigationView& nav, std::string lcr_string) {
+16
View File
@@ -321,6 +321,22 @@ class DebugFontsView : public View {
NavigationView& nav_;
};
class DebugScreenTest : public View {
public:
DebugScreenTest(NavigationView& nav);
bool on_key(KeyEvent key) override;
bool on_encoder(EncoderEvent delta) override;
bool on_touch(TouchEvent event) override;
uint16_t semirand();
void paint(Painter& painter) override;
private:
NavigationView& nav_;
Point pen_pos{};
Color pen_color{0};
int16_t pen_size{10};
};
/*class DebugLCRView : public View {
public:
DebugLCRView(NavigationView& nav, std::string lcrstring);
+35 -12
View File
@@ -39,7 +39,9 @@ namespace fs = std::filesystem;
namespace ui {
static const fs::path txt_ext{u".TXT"};
static const fs::path ppl_ext{u".PPL"};
static const fs::path c8_ext{u".C8"};
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"};
} // namespace ui
@@ -78,14 +80,15 @@ fs::path get_partner_file(fs::path path) {
return {};
auto ext = path.extension();
if (path_iequal(ext, txt_ext))
ext = c16_ext;
else if (path_iequal(ext, c16_ext))
ext = txt_ext;
else
if (is_cxx_capture_file(path))
path.replace_extension(txt_ext);
else if (path_iequal(ext, txt_ext)) {
path.replace_extension(c8_ext);
if (!fs::file_exists(path))
path.replace_extension(c16_ext);
} else
return {};
path.replace_extension(ext);
return fs::file_exists(path) && !fs::is_directory(path) ? path : fs::path{};
}
@@ -141,6 +144,7 @@ void FileManBaseView::load_directory_contents(const fs::path& dir_path) {
current_path = dir_path;
entry_list.clear();
auto filtering = !extension_filter.empty();
bool cxx_file = path_iequal(cxx_ext, extension_filter);
text_current.set(dir_path.empty() ? "(sd root)" : truncate(dir_path, 24));
@@ -150,7 +154,7 @@ void FileManBaseView::load_directory_contents(const fs::path& dir_path) {
continue;
if (fs::is_regular_file(entry.status())) {
if (!filtering || path_iequal(entry.path().extension(), extension_filter))
if (!filtering || path_iequal(entry.path().extension(), extension_filter) || (cxx_file && is_cxx_capture_file(entry.path())))
insert_sorted(entry_list, {entry.path(), (uint32_t)entry.size(), false});
} else if (fs::is_directory(entry.status())) {
insert_sorted(entry_list, {entry.path(), 0, true});
@@ -399,11 +403,17 @@ void FileManagerView::refresh_widgets(const bool v) {
set_dirty();
}
void FileManagerView::on_rename() {
void FileManagerView::on_rename(std::string_view hint) {
auto& entry = get_selected_entry();
name_buffer = entry.path.filename().string();
uint32_t cursor_pos = (uint32_t)name_buffer.length();
// Append the hint to the filename stem as a rename suggestion.
name_buffer = entry.path.stem().string();
if (!hint.empty())
name_buffer += "_" + std::string{hint};
name_buffer += entry.path.extension().string();
// Set the rename cursor to before the extension to make renaming simpler.
uint32_t cursor_pos = (uint32_t)name_buffer.length();
if (auto pos = name_buffer.find_last_of(".");
pos != name_buffer.npos && !entry.is_directory)
cursor_pos = pos;
@@ -430,6 +440,11 @@ void FileManagerView::on_rename() {
}
void FileManagerView::on_delete() {
if (is_directory(get_selected_full_path()) && !is_empty_directory(get_selected_full_path())) {
nav_.display_modal("Delete", "Directory not empty!");
return;
}
auto name = get_selected_entry().path.filename().string();
nav_.push<ModalMessageView>(
"Delete", "Delete " + name + "\nAre you sure?", YESNO,
@@ -497,7 +512,7 @@ bool FileManagerView::handle_file_open() {
if (path_iequal(txt_ext, ext)) {
nav_.push<TextEditorView>(path);
return true;
} else if (path_iequal(c16_ext, ext) || path_iequal(ppl_ext, ext)) {
} else if (is_cxx_capture_file(path) || path_iequal(ppl_ext, ext)) {
// TODO: Enough memory to push?
nav_.push<PlaylistView>(path);
return true;
@@ -541,6 +556,7 @@ FileManagerView::FileManagerView(
&button_new_dir,
&button_new_file,
&button_open_notepad,
&button_rename_timestamp,
});
menu_view.on_highlight = [this]() {
@@ -564,7 +580,7 @@ FileManagerView::FileManagerView(
button_rename.on_select = [this]() {
if (selected_is_valid())
on_rename();
on_rename("");
};
button_delete.on_select = [this]() {
@@ -610,6 +626,13 @@ FileManagerView::FileManagerView(
} else
nav_.display_modal("Open in Notepad", "Can't open that in Notepad.");
};
button_rename_timestamp.on_select = [this]() {
if (selected_is_valid() && !get_selected_entry().is_directory) {
on_rename(::truncate(to_string_timestamp(rtc_time::now()), 8));
} else
nav_.display_modal("Timestamp Rename", "Can't rename that.");
};
}
} // namespace ui
+7 -1
View File
@@ -207,7 +207,7 @@ class FileManagerView : public FileManBaseView {
ClipboardMode clipboard_mode{ClipboardMode::None};
void refresh_widgets(const bool v);
void on_rename();
void on_rename(std::string_view hint);
void on_delete();
void on_paste();
void on_new_dir();
@@ -272,6 +272,12 @@ class FileManagerView : public FileManBaseView {
{},
&bitmap_icon_notepad,
Color::orange()};
NewButton button_rename_timestamp{
{4 * 8, 34 * 8, 4 * 8, 32},
{},
&bitmap_icon_options_datetime,
Color::orange()};
};
} /* namespace ui */
+223 -6
View File
@@ -26,13 +26,42 @@
#include "event_m0.hpp"
#include "portapack.hpp"
#include "rtc_time.hpp"
#include "tone_key.hpp"
#include "ui_receiver.hpp"
#include "ui_styles.hpp"
#include "utility.hpp"
#include <memory>
using namespace portapack;
using namespace ui;
namespace fs = std::filesystem;
// TODO: Clean up after moving to better lookup tables.
using options_t = OptionsField::options_t;
extern options_t freqman_modulations;
extern options_t freqman_bandwidths[4];
extern options_t freqman_steps;
extern options_t freqman_steps_short;
/* Set options. */
void freqman_set_modulation_option(OptionsField& option) {
option.set_options(freqman_modulations);
}
void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option) {
if (is_valid(modulation))
option.set_options(freqman_bandwidths[modulation]);
}
void freqman_set_step_option(OptionsField& option) {
option.set_options(freqman_steps);
}
void freqman_set_step_option_short(OptionsField& option) {
option.set_options(freqman_steps_short);
}
namespace ui {
/* FreqManBaseView ***************************************/
@@ -147,7 +176,7 @@ FrequencySaveView::FrequencySaveView(
};
button_save.on_select = [this, &nav](Button&) {
db_.insert_entry(entry_, db_.entry_count());
db_.insert_entry(db_.entry_count(), entry_);
nav_.pop();
};
}
@@ -188,8 +217,15 @@ FrequencyLoadView::FrequencyLoadView(
/* FrequencyManagerView **********************************/
void FrequencyManagerView::on_edit_entry() {
auto edit_view = nav_.push<FrequencyEditView>(current_entry());
edit_view->on_save = [this](const freqman_entry& entry) {
db_.replace_entry(current_index(), entry);
freqlist_view.set_dirty();
};
}
void FrequencyManagerView::on_edit_freq() {
// TODO: range edit support.
auto freq_edit_view = nav_.push<FrequencyKeypadView>(current_entry().frequency_a);
freq_edit_view->on_changed = [this](rf::Frequency f) {
auto entry = current_entry();
@@ -240,7 +276,7 @@ void FrequencyManagerView::on_add_entry() {
};
// Add will insert below the currently selected item.
db_.insert_entry(entry, current_index() + 1);
db_.insert_entry(current_index() + 1, entry);
refresh_list(1);
}
@@ -263,20 +299,21 @@ FrequencyManagerView::FrequencyManagerView(
: FreqManBaseView(nav) {
add_children(
{&freqlist_view,
&labels,
&button_add_category,
&button_del_category,
&button_edit_entry,
&rect_padding,
&button_edit_freq,
&button_edit_desc,
&button_add_entry,
&button_del_entry});
freqlist_view.on_select = [this](size_t) {
button_edit_freq.focus();
button_edit_entry.focus();
};
// Allows for quickly exiting control.
freqlist_view.on_leave = [this]() {
button_edit_freq.focus();
button_edit_entry.focus();
};
button_add_category.on_select = [this]() {
@@ -287,6 +324,10 @@ FrequencyManagerView::FrequencyManagerView(
on_del_category();
};
button_edit_entry.on_select = [this](Button&) {
on_edit_entry();
};
button_edit_freq.on_select = [this](Button&) {
on_edit_freq();
};
@@ -304,4 +345,180 @@ FrequencyManagerView::FrequencyManagerView(
};
}
/* FrequencyEditView *************************************/
FrequencyEditView::FrequencyEditView(
NavigationView& nav,
freqman_entry entry)
: nav_{nav},
entry_{std::move(entry)} {
add_children({&labels,
&field_type,
&field_freq_a,
&field_freq_b,
&field_modulation,
&field_bandwidth,
&field_step,
&field_tone,
&field_description,
&text_validation,
&button_save,
&button_cancel});
freqman_set_modulation_option(field_modulation);
populate_bandwidth_options();
populate_step_options();
populate_tone_options();
// Add the "invalid/unset" option. Kind of hacky, but...
field_modulation.options().insert(
field_modulation.options().begin(), {"None", -1});
field_step.options().insert(
field_step.options().begin(), {"None", -1});
field_type.set_by_value((int32_t)entry_.type);
field_type.on_change = [this](size_t, auto value) {
entry_.type = static_cast<freqman_type>(value);
refresh_ui();
};
// TODO: this pattern should be able to be wrapped up.
field_freq_a.set_value(entry_.frequency_a);
field_freq_a.on_change = [this](rf::Frequency f) {
entry_.frequency_a = f;
refresh_ui();
};
field_freq_a.on_edit = [this]() {
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_a.value());
freq_view->on_changed = [this](rf::Frequency f) {
field_freq_a.set_value(f);
};
};
field_freq_b.set_value(entry_.frequency_b);
field_freq_b.on_change = [this](rf::Frequency f) {
entry_.frequency_b = f;
refresh_ui();
};
field_freq_b.on_edit = [this]() {
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_b.value());
freq_view->on_changed = [this](rf::Frequency f) {
field_freq_b.set_value(f);
};
};
field_modulation.set_by_value((int32_t)entry_.modulation);
field_modulation.on_change = [this](size_t, auto value) {
entry_.modulation = static_cast<freqman_index_t>(value);
populate_bandwidth_options();
};
field_bandwidth.set_by_value((int32_t)entry_.bandwidth);
field_bandwidth.on_change = [this](size_t, auto value) {
entry_.bandwidth = static_cast<freqman_index_t>(value);
};
field_step.set_by_value((int32_t)entry_.step);
field_step.on_change = [this](size_t, auto value) {
entry_.step = static_cast<freqman_index_t>(value);
};
field_tone.set_by_value((int32_t)entry_.tone);
field_tone.on_change = [this](size_t, auto value) {
entry_.tone = static_cast<freqman_index_t>(value);
};
field_description.set_text(entry_.description);
field_description.on_change = [this](TextField& tf) {
entry_.description = tf.get_text();
};
field_description.on_select = [this](TextField& tf) {
temp_buffer_ = tf.get_text();
text_prompt(nav_, temp_buffer_, FreqManBaseView::desc_edit_max,
[this, &tf](std::string& new_desc) {
tf.set_text(new_desc);
});
};
button_save.on_select = [this](Button&) {
if (on_save)
on_save(std::move(entry_));
nav_.pop();
};
button_cancel.on_select = [this](Button&) {
nav_.pop();
};
refresh_ui();
}
void FrequencyEditView::focus() {
button_cancel.focus();
}
void FrequencyEditView::refresh_ui() {
// This needs to be called whenever a UI option is changed
// in a way that causes fields to be unused or would make the
// entry fail validation.
auto is_range = entry_.type == freqman_type::Range;
auto is_ham = entry_.type == freqman_type::HamRadio;
auto has_freq_b = is_range || is_ham;
field_freq_b.set_style(has_freq_b ? &Styles::white : &Styles::grey);
field_step.set_style(is_range ? &Styles::white : &Styles::grey);
field_tone.set_style(is_ham ? &Styles::white : &Styles::grey);
if (is_valid(entry_)) {
text_validation.set("Valid");
text_validation.set_style(&Styles::green);
} else {
text_validation.set("Error");
text_validation.set_style(&Styles::red);
}
}
// TODO: The following functions shouldn't be needed once
// freqman_db lookup tables are complete.
void FrequencyEditView::populate_bandwidth_options() {
OptionsField::options_t options;
options.push_back({"None", -1});
if (entry_.modulation < std::size(freqman_bandwidths)) {
auto& bandwidths = freqman_bandwidths[entry_.modulation];
for (auto i = 0u; i < bandwidths.size(); ++i) {
auto& item = bandwidths[i];
options.push_back({item.first, (OptionsField::value_t)i});
}
}
field_bandwidth.set_options(std::move(options));
}
void FrequencyEditView::populate_step_options() {
OptionsField::options_t options;
options.push_back({"None", -1});
for (auto i = 0u; i < freqman_steps.size(); ++i) {
auto& item = freqman_steps[i];
options.push_back({item.first, (OptionsField::value_t)i});
}
field_step.set_options(std::move(options));
}
void FrequencyEditView::populate_tone_options() {
using namespace tonekey;
OptionsField::options_t options;
options.push_back({"None", -1});
for (auto i = 0u; i < tone_keys.size(); ++i) {
auto& item = tone_keys[i];
options.push_back({fx100_string(item.second), (OptionsField::value_t)i});
}
field_tone.set_options(std::move(options));
}
} // namespace ui
+94 -10
View File
@@ -36,17 +36,18 @@ namespace ui {
class FreqManBaseView : public View {
public:
FreqManBaseView(
NavigationView& nav);
FreqManBaseView(NavigationView& nav);
void focus() override;
static constexpr size_t desc_edit_max = 0x80;
protected:
using options_t = OptionsField::options_t;
NavigationView& nav_;
freqman_error error_{NO_ERROR};
std::function<void(void)> on_select_frequency{nullptr};
std::function<void(void)> on_select_frequency{};
void change_category(size_t new_index);
/* Access the categories directly from the OptionsField.
@@ -79,14 +80,13 @@ class FreqManBaseView : public View {
protected:
/* Static so selected category is persisted across UI instances. */
static size_t current_category_index;
static constexpr size_t desc_edit_max = 0x80;
};
// TODO: support for new category.
class FrequencySaveView : public FreqManBaseView {
public:
FrequencySaveView(NavigationView& nav, const rf::Frequency value);
std::string title() const override { return "Save freq"; };
std::string title() const override { return "Save freq"; }
private:
std::string temp_buffer_{};
@@ -122,17 +122,18 @@ class FrequencyLoadView : public FreqManBaseView {
std::function<void(rf::Frequency, rf::Frequency)> on_range_loaded{};
FrequencyLoadView(NavigationView& nav);
std::string title() const override { return "Load freq"; };
std::string title() const override { return "Load freq"; }
};
class FrequencyManagerView : public FreqManBaseView {
public:
FrequencyManagerView(NavigationView& nav);
std::string title() const override { return "Freqman"; };
std::string title() const override { return "Freqman"; }
private:
std::string temp_buffer_{};
void on_edit_entry();
void on_edit_freq();
void on_edit_desc();
void on_add_category();
@@ -140,9 +141,6 @@ class FrequencyManagerView : public FreqManBaseView {
void on_add_entry();
void on_del_entry();
Labels labels{
{{5 * 8, 14 * 16 - 4}, "Edit:", Color::light_grey()}};
NewButton button_add_category{
{23 * 8, 0 * 16, 7 * 4, 20},
{},
@@ -157,6 +155,14 @@ class FrequencyManagerView : public FreqManBaseView {
Color::red(),
true};
Button button_edit_entry{
{0 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
"Edit"};
Rectangle rect_padding{
{15 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
Color::grey()};
Button button_edit_freq{
{0 * 8, 15 * 16, 15 * 8, 2 * 16},
"Frequency"};
@@ -180,4 +186,82 @@ class FrequencyManagerView : public FreqManBaseView {
true};
};
class FrequencyEditView : public View {
public:
std::function<void(const freqman_entry&)> on_save{};
FrequencyEditView(
NavigationView& nav,
freqman_entry entry);
std::string title() const override { return "Freqman Edit"; }
void focus() override;
private:
NavigationView& nav_;
freqman_entry entry_;
std::string temp_buffer_{};
void refresh_ui();
void populate_bandwidth_options();
void populate_step_options();
void populate_tone_options();
Labels labels{
{{5 * 8, 1 * 16}, "Edit Frequency Entry", Color::white()},
{{0 * 8, 3 * 16}, "Entry Type :", Color::light_grey()},
{{0 * 8, 4 * 16}, "Frequency A:", Color::light_grey()},
{{0 * 8, 5 * 16}, "Frequency B:", Color::light_grey()},
{{0 * 8, 6 * 16}, "Modulation :", Color::light_grey()},
{{0 * 8, 7 * 16}, "Bandwidth :", Color::light_grey()},
{{0 * 8, 8 * 16}, "Step :", Color::light_grey()},
{{0 * 8, 9 * 16}, "Tone Freq :", Color::light_grey()},
{{0 * 8, 10 * 16}, "Description:", Color::light_grey()},
};
OptionsField field_type{
{13 * 8, 3 * 16},
8,
{
{"Single", 0},
{"Range", 1},
{"HamRadio", 2},
{"Raw", 3},
}};
FrequencyField field_freq_a{{13 * 8, 4 * 16}};
FrequencyField field_freq_b{{13 * 8, 5 * 16}};
OptionsField field_modulation{{13 * 8, 6 * 16}, 5, {}};
OptionsField field_bandwidth{{13 * 8, 7 * 16}, 7, {}};
OptionsField field_step{{13 * 8, 8 * 16}, 12, {}};
OptionsField field_tone{{13 * 8, 9 * 16}, 13, {}};
TextField field_description{
{13 * 8, 10 * 16, 17 * 8, 1 * 16},
{}};
Text text_validation{
{12 * 8, 12 * 16, 5 * 8, 1 * 16},
{}};
Button button_save{
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
"Save"};
Button button_cancel{
{15 * 8, 17 * 16, 15 * 8, 2 * 16},
"Cancel"};
};
} /* namespace ui */
void freqman_set_bandwidth_option(freqman_index_t modulation, ui::OptionsField& option);
void freqman_set_modulation_option(ui::OptionsField& option);
void freqman_set_step_option(ui::OptionsField& option);
void freqman_set_step_option_short(ui::OptionsField& option);
void freqman_set_tone_option(ui::OptionsField& option);
+1
View File
@@ -23,6 +23,7 @@
#include "ui_level.hpp"
#include "ui_fileman.hpp"
#include "ui_freqman.hpp"
#include "file.hpp"
using namespace portapack;
+12 -12
View File
@@ -24,21 +24,21 @@
#ifndef _UI_LEVEL
#define _UI_LEVEL
#include "ui.hpp"
#include "receiver_model.hpp"
#include "ui_receiver.hpp"
#include "ui_styles.hpp"
#include "freqman.hpp"
#include "analog_audio_app.hpp"
#include "audio.hpp"
#include "ui_mictx.hpp"
#include "portapack_persistent_memory.hpp"
#include "baseband_api.hpp"
#include "ui_spectrum.hpp"
#include "string_format.hpp"
#include "file.hpp"
#include "app_settings.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "file.hpp"
#include "freqman_db.hpp"
#include "portapack_persistent_memory.hpp"
#include "radio_state.hpp"
#include "receiver_model.hpp"
#include "string_format.hpp"
#include "ui.hpp"
#include "ui_mictx.hpp"
#include "ui_receiver.hpp"
#include "ui_spectrum.hpp"
#include "ui_styles.hpp"
namespace ui {
+17 -22
View File
@@ -25,6 +25,7 @@
#include "audio.hpp"
#include "baseband_api.hpp"
#include "irq_controls.hpp"
#include "ui_freqman.hpp"
#include "portapack_hal.hpp"
#include "string_format.hpp"
#include "tonesets.hpp"
@@ -355,13 +356,10 @@ MicTXView::MicTXView(
rxaudio(rx_enabled); // Update now if we have RX audio on
options_tone_key.hidden(0); // we are in FM mode, we should have active the Key-tones & CTCSS option.
rxbw.emplace_back(" NFM1:8k5 ", 0); // restore the original dynamic field_rxbw value.
rxbw.emplace_back(" NFM2:11k ", 1);
rxbw.emplace_back(" FM :16k ", 2);
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
field_rxbw.hidden(0); // we are in FM mode, we need to allow the user set up of the RX NFM BW selection (8K5, 11K, 16K)
field_bw.hidden(0); // we are in FM mode, we need to allow FM deviation parameter, in non FM mode.
freqman_set_bandwidth_option(NFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with NFM options from freqman_db.cpp
field_rxbw.set_by_value(2); // // bw 16k (2) default
field_rxbw.hidden(0); // we are in FM mode, we need to allow the user set up of the RX NFM BW selection (8K5, 11K, 16K)
field_bw.hidden(0); // we are in FM mode, we need to allow FM deviation parameter, in non FM mode.
break;
case 1: //{ "WFM", 1 }
enable_am = false;
@@ -375,13 +373,10 @@ MicTXView::MicTXView(
rxaudio(rx_enabled); // Update now if we have RX audio on
options_tone_key.hidden(0); // we are in WFM mode, we should have active the Key-tones & CTCSS option.
rxbw.emplace_back(" 200k-WFM ", 0); // We allow the user selection of the 3 x WFM BW filters, (0) WFM-200K, (1) WFM-180K, (2) WFM-40K.
rxbw.emplace_back(" 180k-WFM ", 1);
rxbw.emplace_back(" 40k-WFM ", 2);
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
field_rxbw.hidden(0); // we are in WFM mode, we need to show to the user the selected BW WFM filter.
field_bw.hidden(0); // we are in WFM mode, we need to allow WFM deviation parameter, in non FM mode.
freqman_set_bandwidth_option(WFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with WFM options from freqman_db.cpp
field_rxbw.set_by_value(0); // bw 200k (0) default
field_rxbw.hidden(0); // we are in WFM mode, we need to show to the user the selected BW WFM filter.
field_bw.hidden(0); // we are in WFM mode, we need to allow WFM deviation parameter, in non FM mode.
break;
case 2: //{ "AM", 2 }
enable_am = true;
@@ -390,8 +385,8 @@ MicTXView::MicTXView(
set_dirty(); // Refresh display
options_tone_key.hidden(1); // we hide that Key-tones & CTCSS input selecction, (no meaning in AM/DSB/SSB).
rxbw.emplace_back(" DSB1-9k ", 0); // we offer in AM DSB two audio BW 9k / 6k.
rxbw.emplace_back(" DSB2-6k ", 1);
rxbw.emplace_back("DSB 9k", 0); // we offer in AM DSB two audio BW 9k / 6k.
rxbw.emplace_back("DSB 6k", 1);
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
field_rxbw.hidden(0); // we show fixed RX AM BW 6Khz
@@ -404,8 +399,8 @@ MicTXView::MicTXView(
check_rogerbeep.set_value(false); // reset the possible activation of roger beep, because it is not compatible with SSB, by now.
check_rogerbeep.hidden(1); // hide that roger beep selection.
rxbw.emplace_back(" USB+3k ", 0); // locked a fixed option, to display it.
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
rxbw.emplace_back("USB+3k", 0); // locked a fixed option, to display it.
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
set_dirty(); // Refresh display
break;
@@ -415,8 +410,8 @@ MicTXView::MicTXView(
check_rogerbeep.set_value(false); // reset the possible activation of roger beep, because it is not compatible with SSB, by now.
check_rogerbeep.hidden(1); // hide that roger beep selection.
rxbw.emplace_back(" LSB-3k ", 0); // locked a fixed option, to display it.
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
rxbw.emplace_back("LSB-3k", 0); // locked a fixed option, to display it.
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
set_dirty(); // Refresh display
break;
@@ -425,8 +420,8 @@ MicTXView::MicTXView(
rxaudio(rx_enabled); // Update now if we have RX audio on
check_rogerbeep.hidden(0); // make visible again the "rogerbeep" selection.
rxbw.emplace_back("SSB1:USB+3k", 0); // added dynamically two options (index 0,1) to that DSB-C case to the field_rxbw value.
rxbw.emplace_back("SSB2:LSB-3k", 1);
rxbw.emplace_back("USB+3k", 0); // added dynamically two options (index 0,1) to that DSB-SC case to the field_rxbw value.
rxbw.emplace_back("LSB-3k", 1);
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
+6 -6
View File
@@ -180,7 +180,7 @@ class MicTXView : public View {
OptionsField options_ak4951_alc_mode{
{20 * 8, 1 * 8},
11,
10, // Label has 10 chars
{
{" OFF-12kHz", 0}, // Nothing changed from ORIGINAL, keeping ALL programmable AK4951 Digital Block->OFF, sampling 24Khz)
{"+12dB-6kHz", 1}, // ALC-> on, (+12dB's) Auto Vol max + Wind Noise cancel + LPF 6kHz + Pre-amp Mic (+21dB=original)
@@ -198,7 +198,7 @@ class MicTXView : public View {
OptionsField options_wm8731_boost_mode{
{22 * 8, 1 * 8},
5,
8, // Label has 8 chars
{
{"ON +12dB", 0}, // WM8731 Mic Boost ON, original+12dBs condition, easy to saturate ADC sat in high voice, relative G = +12 dB's respect ref level
{"ON +06dB", 1}, // WM8731 Mic Boost ON, original+6 dBs condition, easy to saturate ADC sat in high voice, relative G = +06 dB's respect ref level
@@ -298,11 +298,11 @@ class MicTXView : public View {
OptionsField field_rxbw{
{19 * 8, (23 * 8) + 2},
3,
7,
{
{" NFM1:8k5 ", 0},
{" NFM2:11k ", 1},
{" FM :16k ", 2},
{" 8k5 ", 0}, // Initial dynamic values when we start Mic App.
{" 11k ", 1},
{" 16k ", 2},
}};
NumberField field_squelch{
+9 -7
View File
@@ -27,6 +27,8 @@
#include "convert.hpp"
#include "file_reader.hpp"
#include "io_file.hpp"
#include "io_convert.hpp"
#include "string_format.hpp"
#include "ui_fileman.hpp"
#include "utility.hpp"
@@ -48,7 +50,6 @@ namespace fs = std::filesystem;
namespace ui {
// TODO: consolidate extesions into a shared header?
static const fs::path c16_ext = u".C16";
static const fs::path ppl_ext = u".PPL";
void PlaylistView::load_file(const fs::path& playlist_path) {
@@ -258,7 +259,7 @@ void PlaylistView::send_current_track() {
chThdSleepMilliseconds(current()->ms_delay);
// Open the sample file to send.
auto reader = std::make_unique<FileReader>();
auto reader = std::make_unique<FileConvertReader>();
auto error = reader->open(current()->path);
if (error) {
show_file_error(current()->path, "Can't open file to send.");
@@ -323,9 +324,10 @@ void PlaylistView::update_ui() {
chDbgAssert(!at_end(), "update_ui #1", "current_index_ invalid");
text_filename.set(current()->path.filename().string());
text_sample_rate.set(unit_auto_scale(current()->metadata.sample_rate, 3, 0) + "Hz");
text_sample_rate.set(unit_auto_scale(current()->metadata.sample_rate, 3, (current()->metadata.sample_rate > 1000000) ? 2 : 0) + "Hz");
auto duration = ms_duration(current()->file_size, current()->metadata.sample_rate, 4);
uint8_t sample_size = capture_file_sample_size(current()->path);
auto duration = ms_duration(current()->file_size, current()->metadata.sample_rate, sample_size);
text_duration.set(to_string_time_ms(duration));
field_frequency.set_value(current()->metadata.center_frequency);
@@ -336,7 +338,7 @@ void PlaylistView::update_ui() {
progressbar_track.set_max(playlist_db_.size() - 1);
progressbar_track.set_value(current_index_);
progressbar_transmit.set_max(current()->file_size);
progressbar_transmit.set_max(current()->file_size * sizeof(complex16_t) / sample_size);
}
button_play.set_bitmap(is_active() ? &bitmap_stop : &bitmap_play);
@@ -406,7 +408,7 @@ PlaylistView::PlaylistView(
button_add.on_select = [this, &nav]() {
if (is_active())
return;
auto open_view = nav_.push<FileLoadView>(".C16");
auto open_view = nav_.push<FileLoadView>(".C*");
open_view->push_dir(u"CAPTURES");
open_view->on_changed = [this](fs::path path) {
add_entry(std::move(path));
@@ -459,7 +461,7 @@ PlaylistView::PlaylistView(
auto ext = path.extension();
if (path_iequal(ext, ppl_ext))
on_file_changed(path);
else if (path_iequal(ext, c16_ext))
else if (is_cxx_capture_file(path))
add_entry(fs::path{path});
}
+124 -242
View File
@@ -23,13 +23,17 @@
#include "ui_recon.hpp"
#include "ui_fileman.hpp"
#include "ui_freqman.hpp"
#include "capture_app.hpp"
#include "convert.hpp"
#include "file.hpp"
#include "file_reader.hpp"
#include "tone_key.hpp"
using namespace portapack;
using namespace tonekey;
using portapack::memory::map::backup_ram;
namespace fs = std::filesystem;
namespace ui {
@@ -124,152 +128,81 @@ void ReconView::colorize_waits() {
}
}
bool ReconView::recon_save_freq(const std::string& freq_file_path, size_t freq_index, bool warn_if_exists) {
File recon_file;
bool ReconView::recon_save_freq(const fs::path& path, size_t freq_index, bool warn_if_exists) {
if (frequency_list.size() == 0 || !current_is_valid())
return false;
FreqmanDB freq_db;
if (!freq_db.open(path, /*create*/ true))
return false;
freqman_entry entry = *frequency_list[freq_index]; // Makes a copy.
entry.frequency_a = freq;
entry.frequency_b = 0;
entry.modulation = last_entry.modulation;
entry.bandwidth = last_entry.bandwidth;
entry.type = freqman_type::Single;
// TODO: Use FreqmanDB
auto frequency_to_add = to_freqman_string(entry);
auto result = recon_file.open(freq_file_path); // First recon if freq is already in txt
if (!result.is_valid()) {
char one_char[1]{}; // Read it char by char
std::string line{}; // and put read line in here
bool found{false};
for (size_t pointer = 0; pointer < recon_file.size(); pointer++) {
recon_file.seek(pointer);
recon_file.read(one_char, 1);
if ((int)one_char[0] > 31) { // ascii space upwards
line += one_char[0]; // add it to the textline
} else if (one_char[0] == '\n') { // New Line
if (line.compare(0, frequency_to_add.size(), frequency_to_add) == 0) {
found = true;
break;
}
line.clear(); // Ready for next textline
}
}
if (!found) {
result = recon_file.append(freq_file_path); // Second: append if it is not there
if (!result.is_valid()) {
recon_file.write_line(frequency_to_add);
}
}
if (found && warn_if_exists) {
nav_.display_modal("Error", "Frequency already exists");
}
} else {
result = recon_file.create(freq_file_path); // First freq if no output file
if (!result.is_valid()) {
recon_file.write_line(frequency_to_add);
}
// For ranges, save the current frequency instead.
if (entry.type == freqman_type::Range) {
entry.frequency_a = freq;
entry.frequency_b = 0;
entry.type = freqman_type::Single;
entry.modulation = last_entry.modulation;
entry.bandwidth = last_entry.bandwidth;
entry.step = freqman_invalid_index;
}
auto it = freq_db.find_entry(entry);
auto found = (it != freq_db.end());
if (found && warn_if_exists)
nav_.display_modal("Error", "Frequency already exists");
if (!found)
freq_db.append_entry(entry);
return true;
}
bool ReconView::recon_load_config_from_sd() {
File settings_file{};
size_t length{0};
size_t file_position{0};
char* pos{NULL};
char* line_start{NULL};
char* line_end{NULL};
char file_data[257]{};
uint32_t it{0};
uint32_t nb_params{RECON_SETTINGS_NB_PARAMS};
std::string params[RECON_SETTINGS_NB_PARAMS]{};
make_new_directory(u"SETTINGS");
auto result = settings_file.open(RECON_CFG_FILE);
if (!result.is_valid()) {
while (it < nb_params) {
// Read a 256 bytes block from file
settings_file.seek(file_position);
memset(file_data, 0, 257);
auto read_size = settings_file.read(file_data, 256);
if (read_size.is_error())
break;
file_position += 256;
// Reset line_start to beginning of buffer
line_start = file_data;
pos = line_start;
while ((line_end = strstr(line_start, "\x0A"))) {
length = line_end - line_start - 1;
params[it] = std::string(pos, length);
it++;
line_start = line_end + 1;
pos = line_start;
if (line_start - file_data >= 256)
break;
if (it >= nb_params)
break;
}
if (read_size.value() != 256)
break; // End of file
// Restart at beginning of last incomplete line
file_position -= (file_data + 256 - line_start);
}
}
if (it < nb_params) {
/* bad number of params, signal defaults */
input_file = "RECON";
output_file = "RECON_RESULTS";
recon_lock_duration = RECON_MIN_LOCK_DURATION;
recon_lock_nb_match = RECON_DEF_NB_MATCH;
squelch = -14;
recon_match_mode = RECON_MATCH_CONTINUOUS;
wait = RECON_DEF_WAIT_DURATION;
File settings_file;
auto error = settings_file.open(RECON_CFG_FILE);
if (error)
return false;
auto complete = false;
auto line_nb = 0;
auto reader = FileLineReader(settings_file);
for (const auto& line : reader) {
switch (line_nb) {
case 0:
input_file = trim(line);
break;
case 1:
output_file = trim(line);
break;
case 2:
parse_int(line, recon_lock_duration);
break;
case 3:
parse_int(line, recon_lock_nb_match);
break;
case 4:
parse_int(line, squelch);
break;
case 5:
parse_int(line, recon_match_mode);
break;
case 6:
parse_int(line, wait);
complete = true; // NB: Last entry.
break;
}
if (complete) break;
line_nb++;
}
if (it > 0)
input_file = params[0];
else
input_file = "RECON";
if (it > 1)
output_file = params[1];
else
output_file = "RECON_RESULTS";
if (it > 2)
recon_lock_duration = strtoll(params[2].c_str(), nullptr, 10);
else
recon_lock_duration = RECON_MIN_LOCK_DURATION;
if (it > 3)
recon_lock_nb_match = strtoll(params[3].c_str(), nullptr, 10);
else
recon_lock_nb_match = RECON_DEF_NB_MATCH;
if (it > 4)
squelch = strtoll(params[4].c_str(), nullptr, 10);
else
squelch = -14;
if (it > 5)
recon_match_mode = strtoll(params[5].c_str(), nullptr, 10);
else
recon_match_mode = RECON_MATCH_CONTINUOUS;
if (it > 6)
wait = strtoll(params[6].c_str(), nullptr, 10);
else
wait = RECON_DEF_WAIT_DURATION;
return true;
return complete;
}
bool ReconView::recon_save_config_to_sd() {
@@ -486,6 +419,7 @@ ReconView::ReconView(NavigationView& nav)
if (frequency_list.size() > 0) {
auto new_view = nav_.push<FrequencyKeypadView>(current_index);
new_view->on_changed = [this, &button](rf::Frequency f) {
// NB: This is using the freq keypad to select an index.
f = f / OneMHz;
if (f >= 1 && f <= frequency_list.size()) {
index_stepper = f - 1 - current_index;
@@ -496,17 +430,18 @@ ReconView::ReconView(NavigationView& nav)
};
button_manual_start.on_change = [this]() {
frequency_range.min = frequency_range.min + button_manual_start.get_encoder_delta() * freqman_entry_get_step_value(def_step);
auto step_val = freqman_entry_get_step_value(def_step);
frequency_range.min = frequency_range.min + button_manual_start.get_encoder_delta() * step_val;
if (frequency_range.min < 0) {
frequency_range.min = 0;
}
if (frequency_range.min > (MAX_UFREQ - freqman_entry_get_step_value(def_step))) {
frequency_range.min = MAX_UFREQ - freqman_entry_get_step_value(def_step);
if (frequency_range.min > (MAX_UFREQ - step_val)) {
frequency_range.min = MAX_UFREQ - step_val;
}
if (frequency_range.min > (frequency_range.max - freqman_entry_get_step_value(def_step))) {
frequency_range.max = frequency_range.min + freqman_entry_get_step_value(def_step);
if (frequency_range.min > (frequency_range.max - step_val)) {
frequency_range.max = frequency_range.min + step_val;
if (frequency_range.max > MAX_UFREQ) {
frequency_range.min = MAX_UFREQ - freqman_entry_get_step_value(def_step);
frequency_range.min = MAX_UFREQ - step_val;
frequency_range.max = MAX_UFREQ;
}
}
@@ -516,18 +451,19 @@ ReconView::ReconView(NavigationView& nav)
};
button_manual_end.on_change = [this]() {
frequency_range.max = frequency_range.max + button_manual_end.get_encoder_delta() * freqman_entry_get_step_value(def_step);
if (frequency_range.max < (freqman_entry_get_step_value(def_step) + 1)) {
frequency_range.max = (freqman_entry_get_step_value(def_step) + 1);
auto step_val = freqman_entry_get_step_value(def_step);
frequency_range.max = frequency_range.max + button_manual_end.get_encoder_delta() * step_val;
if (frequency_range.max < (step_val + 1)) {
frequency_range.max = (step_val + 1);
}
if (frequency_range.max > MAX_UFREQ) {
frequency_range.max = MAX_UFREQ;
}
if (frequency_range.max < (frequency_range.min + freqman_entry_get_step_value(def_step))) {
frequency_range.min = frequency_range.max - freqman_entry_get_step_value(def_step);
if (frequency_range.max < (freqman_entry_get_step_value(def_step) + 1)) {
if (frequency_range.max < (frequency_range.min + step_val)) {
frequency_range.min = frequency_range.max - step_val;
if (frequency_range.max < (step_val + 1)) {
frequency_range.min = 1;
frequency_range.max = (freqman_entry_get_step_value(def_step) + 1);
frequency_range.max = (step_val + 1);
}
}
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
@@ -607,87 +543,9 @@ ReconView::ReconView(NavigationView& nav)
};
button_remove.on_select = [this](ButtonWithEncoder&) {
// TODO: Use FreqmanDB
if (frequency_list.size() > 0) {
if (!manual_mode) {
// scanner or recon (!scanner) mode
// in both we delete index from live view, but only remove in output file in scanner_mode
if (current_index >= (int32_t)frequency_list.size()) {
current_index = frequency_list.size() - 1;
}
frequency_list.erase(frequency_list.begin() + current_index);
if (current_index >= (int32_t)frequency_list.size()) {
current_index = frequency_list.size() - 1;
}
if (frequency_list.size() > 0) {
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
update_description();
}
// also remove from output file if in scanner mode
if (scanner_mode) {
File freqman_file{};
delete_file(freq_file_path);
auto result = freqman_file.create(freq_file_path);
if (!result.is_valid()) {
for (size_t n = 0; n < frequency_list.size(); n++) {
auto line = to_freqman_string(*frequency_list[n]);
freqman_file.write_line(line);
}
}
}
} else if (manual_mode) // only remove from output
{
File recon_file{};
File tmp_recon_file{};
std::string tmp_freq_file_path{freq_file_path + ".TMP"};
handle_remove_current_item();
freqman_entry entry = current_entry();
entry.frequency_a = freq;
entry.frequency_b = 0;
entry.modulation = last_entry.modulation;
entry.bandwidth = last_entry.bandwidth;
entry.type = freqman_type::Single;
auto frequency_to_add = to_freqman_string(entry);
delete_file(tmp_freq_file_path);
auto result = tmp_recon_file.create(tmp_freq_file_path); // First recon if freq is already in txt
if (!result.is_valid()) {
bool found = false;
result = recon_file.open(freq_file_path); // First recon if freq is already in txt
if (!result.is_valid()) {
char one_char[1]{}; // Read it char by char
std::string line{}; // and put read line in here
for (size_t pointer = 0; pointer < recon_file.size(); pointer++) {
recon_file.seek(pointer);
recon_file.read(one_char, 1);
if ((int)one_char[0] > 31) { // ascii space upwards
line += one_char[0]; // add it to the textline
} else if (one_char[0] == '\n') { // new Line
if (line.compare(0, frequency_to_add.size(), frequency_to_add) == 0) {
found = true;
} else {
tmp_recon_file.write_line(frequency_to_add);
}
line.clear(); // ready for next textline
}
}
if (found) {
delete_file(freq_file_path);
rename_file(tmp_freq_file_path, freq_file_path);
} else {
delete_file(tmp_freq_file_path);
}
}
}
}
receiver_model.set_target_frequency(current_entry().frequency_a); // retune
}
if (frequency_list.size() == 0) {
text_cycle.set_text(" ");
desc_cycle.set("no entries in list"); // Show new description
delete_file(freq_file_path);
}
// TODO: refresh UI.
timer = 0;
freq_lock = 0;
};
@@ -766,9 +624,8 @@ ReconView::ReconView(NavigationView& nav)
};
button_restart.on_select = [this](Button&) {
frequency_file_load(true);
if (frequency_list.size() > 0) {
def_step = step_mode.selected_index(); // Use def_step from manual selector
frequency_file_load(true);
if (fwd) {
button_dir.set_text("FW>");
} else {
@@ -921,8 +778,7 @@ ReconView::ReconView(NavigationView& nav)
recon_redraw();
}
void ReconView::frequency_file_load(bool stop_all_before) {
(void)(stop_all_before);
void ReconView::frequency_file_load(bool) {
if (field_mode.selected_index_value() != SPEC_MODULATION)
audio::output::stop();
@@ -940,6 +796,7 @@ void ReconView::frequency_file_load(bool stop_all_before) {
desc_cycle.set_style(&Styles::blue);
button_scanner_mode.set_text("RECON");
}
freqman_load_options options{
.load_freqs = load_freqs,
.load_ranges = load_ranges,
@@ -1033,7 +890,6 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
if (recon) {
if (!timer) {
status = 0;
continuous_lock = false;
freq_lock = 0;
timer = local_recon_lock_duration;
big_display.set_style(&Styles::white);
@@ -1050,22 +906,19 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
}
if (db > squelch) // MATCHING LEVEL
{
continuous_lock = true;
freq_lock++;
timer += time_interval; // give some more time for next lock
} else {
// continuous, direct cut it if not consecutive match after 1 first match
if (recon_match_mode == RECON_MATCH_CONTINUOUS) {
if (freq_lock > 0) {
timer = 0;
continuous_lock = false;
}
freq_lock = 0;
timer = 0;
}
}
}
if (freq_lock >= recon_lock_nb_match) // LOCKED
{
if (status != 2) {
continuous_lock = false;
status = 2;
// FREQ IS STRONG: GREEN and recon will pause when on_statistics_update()
if ((!scanner_mode) && autosave && frequency_list.size() > 0) {
@@ -1106,10 +959,9 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
last_timer = timer;
text_timer.set("TIMER: " + to_string_dec_int(timer));
}
if (timer) {
if (!continuous_lock || recon_match_mode == RECON_MATCH_SPARSE) {
timer -= time_interval;
}
if (timer != 0) {
timer -= time_interval;
if (timer < 0) {
timer = 0;
}
@@ -1269,7 +1121,6 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
void ReconView::recon_pause() {
timer = 0;
freq_lock = 0;
continuous_lock = false;
recon = false;
if (field_mode.selected_index_value() != SPEC_MODULATION)
@@ -1282,7 +1133,6 @@ void ReconView::recon_pause() {
void ReconView::recon_resume() {
timer = 0;
freq_lock = 0;
continuous_lock = false;
recon = true;
if (field_mode.selected_index_value() != SPEC_MODULATION)
@@ -1401,8 +1251,9 @@ size_t ReconView::change_mode(freqman_index_t new_mod) {
}
if (new_mod != SPEC_MODULATION) {
button_audio_app.set_text("AUDIO");
// TODO: Oversampling.
record_view->set_sampling_rate(recording_sampling_rate);
// reset receiver model to fix bug when going from SPEC to audio, the sound is distorded
// reset receiver model to fix bug when going from SPEC to audio, the sound is distorted
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
} else {
@@ -1430,4 +1281,35 @@ void ReconView::handle_coded_squelch(const uint32_t value) {
text_ctcss.set(" ");
}
void ReconView::handle_remove_current_item() {
if (frequency_list.empty() || !current_is_valid())
return;
auto entry = current_entry(); // Copy the current entry.
// In Scanner or Recon modes, remove from the in-memory list.
if (mode() != recon_mode::Manual) {
if (current_is_valid()) {
frequency_list.erase(frequency_list.begin() + current_index);
}
if (frequency_list.size() > 0) {
current_index = clip<int32_t>(current_index, 0u, frequency_list.size() - 1);
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
update_description();
} else {
current_index = 0;
}
}
// In Scanner or Manual mode, remove the entry from the output file.
if (mode() != recon_mode::Recon) {
FreqmanDB freq_db;
if (!freq_db.open(freq_file_path))
return;
freq_db.delete_entry(entry);
}
}
} /* namespace ui */
+17 -3
View File
@@ -46,6 +46,12 @@ namespace ui {
#define RECON_CFG_FILE "SETTINGS/recon.cfg"
enum class recon_mode : uint8_t {
Recon,
Scanner,
Manual
};
class ReconView : public View {
public:
ReconView(NavigationView& nav);
@@ -82,9 +88,10 @@ class ReconView : public View {
void recon_redraw();
void handle_retune();
void handle_coded_squelch(const uint32_t value);
void handle_remove_current_item();
bool recon_load_config_from_sd();
bool recon_save_config_to_sd();
bool recon_save_freq(const std::string& freq_file_path, size_t index, bool warn_if_exists);
bool recon_save_freq(const std::filesystem::path& path, size_t index, bool warn_if_exists);
// placeholder for possible void recon_start_recording();
void recon_stop_recording();
@@ -92,8 +99,15 @@ class ReconView : public View {
bool current_is_valid();
freqman_entry& current_entry();
// TODO: consolidate mode bools and use recon_mode.
recon_mode mode() const {
if (manual_mode) return recon_mode::Manual;
if (scanner_mode) return recon_mode::Scanner;
return recon_mode::Recon;
}
jammer::jammer_range_t frequency_range{false, 0, MAX_UFREQ}; // perfect for manual recon task too...
int32_t squelch{0};
int32_t squelch{RECON_DEF_SQUELCH};
int32_t db{0};
int32_t timer{0};
int32_t wait{RECON_DEF_WAIT_DURATION}; // in msec. if > 0 wait duration after a lock, if < 0 duration is set to 'wait' unless there is no more activity
@@ -117,7 +131,7 @@ class ReconView : public View {
bool user_pause{false};
bool auto_record_locked{false};
bool is_recording{false};
uint32_t recon_lock_nb_match{3};
uint32_t recon_lock_nb_match{RECON_DEF_NB_MATCH};
uint32_t recon_lock_duration{RECON_MIN_LOCK_DURATION};
uint32_t recon_match_mode{RECON_MATCH_CONTINUOUS};
bool scanner_mode{false};
@@ -69,10 +69,9 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
button_save_freqs.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".TXT");
open_view->push_dir(freqman_dir);
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
std::string dir_filter = "FREQMAN/";
std::string str_file_path = new_file_path.string();
if (str_file_path.find(dir_filter) != string::npos) { // assert file from the FREQMAN folder
if (new_file_path.native().find(freqman_dir.native()) == 0) {
_output_file = new_file_path.stem().string();
button_output_file.set_text(_output_file);
} else {
@@ -84,7 +83,7 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
button_output_file.on_select = [this, &nav](Button&) {
text_prompt(nav, _output_file, 28,
[this](std::string& buffer) {
_output_file = buffer;
_output_file = std::move(buffer);
button_output_file.set_text(_output_file);
});
};
@@ -50,6 +50,8 @@
// maximum lock duration
#define RECON_MAX_LOCK_DURATION 9900
#define RECON_DEF_SQUELCH -14
// default number of match to have a lock
#define RECON_DEF_NB_MATCH 3
#define RECON_MIN_LOCK_DURATION 100 // have to be >= and a multiple of STATS_UPDATE_INTERVAL
+161 -206
View File
@@ -21,19 +21,25 @@
*/
#include "ui_scanner.hpp"
#include "optional.hpp"
#include "ui_fileman.hpp"
#include "ui_freqman.hpp"
using namespace portapack;
namespace fs = std::filesystem;
namespace ui {
static const fs::path default_scan_file{u"FREQMAN/SCANNER.TXT"};
ScannerThread::ScannerThread(std::vector<rf::Frequency> frequency_list)
: frequency_list_{std::move(frequency_list)} {
_manual_search = false;
create_thread();
}
ScannerThread::ScannerThread(const jammer::jammer_range_t& frequency_range, size_t def_step_hz)
ScannerThread::ScannerThread(const scanner_range_t& frequency_range, size_t def_step_hz)
: frequency_range_(frequency_range), def_step_hz_(def_step_hz) {
_manual_search = true;
create_thread();
@@ -79,7 +85,7 @@ void ScannerThread::set_freq_del(const rf::Frequency v) {
}
// Force a one-time forward or reverse frequency index change; OK to do this without pausing scan thread
//(used when rotary encoder is turned)
// (used when rotary encoder is turned)
void ScannerThread::set_index_stepper(const int32_t v) {
_index_stepper = v;
}
@@ -227,17 +233,26 @@ void ScannerView::handle_retune(int64_t freq, uint32_t freq_idx) {
}
if (!manual_search) {
if (frequency_list.size() > 0) {
if (entries.size() > 0)
text_current_index.set(to_string_dec_uint(freq_idx + 1, 3));
}
if (freq_idx < description_list.size() && description_list[freq_idx].size() > 1)
desc_current_index.set(description_list[freq_idx]); // Show description from file
if (freq_idx < entries.size() && entries[freq_idx].description.size() > 1)
text_current_desc.set(entries[freq_idx].description); // Show description from file
else
desc_current_index.set(desc_freq_list_scan); // Show Scan file name (no description in file)
text_current_desc.set(loaded_filename()); // Show Scan file name (no description in file)
}
}
std::string ScannerView::loaded_filename() const {
auto filename = loaded_path.filename().string();
if (filename.length() > 23) { // Truncate and add ellipses if long file name
filename.resize(22);
filename = filename + "+";
}
return filename;
}
void ScannerView::focus() {
button_load.focus();
}
@@ -253,46 +268,46 @@ ScannerView::~ScannerView() {
void ScannerView::show_max_index() { // show total number of freqs to scan
text_current_index.set("---");
if (frequency_list.size() == FREQMAN_MAX_PER_FILE) {
if (entries.size() == FREQMAN_MAX_PER_FILE) {
text_max_index.set_style(&Styles::red);
text_max_index.set("/ " + to_string_dec_uint(FREQMAN_MAX_PER_FILE) + " (DB MAX!)");
} else {
text_max_index.set_style(&Styles::grey);
text_max_index.set("/ " + to_string_dec_uint(frequency_list.size()));
text_max_index.set("/ " + to_string_dec_uint(entries.size()));
}
}
ScannerView::ScannerView(
NavigationView& nav)
: nav_{nav}, loaded_file_name{"SCANNER"} {
add_children({&labels,
&field_lna,
&field_vga,
&field_rf_amp,
&field_volume,
&field_bw,
&field_squelch,
&field_browse_wait,
&field_lock_wait,
&button_load,
&button_clear,
&rssi,
&text_current_index,
&text_max_index,
&desc_current_index,
&big_display,
&button_manual_start,
&button_manual_end,
&field_mode,
&field_step,
&button_manual_search,
&button_pause,
&button_dir,
&button_audio_app,
&button_mic_app,
&button_add,
&button_remove
: nav_{nav} {
add_children({
&labels,
&field_lna,
&field_vga,
&field_rf_amp,
&field_volume,
&field_bw,
&field_squelch,
&field_browse_wait,
&field_lock_wait,
&button_load,
&button_clear,
&rssi,
&text_current_index,
&text_max_index,
&text_current_desc,
&big_display,
&button_manual_start,
&button_manual_end,
&field_mode,
&field_step,
&button_manual_search,
&button_pause,
&button_dir,
&button_audio_app,
&button_mic_app,
&button_add,
&button_remove,
});
// Populate option text for these fields
@@ -311,14 +326,14 @@ ScannerView::ScannerView(
frequency_range.max = stored_freq + 1000000;
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
// Button to load txt files from the FREQMAN folder
// Button to load a Freqman file.
button_load.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".TXT");
open_view->push_dir(freqman_dir);
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
if (new_file_path.native().find(freqman_dir.native()) == 0) {
scan_pause();
frequency_file_load(new_file_path.stem().string(), true);
frequency_file_load(new_file_path);
} else {
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
}
@@ -327,14 +342,13 @@ ScannerView::ScannerView(
// Button to clear in-memory frequency list
button_clear.on_select = [this, &nav](Button&) {
if (scan_thread && frequency_list.size()) {
if (scan_thread && entries.size()) {
scan_thread->stop(); // STOP SCANNER THREAD
frequency_list.clear();
description_list.clear();
entries.clear();
show_max_index(); // UPDATE new list size on screen
text_current_index.set("");
desc_current_index.set(desc_freq_list_scan);
text_current_desc.set(loaded_filename());
scan_thread->set_freq_lock(0); // Reset the scanner lock
// FUTURE: Consider switching to manual search mode automatically after clear (but would need to validate freq range)
@@ -403,16 +417,15 @@ ScannerView::ScannerView(
// Button to delete current frequency from scan Freq List
button_remove.on_select = [this](Button&) {
if (scan_thread && (frequency_list.size() > current_index)) {
if (scan_thread && (entries.size() > current_index)) {
scan_thread->set_scanning(false); // PAUSE Scanning if necessary
// Remove frequency from the Freq List in memory (it is not removed from the file)
scan_thread->set_freq_del(frequency_list[current_index]);
description_list.erase(description_list.begin() + current_index);
frequency_list.erase(frequency_list.begin() + current_index);
// Remove frequency from the Freq List in memory (it is not removed from the file).
scan_thread->set_freq_del(entries[current_index].freq);
entries.erase(entries.begin() + current_index);
show_max_index(); // UPDATE new list size on screen
desc_current_index.set(""); // Clean up description (cosmetic detail)
text_current_desc.set(""); // Clean up description (cosmetic detail)
scan_thread->set_freq_lock(0); // Reset the scanner lock
}
};
@@ -431,15 +444,7 @@ ScannerView::ScannerView(
manual_search = false; // Switch to List Scan mode
}
audio::output::stop();
if (scan_thread)
scan_thread->stop(); // STOP SCANNER THREAD
if (userpause) // If user-paused, resume
user_resume();
start_scan_thread(); // RESTART SCANNER THREAD in selected mode
restart_scan();
};
// Mode field was changed (AM/NFM/WFM)
@@ -466,17 +471,8 @@ ScannerView::ScannerView(
field_step.on_change = [this](size_t, OptionsField::value_t v) {
receiver_model.set_frequency_step(v);
if (manual_search && scan_thread) {
// Restart scan thread with new step value
scan_thread->stop(); // STOP SCANNER THREAD
// Resuming pause automatically
// FUTURE: Consider whether we should stay in Pause mode...
if (userpause) // If user-paused, resume
user_resume();
start_scan_thread(); // RESTART SCANNER THREAD in Manual Search mode
}
if (manual_search && scan_thread)
restart_scan();
};
// Button to toggle Forward/Reverse
@@ -490,68 +486,37 @@ ScannerView::ScannerView(
bigdisplay_update(BDC_GREY); // Back to grey color
};
// TODO: remove this parsing?
// Button to add current frequency (found during Search) to the Scan Frequency List
button_add.on_select = [this](Button&) {
File scanner_file;
const std::string freq_file_path = "FREQMAN/" + loaded_file_name + ".TXT";
auto result = scanner_file.open(freq_file_path); // First search if freq is already in txt
FreqmanDB db;
if (db.open(loaded_path, /*create*/ true)) {
freqman_entry entry{
.frequency_a = current_frequency,
.type = freqman_type::Single,
};
if (!result.is_valid()) {
const std::string frequency_to_add = "f=" + to_string_dec_uint(current_frequency / 1000) + to_string_dec_uint(current_frequency % 1000UL, 3, '0');
bool found = false;
constexpr size_t buffer_size = 1024;
char buffer[buffer_size];
for (size_t pointer = 0, freq_str_idx = 0; pointer < scanner_file.size(); pointer += buffer_size) {
size_t adjusted_buffer_size;
if (pointer + buffer_size >= scanner_file.size()) {
memset(buffer, 0, sizeof(buffer));
adjusted_buffer_size = scanner_file.size() - pointer;
} else {
adjusted_buffer_size = buffer_size;
}
scanner_file.seek(pointer);
scanner_file.read(buffer, adjusted_buffer_size);
for (size_t i = 0; i < adjusted_buffer_size; ++i) {
if (buffer[i] == frequency_to_add.data()[freq_str_idx]) {
++freq_str_idx;
if (freq_str_idx >= frequency_to_add.size()) {
found = true;
break;
}
} else {
freq_str_idx = 0;
}
}
if (found) {
break;
}
}
// Look for existing entry with same frequency.
auto it = db.find_entry([&entry](const auto& e) {
return e.frequency_a == entry.frequency_a;
});
auto found = (it != db.end());
if (found) {
nav_.display_modal("Error", "Frequency already exists");
bigdisplay_update(-1); // After showing an error
bigdisplay_update(-1); // Need to poke this control after displaying modal?
} else {
scanner_file.append(freq_file_path); // Second: append if it is not there
scanner_file.write_line(frequency_to_add);
db.append_entry(entry);
// Add to frequency_list in memory too, since we can now switch back from manual mode
// Note that we are allowing freqs to be added to file (code above) that exceed the max count we can load into memory
if (frequency_list.size() < FREQMAN_MAX_PER_FILE) {
frequency_list.push_back(current_frequency);
description_list.push_back("");
// Note that we are allowing freqs to be added to file (code above) that exceed the
// max count we can load into memory.
if (entries.size() < FREQMAN_MAX_PER_FILE) {
entries.push_back({current_frequency, ""});
show_max_index(); // Display updated frequency list size
}
}
} else {
nav_.display_modal("Error", "Cannot open " + loaded_file_name + ".TXT\nfor appending freq.");
bigdisplay_update(-1); // After showing an error
nav_.display_modal("Error", "Cannot open " + loaded_path.filename().string() + "\nfor appending freq.");
bigdisplay_update(-1); // Need to poke this control after displaying modal?
}
};
@@ -563,108 +528,78 @@ ScannerView::ScannerView(
field_lock_wait.set_value(2);
field_squelch.on_change = [this](int32_t v) { squelch = v; };
field_squelch.set_value(-10);
field_squelch.set_value(-30);
// LEARN FREQUENCIES
std::string scanner_txt = "SCANNER";
frequency_file_load(scanner_txt);
// LOAD FREQUENCIES
frequency_file_load(default_scan_file);
}
void ScannerView::frequency_file_load(std::string file_name, bool stop_all_before) {
bool found_range{false};
bool found_single{false};
void ScannerView::frequency_file_load(const fs::path& path) {
freqman_index_t def_mod_index{freqman_invalid_index};
freqman_index_t def_bw_index{freqman_invalid_index};
freqman_index_t def_step_index{freqman_invalid_index};
// stop everything running now if required
if (stop_all_before) {
scan_thread->stop();
frequency_list.clear(); // clear the existing frequency list (expected behavior)
description_list.clear();
FreqmanDB db;
if (!db.open(path)) {
text_current_desc.set("NO " + path.filename().string());
loaded_path = default_scan_file;
return;
}
if (load_freqman_file(file_name, database, {})) {
loaded_file_name = file_name;
for (auto& entry_ptr : database) {
if (frequency_list.size() >= FREQMAN_MAX_PER_FILE)
entries.clear();
loaded_path = path;
Optional<scanner_range_t> range;
for (auto entry : db) {
if (is_invalid(def_mod_index))
def_mod_index = entry.modulation;
if (is_invalid(def_bw_index))
def_bw_index = entry.bandwidth;
if (is_invalid(def_step_index))
def_step_index = entry.step;
switch (entry.type) {
case freqman_type::Single:
entries.push_back({entry.frequency_a, entry.description});
break;
case freqman_type::HamRadio:
entries.push_back({entry.frequency_a, "R: " + entry.description});
entries.push_back({entry.frequency_b, "T: " + entry.description});
break;
case freqman_type::Range:
// NB: Only the first range will be loaded.
if (!range)
range = {entry.frequency_a, entry.frequency_b};
break;
default:
break;
auto& entry = *entry_ptr;
// Get modulation & bw & step from file if specified
// Note these values could be different for each line in the file, but we only look at the first one
//
// Note that freqman requires a very specific string for these parameters,
// so check syntax in frequency file if specified value isn't being loaded
//
if (is_invalid(def_mod_index))
def_mod_index = entry.modulation;
if (is_invalid(def_bw_index))
def_bw_index = entry.bandwidth;
if (is_invalid(def_step_index))
def_step_index = entry.step;
// Get frequency
if (entry.type == freqman_type::Range) {
if (!found_range) {
// Set Start & End Search Range instead of populating the small in-memory frequency table
// NOTE: There may be multiple single frequencies in file, but only one search range is supported.
found_range = true;
frequency_range.min = entry.frequency_a;
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
frequency_range.max = entry.frequency_b;
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
}
} else if (entry.type == freqman_type::Single) {
found_single = true;
frequency_list.push_back(entry.frequency_a);
description_list.push_back(entry.description);
} else if (entry.type == freqman_type::HamRadio) {
// For HAM repeaters, add both receive & transmit frequencies to scan list and modify description
// (FUTURE fw versions might handle these differently)
found_single = true;
frequency_list.push_back(entry.frequency_a);
description_list.push_back("R:" + entry.description);
if ((entry.frequency_a != entry.frequency_b) &&
(frequency_list.size() < FREQMAN_MAX_PER_FILE)) {
frequency_list.push_back(entry.frequency_b);
description_list.push_back("T:" + entry.description);
}
}
}
} else {
loaded_file_name = "SCANNER"; // back to the default frequency file
desc_current_index.set(" NO " + file_name + ".TXT FILE ...");
}
desc_freq_list_scan = loaded_file_name + ".TXT";
if (desc_freq_list_scan.length() > 23) { // Truncate description and add ellipses if long file name
desc_freq_list_scan.resize(20);
desc_freq_list_scan = desc_freq_list_scan + "...";
if (entries.size() >= FREQMAN_MAX_PER_FILE)
break;
}
if (is_valid(def_mod_index) && def_mod_index != (freqman_index_t)field_mode.selected_index_value())
field_mode.set_by_value(def_mod_index); // Update mode (also triggers a change callback that disables & reenables RF background)
field_mode.set_by_value(def_mod_index);
if (is_valid(def_bw_index)) // Update BW if specified in file
if (is_valid(def_bw_index))
field_bw.set_selected_index(def_bw_index);
if (is_valid(def_step_index)) // Update step if specified in file
if (is_valid(def_step_index))
field_step.set_selected_index(def_step_index);
audio::output::stop();
// Found range, set it and update UI.
if (range) {
frequency_range = *range;
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
}
if (userpause) // If user-paused, resume
user_resume();
// Scan list if we found one, otherwise do manual range search
manual_search = !found_single;
start_scan_thread();
// Scan entries if any, otherwise do manual range search.
manual_search = entries.empty();
restart_scan();
}
void ScannerView::update_squelch_while_paused(int32_t max_db) {
@@ -753,7 +688,8 @@ void ScannerView::user_resume() {
userpause = false; // Resume scanning
}
void ScannerView::change_mode(freqman_index_t new_mod) { // Before this, do a scan_thread->stop(); After this do a start_scan_thread()
// Before this, do a scan_thread->stop(); After this do a start_scan_thread()
void ScannerView::change_mode(freqman_index_t new_mod) {
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
options_t bw;
@@ -790,22 +726,41 @@ void ScannerView::change_mode(freqman_index_t new_mod) { // Before this, do a s
void ScannerView::start_scan_thread() {
receiver_model.enable();
// Disable squelch on the model because RSSI handler is where the
// actual squelching is applied for this app.
receiver_model.set_squelch_level(0);
show_max_index();
// Start Scanner Thread
// FUTURE: Consider passing additional control flags (fwd,userpause,etc) to scanner thread at start (perhaps in a data structure)
if (manual_search) {
button_manual_search.set_text("SCAN"); // Update meaning of Manual Scan button
desc_current_index.set(desc_freq_range_search);
text_current_desc.set("SEARCHING...");
scan_thread = std::make_unique<ScannerThread>(frequency_range, field_step.selected_index_value());
} else {
button_manual_search.set_text("SRCH"); // Update meaning of Manual Scan button
desc_current_index.set(desc_freq_list_scan);
scan_thread = std::make_unique<ScannerThread>(frequency_list);
text_current_desc.set(loaded_filename());
// TODO: just pass ref to the thread?
std::vector<rf::Frequency> frequency_list;
frequency_list.reserve(entries.size());
for (const auto& entry : entries)
frequency_list.push_back(entry.freq);
scan_thread = std::make_unique<ScannerThread>(std::move(frequency_list));
}
scan_thread->set_scanning_direction(fwd);
}
void ScannerView::restart_scan() {
audio::output::stop();
if (scan_thread) // STOP SCANNER THREAD
scan_thread->stop();
if (userpause) // If user-paused, resume
user_resume();
start_scan_thread(); // RESTART SCANNER THREAD in selected mode
}
} /* namespace ui */
+41 -24
View File
@@ -20,19 +20,20 @@
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "receiver_model.hpp"
#include "audio.hpp"
#include "analog_audio_app.hpp"
#include "baseband_api.hpp"
#include "file.hpp"
#include "freqman.hpp"
#include "freqman_db.hpp"
#include "portapack_persistent_memory.hpp"
#include "radio_state.hpp"
#include "receiver_model.hpp"
#include "string_format.hpp"
#include "ui.hpp"
#include "ui_mictx.hpp"
#include "ui_receiver.hpp"
#include "ui_styles.hpp"
#include "freqman.hpp"
#include "analog_audio_app.hpp"
#include "audio.hpp"
#include "ui_mictx.hpp"
#include "portapack_persistent_memory.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "file.hpp"
#define SCANNER_SLEEP_MS 50 // ms that Scanner Thread sleeps per loop
#define STATISTICS_UPDATES_PER_SEC 10
@@ -40,10 +41,28 @@
namespace ui {
// TODO: There is too much duplicated data in these classes.
// ScannerThread should just use more from the View.
// Or perhaps ScannerThread should just be in the View.
// TODO: Too many functions mix work and UI update.
// Consolidate UI fixup to a single function.
// TODO: Just use freqman_entry.
struct scanner_entry_t {
rf::Frequency freq;
std::string description;
};
struct scanner_range_t {
int64_t min;
int64_t max;
};
class ScannerThread {
public:
ScannerThread(std::vector<rf::Frequency> frequency_list);
ScannerThread(const jammer::jammer_range_t& frequency_range, size_t def_step_hz);
ScannerThread(const scanner_range_t& frequency_range, size_t def_step_hz);
~ScannerThread();
void set_scanning(const bool v);
@@ -65,7 +84,7 @@ class ScannerThread {
private:
std::vector<rf::Frequency> frequency_list_{};
jammer::jammer_range_t frequency_range_{false, 0, 0};
scanner_range_t frequency_range_{0, 0};
size_t def_step_hz_{0};
Thread* thread{nullptr};
@@ -89,10 +108,6 @@ class ScannerView : public View {
void focus() override;
std::string title() const override { return "Scanner"; };
std::vector<rf::Frequency> frequency_list{};
std::vector<std::string> description_list{};
// void set_parent_rect(const Rect new_parent_rect) override;
private:
app_settings::SettingsManager settings_{
@@ -102,18 +117,21 @@ class ScannerView : public View {
RxRadioState radio_state_{};
void start_scan_thread();
void restart_scan();
void change_mode(freqman_index_t mod_type);
void show_max_index();
void scan_pause();
void scan_resume();
void user_resume();
void frequency_file_load(std::string file_name, bool stop_all_before = false);
void frequency_file_load(const std::filesystem::path& path);
void bigdisplay_update(int32_t);
void update_squelch_while_paused(int32_t max_db);
void on_statistics_update(const ChannelStatistics& statistics);
void handle_retune(int64_t freq, uint32_t freq_idx);
jammer::jammer_range_t frequency_range{false, 0, 0}; // perfect for manual scan task too...
std::string loaded_filename() const;
scanner_range_t frequency_range{0, 0};
int32_t squelch{0};
uint32_t browse_timer{0};
uint32_t lock_timer{0};
@@ -122,10 +140,12 @@ class ScannerView : public View {
rf::Frequency bigdisplay_current_frequency{0};
uint32_t browse_wait{0};
uint32_t lock_wait{0};
freqman_db database{};
std::string loaded_file_name;
std::filesystem::path loaded_path{};
std::vector<scanner_entry_t> entries{};
uint32_t current_index{0};
rf::Frequency current_frequency{0};
bool userpause{false};
bool manual_search{false};
bool fwd{true}; // to preserve direction setting even if scan_thread restarted
@@ -137,9 +157,6 @@ class ScannerView : public View {
BDC_RED
};
std::string desc_freq_range_search = "SEARCHING...";
std::string desc_freq_list_scan = "";
Labels labels{
{{0 * 8, 0 * 16}, "LNA: VGA: AMP: VOL:", Color::light_grey()},
{{0 * 8, 1 * 16}, "BW: SQ: Wsa: Wsl:", Color::light_grey()},
@@ -204,7 +221,7 @@ class ScannerView : public View {
{4 * 8, 3 * 16, 18 * 8, 16},
};
Text desc_current_index{
Text text_current_desc{
{0, 4 * 16, 240 - 6 * 8, 16},
};
+1 -1
View File
@@ -482,7 +482,7 @@ class SetEncoderDialView : public View {
{20 * 8, 3 * 16},
6,
{{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_MEDIUM},
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_NORMAL},
{"HIGH", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGH}}};
Button button_save{
@@ -20,13 +20,14 @@
*/
#include "ui_spectrum_painter.hpp"
#include "bmp.hpp"
#include "baseband_api.hpp"
#include "ui_fileman.hpp"
#include "io_file.hpp"
#include "file.hpp"
#include "io_file.hpp"
#include "portapack_persistent_memory.hpp"
#include "ui_fileman.hpp"
#include "ui_freqman.hpp"
using namespace portapack;
+40 -14
View File
@@ -72,6 +72,7 @@ void TextViewer::paint(Painter& painter) {
paint_state_.first_line = first_line;
paint_state_.first_col = first_col;
paint_state_.redraw_text = true;
paint_state_.line = UINT32_MAX; // forget old cursor position when overwritten
}
if (paint_state_.redraw_text) {
@@ -221,6 +222,10 @@ void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) {
r.top() + (int)i * char_height,
clear_width * char_width, char_height},
style().background);
// if cursor line got overwritten, disable XOR of old cursor when displaying new cursor
if (i == paint_state_.line)
paint_state_.line = UINT32_MAX;
}
}
@@ -228,21 +233,32 @@ void TextViewer::paint_cursor(Painter& painter) {
if (!has_focus())
return;
auto draw_cursor = [this, &painter](uint32_t line, uint16_t col, Color c) {
auto r = screen_rect();
line = line - paint_state_.first_line;
col = col - paint_state_.first_col;
auto xor_cursor = [this, &painter](int32_t line, uint16_t col) {
int cursor_width = char_width + 1;
int x = (col - paint_state_.first_col) * char_width - 1;
if (x < 0) { // cursor is one pixel narrower when in left column
cursor_width--;
x = 0;
}
int y = screen_rect().top() + (line - paint_state_.first_line) * char_height;
painter.draw_rectangle(
{(int)col * char_width - 1,
r.top() + (int)line * char_height,
char_width + 1, char_height},
c);
// Converting one row at a time to reduce buffer size
auto pbuf8 = cursor_.pixel_buffer8;
auto pbuf = cursor_.pixel_buffer;
for (auto col = 0; col < char_height; col++) {
// Annoyingly, read_pixels uses a 24-bit pixel format vs draw_pixels which uses 16-bit
portapack::display.read_pixels({x, y + col, cursor_width, 1}, pbuf8, cursor_width);
for (auto i = 0; i < cursor_width; i++)
pbuf[i] = Color(pbuf8[i].r, pbuf8[i].g, pbuf8[i].b).v ^ 0xFFFF;
portapack::display.draw_pixels({x, y + col, cursor_width, 1}, pbuf, cursor_width);
}
};
// Clear old cursor. CONSIDER: XOR cursor?
draw_cursor(paint_state_.line, paint_state_.col, style().background);
draw_cursor(cursor_.line, cursor_.col, style().foreground);
if (paint_state_.line != UINT32_MAX) // only XOR old cursor if it still appears on the screen
xor_cursor(paint_state_.line, paint_state_.col);
xor_cursor(cursor_.line, cursor_.col);
paint_state_.line = cursor_.line;
paint_state_.col = cursor_.col;
}
@@ -325,7 +341,7 @@ static void show_save_prompt(
std::function<void()> on_save,
std::function<void()> continuation) {
nav.display_modal(
"Save?", "Save changes?", YESNO,
"Save?", " Save changes?", YESNO,
[on_save](bool choice) {
if (choice && on_save)
on_save();
@@ -458,7 +474,13 @@ void TextEditorView::open_file(const fs::path& path) {
path_ = {};
file_dirty_ = false;
has_temp_file_ = false;
auto result = FileWrapper::open(path);
auto result = FileWrapper::open(
path, false, [](uint32_t value, uint32_t total) {
Painter p;
auto percent = (value * 100) / total;
auto width = (percent * screen_width) / 100;
p.draw_hline({0, 16}, width, Color::yellow());
});
if (!result) {
nav_.display_modal("Read Error", "Cannot open file:\n" + result.error().what());
@@ -566,6 +588,10 @@ void TextEditorView::prepare_for_write() {
if (has_temp_file_)
return;
// TODO: This would be nice to have but it causes a stack overflow in an ISR?
// Painter p;
// p.draw_string({2, 48}, Styles::yellow, "Creating temporary file...");
// Copy to temp file on write.
has_temp_file_ = true;
delete_temp_file(path_);
@@ -117,6 +117,10 @@ class TextViewer : public Widget {
uint32_t line{};
uint16_t col{};
ScrollDirection dir{ScrollDirection::Vertical};
// Pixel buffer used for cursor XOR'ing - Max cursor width = Max char width + 1
ColorRGB888 pixel_buffer8[ui::char_width + 1]{};
Color pixel_buffer[ui::char_width + 1]{};
} cursor_{};
};
+40 -4
View File
@@ -31,6 +31,16 @@
#include "core_control.hpp"
/* Set true to enable additional checks to ensure
* M4 and M0 are synchronized before passing messages. */
static constexpr bool enforce_core_sync = true;
/* Set true to enable check for baseband messages getting stuck.
* This implies the baseband thread is not dequeuing and has probably stalled.
* NB: This check adds a small amout of overhead to the message sending code
* and may impact application perf if it is sending a lot of messages. */
static constexpr bool check_for_message_hang = true;
using namespace portapack;
namespace baseband {
@@ -40,8 +50,18 @@ static void send_message(const Message* const message) {
// another message is present before setting new message.
shared_memory.baseband_message = message;
creg::m0apptxevent::assert_event();
while (shared_memory.baseband_message)
;
if constexpr (check_for_message_hang) {
auto count = UINT32_MAX;
while (shared_memory.baseband_message && --count)
/* spin */;
if (count == 0)
chDbgPanic("Baseband Send Fail");
} else {
while (shared_memory.baseband_message)
/* spin */;
}
}
void AMConfig::apply() const {
@@ -276,17 +296,28 @@ void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bo
static bool baseband_image_running = false;
void run_image(const portapack::spi_flash::image_tag_t image_tag) {
void run_image(const spi_flash::image_tag_t image_tag) {
if (baseband_image_running) {
chDbgPanic("BBRunning");
}
creg::m4txevent::clear();
shared_memory.clear_baseband_ready();
m4_init(image_tag, portapack::memory::map::m4_code, false);
m4_init(image_tag, memory::map::m4_code, false);
baseband_image_running = true;
creg::m4txevent::enable();
if constexpr (enforce_core_sync) {
// Wait up to 3 seconds for baseband to start handling events.
auto count = 3'000u;
while (!shared_memory.baseband_ready && --count)
chThdSleepMilliseconds(1);
if (count == 0)
chDbgPanic("Baseband Sync Fail");
}
}
void shutdown() {
@@ -321,6 +352,11 @@ void set_sample_rate(const uint32_t sample_rate) {
send_message(&message);
}
void set_oversample_rate(OversampleRate oversample_rate) {
OversampleRateConfigMessage message{oversample_rate};
send_message(&message);
}
void capture_start(CaptureConfig* const config) {
CaptureConfigMessage message{config};
send_message(&message);
+1
View File
@@ -95,6 +95,7 @@ void spectrum_streaming_start();
void spectrum_streaming_stop();
void set_sample_rate(const uint32_t sample_rate);
void set_oversample_rate(OversampleRate oversample_rate);
void capture_start(CaptureConfig* const config);
void capture_stop();
void replay_start(ReplayConfig* const config);
+38
View File
@@ -5607,6 +5607,44 @@ static constexpr Bitmap bitmap_icon_speaker_and_headphones_mute{
{16, 16},
bitmap_icon_speaker_and_headphones_mute_data};
static constexpr uint8_t bitmap_icon_shift_data[] = {
0x00,
0x00,
0x80,
0x00,
0xC0,
0x01,
0xE0,
0x03,
0xF0,
0x07,
0xF8,
0x0F,
0xFC,
0x1F,
0xE0,
0x03,
0xE0,
0x03,
0xE0,
0x03,
0x20,
0x02,
0xE0,
0x03,
0x20,
0x02,
0xE0,
0x03,
0x00,
0x00,
0x00,
0x00,
};
static constexpr Bitmap bitmap_icon_shift{
{16, 16},
bitmap_icon_shift_data};
} /* namespace ui */
#endif /*__BITMAP_HPP__*/
+11 -13
View File
@@ -20,21 +20,20 @@
* Boston, MA 02110-1301, USA.
*/
#include "core_control.hpp"
#include "hal.h"
#include "lpc43xx_cpp.hpp"
using namespace lpc43xx;
#include "message.hpp"
#include "baseband_api.hpp"
#include "core_control.hpp"
#include "hal.h"
#include "lpc43xx_cpp.hpp"
#include "lz4.h"
#include "message.hpp"
#include <cstring>
void m4_init(const portapack::spi_flash::image_tag_t image_tag, const portapack::memory::region_t to, const bool full_reset) {
const portapack::spi_flash::chunk_t* chunk = reinterpret_cast<const portapack::spi_flash::chunk_t*>(portapack::spi_flash::images.base());
using namespace lpc43xx;
using namespace portapack;
void m4_init(const spi_flash::image_tag_t image_tag, const memory::region_t to, const bool full_reset) {
const spi_flash::chunk_t* chunk = reinterpret_cast<const spi_flash::chunk_t*>(spi_flash::images.base());
while (chunk->tag) {
if (chunk->tag == image_tag) {
const void* src = &chunk->data[0];
@@ -49,9 +48,8 @@ void m4_init(const portapack::spi_flash::image_tag_t image_tag, const portapack:
LPC_CREG->M4MEMMAP = to.base();
/* Reset M4 core and optionally all peripherals */
LPC_RGU->RESET_CTRL[0] = (full_reset) ? (1 << 1) // PERIPH_RST
: (1 << 13) // M4_RST
;
LPC_RGU->RESET_CTRL[0] = (full_reset) ? (1 << 1) // PERIPH_RST
: (1 << 13); // M4_RST
return;
}
+43
View File
@@ -0,0 +1,43 @@
/*
* 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 __FIELD_BINDER_H__
#define __FIELD_BINDER_H__
/* There's a common pattern that appears in applications that use
* field widgets. The field value is set, on_change and on_select
* handlers are registered. on_select will read the value and
* set the new value once changed. on_change will be called which
* will write the new value to the backing variable. */
struct binding_traits {
};
template <typename T>
constexpr auto get_traits() {
return {};
}
template <typename TField, typename TTraits = get_traits<TField>()>
class FieldBinding {
};
#endif /*__FIELD_BINDER_H__*/
+33 -1
View File
@@ -21,12 +21,17 @@
*/
#include "file.hpp"
#include "complex.hpp"
#include <algorithm>
#include <codecvt>
#include <cstring>
#include <locale>
namespace fs = std::filesystem;
static const fs::path c8_ext{u".C8"};
static const fs::path c16_ext{u".C16"};
Optional<File::Error> File::open_fatfs(const std::filesystem::path& filename, BYTE mode) {
auto result = f_open(&f, reinterpret_cast<const TCHAR*>(filename.c_str()), mode);
if (result == FR_OK) {
@@ -45,8 +50,11 @@ Optional<File::Error> File::open_fatfs(const std::filesystem::path& filename, BY
}
}
Optional<File::Error> File::open(const std::filesystem::path& filename, bool read_only) {
Optional<File::Error> File::open(const std::filesystem::path& filename, bool read_only, bool create) {
BYTE mode = read_only ? FA_READ : FA_READ | FA_WRITE;
if (create)
mode |= FA_OPEN_ALWAYS;
return open_fatfs(filename, mode);
}
@@ -504,6 +512,19 @@ bool path_iequal(
return false;
}
bool is_cxx_capture_file(const path& filename) {
auto ext = filename.extension();
return path_iequal(c8_ext, ext) || path_iequal(c16_ext, ext);
}
uint8_t capture_file_sample_size(const path& filename) {
if (path_iequal(filename.extension(), c8_ext))
return sizeof(complex8_t);
if (path_iequal(filename.extension(), c16_ext))
return sizeof(complex16_t);
return 0;
}
directory_iterator::directory_iterator(
const std::filesystem::path& path,
const std::filesystem::path& wild)
@@ -547,6 +568,17 @@ bool is_directory(const path& file_path) {
return fr == FR_OK && is_directory(static_cast<file_status>(filinfo.fattrib));
}
bool is_empty_directory(const path& file_path) {
DIR dir;
FILINFO filinfo;
if (!is_directory(file_path))
return false;
auto result = f_findfirst(&dir, &filinfo, reinterpret_cast<const TCHAR*>(file_path.c_str()), (const TCHAR*)u"*");
return !((result == FR_OK) && (filinfo.fname[0] != (TCHAR)'\0'));
}
space_info space(const path& p) {
DWORD free_clusters{0};
FATFS* fs;
+4 -1
View File
@@ -168,6 +168,8 @@ path operator/(const path& lhs, const path& rhs);
/* Case insensitive path equality on underlying "native" string. */
bool path_iequal(const path& lhs, const path& rhs);
bool is_cxx_capture_file(const path& filename);
uint8_t capture_file_sample_size(const path& filename);
using file_status = BYTE;
@@ -246,6 +248,7 @@ bool is_directory(const file_status s);
bool is_regular_file(const file_status s);
bool file_exists(const path& file_path);
bool is_directory(const path& file_path);
bool is_empty_directory(const path& file_path);
space_info space(const path& p);
@@ -319,7 +322,7 @@ class File {
File& operator=(const File&) = delete;
// TODO: Return Result<>.
Optional<Error> open(const std::filesystem::path& filename, bool read_only = true);
Optional<Error> open(const std::filesystem::path& filename, bool read_only = true, bool create = false);
Optional<Error> append(const std::filesystem::path& filename);
Optional<Error> create(const std::filesystem::path& filename);
+37 -2
View File
@@ -26,6 +26,7 @@
#include "file.hpp"
#include "optional.hpp"
#include <functional>
#include <memory>
#include <string_view>
@@ -75,6 +76,8 @@ class BufferWrapper {
}
virtual ~BufferWrapper() {}
std::function<void(Size, Size)> on_read_progress{};
/* Prevent copies */
BufferWrapper(const BufferWrapper&) = delete;
BufferWrapper& operator=(const BufferWrapper&) = delete;
@@ -234,13 +237,23 @@ class BufferWrapper {
line_count_ = start_line_;
Offset offset = start_offset_;
// Report progress every N lines.
constexpr auto report_interval = 100u;
auto result = next_newline(offset);
auto next_report = report_interval;
while (result) {
++line_count_;
if (newlines_.size() < max_newlines)
newlines_.push_back(*result);
offset = *result + 1;
if (on_read_progress && line_count_ > next_report) {
on_read_progress(offset, size());
next_report = line_count_ + report_interval;
}
result = next_newline(offset);
}
}
@@ -397,6 +410,9 @@ class BufferWrapper {
// Number of bytes left to shift.
Offset remaining = size() - src;
Offset offset = size();
Size report_total = remaining;
Size report_interval = report_total / 8;
Size next_report = remaining - report_interval;
while (remaining > 0) {
offset -= std::min(remaining, buffer_size);
@@ -413,6 +429,11 @@ class BufferWrapper {
break;
remaining -= *result;
if (on_read_progress && remaining <= next_report) {
on_read_progress(report_total - remaining, report_total);
next_report = remaining > report_interval ? remaining - report_interval : 0;
}
}
}
@@ -424,6 +445,9 @@ class BufferWrapper {
char buffer[buffer_size];
auto offset = src;
Size report_total = size();
Size report_interval = report_total / 8;
Size next_report = offset + report_interval;
while (true) {
wrapped_->seek(offset);
@@ -438,6 +462,11 @@ class BufferWrapper {
break;
offset += *result;
if (on_read_progress && offset >= next_report) {
on_read_progress(offset, report_total);
next_report = offset + report_interval;
}
}
// Delete the extra bytes at the end of the file.
@@ -463,13 +492,19 @@ class FileWrapper : public BufferWrapper<File, 64> {
template <typename T>
using Result = File::Result<T>;
using Error = File::Error;
static Result<std::unique_ptr<FileWrapper>> open(const std::filesystem::path& path) {
static Result<std::unique_ptr<FileWrapper>> open(
const std::filesystem::path& path,
bool create = false,
std::function<void(Size, Size)> on_read_progress = nullptr) {
auto fw = std::unique_ptr<FileWrapper>(new FileWrapper());
auto error = fw->file_.open(path, /*read_only*/ false);
auto error = fw->file_.open(path, /*read_only*/ false, create);
if (error)
return *error;
if (on_read_progress)
fw->on_read_progress = on_read_progress;
fw->initialize();
return fw;
}
+2 -78
View File
@@ -22,92 +22,16 @@
* Boston, MA 02110-1301, USA.
*/
#include "file.hpp"
#include "freqman.hpp"
#include "tone_key.hpp"
#include "ui_widget.hpp"
#include <algorithm>
using option_t = ui::OptionsField::option_t;
using options_t = ui::OptionsField::options_t;
namespace fs = std::filesystem;
using namespace tonekey;
using namespace ui;
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
extern options_t freqman_modulations;
extern options_t freqman_bandwidths[4];
extern options_t freqman_steps;
extern options_t freqman_steps_short;
extern const option_t* find_by_index(const options_t& options, freqman_index_t index);
// TODO: remove in favor of FreqmanDB
/* Freqman file handling. */
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) {
return parse_freqman_file(get_freqman_path(file_stem), db, options);
}
bool delete_freqman_file(const std::string& file_stem) {
delete_file(get_freqman_path(file_stem));
return true;
}
bool save_freqman_file(const std::string& file_stem, freqman_db& db) {
auto path = get_freqman_path(file_stem);
delete_file(path);
File freqman_file;
auto error = freqman_file.create(path);
if (error)
return false;
for (size_t n = 0; n < db.size(); n++)
freqman_file.write_line(to_freqman_string(*db[n]));
return true;
}
bool create_freqman_file(const std::string& file_stem) {
auto fs_error = make_new_file(get_freqman_path(file_stem));
return fs_error.ok();
}
/* Set options. */
void freqman_set_modulation_option(OptionsField& option) {
option.set_options(freqman_modulations);
}
void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option) {
if (is_valid(modulation))
option.set_options(freqman_bandwidths[modulation]);
}
void freqman_set_step_option(OptionsField& option) {
option.set_options(freqman_steps);
}
void freqman_set_step_option_short(OptionsField& option) {
option.set_options(freqman_steps_short);
}
/* Option value lookup. */
// TODO: use Optional instead of magic values.
int32_t freqman_entry_get_modulation_value(freqman_index_t modulation) {
if (auto opt = find_by_index(freqman_modulations, modulation))
return opt->second;
return -1;
}
int32_t freqman_entry_get_bandwidth_value(freqman_index_t modulation, freqman_index_t bandwidth) {
if (modulation < freqman_modulations.size()) {
if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth))
return opt->second;
}
return -1;
}
int32_t freqman_entry_get_step_value(freqman_index_t step) {
if (auto opt = find_by_index(freqman_steps, step))
return opt->second;
-20
View File
@@ -25,13 +25,7 @@
#ifndef __FREQMAN_H__
#define __FREQMAN_H__
#include <cstring>
#include <string>
#include "file.hpp"
#include "freqman_db.hpp"
#include "string_format.hpp"
#include "ui_receiver.hpp"
#include "ui_widget.hpp"
// Defined for back-compat.
#define FREQMAN_MAX_PER_FILE freqman_default_max_entries
@@ -50,21 +44,7 @@ enum freqman_entry_modulation : uint8_t {
SPEC_MODULATION
};
// TODO: Replace with FreqmanDB.
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options);
bool delete_freqman_file(const std::string& file_stem);
bool save_freqman_file(const std::string& file_stem, freqman_db& db);
bool create_freqman_file(const std::string& file_stem);
void freqman_set_bandwidth_option(freqman_index_t modulation, ui::OptionsField& option);
void freqman_set_modulation_option(ui::OptionsField& option);
void freqman_set_step_option(ui::OptionsField& option);
void freqman_set_step_option_short(ui::OptionsField& option);
void freqman_set_tone_option(ui::OptionsField& option);
// TODO: Can these be removed after Recon is migrated to FreqmanDB?
int32_t freqman_entry_get_modulation_value(freqman_index_t modulation);
int32_t freqman_entry_get_bandwidth_value(freqman_index_t modulation, freqman_index_t bandwidth);
int32_t freqman_entry_get_step_value(freqman_index_t step);
#endif /*__FREQMAN_H__*/
+117 -65
View File
@@ -40,10 +40,6 @@ namespace fs = std::filesystem;
const std::filesystem::path freqman_dir{u"/FREQMAN"};
const std::filesystem::path freqman_extension{u".TXT"};
const std::filesystem::path get_freqman_path(const std::string& stem) {
return freqman_dir / stem + freqman_extension;
}
// NB: Don't include UI headers to keep this code unit testable.
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
@@ -78,9 +74,9 @@ options_t freqman_bandwidths[4] = {
},
{
// SPEC -- TODO: these should be indexes.
{"8k5", 8500},
{"11k", 11000},
{"12k5", 12500},
{"16k", 16000},
{"20k", 20000},
{"25k", 25000},
{"50k", 50000},
{"100k", 100000},
@@ -97,6 +93,7 @@ options_t freqman_bandwidths[4] = {
},
};
// TODO: these should be indexes.
options_t freqman_steps = {
{"0.1kHz ", 100},
{"1kHz ", 1000},
@@ -116,6 +113,7 @@ options_t freqman_steps = {
{"1MHz ", 1000000},
};
// TODO: these should be indexes.
options_t freqman_steps_short = {
{"0.1kHz", 100},
{"1kHz", 1000},
@@ -162,6 +160,27 @@ const option_t* find_by_index(const options_t& options, freqman_index_t index) {
*}
*/
bool operator==(const freqman_entry& lhs, const freqman_entry& rhs) {
auto equal = lhs.type == rhs.type &&
lhs.frequency_a == rhs.frequency_a &&
lhs.description == rhs.description &&
lhs.modulation == rhs.modulation &&
lhs.bandwidth == rhs.bandwidth;
if (!equal)
return false;
if (lhs.type == freqman_type::Range) {
equal = lhs.frequency_b == rhs.frequency_b &&
lhs.step == rhs.step;
} else if (lhs.type == freqman_type::HamRadio) {
equal = lhs.frequency_b == rhs.frequency_b &&
lhs.tone == rhs.tone;
}
return equal;
}
std::string freqman_entry_get_modulation_string(freqman_index_t modulation) {
if (auto opt = find_by_index(freqman_modulations, modulation))
return opt->first;
@@ -188,6 +207,23 @@ std::string freqman_entry_get_step_string_short(freqman_index_t step) {
return {};
}
const std::filesystem::path get_freqman_path(const std::string& stem) {
return freqman_dir / stem + freqman_extension;
}
bool create_freqman_file(const std::string& file_stem) {
auto fs_error = make_new_file(get_freqman_path(file_stem));
return fs_error.ok();
}
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) {
return parse_freqman_file(get_freqman_path(file_stem), db, options);
}
void delete_freqman_file(const std::string& file_stem) {
delete_file(get_freqman_path(file_stem));
}
std::string pretty_string(const freqman_entry& entry, size_t max_length) {
std::string str;
@@ -196,12 +232,12 @@ std::string pretty_string(const freqman_entry& entry, size_t max_length) {
str = to_string_short_freq(entry.frequency_a) + "M: " + entry.description;
break;
case freqman_type::Range:
str = to_string_dec_uint(entry.frequency_a / 1'000'000) + "M-" +
to_string_dec_uint(entry.frequency_b / 1'000'000) + "M: " + entry.description;
str = to_string_rounded_freq(entry.frequency_a, 1) + "M-" +
to_string_rounded_freq(entry.frequency_b, 1) + "M: " + entry.description;
break;
case freqman_type::HamRadio:
str = "R:" + to_string_dec_uint(entry.frequency_a / 1'000'000) + "M,T:" +
to_string_dec_uint(entry.frequency_b / 1'000'000) + "M: " + entry.description;
str = "R:" + to_string_rounded_freq(entry.frequency_a, 1) + "M,T:" +
to_string_rounded_freq(entry.frequency_b, 1) + "M: " + entry.description;
break;
case freqman_type::Raw:
str = entry.description;
@@ -337,55 +373,23 @@ bool parse_freqman_entry(std::string_view str, freqman_entry& entry) {
}
}
// No valid frequency combination was set.
if (entry.type == freqman_type::Unknown)
return false;
// Frequency A must be set for all types
if (entry.frequency_a == 0)
return false;
// Frequency B must be set for type Range or Ham Radio
if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) {
if (entry.frequency_b == 0)
return false;
}
// Ranges should have frequencies A <= B.
if (entry.type == freqman_type::Range) {
if (entry.frequency_a > entry.frequency_b)
return false;
}
// TODO: Consider additional validation:
// - Tone only on HamRadio.
// - Fail on failed parse_int.
// - Fail if bandwidth set before modulation.
return true;
return is_valid(entry);
}
// TODO: Use FreqmanDB iterator.
bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_options options) {
File f;
auto error = f.open(path);
if (error)
FreqmanDB freqman_db;
freqman_db.set_read_raw(false); // Don't return malformed lines.
if (!freqman_db.open(path))
return false;
auto reader = FileLineReader(f);
auto line_count = count_lines(reader);
// Attempt to avoid a re-alloc if possible.
db.clear();
db.reserve(line_count);
for (const auto& line : reader) {
freqman_entry entry{};
if (!parse_freqman_entry(line, entry))
continue;
db.reserve(freqman_db.entry_count());
for (auto entry : freqman_db) {
// Filter by entry type.
if ((entry.type == freqman_type::Single && !options.load_freqs) ||
if (entry.type == freqman_type::Unknown ||
(entry.type == freqman_type::Single && !options.load_freqs) ||
(entry.type == freqman_type::Range && !options.load_ranges) ||
(entry.type == freqman_type::HamRadio && !options.load_hamradios)) {
continue;
@@ -411,10 +415,40 @@ bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_optio
return true;
}
bool is_valid(const freqman_entry& entry) {
// No valid frequency combination was set.
if (entry.type == freqman_type::Unknown)
return false;
// Frequency A must be set for all types
if (entry.frequency_a == 0)
return false;
// Frequency B must be set for type Range or Ham Radio
if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) {
if (entry.frequency_b == 0)
return false;
}
// Ranges should have frequencies A <= B.
if (entry.type == freqman_type::Range) {
if (entry.frequency_a > entry.frequency_b)
return false;
}
// TODO: Consider additional validation:
// - Tone only on HamRadio.
// - Step only on Range
// - Fail on failed parse_int.
// - Fail if bandwidth set before modulation.
return true;
}
/* FreqmanDB ***********************************/
bool FreqmanDB::open(const std::filesystem::path& path) {
auto result = FileWrapper::open(path);
bool FreqmanDB::open(const std::filesystem::path& path, bool create) {
auto result = FileWrapper::open(path, create);
if (!result)
return false;
@@ -426,15 +460,15 @@ void FreqmanDB::close() {
wrapper_.reset();
}
freqman_entry FreqmanDB::operator[](FileWrapper::Line line) const {
auto length = wrapper_->line_length(line);
auto line_text = wrapper_->get_text(line, 0, length);
freqman_entry FreqmanDB::operator[](Index index) const {
auto length = wrapper_->line_length(index);
auto line_text = wrapper_->get_text(index, 0, length);
if (line_text) {
freqman_entry entry;
if (parse_freqman_entry(*line_text, entry))
return entry;
else {
else if (read_raw_) {
entry.type = freqman_type::Raw;
entry.description = trim(*line_text);
return entry;
@@ -444,15 +478,18 @@ freqman_entry FreqmanDB::operator[](FileWrapper::Line line) const {
return {};
}
void FreqmanDB::insert_entry(const freqman_entry& entry, FileWrapper::Line line) {
// TODO: Can be more efficient.
line = clip<uint32_t>(line, 0u, entry_count());
wrapper_->insert_line(line);
replace_entry(line, entry);
void FreqmanDB::insert_entry(Index index, const freqman_entry& entry) {
index = clip<uint32_t>(index, 0u, entry_count());
wrapper_->insert_line(index);
replace_entry(index, entry);
}
void FreqmanDB::replace_entry(FileWrapper::Line line, const freqman_entry& entry) {
auto range = wrapper_->line_range(line);
void FreqmanDB::append_entry(const freqman_entry& entry) {
insert_entry(entry_count(), entry);
}
void FreqmanDB::replace_entry(Index index, const freqman_entry& entry) {
auto range = wrapper_->line_range(index);
if (!range)
return;
@@ -461,8 +498,23 @@ void FreqmanDB::replace_entry(FileWrapper::Line line, const freqman_entry& entry
wrapper_->replace_range(*range, to_freqman_string(entry));
}
void FreqmanDB::delete_entry(FileWrapper::Line line) {
wrapper_->delete_line(line);
void FreqmanDB::delete_entry(Index index) {
wrapper_->delete_line(index);
}
bool FreqmanDB::delete_entry(const freqman_entry& entry) {
auto it = find_entry(entry);
if (it == end())
return false;
delete_entry(it.index());
return true;
}
FreqmanDB::iterator FreqmanDB::find_entry(const freqman_entry& entry) {
return find_entry([&entry](const auto& other) {
return entry == other;
});
}
uint32_t FreqmanDB::entry_count() const {
+66 -21
View File
@@ -52,9 +52,6 @@ constexpr bool is_invalid(freqman_index_t index) {
return index == freqman_invalid_index;
}
/* Gets the full path for a given file stem (no extension). */
const std::filesystem::path get_freqman_path(const std::string& stem);
enum class freqman_type : uint8_t {
Single, // f=
Range, // a=,b=
@@ -151,6 +148,8 @@ struct freqman_entry {
freqman_index_t tone{freqman_invalid_index};
};
bool operator==(const freqman_entry& lhs, const freqman_entry& rhs);
// TODO: These shouldn't be exported.
std::string freqman_entry_get_modulation_string(freqman_index_t modulation);
std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth);
@@ -173,6 +172,12 @@ struct freqman_load_options {
using freqman_entry_ptr = std::unique_ptr<freqman_entry>;
using freqman_db = std::vector<freqman_entry_ptr>;
/* Gets the full path for a given file stem (no extension). */
const std::filesystem::path get_freqman_path(const std::string& stem);
bool create_freqman_file(const std::string& file_stem);
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options);
void delete_freqman_file(const std::string& file_stem);
/* Gets a pretty string representation for an entry. */
std::string pretty_string(const freqman_entry& item, size_t max_length = 30);
@@ -182,54 +187,94 @@ std::string to_freqman_string(const freqman_entry& entry);
bool parse_freqman_entry(std::string_view str, freqman_entry& entry);
bool parse_freqman_file(const std::filesystem::path& path, freqman_db& db, freqman_load_options options);
/* The tricky part of using the file directly is that there can be comments
* and empty lines in the file. This messes up the 'count' calculation.
* Either have to live with 'count' being an upper bound have the callers
* know to expect that entries may be empty. */
// NB: This won't apply implicit mod/bandwidth.
// TODO: Reuse for parse_freqman_file
/* Returns true if the entry is well-formed. */
bool is_valid(const freqman_entry& entry);
/* API wrapper over a Freqman file. Provides CRUD operations
* for freqman_entry instances that are read/written directly
* to the underlying file. */
class FreqmanDB {
public:
using Index = FileWrapper::Line;
/* NB: This iterator is very basic: forward only, read-only. */
class iterator {
public:
iterator(FreqmanDB& db, FileWrapper::Offset line)
: db_{db}, line_{line} {}
iterator(FreqmanDB& db, Index index)
: db_{db}, index_{index} {}
iterator& operator++() {
line_++;
index_++;
if (index_ >= db_.entry_count())
index_ = end_index;
return *this;
}
freqman_entry operator*() const {
return db_[line_];
return db_[index_];
}
bool operator==(const iterator& other) {
return &db_ == &other.db_ && index_ == other.index_;
}
bool operator!=(const iterator& other) {
return &db_ != &other.db_ || line_ != other.line_;
return !(*this == other);
}
Index index() const {
return index_;
}
/* Value indicating the 'end' iterator. */
static constexpr Index end_index = (Index)-1;
private:
FreqmanDB& db_;
FileWrapper::Line line_;
Index index_;
};
bool open(const std::filesystem::path& path);
bool open(const std::filesystem::path& path, bool create = false);
void close();
freqman_entry operator[](FileWrapper::Line line) const;
void insert_entry(const freqman_entry& entry, FileWrapper::Line line);
void replace_entry(FileWrapper::Line line, const freqman_entry& entry);
void delete_entry(FileWrapper::Line line);
freqman_entry operator[](Index index) const;
void insert_entry(Index index, const freqman_entry& entry);
void append_entry(const freqman_entry& entry);
void replace_entry(Index index, const freqman_entry& entry);
void delete_entry(Index index);
bool delete_entry(const freqman_entry& entry);
template <typename Fn>
iterator find_entry(const Fn& predicate) {
// TODO: use std::find, but need to make the iterator compliant.
auto it = begin();
const auto it_end = end();
while (it != it_end) {
if (predicate(*it))
return it;
++it;
}
return it_end;
}
iterator find_entry(const freqman_entry& entry);
uint32_t entry_count() const;
bool empty() const;
/* When true, Raw entries are returned instead of Unknown. */
void set_read_raw(bool v) { read_raw_ = v; }
iterator begin() {
return {*this, 0};
}
iterator end() {
return {*this, entry_count()};
return {*this, iterator::end_index};
}
private:
std::unique_ptr<FileWrapper> wrapper_{};
bool read_raw_{true};
};
#endif /* __FREQMAN_DB_H__ */
+108 -65
View File
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2023 Mark Thompson
*
* This file is part of PortaPack.
*
@@ -23,86 +24,128 @@
#include "utility.hpp"
uint8_t Debounce::state() {
bool v = state_to_report_;
simulated_pulse_ = false;
return v;
}
void Debounce::enable_repeat() {
repeat_enabled_ = true;
}
bool Debounce::get_long_press_enabled() const {
return long_press_enabled_;
}
void Debounce::set_long_press_enabled(bool v) {
long_press_enabled_ = v;
}
bool Debounce::long_press_occurred() {
bool v = long_press_occurred_;
long_press_occurred_ = false;
return v;
}
// Returns TRUE if button state changed (after debouncing)
bool Debounce::feed(const uint8_t bit) {
history_ = (history_ << 1) | (bit & 1);
// "Repeat" handling - simulated button release
if (repeat_ctr_) {
// Make sure the button is still being held continuously
if ((history_ == 0xFF) && !long_press_enabled_) {
// Simulate button press every REPEAT_SUBSEQUENT_DELAY ticks
if (--repeat_ctr_ == 0) {
state_ = !state_;
repeat_ctr_ = REPEAT_SUBSEQUENT_DELAY / 2;
return true;
}
} else {
// It's a real button release; stop simulating
repeat_ctr_ = 0;
}
}
if (state_ == 0) {
// Previous button state was 0 (released);
// Has button been held for DEBOUNCE_COUNT ticks?
if ((history_ & DEBOUNCE_MASK) == DEBOUNCE_MASK) {
// Has button been held for DEBOUNCE_COUNT ticks?
if ((history_ & DEBOUNCE_MASK) == DEBOUNCE_MASK) {
//
// Button is currently pressed;
// Was previous button state 0 (released)?
//
if (state_ == 0) {
//
// Button has been pressed (after filtering glitches), transition 0->1
//
state_ = 1;
held_time_ = 0;
// If long_press_enabled_, state() function masks the button press until it's released
// or until LONG_PRESS_DELAY is reached
if (long_press_enabled_) {
pulse_upon_release_ = true;
state_to_report_ = 0;
return false;
}
state_to_report_ = 1;
return true;
}
// "Repeat" handling - simulated button release
if (repeat_ctr_ && !long_press_enabled_) {
// Simulate button press every REPEAT_SUBSEQUENT_DELAY ticks
// (by toggling reported state every 1/2 of the delay time)
if (--repeat_ctr_ == 0) {
state_to_report_ = !state_to_report_;
repeat_ctr_ = REPEAT_SUBSEQUENT_DELAY / 2;
return true;
}
}
// Keep track of how long button has been held
held_time_++;
if (pulse_upon_release_) {
// Button is being held down and long_press support is enabled for this key:
// if LONG_PRESS_DELAY is reached then finally report that switch is pressed and set flag
// indicating it was a LONG press
// (note that repeat_support and long_press support are mutually exclusive)
if (held_time_ >= LONG_PRESS_DELAY) {
pulse_upon_release_ = false;
long_press_occurred_ = true;
state_to_report_ = 1;
return true;
}
} else if (repeat_enabled_ && !long_press_enabled_) {
// Repeat support -- 4 directional buttons only (unless long_press is enabled)
if (held_time_ >= REPEAT_INITIAL_DELAY) {
// Delay reached; trigger repeat code on NEXT tick
repeat_ctr_ = 1;
held_time_ = 0;
}
}
} else if ((history_ & DEBOUNCE_MASK) == 0) { // Has button been released for at least DEBOUNCE_COUNT ticks?
//
// Button is released;
// Was previous button state 1 (pressed)?
//
if (state_ == 1) {
//
// Button has been released (after filtering glitches), transition 1->0
//
state_ = 0;
long_press_occurred_ = false;
// If button released when long_press_enabled_ and before LONG_PRESS_DELAY was reached;
// allow state() function to finally return a single press indication (simulated pulse).
// Note: In long press mode, apps won't see button press until the button is released.
if (pulse_upon_release_) {
pulse_upon_release_ = false;
simulated_pulse_ = true;
state_to_report_ = 1;
return true;
}
state_to_report_ = 0;
return true;
}
// Reset reported state after application/event has cleared simulated_pulse_
if (state_to_report_ == 1 && !simulated_pulse_) {
state_to_report_ = 0;
return true;
}
} else {
// Previous button state was 1 (pressed);
// Has button been released for DEBOUNCE_COUNT ticks?
if ((history_ & DEBOUNCE_MASK) == 0) {
// Button has been released when long_press_enabled_ and before LONG_PRESS_DELAY was reached;
// allow state() function to finally return a single press indication
// (in long press mode, apps won't see button press until the button is released)
if (pulse_upon_release_) {
// leaving state_==1 for one cycle
pulse_upon_release_ = 0;
} else {
state_ = 0;
}
// Reset long_press_occurred_ flag after button is released
long_press_occurred_ = false;
return true;
}
// Has button been held continuously?
if (history_ == 0xFF) {
held_time_++;
if (pulse_upon_release_) {
// Button is being held down and long_press support is enabled for this key:
// if LONG_PRESS_DELAY is reached then finally report that switch is pressed and set flag
// indicating it was a LONG press
// (note that repease_support and long_press support are mutually exclusive)
if (held_time_ == LONG_PRESS_DELAY) {
long_press_occurred_ = true;
pulse_upon_release_ = 0;
held_time_ = 0;
return true;
}
} else if (repeat_enabled_ && !long_press_enabled_) {
// Repeat support -- 4 directional buttons only (unless long_press is enabled)
if (held_time_ == REPEAT_INITIAL_DELAY) {
// Delay reached; trigger repeat code on NEXT tick
repeat_ctr_ = 1;
held_time_ = 0;
}
}
} else {
// Button not continuously pressed; reset counter
held_time_ = 0;
}
//
// Button is in transition between states;
// Reset counters until button inputs are stable.
//
held_time_ = 0;
repeat_ctr_ = 0;
}
return false;
}
+25 -27
View File
@@ -31,39 +31,37 @@
// # of timer0 ticks before a held button starts being counted as repeated presses
#define REPEAT_INITIAL_DELAY 250
#define REPEAT_SUBSEQUENT_DELAY 92
#define LONG_PRESS_DELAY 1000
#define LONG_PRESS_DELAY 800
class Debounce {
public:
bool feed(const uint8_t bit);
uint8_t state() const {
return (pulse_upon_release_) ? 0 : state_;
}
void enable_repeat() {
repeat_enabled_ = true;
}
void set_long_press_support(bool v) {
long_press_enabled_ = v;
}
bool long_press_occurred() {
bool v = long_press_occurred_;
long_press_occurred_ = false;
return v;
}
uint8_t state();
void enable_repeat();
bool get_long_press_enabled() const;
void set_long_press_enabled(bool v);
bool long_press_occurred();
private:
uint8_t history_{0};
uint8_t state_{0};
bool repeat_enabled_{0};
uint16_t repeat_ctr_{0};
uint16_t held_time_{0};
bool pulse_upon_release_{0};
bool long_press_enabled_{0};
bool long_press_occurred_{0};
uint8_t history_{0}; // shift register of last 8 reads from button hardware state bit
uint8_t state_{0}; // actual button hardware state (after debounce logic), 1=pressed
uint8_t state_to_report_{0}; // pseudo button state reported by state() function (may be masked off or simulated presses)
bool repeat_enabled_{false}; // TRUE if this button is enabled to auto-repeat when held down (ignored if long_press_enabled)
uint16_t repeat_ctr_{0}; // used for timing auto-repeat simulated button presses when button is held down and repeat_enabled
uint16_t held_time_{0}; // number of ticks that the button has been held down (compared against REPEAT and LONG_PRESS delays)
bool pulse_upon_release_{false}; // TRUE when button is being held down when long_press_enabled and LONG_PRESS_DELAY hasn't been reached yet
bool simulated_pulse_{false}; // TRUE if a simulated button press is active following a short button press (only when long_press_enabled)
bool long_press_enabled_{false}; // TRUE when button is in long-press mode (takes precedence over the repeat_enabled flag)
bool long_press_occurred_{false}; // TRUE when button is being held down and LONG_PRESS_DELAY has been reached (only when long_press_enabled)
};
#endif /*__DEBOUNCE_H__*/
+102
View File
@@ -0,0 +1,102 @@
/*
* Copyright (C) 2023 Mark Thompson
*
* 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 "io_convert.hpp"
#include "complex.hpp"
namespace fs = std::filesystem;
static const fs::path c8_ext = u".C8";
namespace file_convert {
// Convert buffer contents from c16 to c8.
// Same buffer used for input & output; input size is bytes; output size is bytes/2.
void c16_to_c8(const void* buffer, File::Size bytes) {
complex16_t* src = (complex16_t*)buffer;
complex8_t* dest = (complex8_t*)buffer;
// Shift isn't used here because it would amplify noise at center freq since it's a signed number
// i.e. ((-1 >> 8) << 8) = -256, whereas (-1 / 256) * 256 = 0
for (File::Size i = 0; i < bytes / sizeof(complex16_t); i++) {
auto re_out = src[i].real() / 256;
auto im_out = src[i].imag() / 256;
dest[i] = {(int8_t)re_out, (int8_t)im_out};
}
}
// Convert c8 buffer to c16 buffer.
// Same buffer used for input & output; input size is bytes; output size is 2*bytes.
void c8_to_c16(const void* buffer, File::Size bytes) {
complex8_t* src = (complex8_t*)buffer;
complex16_t* dest = (complex16_t*)buffer;
uint32_t i = bytes / sizeof(complex8_t);
if (i != 0) {
do {
i--;
auto re_out = (int16_t)src[i].real() * 256; // C8 to C16 conversion;
auto im_out = (int16_t)src[i].imag() * 256; // Can't shift signed numbers left, so using multiply
dest[i] = {(int16_t)re_out, (int16_t)im_out};
} while (i != 0);
}
}
} /* namespace file_convert */
// Automatically enables C8/C16 conversion based on file extension
Optional<File::Error> FileConvertReader::open(const std::filesystem::path& filename) {
convert_c8_to_c16 = path_iequal(filename.extension(), c8_ext);
return file_.open(filename);
}
// If C8 conversion enabled, half the number of bytes are read from the file & expanded to fill the whole buffer.
File::Result<File::Size> FileConvertReader::read(void* const buffer, const File::Size bytes) {
auto read_result = file_.read(buffer, convert_c8_to_c16 ? bytes / 2 : bytes);
if (read_result.is_ok()) {
if (convert_c8_to_c16) {
file_convert::c8_to_c16(buffer, read_result.value());
read_result = read_result.value() * 2;
}
bytes_read_ += read_result.value();
}
return read_result;
}
// Automatically enables C8/C16 conversion based on file extension
Optional<File::Error> FileConvertWriter::create(const std::filesystem::path& filename) {
convert_c16_to_c8 = path_iequal(filename.extension(), c8_ext);
return file_.create(filename);
}
// If C8 conversion is enabled, half the number of bytes are written to the file.
File::Result<File::Size> FileConvertWriter::write(const void* const buffer, const File::Size bytes) {
if (convert_c16_to_c8) {
file_convert::c16_to_c8(buffer, bytes);
}
auto write_result = file_.write(buffer, convert_c16_to_c8 ? bytes / 2 : bytes);
if (write_result.is_ok()) {
if (convert_c16_to_c8) {
write_result = write_result.value() * 2;
}
bytes_written_ += write_result.value();
}
return write_result;
}
+79
View File
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2023 Mark Thompson
*
* 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.
*/
#pragma once
#include "io_file.hpp"
#include "io.hpp"
#include "file.hpp"
#include "optional.hpp"
#include <cstdint>
namespace file_convert {
void c8_to_c16(const void* buffer, File::Size bytes);
void c16_to_c8(const void* buffer, File::Size bytes);
} /* namespace file_convert */
class FileConvertReader : public stream::Reader {
public:
FileConvertReader() = default;
FileConvertReader(const FileConvertReader&) = delete;
FileConvertReader& operator=(const FileConvertReader&) = delete;
FileConvertReader(FileConvertReader&& file) = delete;
FileConvertReader& operator=(FileConvertReader&&) = delete;
Optional<File::Error> open(const std::filesystem::path& filename);
File::Result<File::Size> read(void* const buffer, const File::Size bytes) override;
const File& file() const& { return file_; }
bool convert_c8_to_c16{};
protected:
File file_{};
uint64_t bytes_read_{0};
};
class FileConvertWriter : public stream::Writer {
public:
FileConvertWriter() = default;
FileConvertWriter(const FileConvertWriter&) = delete;
FileConvertWriter& operator=(const FileConvertWriter&) = delete;
FileConvertWriter(FileConvertWriter&& file) = delete;
FileConvertWriter& operator=(FileConvertWriter&&) = delete;
Optional<File::Error> create(const std::filesystem::path& filename);
File::Result<File::Size> write(const void* const buffer, const File::Size bytes) override;
const File& file() const& { return file_; }
bool convert_c16_to_c8{};
protected:
File file_{};
uint64_t bytes_written_{0};
};
+81 -43
View File
@@ -22,32 +22,33 @@
#include "irq_controls.hpp"
#include "ch.h"
#include "hal.h"
#include "debounce.hpp"
#include "encoder.hpp"
#include "event_m0.hpp"
#include "hal.h"
#include "touch.hpp"
#include "touch_adc.hpp"
#include "encoder.hpp"
#include "debounce.hpp"
#include "utility.hpp"
#include <cstdint>
#include <array>
#include "portapack_io.hpp"
#include "hackrf_hal.hpp"
using namespace hackrf::one;
using namespace portapack;
static Thread* thread_controls_event = NULL;
static std::array<Debounce, 8> switch_debounce;
// Index with the Switch enum.
static std::array<Debounce, 6> switch_debounce;
static std::array<Debounce, 2> encoder_debounce;
static_assert(std::size(switch_debounce) == toUType(Switch::Dfu) + 1);
static Encoder encoder;
static volatile uint32_t encoder_position{0};
static volatile uint32_t touch_phase{0};
/* TODO: Change how touch scanning works. It produces a decent amount of noise
@@ -60,11 +61,11 @@ static volatile uint32_t touch_phase{0};
* Noise will only occur when the panel is being touched. Not ideal, but
* an acceptable improvement.
*/
static std::array<portapack::IO::TouchPinsConfig, 3> touch_pins_configs{
static std::array<IO::TouchPinsConfig, 3> touch_pins_configs{
/* State machine will pause here until touch is detected. */
portapack::IO::TouchPinsConfig::SensePressure,
portapack::IO::TouchPinsConfig::SenseX,
portapack::IO::TouchPinsConfig::SenseY,
IO::TouchPinsConfig::SensePressure,
IO::TouchPinsConfig::SenseX,
IO::TouchPinsConfig::SenseY,
};
static touch::Frame temp_frame;
@@ -80,7 +81,7 @@ static bool touch_update() {
const auto current_phase = touch_pins_configs[touch_phase];
switch (current_phase) {
case portapack::IO::TouchPinsConfig::SensePressure: {
case IO::TouchPinsConfig::SensePressure: {
const auto z1 = samples.xp - samples.xn;
const auto z2 = samples.yp - samples.yn;
const auto touch_raw = (z1 > touch::touch_threshold) || (z2 > touch::touch_threshold);
@@ -94,11 +95,11 @@ static bool touch_update() {
}
} break;
case portapack::IO::TouchPinsConfig::SenseX:
case IO::TouchPinsConfig::SenseX:
temp_frame.x += samples;
break;
case portapack::IO::TouchPinsConfig::SenseY:
case IO::TouchPinsConfig::SenseY:
temp_frame.y += samples;
break;
@@ -122,25 +123,57 @@ static bool touch_update() {
static uint8_t switches_raw = 0;
/* The raw data is not packed in a way that makes looping over it easy.
* One option would be an accessor helper (RawSwitch). Another option
* is to swizzle the bits into a friendlier order. */
// /* Type to access the bits in the raw switch data. */
// struct RawSwitch {
// const uint8_t raw_{0};
// uint8_t right() const { return (raw_ >> 0) & 1; }
// uint8_t left() const { return (raw_ >> 1) & 1; }
// uint8_t down() const { return (raw_ >> 2) & 1; }
// uint8_t up() const { return (raw_ >> 3) & 1; }
// uint8_t select() const { return (raw_ >> 4) & 1; }
// uint8_t rot_a() const { return (raw_ >> 5) & 1; }
// uint8_t rot_b() const { return (raw_ >> 6) & 1; }
// uint8_t dfu() const { return (raw_ >> 7) & 1; }};
static uint8_t swizzle_raw(uint8_t raw) {
return (raw & 0x1F) | // Keep the bottom 5 bits the same.
((raw >> 2) & 0x20) | // Shift the DFU bit down to bit 6.
((raw << 1) & 0xC0); // Shift the encoder bits up to be 7 & 8.
}
static bool switches_update(const uint8_t raw) {
// TODO: Only fire event on press, not release?
bool switch_changed = false;
for (size_t i = 0; i < switch_debounce.size(); i++) {
switch_changed |= switch_debounce[i].feed((raw >> i) & 1);
for (size_t i = 0; i < switch_debounce.size(); ++i) {
uint8_t bit = (raw >> i) & 0x01;
switch_changed |= switch_debounce[i].feed(bit);
}
return switch_changed;
}
static bool encoder_update(const uint8_t raw) {
bool encoder_changed = false;
encoder_changed |= encoder_debounce[0].feed((raw >> 6) & 0x01);
encoder_changed |= encoder_debounce[1].feed((raw >> 7) & 0x01);
return encoder_changed;
}
static bool encoder_read() {
const auto delta = encoder.update(
switch_debounce[5].state(),
switch_debounce[6].state());
encoder_debounce[0].state(),
encoder_debounce[1].state());
if (delta != 0) {
encoder_position += delta;
return true;
;
} else {
return false;
}
@@ -149,11 +182,13 @@ static bool encoder_read() {
void timer0_callback(GPTDriver* const) {
eventmask_t event_mask = 0;
if (touch_update()) event_mask |= EVT_MASK_TOUCH;
switches_raw = portapack::io.io_update(touch_pins_configs[touch_phase]);
if (switches_update(switches_raw)) {
switches_raw = swizzle_raw(io.io_update(touch_pins_configs[touch_phase]));
if (switches_update(switches_raw))
event_mask |= EVT_MASK_SWITCHES;
if (encoder_read()) event_mask |= EVT_MASK_ENCODER;
}
if (encoder_update(switches_raw) && encoder_read())
event_mask |= EVT_MASK_ENCODER;
/* Signal event loop */
if (event_mask) {
@@ -189,37 +224,40 @@ void controls_init() {
gptStartContinuous(&GPTD1, timer0_match_count);
// Enable repeat for directional switches only
for (size_t i = 0; i < 4; i++)
switch_debounce[i].enable_repeat();
for (auto i = Switch::Right; i <= Switch::Up; incr(i))
switch_debounce[toUType(i)].enable_repeat();
}
// Note: Called by event handler or apps, not in ISR, so some presses might be missed during high CPU utilization
SwitchesState get_switches_state() {
SwitchesState result;
// Right, Left, Down, Up, & Select switches
for (size_t i = 0; i < result.size() - 1; i++) {
// TODO: Ignore multiple keys at the same time?
// TODO: Ignore multiple keys at the same time?
for (size_t i = 0; i < result.size(); i++)
result[i] = switch_debounce[i].state();
}
// Grab Dfu switch from bit 7 and return in bit 5 so that all switches are grouped together in this 6-bit result
result[(size_t)Switch::Dfu] = switch_debounce[7].state();
return result;
}
// Configure which switches support long press (note those switches will not support Repeat function)
void switches_long_press_enable(SwitchesState switches_long_press_enabled) {
// Right, Left, Down, Up, & Select switches
for (size_t i = 0; i < switches_long_press_enabled.size() - 1; i++) {
switch_debounce[i].set_long_press_support(switches_long_press_enabled[i]);
}
/* Gets the long press enabled state for all the switches. */
SwitchesState get_switches_long_press_config() {
SwitchesState result;
// Dfu switch
switch_debounce[7].set_long_press_support(switches_long_press_enabled[(size_t)Switch::Dfu]);
for (size_t i = 0; i < result.size(); i++)
result[i] = switch_debounce[i].get_long_press_enabled();
return result;
}
bool switch_long_press_occurred(size_t v) {
return (v == (size_t)Switch::Dfu) ? switch_debounce[7].long_press_occurred() : switch_debounce[v].long_press_occurred();
/* Configures which switches support long press.
* NB: those switches will not support Repeat function. */
void set_switches_long_press_config(SwitchesState switch_config) {
for (size_t i = 0; i < switch_config.size(); i++)
switch_debounce[i].set_long_press_enabled(switch_config[i]);
}
bool switch_is_long_pressed(Switch s) {
return switch_debounce[toUType(s)].long_press_occurred();
}
EncoderPosition get_encoder_position() {
+8 -5
View File
@@ -28,25 +28,28 @@
#include "touch.hpp"
// Must be same values as in ui::KeyEvent
enum class Switch {
// TODO: Why not just reuse one or the other?
enum class Switch : uint8_t {
Right = 0,
Left = 1,
Down = 2,
Up = 3,
Sel = 4,
Dfu = 5,
Dfu = 5
};
// Index with the Switch enum.
using SwitchesState = std::bitset<6>;
using EncoderPosition = uint32_t;
void controls_init();
SwitchesState get_switches_state();
EncoderPosition get_encoder_position();
touch::Frame get_touch_frame();
void switches_long_press_enable(SwitchesState v);
bool switch_long_press_occurred(size_t v);
SwitchesState get_switches_long_press_config();
void set_switches_long_press_config(SwitchesState switch_config);
bool switch_is_long_pressed(Switch s);
namespace control {
namespace debug {
+1
View File
@@ -238,6 +238,7 @@ void set_baseband_filter_bandwidth(const uint32_t bandwidth_minimum) {
void set_baseband_rate(const uint32_t rate) {
portapack::clock_manager.set_sampling_frequency(rate);
// TODO: actually set baseband too?
}
void set_antenna_bias(const bool on) {
+36 -2
View File
@@ -162,13 +162,47 @@ std::string to_string_decimal(float decimal, int8_t precision) {
return result;
}
// right-justified frequency in Hz, always 10 characters
std::string to_string_freq(const uint64_t f) {
auto final_str = to_string_dec_int(f / 1000000, 4) + to_string_dec_int(f % 1000000, 6, '0');
std::string final_str{""};
if (f < 1000000)
final_str = to_string_dec_int(f, 10, ' ');
else
final_str = to_string_dec_int(f / 1000000, 4) + to_string_dec_int(f % 1000000, 6, '0');
return final_str;
}
// right-justified frequency in MHz, rounded to 4 decimal places, always 9 characters
std::string to_string_short_freq(const uint64_t f) {
auto final_str = to_string_dec_int(f / 1000000, 4) + "." + to_string_dec_int((f / 100) % 10000, 4, '0');
auto final_str = to_string_dec_int(f / 1000000, 4) + "." + to_string_dec_int(((f + 50) / 100) % 10000, 4, '0');
return final_str;
}
// non-justified non-padded frequency in MHz, rounded to specified number of decimal places
std::string to_string_rounded_freq(const uint64_t f, int8_t precision) {
std::string final_str{""};
static constexpr uint32_t pow10[7] = {
1,
10,
100,
1000,
10000,
100000,
1000000,
};
if (precision < 1) {
final_str = to_string_dec_uint64(f / 1000000);
} else {
if (precision > 6)
precision = 6;
uint32_t divisor = pow10[6 - precision];
final_str = to_string_dec_uint64(f / 1000000) + "." + to_string_dec_int(((f + (divisor / 2)) / divisor) % pow10[precision], precision, '0');
}
return final_str;
}
+1 -1
View File
@@ -59,9 +59,9 @@ std::string to_string_decimal(float decimal, int8_t precision);
std::string to_string_hex(const uint64_t n, const int32_t l = 0);
std::string to_string_hex_array(uint8_t* const array, const int32_t l = 0);
// NB: These pad-left and don't work correctly for values less than 1M.
std::string to_string_freq(const uint64_t f);
std::string to_string_short_freq(const uint64_t f);
std::string to_string_rounded_freq(const uint64_t f, int8_t precision);
std::string to_string_time_ms(const uint32_t ms);
std::string to_string_datetime(const rtc::RTC& value, const TimeFormat format = YMDHMS);
+52 -18
View File
@@ -22,11 +22,10 @@
#include "ui_alphanum.hpp"
#include "portapack.hpp"
#include "hackrf_hal.hpp"
#include "portapack.hpp"
#include "portapack_shared_memory.hpp"
#include <algorithm>
#include "utility.hpp"
namespace ui {
@@ -38,6 +37,7 @@ AlphanumView::AlphanumView(
size_t n;
add_children({
&button_shift,
&labels,
&field_raw,
&text_raw_to_char,
@@ -49,6 +49,19 @@ AlphanumView::AlphanumView(
this->on_button(button);
};
button_shift.set_vertical_center(true);
button_shift.on_select = [this]() {
incr(shift_mode);
// Disallow one-time shift in digits/symbols mode
if ((mode == 1) && (shift_mode == ShiftMode::Shift))
shift_mode = ShiftMode::ShiftLock;
else if (shift_mode > ShiftMode::ShiftLock)
shift_mode = ShiftMode::None;
refresh_keys();
};
n = 0;
for (auto& button : buttons) {
button.id = n;
@@ -56,9 +69,9 @@ AlphanumView::AlphanumView(
focused_button = button.id;
};
button.on_select = button_fn;
button.set_parent_rect({static_cast<Coord>((n % 5) * (240 / 5)),
button.set_parent_rect({static_cast<Coord>((n % 5) * (screen_width / 5)),
static_cast<Coord>((n / 5) * 38 + 24),
240 / 5, 38});
screen_width / 5, 38});
add_child(&button);
n++;
}
@@ -78,38 +91,59 @@ AlphanumView::AlphanumView(
char_add(field_raw.value());
};
// make text_raw_to_char widget display the char value from field_raw
field_raw.on_change = [this](auto) {
text_raw_to_char.set(std::string{static_cast<char>(field_raw.value())});
};
}
void AlphanumView::set_mode(const uint32_t new_mode) {
size_t n = 0;
if (new_mode < 3)
void AlphanumView::set_mode(uint32_t new_mode, ShiftMode new_shift_mode) {
if (new_mode < std::size(key_sets))
mode = new_mode;
else
mode = 0;
const char* key_list = key_sets[mode].second;
shift_mode = new_shift_mode;
refresh_keys();
if (mode + 1 < std::size(key_sets))
button_mode.set_text(key_sets[mode + 1].name);
else
button_mode.set_text(key_sets[0].name);
}
void AlphanumView::refresh_keys() {
auto key_list = shift_mode == ShiftMode::None
? key_sets[mode].normal
: key_sets[mode].shifted;
size_t n = 0;
for (auto& button : buttons) {
const std::string label{
key_list[n]};
button.set_text(label);
button.set_text(std::string{key_list[n]});
n++;
}
if (mode < 2)
button_mode.set_text(key_sets[mode + 1].first);
else
button_mode.set_text(key_sets[0].first);
switch (shift_mode) {
case ShiftMode::None:
button_shift.set_color(Color::dark_grey());
break;
case ShiftMode::Shift:
button_shift.set_color(Color::black());
break;
case ShiftMode::ShiftLock:
button_shift.set_color(Color::dark_blue());
break;
}
}
void AlphanumView::on_button(Button& button) {
const auto c = button.text()[0];
char_add(c);
// TODO: Consolidate shift handling.
if (shift_mode == ShiftMode::Shift) {
shift_mode = ShiftMode::None;
refresh_keys();
}
}
bool AlphanumView::on_encoder(const EncoderEvent delta) {
+36 -12
View File
@@ -29,6 +29,10 @@
#include "ui_textentry.hpp"
#include "ui_menu.hpp"
// TODO: Building this as a custom widget instead of using
// all the Button controls would save a considerable amount of RAM.
// The Buttons each have a backing string but these only need one char.
namespace ui {
class AlphanumView : public TextEntryView {
@@ -43,22 +47,42 @@ class AlphanumView : public TextEntryView {
bool on_encoder(const EncoderEvent delta) override;
private:
const char* const keys_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ, ._";
const char* const keys_lower = "abcdefghijklmnopqrstuvwxyz, ._";
const char* const keys_digit = "0123456789!\"#'()*+-/:;=<>@[\\]?";
enum class ShiftMode : uint8_t {
None,
Shift,
ShiftLock,
};
const std::pair<std::string, const char*> key_sets[3] = {
{"ABC", keys_upper},
{"abc", keys_lower},
{"123", keys_digit}};
const char* const keys_lower = "abcdefghijklmnopqrstuvwxyz, .";
const char* const keys_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ, .";
const char* const keys_digit = "1234567890()'`\"+-*/=<>_\\!?, .";
const char* const keys_symbl = "!@#$%^&*()[]'`\"{}|:;<>-_~?, .";
struct key_set_t {
const char* name;
const char* normal;
const char* shifted;
};
const key_set_t key_sets[2] = {
{"abc", keys_lower, keys_upper},
{"123", keys_digit, keys_symbl}};
int16_t focused_button = 0;
uint32_t mode = 0; // Uppercase
uint32_t mode = 0; // Letters.
ShiftMode shift_mode = ShiftMode::None;
void set_mode(const uint32_t new_mode);
void set_mode(uint32_t new_mode, ShiftMode new_shift_mode = ShiftMode::None);
void refresh_keys();
void on_button(Button& button);
std::array<Button, 30> buttons{};
std::array<Button, 29> buttons{};
NewButton button_shift{
{192, 214, screen_width / 5, 38},
{},
&bitmap_icon_shift,
Color::dark_grey()};
Labels labels{
{{1 * 8, 33 * 8}, "Raw:", Color::light_grey()},
@@ -76,11 +100,11 @@ class AlphanumView : public TextEntryView {
"0"};
Button button_delete{
{9 * 8, 33 * 8, 6 * 8, 32},
{9 * 8, 32 * 8 - 3, 7 * 8, 3 * 16 + 3},
"<DEL"};
Button button_mode{
{16 * 8, 33 * 8, 5 * 8, 32},
{16 * 8, 32 * 8 - 3, 6 * 8, 3 * 16 + 3},
""};
};
+24
View File
@@ -32,6 +32,7 @@ using namespace portapack;
#include "string_format.hpp"
#include "complex.hpp"
#include "ui_styles.hpp"
#include "ui_font_fixed_5x8.hpp"
namespace ui {
@@ -243,6 +244,7 @@ void GeoMap::paint(Painter& painter) {
// Draw the other markers
draw_markers(painter);
draw_scale(painter);
markerListUpdated = false;
set_clean();
}
@@ -283,6 +285,10 @@ void GeoMap::move(const float lon, const float lat) {
x_pos = map_width - map_rect.width();
if (y_pos > (map_height + map_rect.height()))
y_pos = map_height - map_rect.height();
// Scale calculation
float km_per_deg_lon = cos(lat * pi / 180) * 111.321; // 111.321 km/deg longitude at equator, and 0 km at poles
pixels_per_km = (map_rect.width() / 2) / km_per_deg_lon;
}
bool GeoMap::init() {
@@ -318,6 +324,24 @@ bool GeoMap::manual_panning() {
return manual_panning_;
}
void GeoMap::draw_scale(Painter& painter) {
uint16_t km = 100;
uint16_t scale_width = km * pixels_per_km * map_zoom;
while (scale_width > screen_width / 2) {
scale_width /= 2;
km /= 2;
}
std::string km_string = to_string_dec_uint(km) + " km";
display.fill_rectangle({{screen_width - 5 - scale_width, screen_height - 4}, {scale_width, 2}}, Color::black());
display.fill_rectangle({{screen_width - 5, screen_height - 8}, {2, 6}}, Color::black());
display.fill_rectangle({{screen_width - 5 - scale_width, screen_height - 8}, {2, 6}}, Color::black());
painter.draw_string({(uint16_t)(screen_width - 25 - scale_width - km_string.length() * 5 / 2), screen_height - 10}, ui::font::fixed_5x8, Color::black(), Color::white(), km_string);
}
void GeoMap::draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color) {
Point arrow_a, arrow_b, arrow_c;
+2
View File
@@ -179,6 +179,7 @@ class GeoMap : public Widget {
MapMarkerStored store_marker(GeoMarker& marker);
private:
void draw_scale(Painter& painter);
void draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color);
void draw_marker(Painter& painter, const ui::Point itemPoint, const uint16_t itemAngle, const std::string itemTag, const Color color = Color::red(), const Color fontColor = Color::white(), const Color backColor = Color::black());
void draw_markers(Painter& painter);
@@ -199,6 +200,7 @@ class GeoMap : public Widget {
int32_t prev_x_pos{0xFFFF}, prev_y_pos{0xFFFF};
float lat_{};
float lon_{};
float pixels_per_km{};
uint16_t angle_{};
std::string tag_{};
+125 -29
View File
@@ -20,16 +20,15 @@
* Boston, MA 02110-1301, USA.
*/
#include "irq_controls.hpp"
#include "max283x.hpp"
#include "portapack.hpp"
#include "string_format.hpp"
#include "ui_receiver.hpp"
#include "ui_freqman.hpp"
#include "portapack.hpp"
using namespace portapack;
#include "string_format.hpp"
#include "max283x.hpp"
namespace ui {
/* FrequencyField ********************************************************/
@@ -38,15 +37,26 @@ FrequencyField::FrequencyField(
const Point parent_pos)
: Widget{{parent_pos, {8 * 10, 16}}},
length_{11},
range(rf::tuning_range) {
range_{rf::tuning_range} {
initial_switch_config_ = get_switches_long_press_config();
set_focusable(true);
}
FrequencyField::~FrequencyField() {
reset_switch_config();
}
rf::Frequency FrequencyField::value() const {
return value_;
}
void FrequencyField::set_value(rf::Frequency new_value) {
// While in digit mode, don't update if the value is
// out of range. That way going out of range doesn't
// cause all the other digits to reset.
if (digit_mode_ && !range_.contains_inc(new_value))
return;
new_value = clamp_value(new_value);
if (new_value != value_) {
@@ -59,48 +69,98 @@ void FrequencyField::set_value(rf::Frequency new_value) {
}
void FrequencyField::set_step(rf::Frequency new_value) {
step = new_value;
// TODO: Quantize current frequency to a step of the new size?
step_ = new_value;
}
void FrequencyField::set_allow_digit_mode(bool allowed) {
allow_digit_mode_ = allowed;
if (!allowed && digit_mode_) {
digit_mode_ = false;
reset_switch_config();
set_dirty();
}
}
void FrequencyField::paint(Painter& painter) {
const std::string str_value = to_string_short_freq(value_);
const auto str_value = to_string_short_freq(value_);
const auto paint_style = has_focus() ? style().invert() : style();
painter.draw_string(
screen_pos(),
paint_style,
str_value);
// Highlight current digit in digit_mode.
if (digit_mode_) {
auto p = screen_pos();
p += {digit_ * char_width, 0};
painter.draw_char(p, Styles::bg_blue, str_value[digit_]);
}
}
bool FrequencyField::on_key(const ui::KeyEvent event) {
if (event == ui::KeyEvent::Select) {
bool FrequencyField::on_key(KeyEvent event) {
if (event == KeyEvent::Select) {
// Toggle 'digit' mode with long-press.
if (digit_mode_ || key_is_long_pressed(event)) {
digit_mode_ = !digit_mode_;
set_dirty();
return true;
}
if (on_edit) {
on_edit();
return true;
}
}
if (digit_mode_) {
constexpr uint8_t decimal_pos = 4;
int8_t delta = 0;
switch (event) {
case KeyEvent::Up:
set_value(value_ + digit_step());
break;
case KeyEvent::Down:
set_value(value_ - digit_step());
break;
case KeyEvent::Left:
delta = -1;
break;
case KeyEvent::Right:
delta = 1;
break;
default:
return false;
}
if (delta != 0) {
digit_ += delta;
// If on decimal, skip it.
if (digit_ == decimal_pos)
digit_ += delta;
// Otherwise ensure in bounds.
else
digit_ = clip<int8_t>(digit_, 0, 8);
set_dirty();
}
return true;
}
return false;
}
bool FrequencyField::on_encoder(const EncoderEvent delta) {
if (step == 0) { // 'Auto' mode.'
auto ms = RTT2MS(halGetCounterValue());
auto delta_ms = last_ms_ <= ms ? ms - last_ms_ : ms;
last_ms_ = ms;
if (digit_mode_)
set_value(value_ + (delta * digit_step()));
else
set_value(value_ + (delta * step_));
// The goal is to map 'scale' to a range of about 10 to 10M.
// The faster the encoder is rotated, the larger the step.
// Linear doesn't feel right. Hyperbolic felt better.
// To get these magic numbers, I graphed the function until the
// curve shape seemed about right then tested on device.
delta_ms = std::min(145ull, delta_ms) + 5; // Prevent DIV/0
int64_t scale = 200'000'000 / (0.001'55 * std::pow(delta_ms, 5.45)) + 8;
set_value(value() + (delta * scale));
} else {
set_value(value() + (delta * step));
}
return true;
}
@@ -112,13 +172,49 @@ bool FrequencyField::on_touch(const TouchEvent event) {
}
void FrequencyField::on_focus() {
if (on_show_options) {
if (on_show_options)
on_show_options();
}
enable_switch_config();
}
void FrequencyField::on_blur() {
reset_switch_config();
}
rf::Frequency FrequencyField::digit_step() const {
constexpr rf::Frequency steps[] = {
1'000'000'000,
100'000'000,
10'000'000,
1'000'000,
0, // Decimal point position.
100'000,
10'000,
1'000,
100,
};
return digit_ < std::size(steps) ? steps[digit_] : 0;
}
rf::Frequency FrequencyField::clamp_value(rf::Frequency value) {
return range.clip(value);
return range_.clip(value);
}
void FrequencyField::enable_switch_config() {
// Don't enable long press testing when not allowed.
if (!allow_digit_mode_)
return;
// Enable long press on "Select".
SwitchesState config;
config[toUType(Switch::Sel)] = true;
set_switches_long_press_config(config);
}
void FrequencyField::reset_switch_config() {
set_switches_long_press_config(initial_switch_config_);
}
/* FrequencyKeypadView ***************************************************/
+18 -4
View File
@@ -27,6 +27,7 @@
#include "ui_painter.hpp"
#include "ui_widget.hpp"
#include "irq_controls.hpp"
#include "rf_path.hpp"
#include <cstddef>
@@ -45,27 +46,41 @@ class FrequencyField : public Widget {
using range_t = rf::FrequencyRange;
FrequencyField(Point parent_pos);
~FrequencyField();
rf::Frequency value() const;
void set_value(rf::Frequency new_value);
void set_step(rf::Frequency new_value);
void set_allow_digit_mode(bool allowed);
void paint(Painter& painter) override;
bool on_key(ui::KeyEvent event) override;
bool on_key(KeyEvent event) override;
bool on_encoder(EncoderEvent delta) override;
bool on_touch(TouchEvent event) override;
void on_focus() override;
void on_blur() override;
private:
const size_t length_;
const range_t range;
const range_t range_;
rf::Frequency value_{0};
rf::Frequency step{25000};
rf::Frequency step_{25000};
uint64_t last_ms_{0};
uint8_t digit_{3};
bool digit_mode_{false};
bool allow_digit_mode_{true};
SwitchesState initial_switch_config_{};
/* Gets the step value for the given digit when in digit_mode. */
rf::Frequency digit_step() const;
rf::Frequency clamp_value(rf::Frequency value);
void enable_switch_config();
void reset_switch_config();
};
template <size_t N>
@@ -242,7 +257,6 @@ class FrequencyStepView : public OptionsField {
parent_pos,
5,
{
{" Auto", 0}, /* Faster == larger step. */
{" 10", 10}, /* Fine tuning SSB voice pitch,in HF and QO-100 sat #669 */
{" 50", 50}, /* added 50Hz/10Hz,but we do not increase GUI digit decimal */
{" 100", 100},
+16 -4
View File
@@ -308,13 +308,25 @@ WaterfallView::WaterfallView(const bool cursor) {
}
void WaterfallView::on_show() {
// TODO: Assert that baseband is not shutdown.
baseband::spectrum_streaming_start();
start();
}
void WaterfallView::on_hide() {
// TODO: Assert that baseband is not shutdown.
baseband::spectrum_streaming_stop();
stop();
}
void WaterfallView::start() {
if (!running_) {
baseband::spectrum_streaming_start();
running_ = true;
}
}
void WaterfallView::stop() {
if (running_) {
baseband::spectrum_streaming_stop();
running_ = false;
}
}
void WaterfallView::show_audio_spectrum_view(const bool show) {
+5 -1
View File
@@ -131,11 +131,14 @@ class WaterfallView : public View {
WaterfallView& operator=(const WaterfallView&) = delete;
WaterfallView& operator=(WaterfallView&&) = delete;
// TODO: remove these, use start/stop directly instead.
void on_show() override;
void on_hide() override;
void set_parent_rect(const Rect new_parent_rect) override;
void start();
void stop();
void set_parent_rect(const Rect new_parent_rect) override;
void show_audio_spectrum_view(const bool show);
private:
@@ -147,6 +150,7 @@ class WaterfallView : public View {
WaterfallWidget waterfall_widget{};
FrequencyScale frequency_scale{};
bool running_{false};
ChannelSpectrumFIFO* channel_fifo{nullptr};
AudioSpectrum* audio_spectrum_data{nullptr};
-2
View File
@@ -21,9 +21,7 @@
*/
#include "ui_textentry.hpp"
//#include "portapack_persistent_memory.hpp"
#include "ui_alphanum.hpp"
//#include "ui_handwrite.hpp"
using namespace portapack;
+2 -2
View File
@@ -48,9 +48,9 @@ class TextEntryView : public View {
void char_add(const char c);
void char_delete();
TextField text_input;
TextEdit text_input;
Button button_ok{
{22 * 8, 33 * 8, 7 * 8, 32},
{22 * 8, 32 * 8 - 3, 8 * 8, 3 * 16 + 3},
"OK"};
};
+2 -7
View File
@@ -497,13 +497,8 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) {
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
{
"ADS-B",
Color::green(),
&bitmap_icon_adsb,
[&nav]() { nav.push<ADSBRxView>(); },
},
//{ "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>(); }},
+40 -24
View File
@@ -26,6 +26,7 @@ using namespace portapack;
#include "io_file.hpp"
#include "io_wave.hpp"
#include "io_convert.hpp"
#include "baseband_api.hpp"
#include "metadata_file.hpp"
@@ -100,24 +101,27 @@ void RecordView::focus() {
button_record.focus();
}
void RecordView::set_sampling_rate(const size_t new_sampling_rate) {
/* We are changing "REC" icon background to yellow in BW rec Options >600kHz
void RecordView::set_sampling_rate(size_t new_sampling_rate, OversampleRate new_oversample_rate) {
/* We are changing "REC" icon background to yellow in BW rec Options >600kHz
where we are NOT recording full IQ .C16 files (recorded files are decimated ones).
Those decimated recorded files,has not the full IQ samples .
are ok as recorded spectrum indication, but they should not be used by Replay app.
Those decimated recorded files, has not the full IQ samples.
are ok as recorded spectrum indication, but they should not be used by Replay app.
We keep original black background in all the correct IQ .C16 files BW's Options */
if (new_sampling_rate > 4800000) { // > BW >600kHz (fs=8*BW), (750kHz ...2750kHz)
We keep original black background in all the correct IQ .C16 files BW's Options */
if (new_sampling_rate > 4'800'000) { // > BW >600kHz (fs=8*BW), (750kHz...2750kHz)
button_record.set_background(ui::Color::yellow());
} else {
button_record.set_background(ui::Color::black());
}
if (new_sampling_rate != sampling_rate) {
if (new_sampling_rate != sampling_rate ||
new_oversample_rate != oversample_rate) {
stop();
sampling_rate = new_sampling_rate;
oversample_rate = new_oversample_rate;
baseband::set_sample_rate(sampling_rate);
baseband::set_oversample_rate(oversample_rate);
button_record.hidden(sampling_rate == 0);
text_record_filename.hidden(sampling_rate == 0);
@@ -161,12 +165,13 @@ void RecordView::start() {
rtcGetTime(&RTCD1, &datetime);
// ISO 8601
std::string date_time = to_string_dec_uint(datetime.year(), 4, '0') +
to_string_dec_uint(datetime.month(), 2, '0') +
to_string_dec_uint(datetime.day(), 2, '0') + "T" +
to_string_dec_uint(datetime.hour()) +
to_string_dec_uint(datetime.minute()) +
to_string_dec_uint(datetime.second());
std::string date_time =
to_string_dec_uint(datetime.year(), 4, '0') +
to_string_dec_uint(datetime.month(), 2, '0') +
to_string_dec_uint(datetime.day(), 2, '0') + "T" +
to_string_dec_uint(datetime.hour()) +
to_string_dec_uint(datetime.minute()) +
to_string_dec_uint(datetime.second());
base_path = filename_stem_pattern.string() + "_" + date_time + "_" +
trim(to_string_freq(receiver_model.target_frequency())) + "Hz";
@@ -194,18 +199,18 @@ void RecordView::start() {
}
} break;
case FileType::RawS8:
case FileType::RawS16: {
const auto metadata_file_error =
write_metadata_file(get_metadata_path(base_path),
{receiver_model.target_frequency(), sampling_rate / 8});
// Not sure why sample_rate is div. 8, but stored value matches rate settings.
const auto metadata_file_error = write_metadata_file(
get_metadata_path(base_path),
{receiver_model.target_frequency(), sampling_rate / toUType(oversample_rate)});
if (metadata_file_error.is_valid()) {
handle_error(metadata_file_error.value());
return;
}
auto p = std::make_unique<RawFileWriter>();
auto create_error = p->create(base_path.replace_extension(u".C16"));
auto p = std::make_unique<FileConvertWriter>();
auto create_error = p->create(base_path.replace_extension((file_type == FileType::RawS8) ? u".C8" : u".C16"));
if (create_error.is_valid()) {
handle_error(create_error.value());
} else {
@@ -261,13 +266,24 @@ void RecordView::update_status_display() {
text_record_dropped.set(s);
}
/*if (pitch_rssi_enabled) {
button_pitch_rssi.invert_colors();
}*/
/*
if (pitch_rssi_enabled) {
button_pitch_rssi.invert_colors();
}
*/
if (sampling_rate) {
if (sampling_rate > 0) {
const auto space_info = std::filesystem::space(u"");
const uint32_t bytes_per_second = file_type == FileType::WAV ? (sampling_rate * 2) : (sampling_rate / 8 * 4); // TODO: Why 8/4??
// - Audio is 1 int16_t per sample or '2' bytes per sample.
// - C8 captures 2 (I,Q) int8_t per sample or '2' bytes per sample.
// - C16 captures 2 (I,Q) int16_t per sample or '4' bytes per sample.
const auto bytes_per_sample = file_type == FileType::RawS16 ? 4 : 2;
// WAV files are not oversampled, but C8 and C16 are. Divide by the
// oversample rate to get the effective sample rate.
const auto effective_sampling_rate = file_type == FileType::WAV
? sampling_rate
: sampling_rate / toUType(oversample_rate);
const uint32_t bytes_per_second = effective_sampling_rate * bytes_per_sample;
const uint32_t available_seconds = space_info.free / bytes_per_second;
const uint32_t seconds = available_seconds % 60;
const uint32_t available_minutes = available_seconds / 60;
+15 -2
View File
@@ -40,6 +40,7 @@ class RecordView : public View {
std::function<void(std::string)> on_error{};
enum FileType {
RawS8 = 1,
RawS16 = 2,
WAV = 3,
};
@@ -55,7 +56,18 @@ class RecordView : public View {
void focus() override;
void set_sampling_rate(const size_t new_sampling_rate);
/* Sets the sampling rate and the oversampling "decimation" rate.
* These values are passed down to the baseband proc_capture. For
* Audio (WAV) recording, the OversampleRate should not be
* specified and the default will be used. */
/* TODO: Currently callers are expected to have already multiplied the
* sample_rate with the oversample rate. It would be better move that
* logic to a single place. */
void set_sampling_rate(
size_t new_sampling_rate,
OversampleRate new_oversample_rate = OversampleRate::Rate8x);
void set_file_type(const FileType v) { file_type = v; }
void start();
void stop();
@@ -83,10 +95,11 @@ class RecordView : public View {
const std::filesystem::path filename_stem_pattern;
const std::filesystem::path folder;
const FileType file_type;
FileType file_type;
const size_t write_size;
const size_t buffer_count;
size_t sampling_rate{0};
OversampleRate oversample_rate{OversampleRate::Rate8x};
SignalToken signal_token_tick_second{};
Rectangle rect_background{
+26 -18
View File
@@ -46,24 +46,34 @@ Thread* BasebandThread::thread = nullptr;
BasebandThread::BasebandThread(
uint32_t sampling_rate,
BasebandProcessor* const baseband_processor,
const tprio_t priority,
baseband::Direction direction)
: baseband_processor{baseband_processor},
_direction{direction},
sampling_rate{sampling_rate} {
thread = chThdCreateStatic(baseband_thread_wa, sizeof(baseband_thread_wa),
priority, ThreadBase::fn,
this);
baseband::Direction direction,
bool auto_start,
tprio_t priority)
: baseband_processor_{baseband_processor},
direction_{direction},
sampling_rate_{sampling_rate},
priority_{priority} {
if (auto_start) start();
}
BasebandThread::~BasebandThread() {
chThdTerminate(thread);
chThdWait(thread);
thread = nullptr;
if (thread) {
chThdTerminate(thread);
chThdWait(thread);
thread = nullptr;
}
}
void BasebandThread::start() {
if (!thread) {
thread = chThdCreateStatic(
baseband_thread_wa, sizeof(baseband_thread_wa),
priority_, ThreadBase::fn, this);
}
}
void BasebandThread::set_sampling_rate(uint32_t new_sampling_rate) {
sampling_rate = new_sampling_rate;
sampling_rate_ = new_sampling_rate;
}
void BasebandThread::run() {
@@ -71,9 +81,7 @@ void BasebandThread::run() {
baseband::dma::init();
const auto baseband_buffer = std::make_unique<std::array<baseband::sample_t, 8192>>();
baseband::dma::configure(
baseband_buffer->data(),
direction());
baseband::dma::configure(baseband_buffer->data(), direction());
// baseband::dma::allocate(4, 2048);
baseband_sgpio.configure(direction());
@@ -85,10 +93,10 @@ void BasebandThread::run() {
const auto buffer_tmp = baseband::dma::wait_for_buffer();
if (buffer_tmp) {
buffer_c8_t buffer{
buffer_tmp.p, buffer_tmp.count, sampling_rate};
buffer_tmp.p, buffer_tmp.count, sampling_rate_};
if (baseband_processor) {
baseband_processor->execute(buffer);
if (baseband_processor_) {
baseband_processor_->execute(buffer);
}
}
}
+16 -6
View File
@@ -28,13 +28,20 @@
#include <ch.h>
/* NB: Because ThreadBase threads start when then are initialized (by default),
* they should be the last members in a Processor class to ensure the rest of the
* members are fully initialized before data handling starts. If the Procressor
* needs to do additional initialization (in its ctor), set 'auto_start' to false
* and manually call 'start()' on the thread. */
class BasebandThread : public ThreadBase {
public:
BasebandThread(
uint32_t sampling_rate,
BasebandProcessor* const baseband_processor,
const tprio_t priority,
const baseband::Direction direction = baseband::Direction::Receive);
baseband::Direction direction,
bool auto_start = true,
tprio_t priority = (NORMALPRIO + 20));
~BasebandThread();
BasebandThread(const BasebandThread&) = delete;
@@ -42,10 +49,12 @@ class BasebandThread : public ThreadBase {
BasebandThread& operator=(const BasebandThread&) = delete;
BasebandThread& operator=(BasebandThread&&) = delete;
void start() override;
// This getter should die, it's just here to leak information to code that
// isn't in the right place to begin with.
baseband::Direction direction() const {
return _direction;
return direction_;
}
void set_sampling_rate(uint32_t new_sampling_rate);
@@ -53,9 +62,10 @@ class BasebandThread : public ThreadBase {
private:
static Thread* thread;
BasebandProcessor* baseband_processor{nullptr};
baseband::Direction _direction{baseband::Direction::Receive};
uint32_t sampling_rate{0};
BasebandProcessor* baseband_processor_;
baseband::Direction direction_;
uint32_t sampling_rate_;
const tprio_t priority_;
void run() override;
};
+10 -9
View File
@@ -19,21 +19,18 @@
* Boston, MA 02110-1301, USA.
*/
#include "event_m4.hpp"
#include "debug.hpp"
#include "portapack_shared_memory.hpp"
#include "message_queue.hpp"
#include "ch.h"
#include "debug.hpp"
#include "event_m4.hpp"
#include "lpc43xx_cpp.hpp"
using namespace lpc43xx;
#include "message_queue.hpp"
#include "portapack_shared_memory.hpp"
#include <cstdint>
#include <array>
using namespace lpc43xx;
extern "C" {
CH_IRQ_HANDLER(MAPP_IRQHandler) {
@@ -61,6 +58,10 @@ void EventDispatcher::run() {
lpc43xx::creg::m0apptxevent::enable();
// Indicate to the M0 thread that
// M4 is ready to receive message events.
shared_memory.set_baseband_ready();
while (is_running) {
const auto events = wait();
dispatch(events);
+1
View File
@@ -32,6 +32,7 @@ ACARSProcessor::ACARSProcessor() {
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
decim_1.configure(taps_11k0_decim_1.taps, 131072);
packet.clear();
baseband_thread.start();
}
void ACARSProcessor::execute(const buffer_c8_t& buffer) {
+5 -3
View File
@@ -110,9 +110,6 @@ class ACARSProcessor : public BasebandProcessor {
private:
static constexpr size_t baseband_fs = 2457600;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
@@ -138,6 +135,11 @@ class ACARSProcessor : public BasebandProcessor {
};*/
baseband::Packet packet{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
RSSIThread rssi_thread{};
void consume_symbol(const float symbol);
void payload_handler(const baseband::Packet& packet);
};
+4 -4
View File
@@ -36,15 +36,11 @@ using namespace adsb;
class ADSBRXProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
static constexpr size_t baseband_fs = 2000000;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
ADSBFrame frame{};
bool configured{false};
uint32_t prev_mag{0};
@@ -58,6 +54,10 @@ class ADSBRXProcessor : public BasebandProcessor {
uint32_t sample{0};
int32_t re{}, im{};
int32_t amp{0};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
};
#endif
+3 -3
View File
@@ -29,14 +29,11 @@
class ADSBTXProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const p) override;
private:
bool configured = false;
BasebandThread baseband_thread{4000000, this, NORMALPRIO + 20, baseband::Direction::Transmit};
const complex8_t am_lut[4] = {
{127, 0},
{0, 127},
@@ -48,6 +45,9 @@ class ADSBTXProcessor : public BasebandProcessor {
uint32_t phase{0};
TXProgressMessage txprogress_message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{4000000, this, baseband::Direction::Transmit};
};
#endif
+3 -3
View File
@@ -32,14 +32,11 @@
class AFSKProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const msg) override;
private:
bool configured = false;
BasebandThread baseband_thread{AFSK_SAMPLERATE, this, NORMALPRIO + 20, baseband::Direction::Transmit};
uint32_t afsk_samples_per_bit{0};
uint32_t afsk_phase_inc_mark{0};
uint32_t afsk_phase_inc_space{0};
@@ -59,6 +56,9 @@ class AFSKProcessor : public BasebandProcessor {
int8_t re{0}, im{0};
TXProgressMessage txprogress_message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{AFSK_SAMPLERATE, this, baseband::Direction::Transmit};
};
#endif
+6 -6
View File
@@ -38,7 +38,6 @@
class AFSKRxProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
@@ -53,9 +52,6 @@ class AFSKRxProcessor : public BasebandProcessor {
RECEIVE
};
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
@@ -93,9 +89,13 @@ class AFSKRxProcessor : public BasebandProcessor {
bool trigger_word{};
bool triggered{};
void configure(const AFSKRxConfigureMessage& message);
AFSKDataMessage data_message{false, 0};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
void configure(const AFSKRxConfigureMessage& message);
};
#endif /*__PROC_TPMS_H__*/
+1
View File
@@ -30,6 +30,7 @@
AISProcessor::AISProcessor() {
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
decim_1.configure(taps_11k0_decim_1.taps, 131072);
baseband_thread.start();
}
void AISProcessor::execute(const buffer_c8_t& buffer) {
+5 -3
View File
@@ -51,9 +51,6 @@ class AISProcessor : public BasebandProcessor {
private:
static constexpr size_t baseband_fs = 2457600;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
@@ -77,6 +74,11 @@ class AISProcessor : public BasebandProcessor {
this->payload_handler(packet);
}};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
RSSIThread rssi_thread{};
void consume_symbol(const float symbol);
void payload_handler(const baseband::Packet& packet);
};
+5 -5
View File
@@ -38,7 +38,6 @@
class NarrowbandAMAudio : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
@@ -46,9 +45,6 @@ class NarrowbandAMAudio : public BasebandProcessor {
static constexpr size_t decim_2_decimation_factor = 4;
static constexpr size_t channel_filter_decimation_factor = 1;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
@@ -65,6 +61,7 @@ class NarrowbandAMAudio : public BasebandProcessor {
int32_t channel_filter_low_f = 0;
int32_t channel_filter_high_f = 0;
int32_t channel_filter_transition = 0;
bool configured{false};
bool modulation_ssb = false;
dsp::demodulate::AM demod_am{};
@@ -74,7 +71,10 @@ class NarrowbandAMAudio : public BasebandProcessor {
SpectrumCollector channel_spectrum{};
bool configured{false};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
void configure(const AMConfigureMessage& message);
void capture_config(const CaptureConfigMessage& message);
+5 -5
View File
@@ -38,15 +38,11 @@
class WidebandFMAudio : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
static constexpr size_t baseband_fs = 2000000;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
@@ -55,8 +51,12 @@ class WidebandFMAudio : public BasebandProcessor {
AudioSpectrum audio_spectrum{};
TvCollector channel_spectrum{};
std::array<complex16_t, 256> spectrum{};
bool configured{false};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
void configure(const WFMConfigureMessage& message);
};
+4 -4
View File
@@ -75,7 +75,6 @@ static uint16_t crc_ccitt_tab[256] = {
class APRSRxProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
@@ -90,9 +89,6 @@ class APRSRxProcessor : public BasebandProcessor {
IN_FRAME
};
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
@@ -135,6 +131,10 @@ class APRSRxProcessor : public BasebandProcessor {
aprs::APRSPacket aprs_packet{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
void configure(const APRSRxConfigureMessage& message);
void capture_config(const CaptureConfigMessage& message);
void parse_packet();
+3 -2
View File
@@ -37,8 +37,6 @@ class AudioTXProcessor : public BasebandProcessor {
private:
static constexpr size_t baseband_fs = 1536000;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Transmit};
std::unique_ptr<StreamOutput> stream{};
ToneGen tone_gen{};
@@ -61,6 +59,9 @@ class AudioTXProcessor : public BasebandProcessor {
TXProgressMessage txprogress_message{};
RequestSignalMessage sig_message{RequestSignalMessage::Signal::FillRequest};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Transmit};
};
#endif
+5 -6
View File
@@ -39,16 +39,12 @@
class BTLERxProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
static constexpr size_t baseband_fs = 4000000;
static constexpr size_t audio_fs = baseband_fs / 8 / 8 / 2;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
@@ -81,10 +77,13 @@ class BTLERxProcessor : public BasebandProcessor {
int RB_SIZE{1000};
bool configured{false};
AFSKDataMessage data_message{false, 0};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
void configure(const BTLERxConfigureMessage& message);
AFSKDataMessage data_message{false, 0};
};
#endif /*__PROC_BTLERX_H__*/
+36 -9
View File
@@ -23,21 +23,21 @@
#include "proc_capture.hpp"
#include "dsp_fir_taps.hpp"
#include "event_m4.hpp"
#include "utility.hpp"
CaptureProcessor::CaptureProcessor() {
decim_0.configure(taps_200k_decim_0.taps, 33554432);
decim_0_4.configure(taps_200k_decim_0.taps, 33554432);
decim_0_8.configure(taps_200k_decim_0.taps, 33554432);
decim_1.configure(taps_200k_decim_1.taps, 131072);
channel_spectrum.set_decimation_factor(1);
baseband_thread.start();
}
void CaptureProcessor::execute(const buffer_c8_t& buffer) {
/* 2.4576MHz, 2048 samples */
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
const auto decim_0_out = decim_0_execute(buffer, dst_buffer);
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
const auto& decimator_out = decim_1_out;
const auto& channel = decimator_out;
@@ -66,9 +66,19 @@ void CaptureProcessor::on_message(const Message* const message) {
channel_spectrum.on_message(message);
break;
case Message::ID::SamplerateConfig:
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message));
case Message::ID::SamplerateConfig: {
auto config = reinterpret_cast<const SamplerateConfigMessage*>(message);
baseband_fs = config->sample_rate;
update_for_rate_change();
break;
}
case Message::ID::OversampleRateConfig: {
auto config = reinterpret_cast<const OversampleRateConfigMessage*>(message);
oversample_rate = config->oversample_rate;
update_for_rate_change();
break;
}
case Message::ID::CaptureConfig:
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
@@ -79,11 +89,14 @@ void CaptureProcessor::on_message(const Message* const message) {
}
}
void CaptureProcessor::samplerate_config(const SamplerateConfigMessage& message) {
baseband_fs = message.sample_rate;
void CaptureProcessor::update_for_rate_change() {
baseband_thread.set_sampling_rate(baseband_fs);
size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor;
auto decim_0_factor = oversample_rate == OversampleRate::Rate8x
? decim_0_4.decimation_factor
: decim_0_8.decimation_factor;
size_t decim_0_output_fs = baseband_fs / decim_0_factor;
size_t decim_1_input_fs = decim_0_output_fs;
size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
@@ -104,6 +117,20 @@ void CaptureProcessor::capture_config(const CaptureConfigMessage& message) {
}
}
buffer_c16_t CaptureProcessor::decim_0_execute(const buffer_c8_t& src, const buffer_c16_t& dst) {
switch (oversample_rate) {
case OversampleRate::Rate8x:
return decim_0_4.execute(src, dst);
case OversampleRate::Rate16x:
return decim_0_8.execute(src, dst);
default:
chDbgPanic("Unhandled OversampleRate");
return {};
}
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<CaptureProcessor>()};
event_dispatcher.run();
+19 -10
View File
@@ -28,9 +28,7 @@
#include "rssi_thread.hpp"
#include "dsp_decimate.hpp"
#include "spectrum_collector.hpp"
#include "stream_input.hpp"
#include <array>
@@ -41,23 +39,24 @@ class CaptureProcessor : public BasebandProcessor {
CaptureProcessor();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
// TODO: Repeated value needs to be transmitted from application side.
size_t baseband_fs = 0;
size_t baseband_fs = 3072000;
static constexpr auto spectrum_rate_hz = 50.0f;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
/* NB: There are two decimation passes: 0 and 1. In pass 0, one of
* the following will be selected based on the oversample rate.
* use decim_0_4 for an overall decimation factor of 8.
* use decim_0_8 for an overall decimation factor of 16. */
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0_4{};
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_8{};
dsp::decimate::FIRC16xR16x16Decim2 decim_1{};
int32_t channel_filter_low_f = 0;
int32_t channel_filter_high_f = 0;
@@ -68,9 +67,19 @@ class CaptureProcessor : public BasebandProcessor {
SpectrumCollector channel_spectrum{};
size_t spectrum_interval_samples = 0;
size_t spectrum_samples = 0;
OversampleRate oversample_rate{OversampleRate::Rate8x};
void samplerate_config(const SamplerateConfigMessage& message);
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
RSSIThread rssi_thread{};
/* Called to update members when the sample rate or oversample rate is changed. */
void update_for_rate_change();
void capture_config(const CaptureConfigMessage& message);
/* Dispatch to the correct decim_0 based on oversample rate. */
buffer_c16_t decim_0_execute(const buffer_c8_t& src, const buffer_c16_t& dst);
};
#endif /*__PROC_CAPTURE_HPP__*/
+4 -3
View File
@@ -67,9 +67,6 @@ class ERTProcessor : public BasebandProcessor {
const size_t samples_per_symbol = channel_sampling_rate / symbol_rate;
const float clock_recovery_rate = symbol_rate * 2;
BasebandThread baseband_thread{baseband_sampling_rate, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
clock_recovery::ClockRecovery<clock_recovery::FixedErrorFilter> clock_recovery{
clock_recovery_rate,
symbol_rate,
@@ -116,6 +113,10 @@ class ERTProcessor : public BasebandProcessor {
float offset_i{0.0f};
float offset_q{0.0f};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_sampling_rate, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
float abs(const complex8_t& v);
};
+3 -3
View File
@@ -29,14 +29,11 @@
class FSKProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const p) override;
private:
bool configured = false;
BasebandThread baseband_thread{2280000, this, NORMALPRIO + 20, baseband::Direction::Transmit};
uint32_t samples_per_bit{0};
uint32_t length{0};
@@ -48,6 +45,9 @@ class FSKProcessor : public BasebandProcessor {
uint32_t phase{0}, sphase{0};
TXProgressMessage txprogress_message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{2280000, this, baseband::Direction::Transmit};
};
#endif
+27 -27
View File
@@ -29,7 +29,7 @@
#include "utility.hpp"
ReplayProcessor::ReplayProcessor() {
GPSReplayProcessor::GPSReplayProcessor() {
channel_filter_low_f = taps_200k_decim_1.low_frequency_normalized * 1000000;
channel_filter_high_f = taps_200k_decim_1.high_frequency_normalized * 1000000;
channel_filter_transition = taps_200k_decim_1.transition_normalized * 1000000;
@@ -39,44 +39,44 @@ ReplayProcessor::ReplayProcessor() {
channel_spectrum.set_decimation_factor(1);
configured = false;
baseband_thread.start();
}
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
/* 4MHz, 2048 samples */
void GPSReplayProcessor::execute(const buffer_c8_t& buffer) {
/* 2.6MHz, 2048 samples */
if (!configured) return;
if (!configured || !stream) return;
// File data is in C16 format, we need C8
// File samplerate is 500kHz, we're at 4MHz
// iq_buffer can only be 512 C16 samples (RAM limitation)
// To fill up the 2048-sample C8 buffer, we need:
// 2048 samples * 2 bytes per sample = 4096 bytes
// Since we're oversampling by 4M/500k = 8, we only need 2048/8 = 256 samples from the file and duplicate them 8 times each
// So 256 * 4 bytes per sample (C16) = 1024 bytes from the file
if (stream) { // sizeof(*buffer.p) = sizeof(C8) = 2*int8 = 2 bytes //buffer.count = 2048
const size_t bytes_to_read = sizeof(*buffer.p) * 1 * (buffer.count); // *2 (C16), /8 (oversampling) should be == 1024
bytes_read += stream->read(iq_buffer.p, bytes_to_read);
}
// File data is in C8 format, which is what we need
// File samplerate is 2.6MHz, which is what we need
// To fill up the 2048-sample C8 buffer @ 2 bytes per sample = 4096 bytes
const size_t bytes_to_read = sizeof(*buffer.p) * 1 * (buffer.count);
size_t bytes_read_this_iteration = stream->read(iq_buffer.p, bytes_to_read);
size_t samples_read_this_iteration = bytes_read_this_iteration / sizeof(*buffer.p);
// Fill and "stretch"
for (size_t i = 0; i < buffer.count; i++) {
auto re_out = iq_buffer.p[i].real();
auto im_out = iq_buffer.p[i].imag();
buffer.p[i] = {(int8_t)re_out, (int8_t)im_out};
}
bytes_read += bytes_read_this_iteration;
spectrum_samples += buffer.count;
// NB: Couldn't we have just read the data into buffer.p to start with, or some DMA/cache coherency concern?
//
// for (size_t i = 0; i < buffer.count; i++) {
// auto re_out = iq_buffer.p[i].real();
// auto im_out = iq_buffer.p[i].imag();
// buffer.p[i] = {(int8_t)re_out, (int8_t)im_out};
// }
memcpy(buffer.p, iq_buffer.p, bytes_read_this_iteration); // memcpy should be more efficient than 1 byte at a time
spectrum_samples += samples_read_this_iteration;
if (spectrum_samples >= spectrum_interval_samples) {
spectrum_samples -= spectrum_interval_samples;
txprogress_message.progress = bytes_read / 1024; // Inform UI about progress
txprogress_message.progress = bytes_read; // Inform UI about progress
txprogress_message.done = false;
shared_memory.application_queue.push(txprogress_message);
}
}
void ReplayProcessor::on_message(const Message* const message) {
void GPSReplayProcessor::on_message(const Message* const message) {
switch (message->id) {
case Message::ID::UpdateSpectrum:
case Message::ID::SpectrumStreamingConfig:
@@ -103,13 +103,13 @@ void ReplayProcessor::on_message(const Message* const message) {
}
}
void ReplayProcessor::samplerate_config(const SamplerateConfigMessage& message) {
void GPSReplayProcessor::samplerate_config(const SamplerateConfigMessage& message) {
baseband_fs = message.sample_rate;
baseband_thread.set_sampling_rate(baseband_fs);
spectrum_interval_samples = baseband_fs / spectrum_rate_hz;
}
void ReplayProcessor::replay_config(const ReplayConfigMessage& message) {
void GPSReplayProcessor::replay_config(const ReplayConfigMessage& message) {
if (message.config) {
stream = std::make_unique<StreamOutput>(message.config);
@@ -121,7 +121,7 @@ void ReplayProcessor::replay_config(const ReplayConfigMessage& message) {
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<ReplayProcessor>()};
EventDispatcher event_dispatcher{std::make_unique<GPSReplayProcessor>()};
event_dispatcher.run();
return 0;
}
+7 -6
View File
@@ -34,20 +34,17 @@
#include <array>
#include <memory>
class ReplayProcessor : public BasebandProcessor {
class GPSReplayProcessor : public BasebandProcessor {
public:
ReplayProcessor();
GPSReplayProcessor();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
size_t baseband_fs = 0;
size_t baseband_fs = 3072000;
static constexpr auto spectrum_rate_hz = 50.0f;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Transmit};
std::array<complex8_t, 2048> iq{};
const buffer_c8_t iq_buffer{
iq.data(),
@@ -72,6 +69,10 @@ class ReplayProcessor : public BasebandProcessor {
TXProgressMessage txprogress_message{};
RequestSignalMessage sig_message{RequestSignalMessage::Signal::FillRequest};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{
baseband_fs, this, baseband::Direction::Transmit, /*auto_start*/ false};
};
#endif /*__PROC_GPS_SIM_HPP__*/
+3 -3
View File
@@ -33,14 +33,11 @@ using namespace jammer;
class JammerProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const msg) override;
private:
bool configured{false};
BasebandThread baseband_thread{3072000, this, NORMALPRIO + 20, baseband::Direction::Transmit};
JammerChannel* jammer_channels{};
JammerType noise_type{};
@@ -54,6 +51,9 @@ class JammerProcessor : public BasebandProcessor {
int8_t sample{0};
int8_t re{0}, im{0};
RetuneMessage message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{3072000, this, baseband::Direction::Transmit};
};
#endif
+3 -3
View File
@@ -32,7 +32,6 @@
class MicTXProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const msg) override;
private:
@@ -40,8 +39,6 @@ class MicTXProcessor : public BasebandProcessor {
bool configured{false};
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Transmit};
int16_t audio_data[64];
buffer_s16_t audio_buffer{
audio_data,
@@ -74,6 +71,9 @@ class MicTXProcessor : public BasebandProcessor {
AudioLevelReportMessage level_message{};
TXProgressMessage txprogress_message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Transmit};
};
#endif
+7 -7
View File
@@ -42,15 +42,11 @@
class NarrowbandFMAudio : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
static constexpr size_t baseband_fs = 3072000;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
@@ -97,12 +93,16 @@ class NarrowbandFMAudio : public BasebandProcessor {
static constexpr float ki = 1.0f / k;
bool configured{false};
// RequestSignalMessage sig_message { RequestSignalMessage::Signal::Squelched };
CodedSquelchMessage ctcss_message{0};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
void pitch_rssi_config(const PitchRSSIConfigureMessage& message);
void configure(const NBFMConfigureMessage& message);
void capture_config(const CaptureConfigMessage& message);
// RequestSignalMessage sig_message { RequestSignalMessage::Signal::Squelched };
CodedSquelchMessage ctcss_message{0};
};
#endif /*__PROC_NFM_AUDIO_H__*/
+2 -1
View File
@@ -31,7 +31,8 @@ class NOOPProcessor : public BasebandProcessor {
void execute(const buffer_c8_t& buffer) override;
private:
BasebandThread baseband_thread{1536000, this, NORMALPRIO + 20, baseband::Direction::Transmit};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{1536000, this, baseband::Direction::Transmit};
};
#endif
+5 -6
View File
@@ -39,16 +39,12 @@
class NRFRxProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
static constexpr size_t baseband_fs = 4000000;
static constexpr size_t audio_fs = baseband_fs / 8 / 8 / 2;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
@@ -82,10 +78,13 @@ class NRFRxProcessor : public BasebandProcessor {
int RB_SIZE{1000};
bool configured{false};
AFSKDataMessage data_message{false, 0};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
void configure(const NRFRxConfigureMessage& message);
AFSKDataMessage data_message{false, 0};
};
#endif /*__PROC_NRFRX_H__*/
+3 -3
View File
@@ -29,14 +29,11 @@
class OOKProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const p) override;
private:
bool configured = false;
BasebandThread baseband_thread{2280000, this, NORMALPRIO + 20, baseband::Direction::Transmit};
uint32_t samples_per_bit{0};
uint8_t repeat{0};
uint32_t length{0};
@@ -67,6 +64,9 @@ class OOKProcessor : public BasebandProcessor {
size_t scan_progress{0};
uint8_t scan_done{true};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{2280000, this, baseband::Direction::Transmit};
size_t duval_algo_step();
void scan_process(const buffer_c8_t& buffer);
bool scan_init(unsigned int order);
+4 -5
View File
@@ -129,7 +129,6 @@ class SmoothVals {
class POCSAGProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
int OnDataFrame(int len, int baud);
@@ -138,9 +137,6 @@ class POCSAGProcessor : public BasebandProcessor {
private:
static constexpr size_t baseband_fs = 3072000;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
RSSIThread rssi_thread{NORMALPRIO + 10};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
@@ -166,7 +162,6 @@ class POCSAGProcessor : public BasebandProcessor {
// ----------------------------------------
// Frame extractraction methods and members
// ----------------------------------------
private:
void initFrameExtraction();
struct FIFOStruct {
unsigned long codeword;
@@ -219,6 +214,10 @@ class POCSAGProcessor : public BasebandProcessor {
bool m_gotSync{false};
int m_numCode{0};
bool m_inverted{false};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
};
#endif /*__PROC_POCSAG_H__*/
+3 -3
View File
@@ -33,14 +33,11 @@
class RDSProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const msg) override;
private:
uint32_t* rdsdata{};
BasebandThread baseband_thread{2280000, this, NORMALPRIO + 20, baseband::Direction::Transmit};
uint16_t message_length{0};
int8_t re{0}, im{0};
uint8_t mphase{0}, s{0};
@@ -132,6 +129,9 @@ class RDSProcessor : public BasebandProcessor {
0, -14, -27, -41, -53, -66, -77, -88,
-99, -109, -118, -126, -134, -141, -147, -152,
-157, -160, -163, -166, -167, -168, -168, -167};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{2280000, this, baseband::Direction::Transmit};
};
#endif
+12 -8
View File
@@ -38,12 +38,15 @@ ReplayProcessor::ReplayProcessor() {
channel_spectrum.set_decimation_factor(1);
configured = false;
baseband_thread.start();
}
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
/* 4MHz, 2048 samples */
if (!configured) return;
if (!configured || !stream) return;
buffer_c16_t iq_buffer{iq.data(), iq.size(), baseband_fs / 8};
// File data is in C16 format, we need C8
// File samplerate is 500kHz, we're at 4MHz
@@ -52,14 +55,15 @@ void ReplayProcessor::execute(const buffer_c8_t& buffer) {
// 2048 samples * 2 bytes per sample = 4096 bytes
// Since we're oversampling by 4M/500k = 8, we only need 2048/8 = 256 samples from the file and duplicate them 8 times each
// So 256 * 4 bytes per sample (C16) = 1024 bytes from the file
if (stream) {
const size_t bytes_to_read = sizeof(*buffer.p) * 2 * (buffer.count / 8); // *2 (C16), /8 (oversampling) should be == 1024
bytes_read += stream->read(iq_buffer.p, bytes_to_read);
}
const size_t bytes_to_read = sizeof(*buffer.p) * 2 * (buffer.count / 8); // *2 (C16), /8 (oversampling) should be == 1024
size_t bytes_read_this_iteration = stream->read(iq_buffer.p, bytes_to_read);
size_t oversamples_this_iteration = bytes_read_this_iteration * 8 / (sizeof(*buffer.p) * 2);
bytes_read += bytes_read_this_iteration;
// Fill and "stretch"
for (size_t i = 0; i < buffer.count; i++) {
if (i & 3) {
for (size_t i = 0; i < oversamples_this_iteration; i++) {
if (i & 7) {
buffer.p[i] = buffer.p[i - 1];
} else {
auto re_out = iq_buffer.p[i >> 3].real() >> 8;
@@ -68,7 +72,7 @@ void ReplayProcessor::execute(const buffer_c8_t& buffer) {
}
}
spectrum_samples += buffer.count;
spectrum_samples += oversamples_this_iteration;
if (spectrum_samples >= spectrum_interval_samples) {
spectrum_samples -= spectrum_interval_samples;
channel_spectrum.feed(iq_buffer, channel_filter_low_f, channel_filter_high_f, channel_filter_transition);
+5 -8
View File
@@ -38,20 +38,13 @@ class ReplayProcessor : public BasebandProcessor {
ReplayProcessor();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
size_t baseband_fs = 0;
size_t baseband_fs = 3072000;
static constexpr auto spectrum_rate_hz = 50.0f;
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Transmit};
std::array<complex16_t, 256> iq{};
const buffer_c16_t iq_buffer{
iq.data(),
iq.size(),
baseband_fs / 8};
int32_t channel_filter_low_f = 0;
int32_t channel_filter_high_f = 0;
@@ -71,6 +64,10 @@ class ReplayProcessor : public BasebandProcessor {
TXProgressMessage txprogress_message{};
RequestSignalMessage sig_message{RequestSignalMessage::Signal::FillRequest};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{
baseband_fs, this, baseband::Direction::Transmit, /*auto_start*/ false};
};
#endif /*__PROC_REPLAY_HPP__*/
+3 -3
View File
@@ -30,14 +30,11 @@
class SigGenProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const msg) override;
private:
bool configured{false};
BasebandThread baseband_thread{1536000, this, NORMALPRIO + 20, baseband::Direction::Transmit};
uint32_t tone_delta{0}, fm_delta{}, tone_phase{0};
uint8_t tone_shape{};
uint32_t sample_count{0};
@@ -51,6 +48,9 @@ class SigGenProcessor : public BasebandProcessor {
// uint8_t lfsr { }, bit { }; // Finally not used lfsr of 8 bits , bit must be 8-bit to allow bit<<7 later in the code */
TXProgressMessage txprogress_message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{1536000, this, baseband::Direction::Transmit};
};
#endif
+1
View File
@@ -35,6 +35,7 @@ SondeProcessor::SondeProcessor() {
audio_output.configure(false);
tone_gen.configure(BEEP_BASE_FREQ, 1.0, ToneGen::tone_type::sine, AUDIO_SAMPLE_RATE);
baseband_thread.start();
}
void SondeProcessor::execute(const buffer_c8_t& buffer) {

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