Compare commits

...

48 Commits

Author SHA1 Message Date
Bernd Herzog 9525738118 Config mode hackrf r9 disable external tcxo fallback (#1434)
* implemented auto disable of external tcxo if r9 crash during boot
2023-09-05 10:32:44 +02:00
Bernd Herzog 62310ad9a9 Config mode (#1433)
* Allow code to run without a token
* Reverted submodule change by mistake
* WIP force tcxo
* Added check to hide if not an R9
* Fixed comments
* Updated name
* Fixed crashing on TCXO switching
* Fixed the reboot issue
* Updated boot order
* Cleaned up comments
* added new recovery mode
* added IO
* implemented cpld mode change
* whitespace change
* renamed config mode
* inverted logic
* removed r9 dependency
* added disable external tcxo option to config mode
* fixed CLKIN detection for r9
* integrated tcxo setting into clock selection
Co-authored-by @jLynx
2023-09-04 17:46:07 +02:00
Kyle Reed 4819a2f4e2 Decode status widget (#1431)
* Initial cleanup of pocsag beta, using DSP filters

* Better filter params

* Better filter

* Add signal diagnostics widgets

* POCSAG procs sends stats messages

* Only draw 32 bits

* Add AudioNormalizer filter
2023-09-03 21:49:44 -07:00
Brumi-2021 2435ee780f Small fine tuning xOVS selection for BW 25k (#1430)
* Small fine tuning xOVS selection for BW 25k

* Comments , same growing order ending with  dot.
2023-09-02 00:35:43 +02:00
Kyle Reed 5d602ece5c Add second pocsag proc for experimenting (#1428) 2023-08-31 08:05:49 +02:00
Mark Thompson 900086c1c9 Fixed 0.1Hz error in tone key frequencies 146.2, 156.7, and 162.2 (#1427)
* Fixed tone key freq truncation in float->int conversion
* Round integer division when displaying tone key freq
2023-08-31 08:04:47 +02:00
Kyle Reed 07be74701f POCSAG Squelch and Filtering (#1424)
* Disable audio processing when not using squelch
* Add 2k4 lowpass fitler for 24Khz audio for POCSAG
2023-08-30 19:24:35 +02:00
Kyle Reed f46e20c977 Explain and clean up decimator scalars (#1422) 2023-08-30 09:05:49 -07:00
Kyle Reed 4bc752b7a8 Support file count on sub-folders (#1423) 2023-08-30 09:05:35 -07:00
Brumi-2021 c28b608146 Minor issue : Slow down scrolling speed in BW 1.5Mhz onwards , and adding some TODO comments. (#1419)
* Slow down scrolling speed in BW 1.5Mhz onwards
* Revert added comments
2023-08-30 17:13:40 +02:00
Kyle Reed bd4948602a Fix RSSI for apps using proc_capture (#1420)
* Fix RSSI for apps using proc_capture
* Update comment
2023-08-30 17:13:14 +02:00
Mark Thompson 4774b3097f Support for displaying stack on screen after a fault (for debug) (#1416)
* Support for displaying stack on screen after a fault

* Update debug.cpp

* Highlight possible addresses on stack & support Up key

* Add small background white style

* Add small background white style

* Update debug.cpp

* Highlight fault PC in yellow

* Add bg_yellow_small style

* Add bg_yellow_small style

* Clang - trailing space
2023-08-29 23:26:58 -05:00
Kyle Reed de81156223 Support for 1.25MHz capture (#1418)
* Advanced draft decim /4 just waterfall ok

* apply some Kall's corrections + formatting

* Tidy up both decim_factors

* New refine optimizations

* Format issues

* more format issues ...mmmm

* comments update

* WIP Cleanup

* WIP

* WIP - add variant

* Use std::visit to dispatch MultiDecimator -- fluent API

* Clean up comments

* Merge next and fix compilation

* Fix odd loop in BlockDecimator

* Clean up spectrum math

* Descibe spectrum update math better, more clear math.

* Apply spectrum interval correction at 1.5M

* Increase replay buffer to handle x4 ovs

---------

Co-authored-by: Brumi-2021 <ea3hqj@gmail.com>
2023-08-29 19:26:38 +02:00
Kyle Reed e7e1bedcad Support squelch in pocsag (#1415)
* Support squelch in pocsag

* Revert smooth threshold
2023-08-27 15:56:40 -07:00
Mark Thompson d8930db8af Add Stack Dump option to debug menu and to GURU meditation fault (#1414)
* Stack dump

* Stack dump

* Stack dump

* Stack dump

* Stack dump

* Stack dump

* Update debug.cpp

* Clang

* Update debug.cpp

* Skip dumping unused bytes of stack
2023-08-27 00:33:27 -05:00
Kyle Reed 014db9e233 Option to hide address only messages (#1413) 2023-08-26 19:32:02 -07:00
Kyle Reed 933920edfd POCSAG State machine fix (#1410)
* Reset color for well-formed message fragments

* better colors

* Fix POGSAG decode state machine

* Invert is_message to make more clear

* Use new escape string constants

* Run ECC twice, better diagnostics

* center status icon

---------

Co-authored-by: kallanreed <kallanreed@noreply.github.com>
2023-08-26 13:43:34 -05:00
Mark Thompson cf25d85d51 Declare escape strings for colored text (#1409)
* Color strings

* Color strings

* Color strings

* Color strings

* Color strings

* Color strings & fix first group scrolling off screen

* Color strings

* Color strings

* Clang

* Clang

* Clang attempt #3

* Update ui_painter.cpp

* Update ui_widget.cpp

* Clang test

* Clang

* Clang test

* Update ui_about_simple.cpp

* Update lge_app.cpp
2023-08-25 20:01:37 -05:00
Kyle Reed 9af1308e29 Pocsag UX Revamp (#1408)
* Set bandwidth_filter in pogsac

* WIP pocsag UI revamp

* Settings UI, better console, UI Revamp

* Add baud rate to error messages

* Reset last_addr in error cases

* Show malformed messages as different color vs hiding

* Use light grey to indicate low quality decode

---------

Co-authored-by: kallanreed <kallanreed@noreply.github.com>
2023-08-25 14:45:02 -07:00
Mark Thompson f4496d8f45 Enable lto optimization for application code only (#1405)
Enabling for tonight's nightly build.  (Haven't heard of any issues yet.)
2023-08-24 11:14:57 -05:00
Mark Thompson 966d1c938b Automatically switch to C8 capture format when bandwidth >1.5MHz (#1404)
* Automatically select C8

* Automatically select C8
2023-08-24 10:14:18 +02:00
Kyle Reed f537c7896e Scanner persisted freq file, TextField for current item (#1403)
* Don't truncate string passed to Text widget

* Focus TextField on touch like other fields

* TextField for current, save last opened freq file
2023-08-23 11:51:28 -07:00
Kyle Reed 4a1479957c More settings and cleanup (#1402) 2023-08-22 12:09:59 -07:00
Kyle Reed dc9a16c54b Looking Glass - persist range/preset settings, UI tweaks (#1401)
* WIP

* Add encoder support for TextField

* Working settings, use TextField

* Remove unneeded blanking rectangle
2023-08-22 07:56:10 -07:00
Kyle Reed a476647d70 Don't use raw new/delete (#1398)
* Use unique_ptr in ui_btngrid
* Use unique_ptr for ui_menu
* Use unique_ptr for rssi_dma
* Use unique_ptr for painter
2023-08-21 10:17:23 +02:00
Mark Thompson 95a48e5693 Store search range in settings file (#1397)
* MAX_UFREQ
* Use settings file for search range
2023-08-21 10:16:49 +02:00
Mark Thompson 09404ca198 Default to AK4951 speaker disabled (#1396) 2023-08-20 22:28:23 +02:00
Kyle Reed 564f76b47d Use new settings API in recon and scanner (#1394)
* Use new settings in recon
* Trim on string value read, remove dupe entry
* Check update_range flag when setting values
* Add a few saved settings to scanner
* Add copywrite note
2023-08-20 22:28:02 +02:00
Mark Thompson c6424f1623 Display degree symbol in TPMS, Sonde, and Temperature apps; disabled Font Viewer (#1388)
* Display degrees symbol; disable Font Viewer app to save ROM space
2023-08-20 11:14:22 +02:00
Mark Thompson a4325ac20d Disable audio by default at power-up (#1393) 2023-08-20 11:13:04 +02:00
Kyle Reed d8a6422d37 Consolidate old and new style app settings (#1391)
* Consolidate old and new style app settings

* Remove unused ctor
2023-08-19 09:02:26 -07:00
gullradriel cc963c3562 Recon: Only update manual field if current entry is a range (#1387) 2023-08-18 22:05:28 +02:00
Kyle Reed 63f99742fc First pass at custom app-settings support (#1381)
* First draft of custom app settings support.

* WIP new settings

* Working per-app custom settings

* Revert design to use "bound settings"
2023-08-18 12:35:41 -07:00
Brumi-2021 a4636d7872 Finalise all low bit rate Capture App with x64 (#1386)
* Finalise all low bit rate Capture App with x64

* Adding back also 25k BW option
2023-08-18 14:49:22 +02:00
gullradriel bd2ee03e44 Restoring part of old timer, fixed sd loading (#1385) 2023-08-17 17:28:12 +02:00
Mark Thompson deeb81c183 Correct text field width (#1383) 2023-08-17 00:45:24 +02:00
Brumi-2021 12dbf957b3 Extend REC low bit rate bandwith options in Capture App till 75khz (#1380)
* working_x32_Capture_till_75Khz

* format issues

* apply better accurate comments

* auto format issues
2023-08-16 23:26:59 +02:00
Mark Thompson e1cc0b1ad0 Show file count in each directory, and moved "Too many files" message (#1376)
* Added file_count() function
* Show file count in each directory; moved "Too many files!" warning
2023-08-16 10:00:46 +02:00
gullradriel f079d57fc6 Recon: fixed behavior of auto update m-ranges (#1374) and coloring
* Fixed behavior of auto update m-ranges, added min and max to sd load and save if option not used
* changing xor swap by std::swap
* Fixed consecutive green color
2023-08-15 18:29:16 +02:00
Brumi-2021 09b9ed642f avoid yellow icon in Audio App (minor issue) (#1372)
* avoid yellow icon in Audio App

* clear readability

* just more clear parentesis
2023-08-13 16:52:54 -05:00
Mark Thompson e74a9f3b41 Limit text string length to fit Text widget rectangle (#1370)
* Limit string length to fit Text rectangle

* Update ui_widget.cpp
2023-08-13 20:34:46 +02:00
Mark Thompson ff7a9d10cb Load App Settings *after* initializing RadioState (#1371) 2023-08-13 20:33:31 +02:00
Brumi-2021 5cfd7f1dc2 Small error correction to PR 1367 (#1368) 2023-08-12 10:57:16 -05:00
Brumi-2021 853ca2ef53 Solve_quality_problem_low_bit_rate_100k_150k_REC_Capture_App (#1367)
* Solve_low_bit_rate_150k_Capture_App

* Applying  review comments.

* format issues

* Adding back requested previous low bit rates
2023-08-12 09:20:15 -05:00
Brumi-2021 cb0a4854f5 Increase REC BW Option till 1Mhz Capture App (#1352)
* Increase REC BW Option till 1Mhz Capture App

* Format issues

* miss type correction

* format issues
2023-08-06 21:54:24 +02:00
Mark Thompson 1188157a3e Reject bmp images that aren't 240 pixels wide (#1351) 2023-08-05 17:14:18 +02:00
gullradriel 2ae639412d fix/simplify internal timing (#1348)
* fix/simplify internal timing
2023-08-04 07:56:39 +02:00
Kyle Reed 37386c29cb Oversample (#1336)
* WIP Oversample cleanup

* WIP

* WIP

* WIP dynamic interpolation

* WIP cleanup

* Fix math errors

* Add some optional assertions

* Add support for x32 interpolation

* Update proc_replay.cpp

Typo
2023-08-02 21:59:26 +02:00
115 changed files with 3175 additions and 1304 deletions
+2 -1
View File
@@ -44,7 +44,7 @@ if(cpp20_supported)
else()
set(USE_CPPOPT "-std=c++17")
endif()
set(USE_CPPOPT "${USE_CPPOPT} -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized -Wno-volatile")
set(USE_CPPOPT "${USE_CPPOPT} -flto -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized -Wno-volatile")
# Enable this if you want the linker to remove unused code and data
set(USE_LINK_GC yes)
@@ -316,6 +316,7 @@ set(CPPSRC
# ui_replay_view.cpp
# ui_script.cpp
ui_sd_card_debug.cpp
config_mode.cpp
${CPLD_20150901_DATA_CPP}
${CPLD_20170522_DATA_CPP}
${HACKRF_CPLD_DATA_CPP}
+164 -123
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2022 Arjan Onwezen
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -23,7 +24,9 @@
#include "app_settings.hpp"
#include "convert.hpp"
#include "file.hpp"
#include "file_reader.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
#include "utility.hpp"
@@ -34,145 +37,139 @@
namespace fs = std::filesystem;
using namespace portapack;
using namespace std::literals;
namespace app_settings {
namespace {
fs::path get_settings_path(const std::string& app_name) {
return fs::path{u"/SETTINGS"} / app_name + u".ini";
}
} // namespace
template <typename T>
static void read_setting(
std::string_view file_content,
std::string_view setting_name,
T& value) {
auto pos = file_content.find(setting_name);
if (pos != file_content.npos) {
pos += setting_name.length();
value = strtoll(&file_content[pos], nullptr, 10);
}
void BoundSetting::parse(std::string_view value) {
switch (type_) {
case SettingType::I64:
parse_int(value, as<int64_t>());
break;
case SettingType::I32:
parse_int(value, as<int32_t>());
break;
case SettingType::U32:
parse_int(value, as<uint32_t>());
break;
case SettingType::U8:
parse_int(value, as<uint8_t>());
break;
case SettingType::String:
as<std::string>() = trim(value);
break;
case SettingType::Bool: {
int parsed = 0;
parse_int(value, parsed);
as<bool>() = (parsed != 0);
break;
}
};
}
template <typename T>
static void write_setting(File& file, std::string_view setting_name, const T& value) {
// NB: Not using file.write_line speed this up. This happens on every
void BoundSetting::write(File& file) const {
// NB: Write directly without allocations. This happens on every
// app exit when enabled so should be fast to keep the UX responsive.
StringFormatBuffer buffer;
size_t length = 0;
auto value_str = to_string_dec_uint(value, buffer, length);
file.write(setting_name.data(), setting_name.length());
file.write(value_str, length);
file.write(name_.data(), name_.length());
file.write("=", 1);
switch (type_) {
case SettingType::I64:
file.write(to_string_dec_int(as<int64_t>(), buffer, length), length);
break;
case SettingType::I32:
file.write(to_string_dec_int(as<int32_t>(), buffer, length), length);
break;
case SettingType::U32:
file.write(to_string_dec_uint(as<uint32_t>(), buffer, length), length);
break;
case SettingType::U8:
file.write(to_string_dec_uint(as<uint8_t>(), buffer, length), length);
break;
case SettingType::String: {
const auto& str = as<std::string>();
file.write(str.data(), str.length());
break;
}
case SettingType::Bool:
file.write(as<bool>() ? "1" : "0", 1);
break;
}
file.write("\r\n", 2);
}
static fs::path get_settings_path(const std::string& app_name) {
return fs::path{u"/SETTINGS"} / app_name + u".ini";
SettingsStore::SettingsStore(std::string_view store_name, SettingBindings bindings)
: store_name_{store_name}, bindings_{bindings} {
reload();
}
namespace setting {
constexpr std::string_view baseband_bandwidth = "baseband_bandwidth="sv;
constexpr std::string_view sampling_rate = "sampling_rate="sv;
constexpr std::string_view lna = "lna="sv;
constexpr std::string_view vga = "vga="sv;
constexpr std::string_view rx_amp = "rx_amp="sv;
constexpr std::string_view tx_amp = "tx_amp="sv;
constexpr std::string_view tx_gain = "tx_gain="sv;
constexpr std::string_view channel_bandwidth = "channel_bandwidth="sv;
constexpr std::string_view rx_frequency = "rx_frequency="sv;
constexpr std::string_view tx_frequency = "tx_frequency="sv;
constexpr std::string_view step = "step="sv;
constexpr std::string_view modulation = "modulation="sv;
constexpr std::string_view am_config_index = "am_config_index="sv;
constexpr std::string_view nbfm_config_index = "nbfm_config_index="sv;
constexpr std::string_view wfm_config_index = "wfm_config_index="sv;
constexpr std::string_view squelch = "squelch="sv;
constexpr std::string_view volume = "volume="sv;
} // namespace setting
// TODO: Only load/save values that are declared used.
// This will prevent switching apps from changing setting unnecessarily.
// TODO: Track which values are actually read.
// TODO: Maybe just use a dictionary which would allow for custom settings.
// TODO: Create a control value binding which will allow controls to
// be declaratively bound to a setting and persistence will be magic.
ResultCode load_settings(const std::string& app_name, AppSettings& settings) {
if (!portapack::persistent_memory::load_app_settings())
return ResultCode::SettingsDisabled;
auto file_path = get_settings_path(app_name);
auto data = File::read_file(file_path);
if (!data)
return ResultCode::LoadFailed;
if (flags_enabled(settings.mode, Mode::TX)) {
read_setting(*data, setting::tx_frequency, settings.tx_frequency);
read_setting(*data, setting::tx_amp, settings.tx_amp);
read_setting(*data, setting::tx_gain, settings.tx_gain);
read_setting(*data, setting::channel_bandwidth, settings.channel_bandwidth);
}
if (flags_enabled(settings.mode, Mode::RX)) {
read_setting(*data, setting::rx_frequency, settings.rx_frequency);
read_setting(*data, setting::lna, settings.lna);
read_setting(*data, setting::vga, settings.vga);
read_setting(*data, setting::rx_amp, settings.rx_amp);
read_setting(*data, setting::modulation, settings.modulation);
read_setting(*data, setting::am_config_index, settings.am_config_index);
read_setting(*data, setting::nbfm_config_index, settings.nbfm_config_index);
read_setting(*data, setting::wfm_config_index, settings.wfm_config_index);
read_setting(*data, setting::squelch, settings.squelch);
}
read_setting(*data, setting::baseband_bandwidth, settings.baseband_bandwidth);
read_setting(*data, setting::sampling_rate, settings.sampling_rate);
read_setting(*data, setting::step, settings.step);
read_setting(*data, setting::volume, settings.volume);
return ResultCode::Ok;
SettingsStore::~SettingsStore() {
save();
}
ResultCode save_settings(const std::string& app_name, AppSettings& settings) {
if (!portapack::persistent_memory::save_app_settings())
return ResultCode::SettingsDisabled;
void SettingsStore::reload() {
load_settings(store_name_, bindings_);
}
File settings_file;
auto file_path = get_settings_path(app_name);
ensure_directory(file_path.parent_path());
void SettingsStore::save() const {
save_settings(store_name_, bindings_);
}
auto error = settings_file.create(file_path);
bool load_settings(std::string_view store_name, SettingBindings& bindings) {
File f;
auto path = get_settings_path(std::string{store_name});
auto error = f.open(path);
if (error)
return ResultCode::SaveFailed;
return false;
if (flags_enabled(settings.mode, Mode::TX)) {
write_setting(settings_file, setting::tx_frequency, settings.tx_frequency);
write_setting(settings_file, setting::tx_amp, settings.tx_amp);
write_setting(settings_file, setting::tx_gain, settings.tx_gain);
write_setting(settings_file, setting::channel_bandwidth, settings.channel_bandwidth);
auto reader = FileLineReader(f);
for (const auto& line : reader) {
auto cols = split_string(line, '=');
if (cols.size() != 2)
continue;
// Find a binding with the name.
auto it = std::find_if(
bindings.begin(), bindings.end(),
[name = cols[0]](auto& bound_setting) {
return name == bound_setting.name();
});
// If found, parse the value.
if (it != bindings.end())
it->parse(cols[1]);
}
if (flags_enabled(settings.mode, Mode::RX)) {
write_setting(settings_file, setting::rx_frequency, settings.rx_frequency);
write_setting(settings_file, setting::lna, settings.lna);
write_setting(settings_file, setting::vga, settings.vga);
write_setting(settings_file, setting::rx_amp, settings.rx_amp);
write_setting(settings_file, setting::modulation, settings.modulation);
write_setting(settings_file, setting::am_config_index, settings.am_config_index);
write_setting(settings_file, setting::nbfm_config_index, settings.nbfm_config_index);
write_setting(settings_file, setting::wfm_config_index, settings.wfm_config_index);
write_setting(settings_file, setting::squelch, settings.squelch);
}
write_setting(settings_file, setting::baseband_bandwidth, settings.baseband_bandwidth);
write_setting(settings_file, setting::sampling_rate, settings.sampling_rate);
write_setting(settings_file, setting::step, settings.step);
write_setting(settings_file, setting::volume, settings.volume);
return ResultCode::Ok;
return true;
}
bool save_settings(std::string_view store_name, const SettingBindings& bindings) {
File f;
auto path = get_settings_path(std::string{store_name});
auto error = f.create(path);
if (error)
return false;
for (const auto& bound_setting : bindings)
bound_setting.write(f);
return true;
}
namespace app_settings {
void copy_to_radio_model(const AppSettings& settings) {
// NB: Don't actually adjust the radio here or it will hang.
// NB: Don't actually adjust the radio here or it may hang.
// Specifically 'modulation' which requires a running baseband.
if (flags_enabled(settings.mode, Mode::TX)) {
@@ -230,27 +227,71 @@ void copy_from_radio_model(AppSettings& settings) {
}
/* SettingsManager *************************************************/
SettingsManager::SettingsManager(std::string_view app_name, Mode mode, Options options)
: SettingsManager{app_name, mode, options, {}} {}
SettingsManager::SettingsManager(
std::string app_name,
std::string_view app_name,
Mode mode,
Options options)
: app_name_{std::move(app_name)},
SettingBindings additional_settings)
: SettingsManager{app_name, mode, Options::None, std::move(additional_settings)} {}
SettingsManager::SettingsManager(
std::string_view app_name,
Mode mode,
Options options,
SettingBindings additional_settings)
: app_name_{app_name},
settings_{},
bindings_{},
loaded_{false} {
settings_.mode = mode;
settings_.options = options;
auto result = load_settings(app_name_, settings_);
if (result == ResultCode::Ok) {
loaded_ = true;
copy_to_radio_model(settings_);
if (!portapack::persistent_memory::load_app_settings())
return;
// Pre-alloc enough for app settings and additional settings.
additional_settings.reserve(17 + additional_settings.size());
bindings_ = std::move(additional_settings);
// Transmitter model settings.
if (flags_enabled(mode, Mode::TX)) {
bindings_.emplace_back("tx_frequency"sv, &settings_.tx_frequency);
bindings_.emplace_back("tx_amp"sv, &settings_.tx_amp);
bindings_.emplace_back("tx_gain"sv, &settings_.tx_gain);
bindings_.emplace_back("channel_bandwidth"sv, &settings_.channel_bandwidth);
}
// Receiver model settings.
if (flags_enabled(mode, Mode::RX)) {
bindings_.emplace_back("rx_frequency"sv, &settings_.rx_frequency);
bindings_.emplace_back("lna"sv, &settings_.lna);
bindings_.emplace_back("vga"sv, &settings_.vga);
bindings_.emplace_back("rx_amp"sv, &settings_.rx_amp);
bindings_.emplace_back("modulation"sv, &settings_.modulation);
bindings_.emplace_back("am_config_index"sv, &settings_.am_config_index);
bindings_.emplace_back("nbfm_config_index"sv, &settings_.nbfm_config_index);
bindings_.emplace_back("wfm_config_index"sv, &settings_.wfm_config_index);
bindings_.emplace_back("squelch"sv, &settings_.squelch);
}
// Common model settings.
bindings_.emplace_back("baseband_bandwidth"sv, &settings_.baseband_bandwidth);
bindings_.emplace_back("sampling_rate"sv, &settings_.sampling_rate);
bindings_.emplace_back("step"sv, &settings_.step);
bindings_.emplace_back("volume"sv, &settings_.volume);
loaded_ = load_settings(app_name_, bindings_);
if (loaded_)
copy_to_radio_model(settings_);
}
SettingsManager::~SettingsManager() {
if (portapack::persistent_memory::save_app_settings()) {
copy_from_radio_model(settings_);
save_settings(app_name_, settings_);
save_settings(app_name_, bindings_);
}
}
+77 -13
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2022 Arjan Onwezen
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -27,21 +28,85 @@
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include "file.hpp"
#include "max283x.hpp"
#include "string_format.hpp"
namespace app_settings {
// Bring in the string_view literal.
using std::literals::operator""sv;
enum class ResultCode : uint8_t {
Ok, // settings found
LoadFailed, // settings (file) not found
SaveFailed, // unable to save settings
SettingsDisabled, // load/save disabled in settings
/* Represents a named setting bound to a variable instance. */
/* Using void* instead of std::variant, because variant is a pain to dispatch over. */
class BoundSetting {
/* The type of bound setting. */
enum class SettingType : uint8_t {
I64,
I32,
U32,
U8,
String,
Bool,
};
public:
BoundSetting(std::string_view name, int64_t* target)
: name_{name}, target_{target}, type_{SettingType::I64} {}
BoundSetting(std::string_view name, int32_t* target)
: name_{name}, target_{target}, type_{SettingType::I32} {}
BoundSetting(std::string_view name, uint32_t* target)
: name_{name}, target_{target}, type_{SettingType::U32} {}
BoundSetting(std::string_view name, uint8_t* target)
: name_{name}, target_{target}, type_{SettingType::U8} {}
BoundSetting(std::string_view name, std::string* target)
: name_{name}, target_{target}, type_{SettingType::String} {}
BoundSetting(std::string_view name, bool* target)
: name_{name}, target_{target}, type_{SettingType::Bool} {}
std::string_view name() const { return name_; }
void parse(std::string_view value);
void write(File& file) const;
private:
template <typename T>
constexpr auto& as() const {
return *reinterpret_cast<T*>(target_);
}
std::string_view name_;
void* target_;
SettingType type_;
};
using SettingBindings = std::vector<BoundSetting>;
/* RAII wrapper for Settings that loads/saves to the SD card. */
class SettingsStore {
public:
SettingsStore(std::string_view store_name, SettingBindings bindings);
~SettingsStore();
void reload();
void save() const;
private:
std::string_view store_name_;
SettingBindings bindings_;
};
bool load_settings(std::string_view store_name, SettingBindings& bindings);
bool save_settings(std::string_view store_name, const SettingBindings& bindings);
namespace app_settings {
enum class Mode : uint8_t {
RX = 0x01,
TX = 0x02,
@@ -55,7 +120,6 @@ enum class Options {
UseGlobalTargetFrequency = 0x0001,
};
// TODO: separate types for TX/RX or union?
/* NB: See RX/TX model headers for default values. */
struct AppSettings {
Mode mode = Mode::RX;
@@ -80,21 +144,20 @@ struct AppSettings {
uint8_t volume;
};
ResultCode load_settings(const std::string& app_name, AppSettings& settings);
ResultCode save_settings(const std::string& app_name, AppSettings& settings);
/* Copies common values to the receiver/transmitter models. */
void copy_to_radio_model(const AppSettings& settings);
/* Copies common values from the receiver/transmitter models. */
void copy_from_radio_model(AppSettings& settings);
/* RAII wrapper for automatically loading and saving settings for an app.
/* RAII wrapper for automatically loading and saving radio settings for an app.
* NB: This should be added to a class before any LNA/VGA controls so that
* the receiver/transmitter models are set before the control ctors run. */
class SettingsManager {
public:
SettingsManager(std::string app_name, Mode mode, Options options = Options::None);
SettingsManager(std::string_view app_name, Mode mode, Options options = Options::None);
SettingsManager(std::string_view app_name, Mode mode, SettingBindings additional_settings);
SettingsManager(std::string_view app_name, Mode mode, Options options, SettingBindings additional_settings);
~SettingsManager();
SettingsManager(const SettingsManager&) = delete;
@@ -109,8 +172,9 @@ class SettingsManager {
AppSettings& raw() { return settings_; }
private:
std::string app_name_;
std::string_view app_name_;
AppSettings settings_;
SettingBindings bindings_;
bool loaded_;
};
+26 -17
View File
@@ -60,30 +60,39 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
};
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. */
option_bandwidth.on_change = [this](size_t, uint32_t bandwidth) {
/* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
* provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */
auto sample_rate = bandwidth;
/* base_rate (bandwidth) 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);
/* 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.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);
// record_view determines the correct oversampling to apply and returns the actual sample rate.
// NB: record_view is what actually updates proc_capture baseband settings.
auto actual_sample_rate = record_view.set_sampling_rate(sample_rate);
// Update the radio model with the actual sampling rate.
receiver_model.set_sampling_rate(actual_sample_rate);
// Get suitable anti-aliasing BPF bandwidth for MAX2837 given the actual sample rate.
auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate);
receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth);
// Automatically switch default capture format to C8 when bandwidth setting is increased to >=1.5MHz
if ((bandwidth >= 1500000) && (previous_bandwidth < 1500000)) {
option_format.set_selected_index(1);
}
previous_bandwidth = bandwidth;
waterfall.start();
};
receiver_model.enable();
option_bandwidth.set_selected_index(7); // Preselected default option 500kHz.
option_bandwidth.set_by_value(500000); // better by_value than by option_bandwidth.set_selected_index(4), Preselected default option 500kHz.
record_view.on_error = [&nav](std::string message) {
nav.display_modal("Error", message);
@@ -46,6 +46,7 @@ class CaptureAppView : public View {
private:
static constexpr ui::Dim header_height = 3 * 16;
uint32_t previous_bandwidth{500000};
NavigationView& nav_;
RxRadioState radio_state_{ReceiverModel::Mode::Capture};
+7 -11
View File
@@ -69,7 +69,7 @@ void LGEView::generate_frame_touche() {
// 0D 96 02 12 0E 00 46 28 01 45 27 01 44 23 66 30
std::vector<uint8_t> data{0x46, 0x28, 0x01, 0x45, 0x27, 0x01, 0x44, 0x23};
console.write("\n\x1B\x07Touche:\x1B\x10");
console.write("\n" STR_COLOR_LIGHT_GREY "Touche:");
generate_lge_frame(0x96, (field_player.value() << 8) | field_room.value(), 0x0001, data);
}
@@ -111,7 +111,7 @@ void LGEView::generate_frame_nickname() {
data.insert(data.end(), data_footer.begin(), data_footer.end());
console.write("\n\x1B\x0ESet nickname:\x1B\x10");
console.write("\n" STR_COLOR_YELLOW "Set nickname:");
generate_lge_frame(0x02, 0x001A, field_player.value(), data);
}
@@ -141,7 +141,7 @@ void LGEView::generate_frame_team() {
data.push_back(field_team.value() - 1); // Color ?
console.write("\n\x1B\x0ASet team:\x1B\x10");
console.write("\n" STR_COLOR_GREEN "Set team:");
generate_lge_frame(0x03, data);
}
@@ -173,9 +173,7 @@ void LGEView::generate_frame_broadcast_nickname() {
data.push_back(field_team.value());
console.write(
"\n\x1B\x09"
"Broadcast nickname:\x1B\x10");
console.write("\n" STR_COLOR_BLUE "Broadcast nickname:");
generate_lge_frame(0x04, data);
}
@@ -187,14 +185,14 @@ void LGEView::generate_frame_start() {
// data[0] = field_room.value(); // ?
console.write("\n\x1B\x0DStart:\x1B\x10");
console.write("\n" STR_COLOR_MAGENTA "Start:");
generate_lge_frame(0x05, data);
}
void LGEView::generate_frame_gameover() {
std::vector<uint8_t> data{(uint8_t)field_room.value()};
console.write("\n\x1B\x0CGameover:\x1B\x10");
console.write("\n" STR_COLOR_RED "Gameover:");
generate_lge_frame(0x0D, data);
}
@@ -229,9 +227,7 @@ void LGEView::generate_frame_collier() {
data.push_back(checksum - id);
console.write(
"\n\x1B\x06"
"Config:\x1B\x10");
console.write("\n" STR_COLOR_DARK_YELLOW "Config:");
generate_lge_frame(0x00, 0x3713, 0x3713, data);
}
+225 -100
View File
@@ -22,15 +22,15 @@
#include "pocsag_app.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "portapack_persistent_memory.hpp"
#include "string_format.hpp"
#include "utility.hpp"
using namespace portapack;
using namespace pocsag;
#include "string_format.hpp"
#include "utility.hpp"
#include "audio.hpp"
namespace pmem = portapack::persistent_memory;
void POCSAGLogger::log_raw_data(const pocsag::POCSAGPacket& packet, const uint32_t frequency) {
std::string entry = "Raw: F:" + to_string_dec_uint(frequency) + "Hz " +
@@ -43,52 +43,101 @@ void POCSAGLogger::log_raw_data(const pocsag::POCSAGPacket& packet, const uint32
log_file.write_entry(packet.timestamp(), entry);
}
void POCSAGLogger::log_decoded(
const pocsag::POCSAGPacket& packet,
const std::string text) {
log_file.write_entry(packet.timestamp(), text);
void POCSAGLogger::log_decoded(Timestamp timestamp, const std::string& text) {
log_file.write_entry(timestamp, text);
}
namespace ui {
POCSAGSettingsView::POCSAGSettingsView(
NavigationView& nav,
POCSAGSettings& settings)
: settings_{settings} {
add_children(
{&check_beta,
&check_log,
&check_log_raw,
&check_small_font,
&check_hide_bad,
&check_hide_addr_only,
&check_ignore,
&field_ignore,
&button_save});
check_beta.set_value(settings_.use_new_proc);
check_log.set_value(settings_.enable_logging);
check_log_raw.set_value(settings_.enable_raw_log);
check_small_font.set_value(settings_.enable_small_font);
check_hide_bad.set_value(settings_.hide_bad_data);
check_hide_addr_only.set_value(settings_.hide_addr_only);
check_ignore.set_value(settings_.enable_ignore);
field_ignore.set_value(settings_.address_to_ignore);
button_save.on_select = [this, &nav](Button&) {
settings_.use_new_proc = check_beta.value();
settings_.enable_logging = check_log.value();
settings_.enable_raw_log = check_log_raw.value();
settings_.enable_small_font = check_small_font.value();
settings_.hide_bad_data = check_hide_bad.value();
settings_.hide_addr_only = check_hide_addr_only.value();
settings_.enable_ignore = check_ignore.value();
settings_.address_to_ignore = field_ignore.value();
nav.pop();
};
}
POCSAGAppView::POCSAGAppView(NavigationView& nav)
: nav_{nav} {
baseband::run_image(portapack::spi_flash::image_tag_pocsag);
if (settings_.use_new_proc)
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
else
baseband::run_image(portapack::spi_flash::image_tag_pocsag);
add_children({&rssi,
&channel,
&audio,
&field_rf_amp,
&field_lna,
&field_vga,
&field_frequency,
&check_log,
&field_volume,
&check_ignore,
&sym_ignore,
&console});
add_children(
{&rssi,
&audio,
&field_rf_amp,
&field_lna,
&field_vga,
&field_frequency,
&field_squelch,
&field_volume,
&image_status,
&text_packet_count,
&widget_bits,
&widget_frames,
&button_ignore_last,
&button_config,
&console});
if (!settings_.loaded())
// No app settings, use fallbacks.
if (!app_settings_.loaded()) {
field_frequency.set_value(initial_target_frequency);
receiver_model.enable();
// TODO: app setting instead?
uint32_t ignore_address;
ignore_address = persistent_memory::pocsag_ignore_address();
// TODO: is this common enough for a helper?
for (size_t c = 0; c < 7; c++) {
sym_ignore.set_sym(6 - c, ignore_address % 10);
ignore_address /= 10;
settings_.address_to_ignore = pmem::pocsag_ignore_address();
settings_.enable_ignore = settings_.address_to_ignore > 0;
}
logger = std::make_unique<POCSAGLogger>();
if (logger)
logger->append(LOG_ROOT_DIR "/POCSAG.TXT");
logger.append(LOG_ROOT_DIR "/POCSAG.TXT");
field_squelch.set_value(receiver_model.squelch_level());
field_squelch.on_change = [this](int32_t v) {
receiver_model.set_squelch_level(v);
};
button_ignore_last.on_select = [this](Button&) {
settings_.enable_ignore = true;
settings_.address_to_ignore = last_address;
};
button_config.on_select = [this](Button&) {
nav_.push<POCSAGSettingsView>(settings_);
nav_.set_on_pop([this]() { refresh_ui(); });
};
refresh_ui();
audio::output::start();
receiver_model.enable();
baseband::set_pocsag();
}
@@ -98,81 +147,157 @@ void POCSAGAppView::focus() {
POCSAGAppView::~POCSAGAppView() {
audio::output::stop();
// Save ignored address
persistent_memory::set_pocsag_ignore_address(sym_ignore.value_dec_u32());
receiver_model.disable();
baseband::shutdown();
// Save pmem settings. TODO: Even needed anymore?
pmem::set_pocsag_ignore_address(settings_.address_to_ignore);
pmem::set_pocsag_last_address(pocsag_state.address); // For POCSAG TX.
}
void POCSAGAppView::refresh_ui() {
console.set_style(
settings_.enable_small_font
? &Styles::white_small
: &Styles::white);
}
void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefix) {
bool bad_data = pocsag_state.errors >= 3;
// Too many errors for reliable decode.
if (bad_data && hide_bad_data()) {
console.write("\n" STR_COLOR_MAGENTA + prefix + " Too many decode errors.");
last_address = 0;
return;
}
// Ignored address.
if (ignore() && pocsag_state.address == settings_.address_to_ignore) {
console.write("\n" STR_COLOR_CYAN + prefix + " Ignored: " + to_string_dec_uint(pocsag_state.address));
last_address = pocsag_state.address;
return;
}
// Color indicates the message has a lot of decoding errors.
std::string color = bad_data ? STR_COLOR_MAGENTA : STR_COLOR_WHITE;
std::string console_info = "\n" + color + prefix;
console_info += " #" + to_string_dec_uint(pocsag_state.address);
console_info += " F" + to_string_dec_uint(pocsag_state.function);
if (pocsag_state.out_type == ADDRESS) {
last_address = pocsag_state.address;
if (!hide_addr_only()) {
console.write(console_info);
if (logging()) {
logger.log_decoded(
timestamp,
to_string_dec_uint(pocsag_state.address) +
" F" + to_string_dec_uint(pocsag_state.function));
}
}
} else if (pocsag_state.out_type == MESSAGE) {
if (pocsag_state.address != last_address) {
// New message
last_address = pocsag_state.address;
console.writeln(console_info);
console.write(color + pocsag_state.output);
} else {
// Message continues...
console.write(color + pocsag_state.output);
}
if (logging()) {
logger.log_decoded(
timestamp,
to_string_dec_uint(pocsag_state.address) +
" F" + to_string_dec_uint(pocsag_state.function) +
" " + pocsag_state.output);
}
}
}
static Color get_status_color(const POCSAGState& state) {
if (state.out_type == IDLE)
return Color::white();
switch (state.mode) {
case STATE_CLEAR:
return Color::cyan();
case STATE_HAVE_ADDRESS:
return Color::yellow();
case STATE_GETTING_MSG:
return Color::green();
}
// Shouldn't get here...
return Color::red();
}
void POCSAGAppView::on_packet(const POCSAGPacketMessage* message) {
std::string alphanum_text = "";
const uint32_t roundVal = 50;
const uint32_t bitrate_rounded = roundVal * ((message->packet.bitrate() + (roundVal / 2)) / roundVal);
auto bitrate = to_string_dec_uint(bitrate_rounded);
auto timestamp = to_string_datetime(message->packet.timestamp(), HM);
auto prefix = timestamp + " " + bitrate;
if (message->packet.flag() != NORMAL)
console.writeln("\n\x1B\x0CRC ERROR: " + pocsag::flag_str(message->packet.flag()));
else {
pocsag_decode_batch(message->packet, &pocsag_state);
// Display packet count to be able to tell whether baseband sent a packet for a tone.
++packet_count;
text_packet_count.set(to_string_dec_uint(packet_count));
if ((ignore()) && (pocsag_state.address == sym_ignore.value_dec_u32())) {
// Ignore (inform, but no log)
// console.write("\n\x1B\x03" + to_string_time(message->packet.timestamp()) +
// " Ignored address " + to_string_dec_uint(pocsag_state.address));
return;
}
// Too many errors for reliable decode
if ((ignore()) && (pocsag_state.errors >= 3)) {
return;
}
if (logging_raw())
logger.log_raw_data(message->packet, receiver_model.target_frequency());
std::string console_info;
const uint32_t roundVal = 50;
const uint32_t bitrate = roundVal * ((message->packet.bitrate() + (roundVal / 2)) / roundVal);
console_info = "\n" + to_string_datetime(message->packet.timestamp(), HM);
console_info += " " + to_string_dec_uint(bitrate);
console_info += " ADDR:" + to_string_dec_uint(pocsag_state.address);
console_info += " F" + to_string_dec_uint(pocsag_state.function);
if (message->packet.flag() != NORMAL) {
console.writeln("\n" STR_COLOR_RED + prefix + " CRC ERROR: " + pocsag::flag_str(message->packet.flag()));
last_address = 0;
} else {
// Set color before to be able to see if decode gets stuck.
image_status.set_foreground(Color::magenta());
pocsag_state.codeword_index = 0;
pocsag_state.errors = 0;
// Store last received address for POCSAG TX
persistent_memory::set_pocsag_last_address(pocsag_state.address);
// Handle multiple messages (if any).
while (pocsag_decode_batch(message->packet, pocsag_state))
handle_decoded(message->packet.timestamp(), prefix);
if (pocsag_state.out_type == ADDRESS) {
// Address only
console.write(console_info);
if (logger && logging()) {
logger->log_decoded(
message->packet, to_string_dec_uint(pocsag_state.address) +
" F" + to_string_dec_uint(pocsag_state.function) +
" Address only");
}
last_address = pocsag_state.address;
} else if (pocsag_state.out_type == MESSAGE) {
if (pocsag_state.address != last_address) {
// New message
console.writeln(console_info);
console.write(pocsag_state.output);
last_address = pocsag_state.address;
} else {
// Message continues...
console.write(pocsag_state.output);
}
if (logger && logging())
logger->log_decoded(
message->packet, to_string_dec_uint(pocsag_state.address) +
" F" + to_string_dec_uint(pocsag_state.function) +
" Alpha: " + pocsag_state.output);
}
// Handle the remainder.
handle_decoded(message->packet.timestamp(), prefix);
}
// TODO: make setting.
// Log raw data whatever it contains
/*if (logger && logging())
logger->log_raw_data(message->packet, receiver_model.target_frequency());*/
// Set status icon color to indicate state machine state.
image_status.set_foreground(get_status_color(pocsag_state));
}
void POCSAGAppView::on_stats(const POCSAGStatsMessage* stats) {
widget_bits.set_bits(stats->current_bits);
widget_frames.set_frames(stats->current_frames);
widget_frames.set_sync(stats->has_sync);
}
void BitsIndicator::paint(Painter&) {
auto p = screen_pos();
for (size_t i = 0; i < sizeof(bits_) * 8; ++i) {
auto is_set = ((bits_ >> i) & 0x1) == 1;
int x = p.x() + (i / height);
int y = p.y() + (i % height);
display.draw_pixel({x, y}, is_set ? Color::white() : Color::black());
}
}
void FrameIndicator::paint(Painter& painter) {
auto p = screen_pos();
painter.draw_rectangle({p, {2, height}}, has_sync_ ? Color::green() : Color::grey());
for (size_t i = 0; i < height; ++i) {
auto p2 = p + Point{2, 16 - (int)i};
painter.draw_hline(p2, 2, i < frame_count_ ? Color::white() : Color::black());
}
}
} /* namespace ui */
+198 -41
View File
@@ -27,12 +27,15 @@
#include "ui_freq_field.hpp"
#include "ui_receiver.hpp"
#include "ui_rssi.hpp"
#include "ui_styles.hpp"
#include "log_file.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "log_file.hpp"
#include "pocsag.hpp"
#include "pocsag_packet.hpp"
#include "radio_state.hpp"
#include <functional>
class POCSAGLogger {
public:
@@ -41,7 +44,7 @@ class POCSAGLogger {
}
void log_raw_data(const pocsag::POCSAGPacket& packet, const uint32_t frequency);
void log_decoded(const pocsag::POCSAGPacket& packet, const std::string text);
void log_decoded(Timestamp timestamp, const std::string& text);
private:
LogFile log_file{};
@@ -49,6 +52,119 @@ class POCSAGLogger {
namespace ui {
class BitsIndicator : public Widget {
public:
BitsIndicator(Point position)
: Widget{{position, {2, height}}} {}
void paint(Painter& painter) override;
void set_bits(uint32_t bits) {
if (bits != bits_) {
bits_ = bits;
set_dirty();
}
}
private:
static constexpr uint8_t height = 16;
uint32_t bits_ = 0;
};
class FrameIndicator : public Widget {
public:
FrameIndicator(Point position)
: Widget{{position, {4, height}}} {}
void paint(Painter& painter) override;
void set_frames(uint8_t frame_count) {
if (frame_count != frame_count_) {
frame_count_ = frame_count;
set_dirty();
}
}
void set_sync(bool has_sync) {
if (has_sync != has_sync_) {
has_sync_ = has_sync;
set_dirty();
}
}
private:
static constexpr uint8_t height = 16;
uint8_t frame_count_ = 0;
bool has_sync_ = false;
};
struct POCSAGSettings {
bool enable_small_font = false;
bool enable_logging = false;
bool enable_raw_log = false;
bool enable_ignore = false;
bool hide_bad_data = false;
bool hide_addr_only = false;
uint32_t address_to_ignore = 0;
bool use_new_proc = false;
};
class POCSAGSettingsView : public View {
public:
POCSAGSettingsView(NavigationView& nav, POCSAGSettings& settings);
std::string title() const override { return "POCSAG Config"; };
void focus() override { button_save.focus(); }
private:
POCSAGSettings& settings_;
Checkbox check_beta{
{0 * 8 + 2, 18 * 16 - 4},
6,
"Beta",
true /*small*/};
Checkbox check_log{
{2 * 8, 2 * 16},
10,
"Enable Log"};
Checkbox check_log_raw{
{2 * 8, 4 * 16},
12,
"Log Raw Data"};
Checkbox check_small_font{
{2 * 8, 6 * 16},
4,
"Use Small Font"};
Checkbox check_hide_bad{
{2 * 8, 8 * 16},
22,
"Hide Bad Data"};
Checkbox check_hide_addr_only{
{2 * 8, 10 * 16},
22,
"Hide Addr Only"};
Checkbox check_ignore{
{2 * 8, 12 * 16},
22,
"Enable Ignored Address"};
NumberField field_ignore{
{7 * 8, 13 * 16 + 8},
7,
{0, 9999999},
1,
'0'};
Button button_save{
{11 * 8, 16 * 16, 10 * 8, 2 * 16},
"Save"};
};
class POCSAGAppView : public View {
public:
POCSAGAppView(NavigationView& nav);
@@ -58,60 +174,94 @@ class POCSAGAppView : public View {
void focus() override;
private:
static constexpr uint32_t initial_target_frequency = 466175000;
bool logging() const { return check_log.value(); };
bool ignore() const { return check_ignore.value(); };
static constexpr uint32_t initial_target_frequency = 466'175'000;
bool logging() const { return settings_.enable_logging; };
bool logging_raw() const { return settings_.enable_raw_log; };
bool ignore() const { return settings_.enable_ignore; };
bool hide_bad_data() const { return settings_.hide_bad_data; };
bool hide_addr_only() const { return settings_.hide_addr_only; };
NavigationView& nav_;
RxRadioState radio_state_{};
app_settings::SettingsManager settings_{
"rx_pocsag", app_settings::Mode::RX};
// Settings
POCSAGSettings settings_{};
app_settings::SettingsManager app_settings_{
"rx_pocsag"sv,
app_settings::Mode::RX,
{
{"small_font"sv, &settings_.enable_small_font},
{"enable_logging"sv, &settings_.enable_logging},
{"enable_ignore"sv, &settings_.enable_ignore},
{"address_to_ignore"sv, &settings_.address_to_ignore},
{"hide_bad_data"sv, &settings_.hide_bad_data},
{"hide_addr_only"sv, &settings_.hide_addr_only},
{"use_new_proc"sv, &settings_.use_new_proc},
}};
void refresh_ui();
void handle_decoded(Timestamp timestamp, const std::string& prefix);
void on_packet(const POCSAGPacketMessage* message);
void on_stats(const POCSAGStatsMessage* stats);
uint32_t last_address = 0xFFFFFFFF;
pocsag::POCSAGState pocsag_state{};
RFAmpField field_rf_amp{
{13 * 8, 0 * 16}};
LNAGainField field_lna{
{15 * 8, 0 * 16}};
VGAGainField field_vga{
{18 * 8, 0 * 16}};
RSSI rssi{
{21 * 8, 0, 6 * 8, 4}};
Channel channel{
{21 * 8, 5, 6 * 8, 4}};
Audio audio{
{21 * 8, 10, 6 * 8, 4}};
pocsag::EccContainer ecc{};
pocsag::POCSAGState pocsag_state{&ecc};
POCSAGLogger logger{};
uint16_t packet_count = 0;
RxFrequencyField field_frequency{
{0 * 8, 0 * 8},
nav_};
RFAmpField field_rf_amp{
{11 * 8, 0 * 16}};
LNAGainField field_lna{
{13 * 8, 0 * 16}};
VGAGainField field_vga{
{16 * 8, 0 * 16}};
RSSI rssi{
{19 * 8 - 4, 3, 6 * 8, 4}};
Audio audio{
{19 * 8 - 4, 8, 6 * 8, 4}};
NumberField field_squelch{
{25 * 8, 0 * 16},
2,
{0, 99},
1,
' ',
true /*wrap*/};
AudioVolumeField field_volume{
{28 * 8, 0 * 16}};
Checkbox check_ignore{
{0 * 8, 21},
8,
"Ign addr",
false};
SymField sym_ignore{
{13 * 8, 25},
7,
SymField::SYMFIELD_DEC};
Image image_status{
{0 * 8 + 4, 1 * 16 + 2, 16, 16},
&bitmap_icon_pocsag,
Color::white(),
Color::black()};
Checkbox check_log{
{240 - 8 * 8, 21},
3,
"Log",
false};
Text text_packet_count{
{3 * 8, 1 * 16 + 2, 5 * 8, 16},
"0"};
BitsIndicator widget_bits{
{9 * 7 + 6, 1 * 16 + 2}};
FrameIndicator widget_frames{
{9 * 8, 1 * 16 + 2}};
Button button_ignore_last{
{10 * 8, 1 * 16, 12 * 8, 20},
"Ignore Last"};
Button button_config{
{22 * 8, 1 * 16, 8 * 8, 20},
"Config"};
Console console{
{0, 3 * 16, 240, 256}};
std::unique_ptr<POCSAGLogger> logger{};
void on_packet(const POCSAGPacketMessage* message);
{0, 2 * 16 + 6, screen_width, screen_height - 56}};
MessageHandlerRegistration message_handler_packet{
Message::ID::POCSAGPacket,
@@ -119,6 +269,13 @@ class POCSAGAppView : public View {
const auto message = static_cast<const POCSAGPacketMessage*>(p);
this->on_packet(message);
}};
MessageHandlerRegistration message_handler_stats{
Message::ID::POCSAGStats,
[this](Message* const p) {
const auto stats = static_cast<const POCSAGStatsMessage*>(p);
this->on_stats(stats);
}};
};
} /* namespace ui */
+2 -2
View File
@@ -124,7 +124,7 @@ void ReplayAppView::start() {
if (reader) {
button_play.set_bitmap(&bitmap_stop);
baseband::set_sample_rate(sample_rate * 8);
baseband::set_sample_rate(sample_rate, OversampleRate::x8);
replay_thread = std::make_unique<ReplayThread>(
std::move(reader),
@@ -136,7 +136,7 @@ void ReplayAppView::start() {
});
}
transmitter_model.set_sampling_rate(sample_rate * 8);
transmitter_model.set_sampling_rate(sample_rate * toUType(OversampleRate::x8));
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
transmitter_model.enable();
+3 -3
View File
@@ -146,9 +146,9 @@ class TPMSAppView : public View {
OptionsField options_temperature{
{9 * 8, 0 * 16},
1,
{{"C", 0},
{"F", 1}}};
2,
{{STR_DEGREES_C, 0},
{STR_DEGREES_F, 1}}};
RFAmpField field_rf_amp{
{13 * 8, 0 * 16}};
+10 -8
View File
@@ -8,7 +8,7 @@ AboutView::AboutView(NavigationView& nav) {
nav.pop();
};
console.writeln("\x1B\x07List of contributors:\x1B\x10");
console.writeln(STR_COLOR_LIGHT_GREY "List of contributors:");
console.writeln("");
}
@@ -20,7 +20,7 @@ void AboutView::update() {
case 1:
// TODO: Generate this automatically from github
// https://github.com/eried/portapack-mayhem/graphs/contributors?to=2022-01-01&from=2020-04-12&type=c
console.writeln("\x1B\x06Mayhem:\x1B\x10");
console.writeln(STR_COLOR_DARK_YELLOW "Mayhem:");
console.writeln("eried,euquiq,gregoryfenton");
console.writeln("johnelder,jwetzell,nnemanjan00");
console.writeln("N0vaPixel,klockee,GullCode");
@@ -34,13 +34,15 @@ void AboutView::update() {
console.writeln("notpike,jLynx,zigad");
console.writeln("MichalLeonBorsuk,jimilinuxguy");
console.writeln("kallanreed,bernd-herzog");
break;
case 2:
console.writeln("NotherNgineer,zxkmm,u-foka");
console.writeln("");
break;
case 2:
case 3:
// https://github.com/eried/portapack-mayhem/graphs/contributors?to=2020-04-12&from=2015-07-31&type=c
console.writeln("\x1B\x06Havoc:\x1B\x10");
console.writeln(STR_COLOR_DARK_YELLOW "Havoc:");
console.writeln("furrtek,mrmookie,NotPike");
console.writeln("mjwaxios,ImDroided,Giorgiofox");
console.writeln("F4GEV,z4ziggy,xmycroftx");
@@ -51,16 +53,16 @@ void AboutView::update() {
console.writeln("");
break;
case 3:
case 4:
// https://github.com/eried/portapack-mayhem/graphs/contributors?from=2014-07-05&to=2015-07-31&type=c
console.writeln("\x1B\x06PortaPack:\x1B\x10");
console.writeln(STR_COLOR_DARK_YELLOW "PortaPack:");
console.writeln("jboone,argilo");
console.writeln("");
break;
case 4:
case 5:
// https://github.com/mossmann/hackrf/graphs/contributors
console.writeln("\x1B\x06HackRF:\x1B\x10");
console.writeln(STR_COLOR_DARK_YELLOW "HackRF:");
console.writeln("mossmann,dominicgs,bvernoux");
console.writeln("bgamari,schneider42,miek");
console.writeln("willcode,hessu,Sec42");
+4 -6
View File
@@ -40,24 +40,22 @@ void RecentEntriesTable<AircraftRecentEntries>::draw(
const Rect& target_rect,
Painter& painter,
const Style& style) {
char aged_color;
Color target_color;
auto entry_age = entry.age;
std::string entry_string;
// Color decay for flights not being updated anymore
if (entry_age < ADSB_CURRENT) {
aged_color = 0x10;
entry_string = "";
target_color = Color::green();
} else if (entry_age < ADSB_RECENT) {
aged_color = 0x07;
entry_string = STR_COLOR_LIGHT_GREY;
target_color = Color::light_grey();
} else {
aged_color = 0x08;
entry_string = STR_COLOR_DARK_GREY;
target_color = Color::grey();
}
std::string entry_string = "\x1B";
entry_string += aged_color;
entry_string +=
(entry.callsign[0] != ' ' ? entry.callsign + " " : entry.icaoStr + " ") +
to_string_dec_uint((unsigned int)(entry.pos.altitude / 100), 4) +
+1 -4
View File
@@ -42,15 +42,12 @@ void RecentEntriesTable<APRSRecentEntries>::draw(
const Rect& target_rect,
Painter& painter,
const Style& style) {
char aged_color;
Color target_color;
// auto entry_age = entry.age;
target_color = Color::green();
aged_color = 0x10;
std::string entry_string = "\x1B";
entry_string += aged_color;
std::string entry_string = "";
entry_string += entry.source_formatted;
entry_string.append(10 - entry.source_formatted.size(), ' ');
+4 -2
View File
@@ -20,6 +20,7 @@
*/
#include "ui_debug.hpp"
#include "debug.hpp"
#include "ch.h"
@@ -127,7 +128,7 @@ TemperatureWidget::temperature_t TemperatureWidget::temperature(const sample_t s
}
std::string TemperatureWidget::temperature_str(const temperature_t temperature) const {
return to_string_dec_int(temperature, temp_len - 1) + "C";
return to_string_dec_int(temperature, temp_len - 2) + STR_DEGREES_C;
}
Coord TemperatureWidget::screen_y(
@@ -406,7 +407,8 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
{"Touch Test", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugScreenTest>(); }},
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }},
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
//{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }}, // temporarily disabled to conserve ROM space
});
set_max_rows(2); // allow wider buttons
}
+1 -1
View File
@@ -105,7 +105,7 @@ class TemperatureWidget : public Widget {
static constexpr temperature_t display_temp_min = -10; // Accomodate negative values, present in cold startup cases
static constexpr temperature_t display_temp_scale = 3;
static constexpr int bar_width = 1;
static constexpr int temp_len = 4; // Now scale shows up to 4 chars ("-10C")
static constexpr int temp_len = 5; // Now scale shows up to 5 chars ("-10ºC")
};
class TemperatureView : public View {
+8 -8
View File
@@ -57,14 +57,14 @@ class DfuMenu : public View {
{{6 * CHARACTER_WIDTH, 11 * LINE_HEIGHT}, "M4 miss:", Color::dark_cyan()},
{{6 * CHARACTER_WIDTH, 12 * LINE_HEIGHT}, "uptime:", Color::dark_cyan()}};
Text text_info_line_1{{15 * CHARACTER_WIDTH, 5 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_2{{15 * CHARACTER_WIDTH, 6 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_3{{15 * CHARACTER_WIDTH, 7 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_4{{15 * CHARACTER_WIDTH, 8 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_5{{15 * CHARACTER_WIDTH, 9 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_6{{15 * CHARACTER_WIDTH, 10 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_7{{15 * CHARACTER_WIDTH, 11 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_8{{15 * CHARACTER_WIDTH, 12 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_1{{15 * CHARACTER_WIDTH, 5 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_2{{15 * CHARACTER_WIDTH, 6 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_3{{15 * CHARACTER_WIDTH, 7 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_4{{15 * CHARACTER_WIDTH, 8 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_5{{15 * CHARACTER_WIDTH, 9 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_6{{15 * CHARACTER_WIDTH, 10 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_7{{15 * CHARACTER_WIDTH, 11 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_8{{15 * CHARACTER_WIDTH, 12 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
};
class DfuMenu2 : public View {
+16 -9
View File
@@ -185,7 +185,6 @@ FileManBaseView::FileManBaseView(
extension_filter{filter} {
add_children({&labels,
&text_current,
&text_info,
&button_exit});
button_exit.on_select = [this, &nav](Button&) {
@@ -250,8 +249,13 @@ void FileManBaseView::refresh_list() {
auto entry_name = truncate(entry.path, 20);
if (entry.is_directory) {
auto size_str =
(entry.path == parent_dir_path)
? ""
: to_string_dec_uint(file_count(current_path / entry.path));
menu_view.add_item(
{entry_name,
{entry_name + std::string(21 - entry_name.length(), ' ') + size_str,
ui::Color::yellow(),
&bitmap_icon_dir,
[this](KeyEvent key) {
@@ -278,7 +282,6 @@ void FileManBaseView::refresh_list() {
break;
}
text_info.set(menu_view.item_count() >= max_items_shown ? "Too many files!" : "");
menu_view.set_highlighted(prev_highlight);
}
@@ -342,7 +345,6 @@ FileSaveView::FileSaveView(
file_{ file }
{
add_children({
&labels,
&text_path,
&button_edit_path,
&text_name,
@@ -546,7 +548,6 @@ FileManagerView::FileManagerView(
add_children({
&menu_view,
&labels,
&text_date,
&button_rename,
&button_delete,
@@ -560,10 +561,16 @@ FileManagerView::FileManagerView(
});
menu_view.on_highlight = [this]() {
if (selected_is_valid())
text_date.set(to_string_FAT_timestamp(file_created_date(get_selected_full_path())));
else
text_date.set("");
if (menu_view.highlighted_index() >= max_items_shown - 1) {
text_date.set_style(&Styles::red);
text_date.set("Too many files!");
} else {
text_date.set_style(&Styles::grey);
if (selected_is_valid())
text_date.set((is_directory(get_selected_full_path()) ? "Created " : "Modified ") + to_string_FAT_timestamp(file_created_date(get_selected_full_path())));
else
text_date.set("");
}
};
refresh_list();
+1 -9
View File
@@ -115,11 +115,6 @@ class FileManBaseView : public View {
{0, 2 * 8, 240, 26 * 8},
true};
// HACK: for item count limit.
Text text_info{
{1 * 8, 35 * 8, 15 * 8, 16},
""};
Button button_exit{
{22 * 8, 34 * 8, 8 * 8, 32},
"Exit"};
@@ -218,11 +213,8 @@ class FileManagerView : public FileManBaseView {
// True if the selected entry is a real file item.
bool selected_is_valid() const;
Labels labels{
{{0, 26 * 8}, "Created ", Color::light_grey()}};
Text text_date{
{8 * 8, 26 * 8, 19 * 8, 16},
{0 * 8, 26 * 8, 28 * 8, 16},
""};
NewButton button_rename{
+17 -11
View File
@@ -24,7 +24,9 @@
#include "ui_level.hpp"
#include "ui_fileman.hpp"
#include "ui_freqman.hpp"
#include "baseband_api.hpp"
#include "file.hpp"
#include "oversample.hpp"
using namespace portapack;
using namespace tonekey;
@@ -174,48 +176,52 @@ size_t LevelView::change_mode(freqman_index_t new_mod) {
switch (new_mod) {
case AM_MODULATION:
freqman_set_bandwidth_option(new_mod, field_bw);
// bw DSB (0) default
field_bw.set_by_value(0);
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
receiver_model.set_am_configuration(field_bw.selected_index_value());
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_am_configuration(n); };
// bw DSB (0) default
field_bw.set_by_value(0);
text_ctcss.set(" ");
break;
case NFM_MODULATION:
freqman_set_bandwidth_option(new_mod, field_bw);
// bw 16k (2) default
field_bw.set_by_value(2);
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
receiver_model.set_nbfm_configuration(field_bw.selected_index_value());
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_nbfm_configuration(n); };
// bw 16k (2) default
field_bw.set_by_value(2);
break;
case WFM_MODULATION:
freqman_set_bandwidth_option(new_mod, field_bw);
// bw 200k (0) only/default
field_bw.set_by_value(0);
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
receiver_model.set_wfm_configuration(field_bw.selected_index_value());
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_wfm_configuration(n); };
// bw 200k (0) only/default
field_bw.set_by_value(0);
text_ctcss.set(" ");
break;
case SPEC_MODULATION:
freqman_set_bandwidth_option(new_mod, field_bw);
baseband::run_image(portapack::spi_flash::image_tag_capture);
receiver_model.set_modulation(ReceiverModel::Mode::Capture);
field_bw.set_by_value(0);
field_bw.on_change = [this](size_t, OptionsField::value_t sampling_rate) {
auto anti_alias_baseband_bandwidth_filter = filter_bandwidth_for_sampling_rate(sampling_rate);
receiver_model.set_sampling_rate(sampling_rate);
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
// Baseband needs to know the desired sampling and oversampling rates.
baseband::set_sample_rate(sampling_rate, get_oversample_rate(sampling_rate));
// The radio needs to know the effective sampling rate.
auto actual_sampling_rate = get_actual_sample_rate(sampling_rate);
receiver_model.set_sampling_rate(actual_sampling_rate);
receiver_model.set_baseband_bandwidth(filter_bandwidth_for_sampling_rate(actual_sampling_rate));
};
field_bw.set_by_value(0);
default:
break;
}
if (new_mod != SPEC_MODULATION) {
// 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);
}
-1
View File
@@ -64,7 +64,6 @@ class LevelView : public View {
// TODO: needed?
int32_t db{0};
long long int MAX_UFREQ = {7200000000}; // maximum usable freq
rf::Frequency freq_ = {0};
Labels labels{
+142 -169
View File
@@ -74,8 +74,8 @@ rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) {
void GlassView::on_marker_change() {
marker = get_freq_from_bin_pos(marker_pixel_index);
button_marker.set_text(to_string_short_freq(marker));
PlotMarker(marker_pixel_index); // Refresh marker on screen
field_marker.set_text(to_string_short_freq(marker));
plot_marker(marker_pixel_index); // Refresh marker on screen
}
void GlassView::retune() {
@@ -87,23 +87,13 @@ void GlassView::retune() {
baseband::spectrum_streaming_start(); // Do the RX
}
void GlassView::on_lna_changed(int32_t v_db) {
receiver_model.set_lna(v_db);
}
void GlassView::on_vga_changed(int32_t v_db) {
receiver_model.set_vga(v_db);
}
void GlassView::reset_live_view(bool clear_screen) {
void GlassView::reset_live_view() {
max_freq_hold = 0;
max_freq_power = -1000;
if (clear_screen) {
// only clear screen in peak mode
if (live_frequency_view == 2) {
display.fill_rectangle({{0, 108 + 16}, {SCREEN_W, 320 - (108 + 16)}}, {0, 0, 0});
}
}
// Clear screen in peak mode.
if (live_frequency_view == 2)
display.fill_rectangle({{0, 108 + 16}, {SCREEN_W, SCREEN_H - (108 + 16)}}, {0, 0, 0});
}
void GlassView::add_spectrum_pixel(uint8_t power) {
@@ -134,15 +124,15 @@ void GlassView::add_spectrum_pixel(uint8_t power) {
uint8_t color_gradient = (point * 255) / 212;
// clear if not in peak view
if (live_frequency_view != 2) {
display.fill_rectangle({{xpos, 108 + 16}, {1, 320 - point}}, {0, 0, 0});
display.fill_rectangle({{xpos, 108 + 16}, {1, SCREEN_H - point}}, {0, 0, 0});
}
display.fill_rectangle({{xpos, 320 - point}, {1, point}}, {color_gradient, 0, uint8_t(255 - color_gradient)});
display.fill_rectangle({{xpos, SCREEN_H - point}, {1, point}}, {color_gradient, 0, uint8_t(255 - color_gradient)});
}
if (last_max_freq != max_freq_hold) {
last_max_freq = max_freq_hold;
freq_stats.set("MAX HOLD: " + to_string_short_freq(max_freq_hold));
}
PlotMarker(marker_pixel_index);
plot_marker(marker_pixel_index);
} else {
display.draw_pixels({{0, display.scroll(1)}, {SCREEN_W, 1}}, spectrum_row); // new line at top, one less var, speedier
}
@@ -151,8 +141,8 @@ void GlassView::add_spectrum_pixel(uint8_t power) {
}
bool GlassView::process_bins(uint8_t* powerlevel) {
bins_Hz_size += each_bin_size; // add pixel to fulfilled bag of Hz
if (bins_Hz_size >= marker_pixel_step) // new pixel fullfilled
bins_hz_size += each_bin_size; // add pixel to fulfilled bag of Hz
if (bins_hz_size >= marker_pixel_step) // new pixel fullfilled
{
if (*powerlevel > min_color_power)
add_spectrum_pixel(*powerlevel); // Pixel will represent max_power
@@ -162,7 +152,7 @@ bool GlassView::process_bins(uint8_t* powerlevel) {
if (!pixel_index) // Received indication that a waterfall line has been completed
{
bins_Hz_size = 0; // Since this is an entire pixel line, we don't carry "Pixels into next bin"
bins_hz_size = 0; // Since this is an entire pixel line, we don't carry "Pixels into next bin"
if (mode != LOOKING_GLASS_SINGLEPASS) {
f_center = f_center_ini;
retune();
@@ -170,18 +160,18 @@ bool GlassView::process_bins(uint8_t* powerlevel) {
baseband::spectrum_streaming_start();
return true; // signal a new line
}
bins_Hz_size -= marker_pixel_step; // reset bins size, but carrying the eventual excess Hz into next pixel
bins_hz_size -= marker_pixel_step; // reset bins size, but carrying the eventual excess Hz into next pixel
}
return false;
}
// Apparently, the spectrum object returns an array of SPEC_NB_BINS (256) bins
// Each having the radio signal power for it's corresponding frequency slot
// Each having the radio signal power for its corresponding frequency slot
void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
baseband::spectrum_streaming_stop();
// Convert bins of this spectrum slice into a representative max_power and when enough, into pixels
// we actually need SCREEN_W (240) of those bins
for (bin = 0; bin < bin_length; bin++) {
for (uint8_t bin = 0; bin < bin_length; bin++) {
get_max_power(spectrum, bin, max_power);
// process dc spike if enable
if (bin == 119) {
@@ -215,7 +205,7 @@ void GlassView::on_show() {
}
void GlassView::on_range_changed() {
reset_live_view(true);
reset_live_view();
f_min = field_frequency_min.value();
f_max = field_frequency_max.value();
f_min = f_min * MHZ_DIV; // Transpose into full frequency realm
@@ -231,8 +221,7 @@ void GlassView::on_range_changed() {
looking_glass_sampling_rate = looking_glass_bandwidth;
each_bin_size = looking_glass_bandwidth / SCREEN_W;
looking_glass_step = looking_glass_bandwidth;
f_center_ini = f_min + (looking_glass_bandwidth / 2); // Initial center frequency for sweep
portapack::display.fill_rectangle({17 * 8, 4 * 16, 2 * 8, 16}, Color::black()); // Clear old marker and whole marker rectangle btw
f_center_ini = f_min + (looking_glass_bandwidth / 2); // Initial center frequency for sweep
} else {
// view is made in multiple pass, use original bin picking
mode = scan_type.selected_index_value();
@@ -253,19 +242,13 @@ void GlassView::on_range_changed() {
}
search_span = looking_glass_range / MHZ_DIV;
marker_pixel_step = looking_glass_range / SCREEN_W; // Each pixel value in Hz
//
button_range.set_text(" "); // clear up to 6 chars
if (locked_range) {
button_range.set_text(">" + to_string_dec_uint(search_span) + "<");
} else {
button_range.set_text(" " + to_string_dec_uint(search_span) + " ");
}
pixel_index = 0; // reset pixel counter
max_power = 0; // reset save max power level
bins_Hz_size = 0; // reset amount of Hz filled up by pixels
//
pixel_index = 0;
max_power = 0;
bins_hz_size = 0;
on_marker_change();
update_range_field();
// set the sample rate and bandwidth
receiver_model.set_sampling_rate(looking_glass_sampling_rate);
@@ -273,11 +256,11 @@ void GlassView::on_range_changed() {
receiver_model.set_squelch_level(0);
f_center = f_center_ini; // Reset sweep into first slice
baseband::set_spectrum(looking_glass_bandwidth, field_trigger.value());
baseband::set_spectrum(looking_glass_bandwidth, trigger);
receiver_model.set_target_frequency(f_center); // tune rx for this slice
}
void GlassView::PlotMarker(uint8_t pos) {
void GlassView::plot_marker(uint8_t pos) {
uint8_t shift_y = 0;
if (live_frequency_view > 0) // plot one line down when in live view
{
@@ -289,7 +272,7 @@ void GlassView::PlotMarker(uint8_t pos) {
portapack::display.fill_rectangle({pos, 106 + shift_y, 1, 2}, Color::red()); // Red marker bottom
}
void GlassView::clip_min(int32_t v) {
void GlassView::update_min(int32_t v) {
int32_t min_size = steps;
if (locked_range)
min_size = search_span;
@@ -299,15 +282,14 @@ void GlassView::clip_min(int32_t v) {
v = 7200 - min_size;
}
if (v > (field_frequency_max.value() - min_size))
field_frequency_max.set_value(v + min_size);
field_frequency_max.set_value(v + min_size, false);
if (locked_range)
field_frequency_max.set_value(v + min_size);
field_frequency_max.set_value(v + min_size, false);
else
field_frequency_min.set_value(v);
on_range_changed();
field_frequency_min.set_value(v, false);
}
void GlassView::clip_max(int32_t v) {
void GlassView::update_max(int32_t v) {
int32_t min_size = steps;
if (locked_range)
min_size = search_span;
@@ -317,12 +299,21 @@ void GlassView::clip_max(int32_t v) {
v = min_size;
}
if (v < (field_frequency_min.value() + min_size))
field_frequency_min.set_value(v - min_size);
field_frequency_min.set_value(v - min_size, false);
if (locked_range)
field_frequency_min.set_value(v - min_size);
field_frequency_min.set_value(v - min_size, false);
else
field_frequency_max.set_value(v);
on_range_changed();
field_frequency_max.set_value(v, false);
}
void GlassView::update_range_field() {
if (!locked_range) {
field_range.set_style(&Styles::white);
field_range.set_text(" " + to_string_dec_uint(search_span) + " ");
} else {
field_range.set_style(&Styles::red);
field_range.set_text(">" + to_string_dec_uint(search_span) + "<");
}
}
GlassView::GlassView(
@@ -335,7 +326,7 @@ GlassView::GlassView(
&field_frequency_max,
&field_lna,
&field_vga,
&button_range,
&field_range,
&steps_config,
&scan_type,
&view_config,
@@ -343,175 +334,156 @@ GlassView::GlassView(
&filter_config,
&field_rf_amp,
&range_presets,
&button_marker,
&field_marker,
&field_trigger,
&button_jump,
&button_rst,
&freq_stats});
load_Presets(); // Load available presets from TXT files (or default)
load_presets(); // Load available presets from TXT files (or default).
preset_index = clip<uint8_t>(preset_index, 0, presets_db.size());
field_frequency_min.set_value(f_min / MHZ_DIV);
field_frequency_min.on_change = [this](int32_t v) {
clip_min(v);
range_presets.set_selected_index(0); // Manual
update_min(v);
on_range_changed();
};
field_frequency_min.set_value(presets_db[0].min); // Defaults to first preset
field_frequency_min.set_step(steps);
field_frequency_min.on_select = [this, &nav](NumberField& field) {
auto new_view = nav_.push<FrequencyKeypadView>(field_frequency_min.value() * MHZ_DIV);
new_view->on_changed = [this, &field](rf::Frequency f) {
clip_min(f / MHZ_DIV);
field_frequency_min.set_value(f / MHZ_DIV);
};
};
field_frequency_max.set_value(f_max / MHZ_DIV);
field_frequency_max.on_change = [this](int32_t v) {
clip_max(v);
range_presets.set_selected_index(0); // Manual
update_max(v);
on_range_changed();
};
field_frequency_max.set_value(presets_db[0].max); // Defaults to first preset
field_frequency_max.set_step(steps);
field_frequency_max.on_select = [this, &nav](NumberField& field) {
auto new_view = nav_.push<FrequencyKeypadView>(field_frequency_max.value() * MHZ_DIV);
new_view->on_changed = [this, &field](rf::Frequency f) {
clip_max(f / MHZ_DIV);
field_frequency_max.set_value(f / MHZ_DIV);
};
};
field_lna.on_change = [this](int32_t v) {
reset_live_view(true);
this->on_lna_changed(v);
};
field_lna.set_value(receiver_model.lna());
field_vga.on_change = [this](int32_t v_db) {
reset_live_view(true);
this->on_vga_changed(v_db);
};
field_vga.set_value(receiver_model.vga());
steps_config.on_change = [this](size_t n, OptionsField::value_t v) {
(void)n;
steps_config.on_change = [this](size_t, OptionsField::value_t v) {
field_frequency_min.set_step(v);
field_frequency_max.set_step(v);
steps = v;
};
steps_config.set_selected_index(0); // default of 1 Mhz steps
steps_config.set_selected_index(0); // 1 Mhz step.
scan_type.on_change = [this](size_t n, OptionsField::value_t v) {
(void)n;
scan_type.on_change = [this](size_t, OptionsField::value_t v) {
mode = v;
on_range_changed();
};
scan_type.set_selected_index(0); // default legacy fast scan
scan_type.set_selected_index(mode);
view_config.on_change = [this](size_t n, OptionsField::value_t v) {
(void)n;
// clear between changes
reset_live_view(true);
if (v == 0) {
live_frequency_view = 0;
level_integration.hidden(true);
freq_stats.hidden(true);
button_jump.hidden(true);
button_rst.hidden(true);
display.scroll_set_area(109, 319); // Restart scroll on the correct coordinates
} else if (v == 1) {
display.fill_rectangle({{0, 108}, {SCREEN_W, 24}}, {0, 0, 0});
live_frequency_view = 1;
display.scroll_disable();
level_integration.hidden(false);
freq_stats.hidden(false);
button_jump.hidden(false);
button_rst.hidden(false);
} else if (v == 2) {
display.fill_rectangle({{0, 108}, {SCREEN_W, 24}}, {0, 0, 0});
live_frequency_view = 2;
display.scroll_disable();
level_integration.hidden(false);
freq_stats.hidden(false);
button_jump.hidden(false);
button_rst.hidden(false);
view_config.on_change = [this](size_t, OptionsField::value_t v) {
reset_live_view(); // Clear between changes.
live_frequency_view = v;
switch (v) {
case 0: // SPEC
level_integration.hidden(true);
freq_stats.hidden(true);
button_jump.hidden(true);
button_rst.hidden(true);
display.scroll_set_area(109, 319); // Restart scroll on the correct coordinates.
break;
case 1: // LEVEL
display.fill_rectangle({{0, 108}, {SCREEN_W, 24}}, {0, 0, 0});
display.scroll_disable();
level_integration.hidden(false);
freq_stats.hidden(false);
button_jump.hidden(false);
button_rst.hidden(false);
break;
case 2: // PEAK
default:
display.fill_rectangle({{0, 108}, {SCREEN_W, 24}}, {0, 0, 0});
display.scroll_disable();
level_integration.hidden(false);
freq_stats.hidden(false);
button_jump.hidden(false);
button_rst.hidden(false);
break;
}
set_dirty();
};
view_config.set_selected_index(0); // default spectrum
view_config.set_selected_index(live_frequency_view);
level_integration.on_change = [this](size_t n, OptionsField::value_t v) {
(void)n;
reset_live_view(true);
level_integration.on_change = [this](size_t, OptionsField::value_t v) {
reset_live_view();
live_frequency_integrate = v;
};
level_integration.set_selected_index(3); // default integration of ( 3 * old value + new_value ) / 4
level_integration.set_selected_index(live_frequency_integrate);
filter_config.on_change = [this](size_t n, OptionsField::value_t v) {
(void)n;
reset_live_view(true);
filter_config.on_change = [this](size_t ix, OptionsField::value_t v) {
reset_live_view();
min_color_power = v;
filter_index = ix;
};
filter_config.set_selected_index(0);
filter_config.set_selected_index(filter_index);
range_presets.on_change = [this](size_t n, OptionsField::value_t v) {
(void)n;
range_presets.on_change = [this](size_t ix, OptionsField::value_t v) {
preset_index = ix;
if (ix == 0) return; // Don't update range for "Manual".
// NB: Don't trigger updates, presets directly set the range
// values without applying step or range lock.
field_frequency_min.set_value(presets_db[v].min, false);
field_frequency_max.set_value(presets_db[v].max, false);
on_range_changed();
};
range_presets.set_selected_index(preset_index);
button_marker.on_change = [this]() {
if (((int)marker_pixel_index + button_marker.get_encoder_delta()) < 0) {
marker_pixel_index = 0;
} else if (((int)marker_pixel_index + button_marker.get_encoder_delta()) > SCREEN_W) {
marker_pixel_index = SCREEN_W;
} else {
marker_pixel_index = marker_pixel_index + button_marker.get_encoder_delta();
}
field_marker.on_encoder_change = [this](TextField&, EncoderEvent delta) {
marker_pixel_index = clip<uint8_t>(marker_pixel_index + delta, 0, SCREEN_W);
on_marker_change();
button_marker.set_encoder_delta(0);
};
button_marker.on_select = [this](ButtonWithEncoder&) {
receiver_model.set_target_frequency(marker); // Center tune rx in marker freq.
auto settings = receiver_model.settings();
settings.frequency_step = MHZ_DIV; // Preset a 1 MHz frequency step into RX -> AUDIO
nav_.replace<AnalogAudioView>(settings); // Jump into audio view
field_marker.on_select = [this](TextField&) {
// Launch Audio with marker frequency.
launch_audio(marker);
};
field_trigger.on_change = [this](int32_t v) {
baseband::set_spectrum(looking_glass_bandwidth, v);
trigger = v;
baseband::set_spectrum(looking_glass_bandwidth, trigger);
};
field_trigger.set_value(32); // Defaults to 32, as normal triggering resolution
field_trigger.set_value(trigger);
button_range.on_select = [this](Button&) {
if (locked_range) {
locked_range = false;
button_range.set_style(&Styles::white);
button_range.set_text(" " + to_string_dec_uint(search_span) + " ");
} else {
locked_range = true;
button_range.set_style(&Styles::red);
button_range.set_text(">" + to_string_dec_uint(search_span) + "<");
}
field_range.on_select = [this](TextField&) {
locked_range = !locked_range;
update_range_field();
};
button_jump.on_select = [this](Button&) {
receiver_model.set_target_frequency(max_freq_hold); // Center tune rx in marker freq.
auto settings = receiver_model.settings();
settings.frequency_step = MHZ_DIV; // Preset a 1 MHz frequency step into RX -> AUDIO
nav_.replace<AnalogAudioView>(settings); // Jump into audio view
// Launch Audio with peak frequency.
launch_audio(max_freq_hold);
};
button_rst.on_select = [this](Button&) {
reset_live_view(true);
reset_live_view();
};
display.scroll_set_area(109, 319);
baseband::set_spectrum(looking_glass_bandwidth, field_trigger.value()); // trigger:
// Discord User jteich: WidebandSpectrum::on_message to set the trigger value. In WidebandSpectrum::execute ,
// it keeps adding the output of the fft to the buffer until "trigger" number of calls are made,
// at which time it pushes the buffer up with channel_spectrum.feed
marker_pixel_index = 120;
on_range_changed();
// trigger:
// Discord User jteich: WidebandSpectrum::on_message to set the trigger value. In WidebandSpectrum::execute,
// it keeps adding the output of the fft to the buffer until "trigger" number of calls are made,
// at which time it pushes the buffer up with channel_spectrum.feed
baseband::set_spectrum(looking_glass_bandwidth, trigger);
marker_pixel_index = SCREEN_W / 2;
on_range_changed(); // Force a UI update.
receiver_model.set_sampling_rate(looking_glass_sampling_rate); // 20mhz
receiver_model.set_baseband_bandwidth(looking_glass_bandwidth); // possible values: 1.75/2.5/3.5/5/5.5/6/7/8/9/10/12/14/15/20/24/28MHz
@@ -519,11 +491,14 @@ GlassView::GlassView(
receiver_model.enable();
}
void GlassView::load_Presets() {
void GlassView::load_presets() {
File presets_file;
auto error = presets_file.open("LOOKINGGLASS/PRESETS.TXT");
presets_db.clear();
// Add the "Manual" entry.
presets_db.push_back({0, 0, "Manual"});
if (!error) {
auto reader = FileLineReader(presets_file);
for (const auto& line : reader) {
@@ -546,14 +521,10 @@ void GlassView::load_Presets() {
}
}
// Couldn't load any from the file, load a default instead.
if (presets_db.empty())
presets_Default();
populate_Presets();
populate_presets();
}
void GlassView::populate_Presets() {
void GlassView::populate_presets() {
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
options_t entries;
@@ -561,12 +532,14 @@ void GlassView::populate_Presets() {
for (const auto& preset : presets_db)
entries.emplace_back(preset.label, entries.size());
range_presets.set_options(entries);
range_presets.set_options(std::move(entries));
}
void GlassView::presets_Default() {
presets_db.clear();
presets_db.push_back({2320, 2560, "DEFAULT WIFI 2.4GHz"});
void GlassView::launch_audio(rf::Frequency center_freq) {
receiver_model.set_target_frequency(center_freq);
auto settings = receiver_model.settings();
settings.frequency_step = MHZ_DIV; // Preset a 1 MHz frequency step into RX -> AUDIO
nav_.replace<AnalogAudioView>(settings); // Jump into audio view
}
} // namespace ui
@@ -72,8 +72,28 @@ class GlassView : public View {
private:
NavigationView& nav_;
RxRadioState radio_state_{ReceiverModel::Mode::SpectrumAnalysis};
// Settings
rf::Frequency f_min = 260 * MHZ_DIV; // Default to 315/433 remote range.
rf::Frequency f_max = 500 * MHZ_DIV;
uint8_t preset_index = 0; // Manual
uint8_t filter_index = 0; // OFF
uint8_t trigger = 32;
uint8_t mode = LOOKING_GLASS_FASTSCAN;
uint8_t live_frequency_view = 0; // Spectrum
uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4
app_settings::SettingsManager settings_{
"rx_glass", app_settings::Mode::RX};
"rx_glass"sv,
app_settings::Mode::RX,
{
{"min"sv, &f_min},
{"max"sv, &f_max},
{"preset"sv, &preset_index},
{"filter"sv, &filter_index},
{"trigger"sv, &trigger},
{"scan_mode"sv, &mode},
{"freq_view"sv, &live_frequency_view},
{"freq_integrate"sv, &live_frequency_integrate},
}};
struct preset_entry {
rf::Frequency min{};
@@ -82,8 +102,9 @@ class GlassView : public View {
};
std::vector<preset_entry> presets_db{};
void clip_min(int32_t v);
void clip_max(int32_t v);
void update_min(int32_t v);
void update_max(int32_t v);
void update_range_field();
void get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power);
rf::Frequency get_freq_from_bin_pos(uint8_t pos);
void on_marker_change();
@@ -94,57 +115,52 @@ class GlassView : public View {
int64_t next_mult_of(int64_t num, int64_t multiplier);
void adjust_range(int64_t* f_min, int64_t* f_max, int64_t width);
void on_range_changed();
void on_lna_changed(int32_t v_db);
void on_vga_changed(int32_t v_db);
void reset_live_view(bool clear_screen);
void reset_live_view();
void add_spectrum_pixel(uint8_t power);
void PlotMarker(uint8_t pos);
void load_Presets();
void txtline_process(std::string& line);
void populate_Presets();
void presets_Default();
void plot_marker(uint8_t pos);
void load_presets();
void populate_presets();
void launch_audio(rf::Frequency center_freq);
rf::Frequency f_min{0}, f_max{0};
rf::Frequency search_span{0};
rf::Frequency f_center{0};
rf::Frequency f_center_ini{0};
rf::Frequency marker{0};
uint8_t marker_pixel_index{0};
rf::Frequency marker_pixel_step{0};
// size of one spectrum bin in Hz
// Size of one spectrum bin in Hz.
rf::Frequency each_bin_size{0};
// consumed number of Hz, used to know if we have filled a 'bag' , a corresponding pixel length on screen
rf::Frequency bins_Hz_size{0};
// Bandwidth of a single spectrum bin.
rf::Frequency bins_hz_size{0};
rf::Frequency looking_glass_sampling_rate{0};
rf::Frequency looking_glass_bandwidth{0};
rf::Frequency looking_glass_range{0};
rf::Frequency looking_glass_step{0};
uint8_t min_color_power{0};
uint8_t min_color_power{0}; // Filter cutoff level.
uint32_t pixel_index{0};
std::array<Color, SCREEN_W> spectrum_row = {0};
std::array<uint8_t, SCREEN_W> spectrum_data = {0};
ChannelSpectrumFIFO* fifo{nullptr};
uint8_t max_power = 0;
int32_t steps = 0;
uint8_t live_frequency_view = 0;
int16_t live_frequency_integrate = 3;
int64_t max_freq_hold = 0;
int16_t max_freq_power = -1000;
std::array<Color, SCREEN_W> spectrum_row{};
std::array<uint8_t, SCREEN_W> spectrum_data{};
ChannelSpectrumFIFO* fifo{};
int32_t steps = 1;
bool locked_range = false;
uint8_t max_power = 0;
rf::Frequency max_freq_hold = 0;
rf::Frequency last_max_freq = 0;
int16_t max_freq_power = -1000;
uint8_t bin_length = SCREEN_W;
uint8_t real_bin_length = SCREEN_W;
uint8_t offset = 0;
uint8_t tune_offset = 0;
uint8_t bin = 0;
int64_t last_max_freq = 0;
uint8_t mode = LOOKING_GLASS_FASTSCAN;
uint8_t ignore_dc = 0;
Labels labels{
{{0, 0}, "MIN: MAX: LNA VGA ", Color::light_grey()},
{{0, 1 * 16}, "RANGE: FILTER: AMP:", Color::light_grey()},
{{0, 0 * 16}, "MIN: MAX: LNA VGA ", Color::light_grey()},
{{0, 1 * 16}, "RANGE: FILTER: AMP:", Color::light_grey()},
{{0, 2 * 16}, "PRESET:", Color::light_grey()},
{{0, 3 * 16}, "MARKER: MHz", Color::light_grey()},
{{0, 3 * 16}, "MARKER: MHz", Color::light_grey()},
{{0, 4 * 16}, "RES: STEP:", Color::light_grey()}};
NumberField field_frequency_min{
@@ -167,8 +183,8 @@ class GlassView : public View {
VGAGainField field_vga{
{27 * 8, 0 * 16}};
Button button_range{
{7 * 8, 1 * 16, 4 * 8, 16},
TextField field_range{
{6 * 8, 1 * 16, 6 * 8, 16},
""};
OptionsField filter_config{
@@ -176,23 +192,21 @@ class GlassView : public View {
4,
{
{"OFF ", 0},
{"MID ", 118}, // 85 +25 (110) + a bit more to kill all blue
{"MID ", 118}, // 85 + 25 (110) + a bit more to kill all blue
{"HIGH", 202}, // 168 + 25 (193)
}};
RFAmpField field_rf_amp{
{29 * 8, 1 * 16}};
{28 * 8, 1 * 16}};
OptionsField range_presets{
{7 * 8, 2 * 16},
20,
{
{" NONE (WIFI 2.4GHz)", 0},
}};
{}};
ButtonWithEncoder button_marker{
{7 * 8, 3 * 16, 10 * 8, 16},
" "};
TextField field_marker{
{7 * 8, 3 * 16, 9 * 8, 16},
""};
NumberField field_trigger{
{4 * 8, 4 * 16},
+10 -11
View File
@@ -24,18 +24,18 @@
#include "ui_playlist.hpp"
#include "baseband_api.hpp"
#include "convert.hpp"
#include "file_reader.hpp"
#include "io_file.hpp"
#include "io_convert.hpp"
#include "oversample.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
#include "string_format.hpp"
#include "ui_fileman.hpp"
#include "utility.hpp"
#include "baseband_api.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
#include <unistd.h>
#include <fstream>
@@ -266,17 +266,16 @@ void PlaylistView::send_current_track() {
return;
}
// ReplayThread starts immediately on construction so
// these need to be set before creating the ReplayThread.
// Update the sample rate in proc_replay baseband.
baseband::set_sample_rate(current()->metadata.sample_rate,
get_oversample_rate(current()->metadata.sample_rate));
// ReplayThread starts immediately on construction; must be set before creating.
transmitter_model.set_target_frequency(current()->metadata.center_frequency);
transmitter_model.set_sampling_rate(current()->metadata.sample_rate * 8);
transmitter_model.set_sampling_rate(get_actual_sample_rate(current()->metadata.sample_rate));
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
transmitter_model.enable();
// Set baseband sample rate too for waterfall to be correct.
// TODO: Why doesn't the transmitter_model just handle this?
baseband::set_sample_rate(transmitter_model.sampling_rate());
// Reset the transmit progress bar.
progressbar_transmit.set_value(0);
+61 -138
View File
@@ -37,6 +37,22 @@ namespace fs = std::filesystem;
namespace ui {
void ReconView::check_update_ranges_from_current() {
if (frequency_list.size() && current_is_valid() && current_entry().type == freqman_type::Range) {
if (update_ranges && !manual_mode) {
button_manual_start.set_text(to_string_short_freq(current_entry().frequency_a));
frequency_range.min = current_entry().frequency_a;
if (current_entry().frequency_b != 0) {
button_manual_end.set_text(to_string_short_freq(current_entry().frequency_b));
frequency_range.max = current_entry().frequency_b;
} else {
button_manual_end.set_text(to_string_short_freq(current_entry().frequency_a));
frequency_range.max = current_entry().frequency_a;
}
}
}
}
bool ReconView::current_is_valid() {
return (unsigned)current_index < frequency_list.size();
}
@@ -160,67 +176,16 @@ bool ReconView::recon_save_freq(const fs::path& path, size_t freq_index, bool wa
return true;
}
bool ReconView::recon_load_config_from_sd() {
make_new_directory(u"SETTINGS");
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++;
}
return complete;
}
bool ReconView::recon_save_config_to_sd() {
File settings_file;
make_new_directory(u"SETTINGS");
auto error = settings_file.create(RECON_CFG_FILE);
if (error)
return false;
settings_file.write_line(input_file);
settings_file.write_line(output_file);
settings_file.write_line(to_string_dec_uint(recon_lock_duration));
settings_file.write_line(to_string_dec_uint(recon_lock_nb_match));
settings_file.write_line(to_string_dec_int(squelch));
settings_file.write_line(to_string_dec_uint(recon_match_mode));
settings_file.write_line(to_string_dec_int(wait));
return true;
void ReconView::load_persisted_settings() {
autostart = persistent_memory::recon_autostart_recon();
autosave = persistent_memory::recon_autosave_freqs();
continuous = persistent_memory::recon_continuous();
filedelete = persistent_memory::recon_clear_output();
load_freqs = persistent_memory::recon_load_freqs();
load_ranges = persistent_memory::recon_load_ranges();
load_hamradios = persistent_memory::recon_load_hamradios();
update_ranges = persistent_memory::recon_update_ranges_when_recon();
auto_record_locked = persistent_memory::recon_auto_record_locked();
}
void ReconView::audio_output_start() {
@@ -290,19 +255,7 @@ void ReconView::handle_retune() {
}
if (last_index != current_index) {
last_index = current_index;
if (frequency_list.size() && current_is_valid() && current_entry().type == freqman_type::Range) {
if (update_ranges && !manual_mode) {
button_manual_start.set_text(to_string_short_freq(current_entry().frequency_a));
frequency_range.min = current_entry().frequency_a;
if (current_entry().frequency_b != 0) {
button_manual_end.set_text(to_string_short_freq(current_entry().frequency_b));
frequency_range.max = current_entry().frequency_b;
} else {
button_manual_end.set_text(to_string_short_freq(current_entry().frequency_a));
frequency_range.max = current_entry().frequency_a;
}
}
}
check_update_ranges_from_current();
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
update_description();
}
@@ -315,7 +268,6 @@ void ReconView::focus() {
ReconView::~ReconView() {
recon_stop_recording();
recon_save_config_to_sd();
if (field_mode.selected_index_value() != SPEC_MODULATION)
audio::output::stop();
receiver_model.disable();
@@ -373,29 +325,17 @@ ReconView::ReconView(NavigationView& nav)
};
def_step = 0;
// HELPER: Pre-setting a manual range, based on stored frequency
rf::Frequency stored_freq = receiver_model.target_frequency();
if (stored_freq - OneMHz > 0)
frequency_range.min = stored_freq - OneMHz;
else
frequency_range.min = 0;
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
if (stored_freq + OneMHz < MAX_UFREQ)
frequency_range.max = stored_freq + OneMHz;
else
frequency_range.max = MAX_UFREQ;
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
load_persisted_settings();
// Loading settings
autostart = persistent_memory::recon_autostart_recon();
autosave = persistent_memory::recon_autosave_freqs();
continuous = persistent_memory::recon_continuous();
filedelete = persistent_memory::recon_clear_output();
load_freqs = persistent_memory::recon_load_freqs();
load_ranges = persistent_memory::recon_load_ranges();
load_hamradios = persistent_memory::recon_load_hamradios();
update_ranges = persistent_memory::recon_update_ranges_when_recon();
auto_record_locked = persistent_memory::recon_auto_record_locked();
// When update_ranges is set or range invalid, use the rx model frequency instead of the saved values.
if (update_ranges || frequency_range.max == 0) {
rf::Frequency stored_freq = receiver_model.target_frequency();
frequency_range.min = clip<rf::Frequency>(stored_freq - OneMHz, 0, MAX_UFREQ);
frequency_range.max = clip<rf::Frequency>(stored_freq + OneMHz, 0, MAX_UFREQ);
}
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
button_manual_start.on_select = [this, &nav](ButtonWithEncoder& button) {
clear_freqlist_for_ui_action();
@@ -562,9 +502,14 @@ ReconView::ReconView(NavigationView& nav)
recon_pause();
if (!frequency_range.min || !frequency_range.max) {
nav_.display_modal("Error", "Both START and END freqs\nneed a value");
} else if (frequency_range.min > frequency_range.max) {
nav_.display_modal("Error", "END freq\nis lower than START");
} else {
if (frequency_range.min > frequency_range.max) {
std::swap(frequency_range.min, frequency_range.max);
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
}
// no need to stop audio in SPEC
if (field_mode.selected_index_value() != SPEC_MODULATION)
audio::output::stop();
@@ -690,16 +635,9 @@ ReconView::ReconView(NavigationView& nav)
input_file = result[0];
output_file = result[1];
freq_file_path = get_freqman_path(output_file).string();
recon_save_config_to_sd();
autosave = persistent_memory::recon_autosave_freqs();
autostart = persistent_memory::recon_autostart_recon();
filedelete = persistent_memory::recon_clear_output();
load_freqs = persistent_memory::recon_load_freqs();
load_ranges = persistent_memory::recon_load_ranges();
load_hamradios = persistent_memory::recon_load_hamradios();
update_ranges = persistent_memory::recon_update_ranges_when_recon();
auto_record_locked = persistent_memory::recon_auto_record_locked();
load_persisted_settings();
ui_settings.save();
frequency_file_load(false);
freqlist_cleared_for_ui_action = false;
@@ -747,7 +685,6 @@ ReconView::ReconView(NavigationView& nav)
file_name.set("=>");
// Loading input and output file from settings
recon_load_config_from_sd();
freq_file_path = get_freqman_path(output_file).string();
field_recon_match_mode.set_selected_index(recon_match_mode);
@@ -839,17 +776,7 @@ void ReconView::frequency_file_load(bool) {
receiver_model.enable();
receiver_model.set_squelch_level(0);
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
if (update_ranges && !manual_mode) {
button_manual_start.set_text(to_string_short_freq(current_entry().frequency_a));
frequency_range.min = current_entry().frequency_a;
if (current_entry().frequency_b != 0) {
button_manual_end.set_text(to_string_short_freq(current_entry().frequency_b));
frequency_range.max = current_entry().frequency_b;
} else {
button_manual_end.set_text(to_string_short_freq(current_entry().frequency_a));
frequency_range.max = current_entry().frequency_a;
}
}
check_update_ranges_from_current();
update_description();
handle_retune();
}
@@ -861,10 +788,6 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
chrono_end = chTimeNow();
if (field_mode.selected_index_value() == SPEC_MODULATION) {
time_interval = chrono_end - chrono_start;
// capping here to avoid double check a frequency because time_interval is more than exactly 100 by a few units
// this is because we are making a measurement and not using a fixed value
if (time_interval > 100)
time_interval = 100;
if (field_lock_wait.value() == 0) {
if (time_interval <= 1) // capping here to avoid freeze because too quick
local_recon_lock_duration = 2; // minimum working tested value
@@ -892,7 +815,6 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
status = 0;
freq_lock = 0;
timer = local_recon_lock_duration;
big_display.set_style(&Styles::white);
}
if (freq_lock < recon_lock_nb_match) // LOCKING
{
@@ -1202,48 +1124,50 @@ size_t ReconView::change_mode(freqman_index_t new_mod) {
switch (new_mod) {
case AM_MODULATION:
freqman_set_bandwidth_option(new_mod, field_bw);
// bw DSB (0) default
field_bw.set_by_value(0);
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
receiver_model.set_am_configuration(field_bw.selected_index_value());
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_am_configuration(n); };
// bw DSB (0) default
field_bw.set_by_value(0);
text_ctcss.set(" ");
recording_sampling_rate = 12000;
break;
case NFM_MODULATION:
freqman_set_bandwidth_option(new_mod, field_bw);
// bw 16k (2) default
field_bw.set_by_value(2);
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
receiver_model.set_nbfm_configuration(field_bw.selected_index_value());
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_nbfm_configuration(n); };
// bw 16k (2) default
field_bw.set_by_value(2);
recording_sampling_rate = 24000;
break;
case WFM_MODULATION:
freqman_set_bandwidth_option(new_mod, field_bw);
// bw 200k (0) default
field_bw.set_by_value(0);
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
receiver_model.set_wfm_configuration(field_bw.selected_index_value());
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_wfm_configuration(n); };
// bw 200k (0) default
field_bw.set_by_value(0);
text_ctcss.set(" ");
recording_sampling_rate = 48000;
break;
case SPEC_MODULATION:
freqman_set_bandwidth_option(new_mod, field_bw);
// bw 200k (0) default
baseband::run_image(portapack::spi_flash::image_tag_capture);
receiver_model.set_modulation(ReceiverModel::Mode::Capture);
field_bw.set_by_value(0);
field_bw.on_change = [this](size_t, OptionsField::value_t sampling_rate) {
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);
// record_view determines the correct oversampling to apply and returns the actual sample rate.
auto actual_sampling_rate = record_view->set_sampling_rate(sampling_rate);
// The radio needs to know the effective sampling rate.
receiver_model.set_sampling_rate(actual_sampling_rate);
receiver_model.set_baseband_bandwidth(filter_bandwidth_for_sampling_rate(actual_sampling_rate));
};
// bw 12k5 (0) default
field_bw.set_by_value(0);
text_ctcss.set(" ");
break;
default:
@@ -1251,7 +1175,6 @@ 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 distorted
receiver_model.set_sampling_rate(3072000);
+18 -3
View File
@@ -68,8 +68,9 @@ class ReconView : public View {
RxRadioState radio_state_{};
app_settings::SettingsManager settings_{
"rx_recon", app_settings::Mode::RX};
"rx_recon"sv, app_settings::Mode::RX};
void check_update_ranges_from_current();
void set_loop_config(bool v);
void clear_freqlist_for_ui_action();
void reset_indexes();
@@ -89,8 +90,7 @@ class ReconView : public View {
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();
void load_persisted_settings();
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();
@@ -163,6 +163,21 @@ class ReconView : public View {
systime_t chrono_start{};
systime_t chrono_end{};
// Persisted settings.
SettingsStore ui_settings{
"recon"sv,
{
{"input_file"sv, &input_file},
{"output_file"sv, &output_file},
{"lock_duration"sv, &recon_lock_duration},
{"lock_nb_match"sv, &recon_lock_nb_match},
{"squelch_level"sv, &squelch},
{"match_mode"sv, &recon_match_mode},
{"match_wait"sv, &wait},
{"range_min"sv, &frequency_range.min},
{"range_max"sv, &frequency_range.max},
}};
std::unique_ptr<RecordView> record_view{};
Labels labels{
@@ -31,9 +31,6 @@
#include "ui_navigation.hpp"
#include "string_format.hpp"
// maximum usable freq
#define MAX_UFREQ 7200000000
// 1Mhz helper
#ifdef OneMHz
#undef OneMHz
+37 -37
View File
@@ -31,8 +31,6 @@ 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;
@@ -234,7 +232,7 @@ void ScannerView::handle_retune(int64_t freq, uint32_t freq_idx) {
if (!manual_search) {
if (entries.size() > 0)
text_current_index.set(to_string_dec_uint(freq_idx + 1, 3));
field_current_index.set_text(to_string_dec_uint(freq_idx + 1, 3));
if (freq_idx < entries.size() && entries[freq_idx].description.size() > 1)
text_current_desc.set(entries[freq_idx].description); // Show description from file
@@ -243,9 +241,20 @@ void ScannerView::handle_retune(int64_t freq, uint32_t freq_idx) {
}
}
void ScannerView::handle_encoder(EncoderEvent delta) {
auto index_step = delta > 0 ? 1 : -1;
if (scan_thread)
scan_thread->set_index_stepper(index_step);
// Restart browse timer when frequency changes.
if (browse_timer != 0)
browse_timer = 1;
}
std::string ScannerView::loaded_filename() const {
auto filename = loaded_path.filename().string();
if (filename.length() > 23) { // Truncate and add ellipses if long file name
auto filename = freqman_file;
if (filename.length() > 23) { // Truncate long file name.
filename.resize(22);
filename = filename + "+";
}
@@ -266,7 +275,7 @@ ScannerView::~ScannerView() {
}
void ScannerView::show_max_index() { // show total number of freqs to scan
text_current_index.set("---");
field_current_index.set_text("---");
if (entries.size() == FREQMAN_MAX_PER_FILE) {
text_max_index.set_style(&Styles::red);
@@ -293,7 +302,7 @@ ScannerView::ScannerView(
&button_load,
&button_clear,
&rssi,
&text_current_index,
&field_current_index,
&text_max_index,
&text_current_desc,
&big_display,
@@ -319,11 +328,7 @@ ScannerView::ScannerView(
field_step.set_by_value(receiver_model.frequency_step()); // Default step interval (Hz)
change_mode((freqman_index_t)field_mode.selected_index_value());
// FUTURE: perhaps additional settings should be stored in persistent memory vs using defaults
rf::Frequency stored_freq = receiver_model.target_frequency();
frequency_range.min = stored_freq - 1000000;
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
frequency_range.max = stored_freq + 1000000;
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
// Button to load a Freqman file.
@@ -340,14 +345,14 @@ ScannerView::ScannerView(
};
};
// Button to clear in-memory frequency list
// Button to clear in-memory frequency list.
button_clear.on_select = [this, &nav](Button&) {
if (scan_thread && entries.size()) {
scan_thread->stop(); // STOP SCANNER THREAD
entries.clear();
show_max_index(); // UPDATE new list size on screen
text_current_index.set("");
field_current_index.set_text("");
text_current_desc.set(loaded_filename());
scan_thread->set_freq_lock(0); // Reset the scanner lock
@@ -355,7 +360,7 @@ ScannerView::ScannerView(
}
};
// Button to configure starting frequency for a manual range search
// Button to configure starting frequency for a manual range search.
button_manual_start.on_select = [this, &nav](Button& button) {
auto new_view = nav_.push<FrequencyKeypadView>(frequency_range.min);
new_view->on_changed = [this, &button](rf::Frequency f) {
@@ -364,7 +369,7 @@ ScannerView::ScannerView(
};
};
// Button to configure ending frequency for a manual range search
// Button to configure ending frequency for a manual range search.
button_manual_end.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(frequency_range.max);
new_view->on_changed = [this, &button](rf::Frequency f) {
@@ -373,7 +378,7 @@ ScannerView::ScannerView(
};
};
// Button to pause/resume scan (note that some other buttons will trigger resume also)
// Button to pause/resume scan (note that some other buttons will trigger resume also).
button_pause.on_select = [this](ButtonWithEncoder&) {
if (userpause)
user_resume();
@@ -384,19 +389,14 @@ ScannerView::ScannerView(
}
};
// Encoder dial causes frequency change when focus is on pause button
// Encoder dial causes frequency change when focus is on pause button or current index.
button_pause.on_change = [this]() {
int32_t encoder_delta{(button_pause.get_encoder_delta() > 0) ? 1 : -1};
if (scan_thread)
scan_thread->set_index_stepper(encoder_delta);
// Restart browse timer when frequency changes
if (browse_timer != 0)
browse_timer = 1;
handle_encoder(button_pause.get_encoder_delta());
button_pause.set_encoder_delta(0);
};
field_current_index.on_encoder_change = [this](TextField&, EncoderEvent delta) {
handle_encoder(delta);
};
// Button to switch to Audio app
button_audio_app.on_select = [this](Button&) {
@@ -489,7 +489,7 @@ ScannerView::ScannerView(
// Button to add current frequency (found during Search) to the Scan Frequency List
button_add.on_select = [this](Button&) {
FreqmanDB db;
if (db.open(loaded_path, /*create*/ true)) {
if (db.open(get_freqman_path(freqman_file), /*create*/ true)) {
freqman_entry entry{
.frequency_a = current_frequency,
.type = freqman_type::Single,
@@ -515,23 +515,27 @@ ScannerView::ScannerView(
}
}
} else {
nav_.display_modal("Error", "Cannot open " + loaded_path.filename().string() + "\nfor appending freq.");
nav_.display_modal("Error", "Cannot open " + freqman_file + ".TXT\nfor appending freq.");
bigdisplay_update(-1); // Need to poke this control after displaying modal?
}
};
// PRE-CONFIGURATION:
field_browse_wait.on_change = [this](int32_t v) { browse_wait = v; };
field_browse_wait.set_value(5);
field_browse_wait.set_value(browse_wait);
field_lock_wait.on_change = [this](int32_t v) { lock_wait = v; };
field_lock_wait.set_value(2);
field_lock_wait.set_value(lock_wait);
field_squelch.on_change = [this](int32_t v) { squelch = v; };
field_squelch.set_value(-30);
field_squelch.set_value(squelch);
// Disable squelch on the model because RSSI handler is where the
// actual squelching is applied for this app.
receiver_model.set_squelch_level(0);
// LOAD FREQUENCIES
frequency_file_load(default_scan_file);
frequency_file_load(get_freqman_path(freqman_file));
}
void ScannerView::frequency_file_load(const fs::path& path) {
@@ -542,12 +546,11 @@ void ScannerView::frequency_file_load(const fs::path& path) {
FreqmanDB db;
if (!db.open(path)) {
text_current_desc.set("NO " + path.filename().string());
loaded_path = default_scan_file;
return;
}
entries.clear();
loaded_path = path;
freqman_file = path.stem().string();
Optional<scanner_range_t> range;
for (auto entry : db) {
@@ -726,9 +729,6 @@ void ScannerView::change_mode(freqman_index_t new_mod) {
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
+27 -13
View File
@@ -37,7 +37,7 @@
#define SCANNER_SLEEP_MS 50 // ms that Scanner Thread sleeps per loop
#define STATISTICS_UPDATES_PER_SEC 10
#define MAX_FREQ_LOCK 10 //# of 50ms cycles scanner locks into freq when signal detected, to verify signal is not spureous
#define MAX_FREQ_LOCK 10 //# of 50ms cycles scanner locks into freq when signal detected, to verify signal is not spurious
namespace ui {
@@ -110,11 +110,29 @@ class ScannerView : public View {
std::string title() const override { return "Scanner"; };
private:
static constexpr const char* default_freqman_file = "SCANNER";
RxRadioState radio_state_{};
// Settings
uint32_t browse_wait{5};
uint32_t lock_wait{2};
int32_t squelch{-30};
scanner_range_t frequency_range{0, MAX_UFREQ};
std::string freqman_file{default_freqman_file};
app_settings::SettingsManager settings_{
"rx_scanner", app_settings::Mode::RX};
"rx_scanner"sv,
app_settings::Mode::RX,
{
{"browse_wait"sv, &browse_wait},
{"lock_wait"sv, &lock_wait},
{"scanner_squelch"sv, &squelch},
{"range_min"sv, &frequency_range.min},
{"range_max"sv, &frequency_range.max},
{"file"sv, &freqman_file},
}};
NavigationView& nav_;
RxRadioState radio_state_{};
void start_scan_thread();
void restart_scan();
@@ -128,20 +146,15 @@ class ScannerView : public View {
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);
void handle_encoder(EncoderEvent delta);
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};
uint32_t color_timer{0};
int32_t bigdisplay_current_color{-2};
rf::Frequency bigdisplay_current_frequency{0};
uint32_t browse_wait{0};
uint32_t lock_wait{0};
std::filesystem::path loaded_path{};
std::vector<scanner_entry_t> entries{};
uint32_t current_index{0};
rf::Frequency current_frequency{0};
@@ -213,8 +226,9 @@ class ScannerView : public View {
{0 * 16, 2 * 16, 15 * 16, 8},
};
Text text_current_index{
TextField field_current_index{
{0, 3 * 16, 3 * 8, 16},
{},
};
Text text_max_index{
@@ -225,9 +239,9 @@ class ScannerView : public View {
{0, 4 * 16, 240 - 6 * 8, 16},
};
BigFrequency big_display{// Show frequency in glamour
{4, 6 * 16, 28 * 8, 52},
0};
BigFrequency big_display{
{4, 6 * 16, 28 * 8, 52},
0};
Button button_manual_start{
{0 * 8, 11 * 16, 11 * 8, 28},
@@ -169,6 +169,8 @@ SetRadioView::SetRadioView(
&button_save,
&button_cancel});
add_children({&disable_external_tcxo});
SetFrequencyCorrectionModel model{
static_cast<int8_t>(pmem::correction_ppb() / 1000), 0};
@@ -221,10 +223,13 @@ SetRadioView::SetRadioView(
send_system_refresh();
};
disable_external_tcxo.set_value(pmem::config_disable_external_tcxo());
button_save.on_select = [this, &nav](Button&) {
const auto model = this->form_collect();
pmem::set_correction_ppb(model.ppm * 1000);
pmem::set_clkout_freq(model.freq);
pmem::set_config_disable_external_tcxo(disable_external_tcxo.value());
clock_manager.enable_clock_output(pmem::clkout_enabled());
nav.pop();
};
@@ -185,6 +185,11 @@ class SetRadioView : public View {
5,
"Turn on bias voltage"};
Checkbox disable_external_tcxo{
{18, 14 * 16},
5,
"Disable external TCXO"};
Button button_save{
{2 * 8, 16 * 16, 12 * 8, 32},
"Save"};
+1 -1
View File
@@ -194,7 +194,7 @@ void SondeView::on_packet(const sonde::Packet& packet) {
if (temp_humid_info.temp != 0) {
double decimals = abs(get_decimals(temp_humid_info.temp, 10, true));
text_temp.set(to_string_dec_int((int)temp_humid_info.temp) + "." + to_string_dec_uint(decimals, 1) + "C");
text_temp.set(to_string_dec_int((int)temp_humid_info.temp) + "." + to_string_dec_uint(decimals, 1) + STR_DEGREES_C);
}
gps_info = packet.get_GPS_data();
+3 -1
View File
@@ -360,6 +360,8 @@ TextEditorView::TextEditorView(NavigationView& nav)
&text_size,
});
viewer.set_font_zoom(enable_zoom);
viewer.on_select = [this]() {
// Treat as if menu button was pressed.
if (button_menu.on_select)
@@ -382,7 +384,7 @@ TextEditorView::TextEditorView(NavigationView& nav)
};
menu.on_zoom() = [this]() {
viewer.toggle_font_zoom();
enable_zoom = viewer.toggle_font_zoom();
refresh_ui();
hide_menu(true);
};
+11 -1
View File
@@ -32,6 +32,7 @@
#include "ui_styles.hpp"
#include "ui_widget.hpp"
#include "app_settings.hpp"
#include "file_wrapper.hpp"
#include "optional.hpp"
@@ -80,7 +81,10 @@ class TextViewer : public Widget {
const Style& style() { return *font_style; }
void set_font_zoom(bool zoom);
void toggle_font_zoom() { set_font_zoom(!font_zoom); };
bool toggle_font_zoom() {
set_font_zoom(!font_zoom);
return font_zoom;
};
private:
bool font_zoom{};
@@ -220,6 +224,12 @@ class TextEditorView : public View {
void on_show() override;
private:
// Settings
bool enable_zoom = false;
SettingsStore settings_store_{
"notepad"sv,
{{"enable_zoom"sv, &enable_zoom}}};
static constexpr size_t max_edit_length = 1024;
std::string edit_line_buffer_{};
+2 -7
View File
@@ -347,13 +347,8 @@ void spectrum_streaming_stop() {
send_message(&message);
}
void set_sample_rate(const uint32_t sample_rate) {
SamplerateConfigMessage message{sample_rate};
send_message(&message);
}
void set_oversample_rate(OversampleRate oversample_rate) {
OversampleRateConfigMessage message{oversample_rate};
void set_sample_rate(uint32_t sample_rate, OversampleRate oversample_rate) {
SampleRateConfigMessage message{sample_rate, oversample_rate};
send_message(&message);
}
+2 -2
View File
@@ -94,8 +94,8 @@ void shutdown();
void spectrum_streaming_start();
void spectrum_streaming_stop();
void set_sample_rate(const uint32_t sample_rate);
void set_oversample_rate(OversampleRate oversample_rate);
/* NB: sample_rate should be desired rate. Don't pre-scale. */
void set_sample_rate(uint32_t sample_rate, OversampleRate oversample_rate = OversampleRate::None);
void capture_start(CaptureConfig* const config);
void capture_stop();
void replay_start(ReplayConfig* const config);
+9 -3
View File
@@ -325,12 +325,18 @@ uint32_t ClockManager::measure_gp_clkin_frequency() {
}
bool ClockManager::loss_of_signal() {
return hackrf_r9
? clock_generator.plla_loss_of_signal()
: clock_generator.clkin_loss_of_signal();
if (hackrf_r9) {
const auto frequency = measure_gp_clkin_frequency();
return (frequency < 9850000) || (frequency > 10150000);
} else {
return clock_generator.clkin_loss_of_signal();
}
}
ClockManager::ReferenceSource ClockManager::detect_reference_source() {
if (portapack::persistent_memory::config_disable_external_tcxo())
return ReferenceSource::Xtal;
if (loss_of_signal()) {
// No external reference. Turn on PortaPack reference (if present).
portapack_tcxo_enable();
+141
View File
@@ -0,0 +1,141 @@
/*
* Copyright (C) 2023 Bernd Herzog
*
* 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 "config_mode.hpp"
#include "core_control.hpp"
#include "hackrf_gpio.hpp"
#include "portapack_hal.hpp"
void config_mode_blink_until_dfu();
void config_mode_set() {
portapack::persistent_memory::set_config_mode_storage(CONFIG_MODE_GUARD_VALUE);
}
bool config_mode_should_enter() {
return portapack::persistent_memory::config_mode_storage() == CONFIG_MODE_GUARD_VALUE;
}
void config_mode_clear() {
portapack::persistent_memory::set_config_mode_storage(CONFIG_MODE_NORMAL_VALUE);
}
uint32_t blink_patterns[] = {
0x00000000, // 0 Off
0xFFFFFFFF, // 1 On
0xF0F0F0F0, // 2 blink fast
0x00FF00FF, // 3 blink slow
0xFFF3FFF3 // 4 inverse blink slow
};
void config_mode_run() {
configure_pins_portapack();
portapack::gpio_dfu.input();
portapack::persistent_memory::cache::init();
if (hackrf_r9) {
// When this runs on the hackrf r9 there likely was a crash during the last boot
// caused by the external tcxo. So we disable it unless the user is intentially
// running the config mode by pressing reset twice followed by pressing DFU.
auto old_disable_external_tcxo = portapack::persistent_memory::config_disable_external_tcxo();
portapack::persistent_memory::set_config_disable_external_tcxo(true);
portapack::persistent_memory::cache::persist();
config_mode_blink_until_dfu();
portapack::persistent_memory::set_config_disable_external_tcxo(old_disable_external_tcxo);
portapack::persistent_memory::cache::persist();
} else {
config_mode_blink_until_dfu();
}
auto last_dfu_btn = portapack::gpio_dfu.read();
int32_t counter = 0;
int8_t blink_pattern_value = portapack::persistent_memory::config_cpld() +
(portapack::persistent_memory::config_disable_external_tcxo() ? 5 : 0);
while (true) {
auto dfu_btn = portapack::gpio_dfu.read();
auto dfu_clicked = last_dfu_btn == true && dfu_btn == false;
last_dfu_btn = dfu_btn;
if (dfu_clicked) {
int8_t value = portapack::persistent_memory::config_cpld() +
(portapack::persistent_memory::config_disable_external_tcxo() ? 5 : 0);
blink_pattern_value = value = (value + 1) % 10;
portapack::persistent_memory::set_config_cpld(value % 5);
portapack::persistent_memory::set_config_disable_external_tcxo((value / 5) == 1);
portapack::persistent_memory::cache::persist();
}
auto tx_blink_pattern = blink_patterns[blink_pattern_value % 5];
auto rx_blink_pattern = blink_patterns[blink_pattern_value / 5];
auto tx_value = ((tx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
if (tx_value) {
hackrf::one::led_tx.on();
} else {
hackrf::one::led_tx.off();
}
auto rx_value = ((rx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
if (rx_value) {
hackrf::one::led_rx.on();
} else {
hackrf::one::led_rx.off();
}
chThdSleepMilliseconds(100);
counter++;
}
}
void config_mode_blink_until_dfu() {
while (true) {
hackrf::one::led_tx.on();
hackrf::one::led_rx.on();
hackrf::one::led_usb.on();
chThdSleepMilliseconds(10);
hackrf::one::led_tx.off();
hackrf::one::led_rx.off();
hackrf::one::led_usb.off();
chThdSleepMilliseconds(115);
auto dfu_btn = portapack::gpio_dfu.read();
if (dfu_btn)
break;
}
while (true) {
chThdSleepMilliseconds(10);
auto dfu_btn = portapack::gpio_dfu.read();
if (!dfu_btn)
break;
}
chThdSleepMilliseconds(10);
}
+37
View File
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 Bernd Herzog
*
* 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 __CONFIG_MODE_H__
#define __CONFIG_MODE_H__
#include "ch.h"
#include "portapack.hpp"
#include "portapack_shared_memory.hpp"
#include "portapack_persistent_memory.hpp"
void config_mode_set();
bool config_mode_should_enter();
void config_mode_clear();
void config_mode_run();
#endif /* __CONFIG_MODE_H__ */
+184 -13
View File
@@ -30,6 +30,7 @@
#include "portapack.hpp"
#include "string_format.hpp"
#include "ui_styles.hpp"
#include "irq_controls.hpp"
using namespace ui;
@@ -46,10 +47,12 @@ void __debug_log(const std::string& msg) {
pg_debug_log->write_entry(msg);
}
void runtime_error(LED);
void runtime_error(uint8_t source);
std::string number_to_hex_string(uint32_t);
void draw_line(int32_t, const char*, regarm_t);
void draw_stack_dump();
static bool error_shown = false;
uint32_t fault_address{0};
void draw_guru_meditation_header(uint8_t source, const char* hint) {
Painter painter;
@@ -67,8 +70,10 @@ void draw_guru_meditation_header(uint8_t source, const char* hint) {
painter.draw_string({48, 24}, Styles::white, "M? Guru");
painter.draw_string({48 + 8 * 8, 24}, Styles::white, "Meditation");
if (source == CORTEX_M0)
if (source == CORTEX_M0) {
painter.draw_string({48 + 8, 24}, Styles::white, "0");
painter.draw_string({24, 320 - 32}, Styles::white, "Press DFU for Stack Dump");
}
if (source == CORTEX_M4)
painter.draw_string({48 + 8, 24}, Styles::white, "4");
@@ -83,11 +88,7 @@ void draw_guru_meditation(uint8_t source, const char* hint) {
draw_guru_meditation_header(source, hint);
}
if (source == CORTEX_M0)
runtime_error(hackrf::one::led_rx);
if (source == CORTEX_M4)
runtime_error(hackrf::one::led_tx);
runtime_error(source);
}
void draw_guru_meditation(uint8_t source, const char* hint, struct extctx* ctxp, uint32_t cfsr = 0) {
@@ -110,6 +111,7 @@ void draw_guru_meditation(uint8_t source, const char* hint, struct extctx* ctxp,
// to see whats causing the fault.
draw_line(80 + i++ * 20, "lr:", ctxp->lr_thd);
draw_line(80 + i++ * 20, "pc:", ctxp->pc);
fault_address = (uint32_t)ctxp->pc;
// see SCB_CFSR_* in libopencm3/cm3/scb.h for details
if (cfsr != 0)
@@ -117,11 +119,7 @@ void draw_guru_meditation(uint8_t source, const char* hint, struct extctx* ctxp,
}
}
if (source == CORTEX_M0)
runtime_error(hackrf::one::led_rx);
if (source == CORTEX_M4)
runtime_error(hackrf::one::led_tx);
runtime_error(source);
}
void draw_line(int32_t y_offset, const char* label, regarm_t value) {
@@ -131,7 +129,9 @@ void draw_line(int32_t y_offset, const char* label, regarm_t value) {
painter.draw_string({15 + 8 * 8, y_offset}, Styles::white, to_string_hex((uint32_t)value, 8));
}
void runtime_error(LED led) {
void runtime_error(uint8_t source) {
LED led = (source == CORTEX_M0) ? hackrf::one::led_rx : hackrf::one::led_tx;
led.off();
while (true) {
@@ -139,9 +139,180 @@ void runtime_error(LED led) {
while (n--)
;
led.toggle();
// Stack dump will cover entire screen, so wait for DFU button press to attempt it
if (swizzled_switches() & (1 << (int)Switch::Dfu)) {
draw_stack_dump();
}
}
}
// This function should only be called with interrupts disabled due to reading swizzled_switches()
// Using the stack while dumping the stack isn't ideal, but hopefully anything imporant is still on the call stack.
void draw_stack_dump() {
Painter painter;
uint32_t* p{&__process_stack_base__};
constexpr int border{8};
int x, y;
int n{0};
bool clear_rect{true};
bool first_pass{true};
// NOTE: Stays in this loop forever unless LEFT button pressed
while (1) {
if (clear_rect) {
painter.fill_rectangle(
{border, border, portapack::display.width() - (border * 2), portapack::display.height() - (border * 2)},
Color::black());
x = y = border;
clear_rect = false;
}
// skip past unused stack words on 1st pass
if (first_pass) {
first_pass = false;
while ((*p == CRT0_STACKS_FILL_PATTERN) && (p < &__process_stack_end__))
p++;
auto stack_space_left = p - &__process_stack_base__;
// NOTE: in situations like a hard fault it seems not possible to write strings longer than 16 characters.
painter.draw_string({x, y}, Styles::white_small, to_string_hex((uint32_t)&__process_stack_base__, 8));
x += 8 * 5;
painter.draw_string({x, y}, Styles::white_small, " M0 STACK free=");
x += 15 * 5;
painter.draw_string({x, y}, Styles::white_small, to_string_dec_uint(stack_space_left));
x = border;
y += 8;
// align subsequent lines to start on 16-byte boundaries
p -= (stack_space_left & 3);
}
// print one page of stack dump
while ((p < &__process_stack_end__) && (y < portapack::display.height() - border - 8)) {
// show address if start of line
if (n++ == 0) {
painter.draw_string({x, y}, Styles::white_small, to_string_hex((uint32_t)p, 8) + ":");
x += 9 * 5 - 2; // note: saving 2 pixels here to prevent reaching right border
}
// show stack word -- highlight if a possible code address (low bit will be set too for !thumb) or actual fault address
bool code_addr = (*p > 0x1400) && (*p < 0x80000) && (((*p) & 0x1) == 0x1); // approximate address range of code .text region in ROM
Style style = (fault_address && *p == fault_address) ? Styles::bg_yellow_small : (code_addr ? Styles::bg_white_small : Styles::white_small);
painter.draw_string({x, y}, style, " " + to_string_hex(*p, 8));
x += 9 * 5;
// new line?
if (n == 4) {
n = 0;
x = border;
y += 8;
}
// point to next stack word
p++;
}
// Out of room on the screen or end of stack - allow Up/Down paging.
// First wait for button release from previous press.
// NOTE: can't call swizzle_switches() with interrupted enabled!
while (swizzled_switches() & ((1 << (int)Switch::Right) | (1 << (int)Switch::Left) | (1 << (int)Switch::Down) | (1 << (int)Switch::Up) | (1 << (int)Switch::Sel) | (1 << (int)Switch::Dfu)))
;
painter.draw_string({border, portapack::display.height() - border - 8}, Styles::white_small, "Use UP/DOWN key");
// Wait for button press.
while (1) {
auto switches = swizzled_switches();
// If LEFT button pressed, exit
if (switches & (1 << (int)Switch::Left))
return;
// If DOWN pressed; continue to show next page
if (switches & (1 << (int)Switch::Down))
break;
// If UP pressed, scroll back by one page
if (switches & (1 << (int)Switch::Up)) {
// Back up pointer by offset on this screen plus one full screen of 8 pixels per line & 4 words per line
// (underflow can't happen here due to stack address range in RAM)
p -= ((y - border) + (portapack::display.height() - (border * 2 + 1 * 8))) / (8 / 4);
if (p < &__process_stack_base__)
p = &__process_stack_base__;
break;
}
// If SEL button pressed, try writing to file (likely to hang in fault condition but heh)
if (switches & (1 << (int)Switch::Sel))
stack_dump();
}
// clear screen to allow new range to be displayed, if we're not at the end
if (p < &__process_stack_end__)
clear_rect = true;
}
}
// Disk I/O in this function doesn't work after a fault
// Using the stack while dumping the stack isn't ideal, but hopefully anything imporant is still on the call stack.
bool stack_dump() {
Painter painter;
std::string debug_dir = "DEBUG";
std::filesystem::path filename{};
File stack_dump_file{};
bool error;
std::string str;
uint32_t* p;
int n{0};
bool data_found{false};
make_new_directory(debug_dir);
filename = next_filename_matching_pattern(debug_dir + "/STACK_DUMP_????.TXT");
error = filename.empty();
if (!error)
error = stack_dump_file.create(filename) != 0;
if (error) {
painter.draw_string({16, 320 - 32}, ui::Styles::red, "ERROR DUMPING " + filename.filename().string() + "!");
return false;
}
for (p = &__process_stack_base__; p < &__process_stack_end__; p++) {
// skip past unused stack words
if (!data_found) {
if (*p == CRT0_STACKS_FILL_PATTERN)
continue;
else {
data_found = true;
auto stack_space_left = p - &__process_stack_base__;
stack_dump_file.write_line(to_string_hex((uint32_t)&__process_stack_base__, 8) + ": Unused words " + to_string_dec_uint(stack_space_left));
// align subsequent lines to start on 16-byte boundaries
p -= (stack_space_left & 3);
}
}
// write address
if (n++ == 0) {
str = to_string_hex((uint32_t)p, 8) + ":";
stack_dump_file.write(str.data(), 9);
}
// write stack dword
str = " " + to_string_hex(*p, 8);
stack_dump_file.write(str.data(), 9);
if (n == 4) {
stack_dump_file.write("\r\n", 2);
n = 0;
}
}
painter.draw_string({16, 320 - 32}, ui::Styles::green, filename.filename().string() + " dumped!");
return true;
}
extern "C" {
#if CH_DBG_ENABLED
void port_halt(void) {
+2
View File
@@ -47,4 +47,6 @@ inline uint32_t get_free_stack_space() {
return stack_space_left;
}
bool stack_dump();
#endif /*__DEBUG_H__*/
+11
View File
@@ -579,6 +579,17 @@ bool is_empty_directory(const path& file_path) {
return !((result == FR_OK) && (filinfo.fname[0] != (TCHAR)'\0'));
}
int file_count(const path& directory) {
int count{0};
for (auto& entry : std::filesystem::directory_iterator(directory, (const TCHAR*)u"*")) {
(void)entry; // avoid unused warning
++count;
}
return count;
}
space_info space(const path& p) {
DWORD free_clusters{0};
FATFS* fs;
+2
View File
@@ -250,6 +250,8 @@ bool file_exists(const path& file_path);
bool is_directory(const path& file_path);
bool is_empty_directory(const path& file_path);
int file_count(const path& dir_path);
space_info space(const path& p);
} /* namespace filesystem */
+22 -13
View File
@@ -76,20 +76,29 @@ options_t freqman_bandwidths[4] = {
// SPEC -- TODO: these should be indexes.
{"12k5", 12500},
{"16k", 16000},
{"20k", 20000},
{"25k", 25000},
{"32k", 32000},
{"50k", 50000},
{"75k", 75000},
{"100k", 100000},
{"150k", 150000},
{"250k", 250000},
{"500k", 500000}, /* Previous Limit bandwith Option with perfect micro SD write .C16 format operaton.*/
{"600k", 600000}, /* That extended option is still possible to record with FW version Mayhem v1.41 (< 2,5MB/sec) */
{"650k", 650000},
{"750k", 750000}, /* From this BW onwards, the LCD is ok, but the recorded file is decimated, (not real file size) */
{"1100k", 1100000},
{"500k", 500000},
{"600k", 600000},
{"750k", 750000},
{"1000k", 1000000}, // Max bandwith for recording in C16 (with fast SD card).
{"1250k", 1250000},
{"1500k", 1500000},
{"1750k", 1750000},
{"2000k", 2000000},
{"2500k", 2500000},
{"2750k", 2750000}, // That is our max Capture option, to keep using later / 8 decimation (22Mhz sampling ADC)
{"2250k", 2250000}, // Max bandwith for recording in C8 (with fast SD card).
{"2500k", 2500000}, // Here and up, LCD is ok, but M4 CPU drops samples.
{"3000k", 3000000},
{"3500k", 3500000},
{"4000k", 4000000},
{"4500k", 4500000},
{"5000k", 5500000},
{"5500k", 5500000}, // Max capture, needs /4 decimation, (22Mhz sampling ADC).
},
};
@@ -267,18 +276,18 @@ std::string to_freqman_string(const freqman_entry& entry) {
switch (entry.type) {
case freqman_type::Single:
append_field("f", to_string_dec_uint64(entry.frequency_a));
append_field("f", to_string_dec_uint(entry.frequency_a));
break;
case freqman_type::Range:
append_field("a", to_string_dec_uint64(entry.frequency_a));
append_field("b", to_string_dec_uint64(entry.frequency_b));
append_field("a", to_string_dec_uint(entry.frequency_a));
append_field("b", to_string_dec_uint(entry.frequency_b));
if (is_valid(entry.step))
append_field("s", freqman_entry_get_step_string_short(entry.step));
break;
case freqman_type::HamRadio:
append_field("r", to_string_dec_uint64(entry.frequency_a));
append_field("t", to_string_dec_uint64(entry.frequency_b));
append_field("r", to_string_dec_uint(entry.frequency_a));
append_field("t", to_string_dec_uint(entry.frequency_b));
if (is_valid(entry.tone))
append_field("c", tonekey::tone_key_value_string(entry.tone));
+4 -2
View File
@@ -139,7 +139,9 @@ static uint8_t switches_raw = 0;
// 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) {
uint8_t swizzled_switches() {
uint8_t raw = io.io_update(touch_pins_configs[touch_phase]);
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.
@@ -183,7 +185,7 @@ void timer0_callback(GPTDriver* const) {
eventmask_t event_mask = 0;
if (touch_update()) event_mask |= EVT_MASK_TOUCH;
switches_raw = swizzle_raw(io.io_update(touch_pins_configs[touch_phase]));
switches_raw = swizzled_switches();
if (switches_update(switches_raw))
event_mask |= EVT_MASK_SWITCHES;
+1
View File
@@ -43,6 +43,7 @@ using SwitchesState = std::bitset<6>;
using EncoderPosition = uint32_t;
void controls_init();
uint8_t swizzled_switches();
SwitchesState get_switches_state();
EncoderPosition get_encoder_position();
touch::Frame get_touch_frame();
+9
View File
@@ -115,6 +115,7 @@ Continuous (Fox-oring)
#include "portapack.hpp"
#include "portapack_shared_memory.hpp"
#include "config_mode.hpp"
#include "message_queue.hpp"
@@ -161,9 +162,17 @@ static void event_loop() {
}
int main(void) {
if (config_mode_should_enter()) {
config_mode_clear();
config_mode_run();
}
config_mode_set();
first_if.init(); /* To avoid initial short Ant_DC_Bias pulse ,we need quick set up GP01_RFF507X =1 */
if (portapack::init()) {
portapack::display.init();
config_mode_clear();
// sdcStart(&SDCD1, nullptr); // Commented out as now happens in portapack.cpp
+4 -3
View File
@@ -408,6 +408,10 @@ bool init() {
portapack::io.init();
/* Cache some configuration data from persistent memory. */
persistent_memory::cache::init();
chThdSleepMilliseconds(10);
clock_manager.init_clock_generator();
i2c0.stop();
@@ -467,9 +471,6 @@ bool init() {
controls_init();
chThdSleepMilliseconds(10);
/* Cache some configuration data from persistent memory. */
persistent_memory::cache::init();
clock_manager.set_reference_ppb(persistent_memory::correction_ppb());
clock_manager.enable_if_clocks();
clock_manager.enable_codec_clocks();
+4 -2
View File
@@ -80,6 +80,8 @@ uint32_t ReplayThread::run() {
chThdSleep(100);
};
constexpr size_t block_size = 512;
// While empty buffers fifo is not empty...
while (!buffers.empty()) {
prefill_buffer = buffers.get_prefill();
@@ -87,10 +89,10 @@ uint32_t ReplayThread::run() {
if (prefill_buffer == nullptr) {
buffers.put_app(prefill_buffer);
} else {
size_t blocks = config.read_size / 512;
size_t blocks = config.read_size / block_size;
for (size_t c = 0; c < blocks; c++) {
auto read_result = reader->read(&((uint8_t*)prefill_buffer->data())[c * 512], 512);
auto read_result = reader->read(&((uint8_t*)prefill_buffer->data())[c * block_size], block_size);
if (read_result.is_error()) {
return READ_ERROR;
}
+17 -10
View File
@@ -59,30 +59,37 @@ static char* to_string_dec_uint_pad_internal(
return q;
}
template <typename Int>
char* to_string_dec_uint_internal(Int n, StringFormatBuffer& buffer, size_t& length) {
static char* to_string_dec_uint_internal(uint64_t n, StringFormatBuffer& buffer, size_t& length) {
auto end = &buffer.back();
auto start = to_string_dec_uint_internal(end, n);
length = end - start;
return start;
}
char* to_string_dec_uint(uint32_t n, StringFormatBuffer& buffer, size_t& length) {
char* to_string_dec_uint(uint64_t n, StringFormatBuffer& buffer, size_t& length) {
return to_string_dec_uint_internal(n, buffer, length);
}
char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length) {
return to_string_dec_uint_internal(n, buffer, length);
char* to_string_dec_int(int64_t n, StringFormatBuffer& buffer, size_t& length) {
bool negative = n < 0;
auto start = to_string_dec_uint(negative ? -n : n, buffer, length);
if (negative) {
*(--start) = '-';
++length;
}
return start;
}
std::string to_string_dec_uint(uint32_t n) {
std::string to_string_dec_int(int64_t n) {
StringFormatBuffer b{};
size_t len{};
char* str = to_string_dec_uint(n, b, len);
char* str = to_string_dec_int(n, b, len);
return std::string(str, len);
}
std::string to_string_dec_uint64(uint64_t n) {
std::string to_string_dec_uint(uint64_t n) {
StringFormatBuffer b{};
size_t len{};
char* str = to_string_dec_uint(n, b, len);
@@ -194,14 +201,14 @@ std::string to_string_rounded_freq(const uint64_t f, int8_t precision) {
};
if (precision < 1) {
final_str = to_string_dec_uint64(f / 1000000);
final_str = to_string_dec_uint(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');
final_str = to_string_dec_uint(f / 1000000) + "." + to_string_dec_int(((f + (divisor / 2)) / divisor) % pow10[precision], precision, '0');
}
return final_str;
}
+6 -6
View File
@@ -43,17 +43,17 @@ const char unit_prefix[7]{'n', 'u', 'm', 0, 'k', 'M', 'G'};
using StringFormatBuffer = std::array<char, 24>;
/* uint conversion without memory allocations. */
char* to_string_dec_uint(uint32_t n, StringFormatBuffer& buffer, size_t& length);
char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length);
/* Integer conversion without memory allocations. */
char* to_string_dec_int(int64_t n, StringFormatBuffer& buffer, size_t& length);
char* to_string_dec_uint(uint64_t n, StringFormatBuffer& buffer, size_t& length);
std::string to_string_dec_uint(uint32_t n);
std::string to_string_dec_uint64(uint64_t n);
std::string to_string_dec_int(int64_t n);
std::string to_string_dec_uint(uint64_t n);
// TODO: Allow l=0 to not fill/justify? Already using this way in ui_spectrum.hpp...
std::string to_string_bin(const uint32_t n, const uint8_t l = 0);
std::string to_string_dec_uint(const uint32_t n, const int32_t l, const char fill = ' ');
std::string to_string_dec_int(const int32_t n, const int32_t l = 0, const char fill = 0);
std::string to_string_dec_int(const int32_t n, const int32_t l, const char fill = 0);
std::string to_string_decimal(float decimal, int8_t precision);
std::string to_string_hex(const uint64_t n, const int32_t l = 0);
+1 -1
View File
@@ -85,7 +85,7 @@ const tone_key_t tone_keys = {
{"Senn. 32.768k", F2Ix100(32768.0)}};
std::string fx100_string(uint32_t f) {
return to_string_dec_uint(f / 100) + "." + to_string_dec_uint((f / 10) % 10);
return to_string_dec_uint(f / 100) + "." + to_string_dec_uint(((f + 5) / 10) % 10);
}
float tone_key_frequency(tone_index index) {
+1 -1
View File
@@ -31,7 +31,7 @@ namespace tonekey {
#define TONE_FREQ_TOLERANCE_CENTIHZ (4 * 100)
#define TONE_DISPLAY_TOGGLE_COUNTER 3
#define F2Ix100(x) (int32_t)(x * 100.0)
#define F2Ix100(x) (int32_t)(x * 100.0 + 0.5) // add 0.5f to round vs truncate during FP->int conversion
using tone_index = int32_t;
using tone_key_t = std::vector<std::pair<std::string, uint32_t>>;
+15 -20
View File
@@ -33,7 +33,6 @@ BtnGridView::BtnGridView(
bool keep_highlight)
: keep_highlight{keep_highlight} {
set_parent_rect(new_parent_rect);
set_focusable(true);
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
@@ -47,10 +46,6 @@ BtnGridView::BtnGridView(
BtnGridView::~BtnGridView() {
rtc_time::signal_tick_second -= signal_token_tick_second;
for (auto item : menu_item_views) {
delete item;
}
}
void BtnGridView::set_max_rows(int rows) {
@@ -68,31 +63,30 @@ void BtnGridView::set_parent_rect(const Rect new_parent_rect) {
arrow_more.set_parent_rect({228, (Coord)(displayed_max * button_h), 8, 8});
displayed_max *= rows_;
// TODO: Clean this up :(
if (menu_item_views.size()) {
for (auto item : menu_item_views) {
remove_child(item);
delete item;
}
// Delete any existing buttons.
if (!menu_item_views.empty()) {
for (auto& item : menu_item_views)
remove_child(item.get());
menu_item_views.clear();
}
button_w = 240 / rows_;
button_w = screen_width / rows_;
for (size_t c = 0; c < displayed_max; c++) {
auto item = new NewButton{};
menu_item_views.push_back(item);
add_child(item);
auto item = std::make_unique<NewButton>();
add_child(item.get());
item->set_parent_rect({(int)(c % rows_) * button_w,
(int)(c / rows_) * button_h,
button_w, button_h});
menu_item_views.push_back(std::move(item));
}
update_items();
}
void BtnGridView::set_arrow_enabled(bool new_value) {
if (new_value) {
void BtnGridView::set_arrow_enabled(bool enabled) {
if (enabled) {
add_child(&arrow_more);
} else {
remove_child(&arrow_more);
@@ -118,6 +112,7 @@ void BtnGridView::add_items(std::initializer_list<GridItem> new_items) {
for (auto item : new_items) {
menu_items.push_back(item);
}
update_items();
}
@@ -130,7 +125,7 @@ void BtnGridView::update_items() {
} else
more = false;
for (NewButton* item : menu_item_views) {
for (auto& item : menu_item_views) {
if ((i + offset) >= menu_items.size()) {
item->hidden(true);
item->set_text(" ");
@@ -152,7 +147,7 @@ void BtnGridView::update_items() {
}
NewButton* BtnGridView::item_view(size_t index) const {
return menu_item_views[index];
return menu_item_views[index].get();
}
bool BtnGridView::set_highlighted(int32_t new_value) {
+5 -3
View File
@@ -31,8 +31,10 @@
#include "signal.hpp"
#include <cstddef>
#include <string>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace ui {
@@ -62,7 +64,7 @@ class BtnGridView : public View {
uint32_t highlighted_index();
void set_parent_rect(const Rect new_parent_rect) override;
void set_arrow_enabled(bool new_value);
void set_arrow_enabled(bool enabled);
void on_focus() override;
void on_blur() override;
bool on_key(const KeyEvent event) override;
@@ -77,7 +79,7 @@ class BtnGridView : public View {
SignalToken signal_token_tick_second{};
std::vector<GridItem> menu_items{};
std::vector<NewButton*> menu_item_views{};
std::vector<std::unique_ptr<NewButton>> menu_item_views{};
Image arrow_more{
{228, 320 - 8, 8, 8},
+10 -16
View File
@@ -103,10 +103,6 @@ MenuView::MenuView(
MenuView::~MenuView() {
rtc_time::signal_tick_second -= signal_token_tick_second;
for (auto item : menu_item_views) {
delete item;
}
}
void MenuView::set_parent_rect(const Rect new_parent_rect) {
@@ -116,23 +112,22 @@ void MenuView::set_parent_rect(const Rect new_parent_rect) {
arrow_more.set_parent_rect({228, (Coord)(displayed_max * item_height), 8, 8});
// TODO: Clean this up :(
if (menu_item_views.size()) {
for (auto item : menu_item_views) {
remove_child(item);
delete item;
}
if (!menu_item_views.empty()) {
for (auto& item : menu_item_views)
remove_child(item.get());
menu_item_views.clear();
}
for (size_t c = 0; c < displayed_max; c++) {
auto item = new MenuItemView{keep_highlight};
menu_item_views.push_back(item);
add_child(item);
auto item = std::make_unique<MenuItemView>(keep_highlight);
add_child(item.get());
Coord y_pos = c * item_height;
item->set_parent_rect({{0, y_pos},
{size().width(), (Coord)item_height}});
menu_item_views.push_back(std::move(item));
}
update_items();
@@ -150,9 +145,8 @@ void MenuView::on_tick_second() {
}
void MenuView::clear() {
for (auto item : menu_item_views) {
for (auto& item : menu_item_views)
item->set_item(nullptr);
}
menu_items.clear();
highlighted_item = 0;
@@ -184,7 +178,7 @@ void MenuView::update_items() {
} else
more = false;
for (auto item : menu_item_views) {
for (auto& item : menu_item_views) {
if (i >= menu_items.size()) break;
// Assign item data to MenuItemViews according to offset
@@ -201,7 +195,7 @@ void MenuView::update_items() {
}
MenuItemView* MenuView::item_view(size_t index) const {
return menu_item_views[index];
return menu_item_views[index].get();
}
bool MenuView::set_highlighted(int32_t new_value) {
+7 -4
View File
@@ -30,8 +30,10 @@
#include "signal.hpp"
#include <cstddef>
#include <string>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace ui {
@@ -75,7 +77,8 @@ class MenuView : public View {
std::function<void(void)> on_left{};
std::function<void(void)> on_highlight{nullptr};
MenuView(Rect new_parent_rect = {0, 0, 240, 304}, bool keep_highlight = false);
MenuView(Rect new_parent_rect = {0, 0, screen_width, screen_height - 16},
bool keep_highlight = false);
~MenuView();
@@ -103,10 +106,10 @@ class MenuView : public View {
SignalToken signal_token_tick_second{};
std::vector<MenuItem> menu_items{};
std::vector<MenuItemView*> menu_item_views{};
std::vector<std::unique_ptr<MenuItemView>> menu_item_views{};
Image arrow_more{
{228, 320 - 8, 8, 8},
{228, screen_height - 8, 8, 8},
&bitmap_more,
Color::white(),
Color::black()};
+2
View File
@@ -35,6 +35,8 @@
#include <algorithm>
#include <functional>
#define MAX_UFREQ 7200000000 // maximum usable frequency
namespace ui {
class FrequencyField : public Widget {
+24 -19
View File
@@ -381,32 +381,37 @@ void WaterfallView::on_audio_spectrum() {
} /* namespace spectrum */
uint32_t filter_bandwidth_for_sampling_rate(int32_t sampling_rate) {
switch (sampling_rate) { // Use the var fs (sampling_rate) to set up BPF aprox < fs_max / 2 by Nyquist theorem.
case 0 ... 2000000: // BW Captured range (0 <= 250kHz max) fs = 8 x 250 kHz.
return 1750000; // Minimum BPF MAX2837 for all those lower BW options.
switch (sampling_rate) { // Use the var fs (sampling_rate) to set up BPF aprox < fs_max / 2 by Nyquist theorem.
case 0 ... 3'500'000: // BW Captured range BW (<=250K) : fs = 8x250k = 2000k, 16x150k = 2400k, 16x100k=1600k,
// 32x75k = 2400k, 32x50k=1600, 32x32k=1024, 64x25k = 1600k, 64x16k = 1024k, 64x12k5 = 800k.
return 1'750'000; // Minimum BPF MAX2837 for all those lower BW options.
case 4000000 ... 6000000: // BW capture range (500k...750kHz max) fs_max = 8 x 750kHz = 6Mhz
// BW 500k...750kHz, ex. 500kHz (fs = 8 x BW = 4Mhz), BW 600kHz (fs = 4,8Mhz), BW 750 kHz (fs = 6Mhz).
return 2500000; // In some IC, MAX2837 appears as 2250000, but both work similarly.
case 4'000'000 ... 7'000'000: // OVS x8, BW capture range (500k...750kHz max) fs_max = 8 x 750k = 6Mhz
// BW 500k...750kHz, ex. 500kHz (fs = 8 x BW = 4Mhz), BW 600kHz (fs = 4,8Mhz), BW 750 kHz (fs = 6Mhz).
return 2'500'000; // In some IC, MAX2837 appears as 2250000, but both work similarly.
case 8800000: // BW capture 1,1Mhz fs = 8 x 1,1Mhz = 8,8Mhz. (1Mhz showed slightly higher noise background).
return 3500000;
case 7'000'001 ... 10'000'000: // OVS x8 and x4, BW capture 1Mhz fs = 8 x 1Mhz = 8Mhz. (1Mhz showed slightly higher noise background).
return 3'500'000; // some low SD cards, if not showing avg. writing speed >4MB/sec, they will produce sammples drop at REC with 1MB and C16 format.
case 14000000: // BW capture 1,75Mhz, fs = 8 x 1,75Mhz = 14Mhz
// Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture.
return 5000000;
case 12'000'000 ... 14'000'000: // OVS x4, BW capture 3Mhz, fs = 4 x 3Mhz = 12Mhz
// Good BPF, good matching, we have some periodical M4 % samples drop.
return 5'000'000;
case 16000000: // BW capture 2Mhz, fs = 8 x 2Mhz = 16Mhz
// Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture.
return 6000000;
case 16'000'000: // OVS x4, BW capture 4Mhz, fs = 4 x 4Mhz = 16Mhz
// Good BPF, good matching, we have some periodical M4 % samples drop.
return 5'500'000;
case 20000000: // BW capture 2,5Mhz, fs = 8 x 2,5 Mhz = 20Mhz
// Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture.
return 7000000;
case 18'000'000: // OVS x4, BW capture 4,5Mhz, fs = 4 x 4,5Mhz = 18Mhz
// Good BPF, good matching, we have some periodical M4 % samples drop.
return 6'000'000;
default: // BW capture 2,75Mhz, fs = 8 x 2,75Mhz = 22Mhz max ADC sampling and others.
case 20'000'000: // OVS x4, BW capture 5Mhz, fs = 4 x 5Mhz = 20Mhz
// Good BPF, good matching, we have some periodical M4 % samples drop.
return 7'000'000;
default: // BW capture 5,5Mhz, fs = 4 x 5,5Mhz = 22Mhz max ADC sampling and others.
// We tested also 9Mhz FPB slightly too much noise floor, better at 8Mhz.
return 8000000;
return 8'000'000;
}
}
+13 -1
View File
@@ -44,6 +44,18 @@ const Style Styles::white_small{
.foreground = Color::white(),
};
const Style Styles::bg_white_small{
.font = ui::font::fixed_5x8,
.background = ui::Color::white(),
.foreground = ui::Color::black(),
};
const Style Styles::bg_yellow_small{
.font = ui::font::fixed_5x8,
.background = ui::Color::yellow(),
.foreground = ui::Color::black(),
};
const Style Styles::yellow{
.font = font::fixed_8x16,
.background = Color::black(),
@@ -102,4 +114,4 @@ const Style Styles::orange{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::orange(),
};
};
+7 -1
View File
@@ -37,6 +37,12 @@ class Styles {
/* White foreground, small font. */
static const Style white_small;
/* White background, small font. */
static const Style bg_white_small;
/* Red background, small font. */
static const Style bg_yellow_small;
/* Yellow foreground. */
static const Style yellow;
@@ -70,4 +76,4 @@ class Styles {
} // namespace ui
#endif /*__UI_STYLES_H__*/
#endif /*__UI_STYLES_H__*/
+1
View File
@@ -219,6 +219,7 @@ SystemStatusView::SystemStatusView(
toggle_mute.set_value(pmem::config_audio_mute());
toggle_stealth.set_value(pmem::stealth_mode());
audio::output::stop();
audio::output::update_audio_mute();
refresh();
}
+22 -20
View File
@@ -30,6 +30,7 @@ using namespace portapack;
#include "baseband_api.hpp"
#include "metadata_file.hpp"
#include "oversample.hpp"
#include "rtc_time.hpp"
#include "string_format.hpp"
#include "utility.hpp"
@@ -101,27 +102,24 @@ void RecordView::focus() {
button_record.focus();
}
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.
uint32_t RecordView::set_sampling_rate(uint32_t new_sampling_rate) {
// Determine the oversampling needed (if any) and the actual sampling rate.
auto oversample_rate = get_oversample_rate(new_sampling_rate);
auto actual_sampling_rate = new_sampling_rate * toUType(oversample_rate);
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)
// Change the "REC" icon background to yellow when the selected rate exceeds hardware limits.
// Above this threshold, samples will be dropped resulting incomplete capture files.
if (new_sampling_rate > 1'250'000) {
button_record.set_background(ui::Color::yellow());
} else {
button_record.set_background(ui::Color::black());
}
if (new_sampling_rate != sampling_rate ||
new_oversample_rate != oversample_rate) {
if (sampling_rate != new_sampling_rate) {
stop();
sampling_rate = new_sampling_rate;
oversample_rate = new_oversample_rate;
baseband::set_sample_rate(sampling_rate);
baseband::set_oversample_rate(oversample_rate);
baseband::set_sample_rate(sampling_rate, oversample_rate);
button_record.hidden(sampling_rate == 0);
text_record_filename.hidden(sampling_rate == 0);
@@ -131,6 +129,16 @@ void RecordView::set_sampling_rate(size_t new_sampling_rate, OversampleRate new_
update_status_display();
}
return actual_sampling_rate;
}
OversampleRate RecordView::get_oversample_rate(uint32_t sample_rate) {
// No oversampling necessary for baseband audio processors.
if (file_type == FileType::WAV)
return OversampleRate::None;
return ::get_oversample_rate(sample_rate);
}
// Setter for datetime and frequency filename
@@ -202,8 +210,7 @@ void RecordView::start() {
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 / toUType(oversample_rate)});
get_metadata_path(base_path), {receiver_model.target_frequency(), sampling_rate});
if (metadata_file_error.is_valid()) {
handle_error(metadata_file_error.value());
return;
@@ -278,12 +285,7 @@ void RecordView::update_status_display() {
// - 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 bytes_per_second = 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;
+8 -12
View File
@@ -56,16 +56,11 @@ class RecordView : public View {
void focus() override;
/* 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);
/* Sets the sampling rate for the baseband.
* NB: Do not pre-apply any oversampling. This function will determine
* the correct amount of oversampling and return the actual sample rate
* that can be used to configure the radio or other UI element. */
uint32_t set_sampling_rate(uint32_t new_sampling_rate);
void set_file_type(const FileType v) { file_type = v; }
@@ -87,6 +82,8 @@ class RecordView : public View {
void handle_capture_thread_done(const File::Error error);
void handle_error(const File::Error error);
OversampleRate get_oversample_rate(uint32_t sample_rate);
// bool pitch_rssi_enabled = false;
// Time Stamp
@@ -98,8 +95,7 @@ class RecordView : public View {
FileType file_type;
const size_t write_size;
const size_t buffer_count;
size_t sampling_rate{0};
OversampleRate oversample_rate{OversampleRate::Rate8x};
uint32_t sampling_rate{0};
SignalToken signal_token_tick_second{};
Rectangle rect_background{
+7
View File
@@ -444,6 +444,13 @@ set(MODE_CPPSRC
)
DeclareTargets(PPOC pocsag)
### POCSAG2 RX
set(MODE_CPPSRC
proc_pocsag2.cpp
)
DeclareTargets(PPO2 pocsag2)
### RDS
set(MODE_CPPSRC
+1 -8
View File
@@ -69,29 +69,22 @@ class BlockDecimator {
set_input_sampling_rate(src.sampling_rate);
while (src_i < src.count) {
for (size_t src_i = 0; src_i < src.count; src_i += factor()) {
buffer[dst_i++] = src.p[src_i];
if (dst_i == buffer.size()) {
callback({buffer.data(), buffer.size(), output_sampling_rate()});
reset_state();
dst_i = 0;
}
src_i += factor();
}
src_i -= src.count;
}
private:
std::array<T, N> buffer{};
uint32_t input_sampling_rate_{0};
size_t factor_{1};
size_t src_i{0};
size_t dst_i{0};
void reset_state() {
src_i = 0;
dst_i = 0;
}
};
+1 -1
View File
@@ -202,8 +202,8 @@ buffer_c16_t FIRC8xR16x24FS4Decim4::execute(
uint32_t* const d = static_cast<uint32_t*>(__builtin_assume_aligned(dst.p, 4));
const auto k = output_scale;
const size_t count = src.count / decimation_factor;
for (size_t i = 0; i < count; i++) {
const vec4_s8* const in = static_cast<const vec4_s8*>(__builtin_assume_aligned(&src.p[i * decimation_factor], 4));
+15 -8
View File
@@ -23,19 +23,26 @@
#define __DSP_DECIMATE_H__
#include <cstdint>
#include <algorithm>
#include <array>
#include <memory>
#include <algorithm>
#include "utility.hpp"
#include "dsp_types.hpp"
#include "simd.hpp"
#include "utility.hpp"
namespace dsp {
namespace decimate {
/* "Saturating" scalars used by decimators to scale either
* 8 or 16 bit complex values into 32 bit complex values.
* Some of the decimators accept a scale factor as part of
* configuration, which is then passed to scale_round_and_pack. */
// c8_to_c32_sat_scalar == 2^25. 2^25 * 2^7 (signed C8 Max) == 2^32.
constexpr int32_t c8_to_c32_sat_scalar = 0x2000000;
// c16_to_c32_sat_scalar == 2^17. 2^17 * 2^15 (signed C16 Max) == 2^32.
constexpr int32_t c16_to_c32_sat_scalar = 0x20000;
class Complex8DecimateBy2CIC3 {
public:
buffer_c16_t execute(
@@ -100,7 +107,7 @@ class FIRC8xR16x24FS4Decim4 {
void configure(
const std::array<tap_t, taps_count>& taps,
const int32_t scale,
const int32_t scale = c8_to_c32_sat_scalar,
const Shift shift = Shift::Down);
buffer_c16_t execute(
@@ -128,7 +135,7 @@ class FIRC8xR16x24FS4Decim8 {
void configure(
const std::array<tap_t, taps_count>& taps,
const int32_t scale,
const int32_t scale = c8_to_c32_sat_scalar,
const Shift shift = Shift::Down);
buffer_c16_t execute(
@@ -151,7 +158,7 @@ class FIRC16xR16x16Decim2 {
void configure(
const std::array<tap_t, taps_count>& taps,
const int32_t scale);
const int32_t scale = c16_to_c32_sat_scalar);
buffer_c16_t execute(
const buffer_c16_t& src,
@@ -173,7 +180,7 @@ class FIRC16xR16x32Decim8 {
void configure(
const std::array<tap_t, taps_count>& taps,
const int32_t scale);
const int32_t scale = c16_to_c32_sat_scalar);
buffer_c16_t execute(
const buffer_c16_t& src,
+6
View File
@@ -36,6 +36,7 @@ bool FMSquelch::execute(const buffer_f32_t& audio) {
squelch_energy_buffer.size()};
non_audio_hpf.execute(audio, squelch_energy);
// "Non-audio" implies "noise" here. Find the loudest noise sample.
float non_audio_max_squared = 0;
for (const auto sample : squelch_energy_buffer) {
const float sample_squared = sample * sample;
@@ -44,9 +45,14 @@ bool FMSquelch::execute(const buffer_f32_t& audio) {
}
}
// Is the noise less than the threshold?
return (non_audio_max_squared < threshold_squared);
}
void FMSquelch::set_threshold(const float new_value) {
threshold_squared = new_value * new_value;
}
bool FMSquelch::enabled() const {
return threshold_squared > 0.0;
}
+3
View File
@@ -31,9 +31,12 @@
class FMSquelch {
public:
/* Check if noise level is lower than threshold.
* Returns true if noise is below threshold. */
bool execute(const buffer_f32_t& audio);
void set_threshold(const float new_value);
bool enabled() const;
private:
static constexpr size_t N = 32;
+2 -2
View File
@@ -29,8 +29,8 @@
#include "event_m4.hpp"
ACARSProcessor::ACARSProcessor() {
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
decim_1.configure(taps_11k0_decim_1.taps, 131072);
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
packet.clear();
baseband_thread.start();
}
+2 -2
View File
@@ -154,8 +154,8 @@ void AFSKRxProcessor::configure(const AFSKRxConfigureMessage& message) {
const size_t demod_input_fs = channel_filter_output_fs;*/
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
decim_1.configure(taps_11k0_decim_1.taps, 131072);
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
channel_filter.configure(taps_11k0_channel.taps, 2);
demod.configure(audio_fs, 5000);
+2 -2
View File
@@ -28,8 +28,8 @@
#include "event_m4.hpp"
AISProcessor::AISProcessor() {
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
decim_1.configure(taps_11k0_decim_1.taps, 131072);
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
baseband_thread.start();
}
+2 -2
View File
@@ -89,8 +89,8 @@ void NarrowbandAMAudio::configure(const AMConfigureMessage& message) {
constexpr size_t channel_filter_input_fs = decim_2_output_fs;
// const size_t channel_filter_output_fs = channel_filter_input_fs / channel_filter_decimation_factor;
decim_0.configure(message.decim_0_filter.taps, 33554432);
decim_1.configure(message.decim_1_filter.taps, 131072);
decim_0.configure(message.decim_0_filter.taps);
decim_1.configure(message.decim_1_filter.taps);
decim_2.configure(message.decim_2_filter.taps, decim_2_decimation_factor);
channel_filter.configure(message.channel_filter.taps, channel_filter_decimation_factor);
channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs;
+2 -2
View File
@@ -223,8 +223,8 @@ void APRSRxProcessor::capture_config(const CaptureConfigMessage& message) {
}
void APRSRxProcessor::configure(const APRSRxConfigureMessage& message) {
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
decim_1.configure(taps_11k0_decim_1.taps, 131072);
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
channel_filter.configure(taps_11k0_channel.taps, 2);
demod.configure(audio_fs, 5000);
+3 -3
View File
@@ -77,8 +77,8 @@ void AudioTXProcessor::on_message(const Message* const message) {
replay_config(*reinterpret_cast<const ReplayConfigMessage*>(message));
break;
case Message::ID::SamplerateConfig:
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message));
case Message::ID::SampleRateConfig:
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
break;
case Message::ID::FIFOData:
@@ -108,7 +108,7 @@ void AudioTXProcessor::replay_config(const ReplayConfigMessage& message) {
}
}
void AudioTXProcessor::samplerate_config(const SamplerateConfigMessage& message) {
void AudioTXProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
resample_inc = (((uint64_t)message.sample_rate) << 16) / baseband_fs; // 16.16 fixed point message.sample_rate
}
+1 -1
View File
@@ -53,7 +53,7 @@ class AudioTXProcessor : public BasebandProcessor {
bool configured{false};
uint32_t bytes_read{0};
void samplerate_config(const SamplerateConfigMessage& message);
void sample_rate_config(const SampleRateConfigMessage& message);
void audio_config(const AudioTXConfigMessage& message);
void replay_config(const ReplayConfigMessage& message);
+2 -2
View File
@@ -276,8 +276,8 @@ void BTLERxProcessor::on_message(const Message* const message) {
void BTLERxProcessor::configure(const BTLERxConfigureMessage& message) {
(void)message; // avoid warning
decim_0.configure(taps_200k_wfm_decim_0.taps, 33554432);
decim_1.configure(taps_200k_wfm_decim_1.taps, 131072);
decim_0.configure(taps_200k_wfm_decim_0.taps);
decim_1.configure(taps_200k_wfm_decim_1.taps);
demod.configure(audio_fs, 5000);
configured = true;
+76 -56
View File
@@ -26,36 +26,32 @@
#include "event_m4.hpp"
#include "utility.hpp"
CaptureProcessor::CaptureProcessor() {
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);
using namespace dsp::decimate;
CaptureProcessor::CaptureProcessor() {
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_1_out = decim_1.execute(decim_0_out, dst_buffer);
const auto& decimator_out = decim_1_out;
const auto& channel = decimator_out;
auto decim_0_out = decim_0.execute(buffer, dst_buffer);
auto out_buffer = decim_1.execute(decim_0_out, dst_buffer);
if (stream) {
const size_t bytes_to_write = sizeof(*decimator_out.p) * decimator_out.count;
const size_t written = stream->write(decimator_out.p, bytes_to_write);
const size_t bytes_to_write = sizeof(*out_buffer.p) * out_buffer.count;
const size_t written = stream->write(out_buffer.p, bytes_to_write);
if (written != bytes_to_write) {
// TODO eventually report error somewhere
// TODO: Send an error message to the app?
}
}
feed_channel_stats(channel);
feed_channel_stats(out_buffer);
spectrum_samples += channel.count;
spectrum_samples += out_buffer.count;
if (spectrum_samples >= spectrum_interval_samples) {
spectrum_samples -= spectrum_interval_samples;
channel_spectrum.feed(channel, channel_filter_low_f, channel_filter_high_f, channel_filter_transition);
channel_spectrum.feed(out_buffer, channel_filter_low_f,
channel_filter_high_f, channel_filter_transition);
}
}
@@ -66,19 +62,9 @@ void CaptureProcessor::on_message(const Message* const message) {
channel_spectrum.on_message(message);
break;
case Message::ID::SamplerateConfig: {
auto config = reinterpret_cast<const SamplerateConfigMessage*>(message);
baseband_fs = config->sample_rate;
update_for_rate_change();
case Message::ID::SampleRateConfig:
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
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));
@@ -89,50 +75,84 @@ void CaptureProcessor::on_message(const Message* const message) {
}
}
void CaptureProcessor::update_for_rate_change() {
void CaptureProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
const auto sample_rate = message.sample_rate;
// The actual sample rate is the requested rate * the oversample rate.
// See oversample.hpp for more details on oversampling.
baseband_fs = sample_rate * toUType(message.oversample_rate);
baseband_thread.set_sampling_rate(baseband_fs);
auto decim_0_factor = oversample_rate == OversampleRate::Rate8x
? decim_0_4.decimation_factor
: decim_0_8.decimation_factor;
// TODO: Do we need to use the taps that the decimators get configured with?
channel_filter_low_f = taps_200k_decim_1.low_frequency_normalized * sample_rate;
channel_filter_high_f = taps_200k_decim_1.high_frequency_normalized * sample_rate;
channel_filter_transition = taps_200k_decim_1.transition_normalized * sample_rate;
size_t decim_0_output_fs = baseband_fs / decim_0_factor;
// Compute the scalar that corrects the oversample_rate to be x8 when computing
// the spectrum update interval. The original implementation only supported x8.
// TODO: Why is this needed here but not in proc_replay? There must be some other
// assumption about x8 oversampling in some component that makes this necessary.
const auto oversample_correction = toUType(message.oversample_rate) / 8.0;
size_t decim_1_input_fs = decim_0_output_fs;
size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
channel_filter_low_f = taps_200k_decim_1.low_frequency_normalized * decim_1_input_fs;
channel_filter_high_f = taps_200k_decim_1.high_frequency_normalized * decim_1_input_fs;
channel_filter_transition = taps_200k_decim_1.transition_normalized * decim_1_input_fs;
spectrum_interval_samples = decim_1_output_fs / spectrum_rate_hz;
// The spectrum update interval controls how often the waterfall is fed new samples.
spectrum_interval_samples = sample_rate / (spectrum_rate_hz * oversample_correction);
spectrum_samples = 0;
}
void CaptureProcessor::capture_config(const CaptureConfigMessage& message) {
if (message.config) {
stream = std::make_unique<StreamInput>(message.config);
} else {
stream.reset();
}
}
// For high sample rates, the M4 is busy collecting samples so the
// waterfall runs slower. Reduce the update interval so it runs faster.
// NB: Trade off: looks nicer, but more frequent updates == more CPU.
if (sample_rate >= 1'500'000)
spectrum_interval_samples /= (sample_rate / 750'000);
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);
switch (message.oversample_rate) {
case OversampleRate::x4:
// M4 can't handle 2 decimation passes for sample rates needing x4.
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<NoopDecim>();
break;
case OversampleRate::Rate16x:
return decim_0_8.execute(src, dst);
case OversampleRate::x8:
// M4 can't handle 2 decimation passes for sample rates <= 600k.
if (message.sample_rate < 600'000) {
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
} else {
// Using 180k taps to provide better filtering with a single pass.
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_180k_wfm_decim_0.taps);
decim_1.set<NoopDecim>();
}
break;
case OversampleRate::x16:
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
break;
case OversampleRate::x32:
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
break;
case OversampleRate::x64:
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
break;
default:
chDbgPanic("Unhandled OversampleRate");
return {};
break;
}
}
void CaptureProcessor::capture_config(const CaptureConfigMessage& message) {
if (message.config)
stream = std::make_unique<StreamInput>(message.config);
else
stream.reset();
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<CaptureProcessor>()};
event_dispatcher.run();
return 0;
}
}
+62 -14
View File
@@ -33,6 +33,56 @@
#include <array>
#include <memory>
#include <tuple>
#include <variant>
/* A decimator that just returns the source buffer. */
class NoopDecim {
public:
static constexpr int decimation_factor = 1;
template <typename Buffer>
Buffer execute(const Buffer& src, const Buffer&) {
// TODO: should this copy to 'dst'?
return {src.p, src.count, src.sampling_rate};
}
};
/* Decimator wrapper that can hold one of a set of decimators and dispatch at runtime. */
template <typename... Args>
class MultiDecimator {
public:
/* Dispatches to the underlying type's execute. */
template <typename Source, typename Destination>
Destination execute(
const Source& src,
const Destination& dst) {
return std::visit(
[&src, &dst](auto&& arg) -> Destination {
return arg.execute(src, dst);
},
decimator_);
}
size_t decimation_factor() const {
return std::visit(
[](auto&& arg) -> size_t {
return arg.decimation_factor;
},
decimator_);
}
/* Sets this decimator to a new instance of the specified decimator type.
* NB: The instance is returned by-ref so 'configure' can easily be called. */
template <typename Decimator>
Decimator& set() {
decimator_ = Decimator{};
return std::get<Decimator>(decimator_);
}
private:
std::variant<Args...> decimator_{};
};
class CaptureProcessor : public BasebandProcessor {
public:
@@ -42,7 +92,7 @@ class CaptureProcessor : public BasebandProcessor {
void on_message(const Message* const message) override;
private:
size_t baseband_fs = 3072000;
size_t baseband_fs = 3072000; // aka: sample_rate
static constexpr auto spectrum_rate_hz = 50.0f;
std::array<complex16_t, 512> dst{};
@@ -50,14 +100,17 @@ class CaptureProcessor : public BasebandProcessor {
dst.data(),
dst.size()};
/* 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{};
/* The actual type will be configured depending on the sample rate. */
MultiDecimator<
dsp::decimate::FIRC8xR16x24FS4Decim4,
dsp::decimate::FIRC8xR16x24FS4Decim8>
decim_0{};
MultiDecimator<
dsp::decimate::FIRC16xR16x16Decim2,
dsp::decimate::FIRC16xR16x32Decim8,
NoopDecim>
decim_1{};
dsp::decimate::FIRC16xR16x16Decim2 decim_1{};
int32_t channel_filter_low_f = 0;
int32_t channel_filter_high_f = 0;
int32_t channel_filter_transition = 0;
@@ -67,19 +120,14 @@ class CaptureProcessor : public BasebandProcessor {
SpectrumCollector channel_spectrum{};
size_t spectrum_interval_samples = 0;
size_t spectrum_samples = 0;
OversampleRate oversample_rate{OversampleRate::Rate8x};
/* 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 sample_rate_config(const SampleRateConfigMessage& message);
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__*/
+3 -3
View File
@@ -83,8 +83,8 @@ void GPSReplayProcessor::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:
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
break;
case Message::ID::ReplayConfig:
@@ -103,7 +103,7 @@ void GPSReplayProcessor::on_message(const Message* const message) {
}
}
void GPSReplayProcessor::samplerate_config(const SamplerateConfigMessage& message) {
void GPSReplayProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
baseband_fs = message.sample_rate;
baseband_thread.set_sampling_rate(baseband_fs);
spectrum_interval_samples = baseband_fs / spectrum_rate_hz;
+1 -1
View File
@@ -64,7 +64,7 @@ class GPSReplayProcessor : public BasebandProcessor {
bool configured{false};
uint32_t bytes_read{0};
void samplerate_config(const SamplerateConfigMessage& message);
void sample_rate_config(const SampleRateConfigMessage& message);
void replay_config(const ReplayConfigMessage& message);
TXProgressMessage txprogress_message{};
+2 -2
View File
@@ -144,8 +144,8 @@ void NarrowbandFMAudio::configure(const NBFMConfigureMessage& message) {
const size_t demod_input_fs = channel_filter_output_fs;
decim_0.configure(message.decim_0_filter.taps, 33554432);
decim_1.configure(message.decim_1_filter.taps, 131072);
decim_0.configure(message.decim_0_filter.taps);
decim_1.configure(message.decim_1_filter.taps);
channel_filter.configure(message.channel_filter.taps, message.channel_decimation);
demod.configure(demod_input_fs, message.deviation);
channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs;
+2 -2
View File
@@ -243,8 +243,8 @@ void NRFRxProcessor::on_message(const Message* const message) {
void NRFRxProcessor::configure(const NRFRxConfigureMessage& message) {
(void)message; // avoir unused warning
decim_0.configure(taps_200k_wfm_decim_0.taps, 33554432);
decim_1.configure(taps_200k_wfm_decim_1.taps, 131072);
decim_0.configure(taps_200k_wfm_decim_0.taps);
decim_1.configure(taps_200k_wfm_decim_1.taps);
demod.configure(audio_fs, 5000);
configured = true;
+57 -12
View File
@@ -3,6 +3,7 @@
* Copyright (C) 2012-2014 Elias Oenal (multimon-ng@eliasoenal.com)
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2016 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -24,16 +25,15 @@
#include "proc_pocsag.hpp"
#include "dsp_iir_config.hpp"
#include "event_m4.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstddef>
#include <algorithm> // std::max
#include <cmath>
void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
// This is called at 1500Hz
if (!configured) return;
// Get 24kHz audio
@@ -41,11 +41,36 @@ void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
auto audio = demod.execute(channel_out, audio_buffer);
smooth.Process(audio.p, audio.count); // Smooth the data to make decoding more accurate
audio_output.write(audio);
// If squelching, check for audio before smoothing because smoothing
// causes the squelch noise detection to fail. Likely because squelch
// looks for HF noise and smoothing is basically a lowpass filter.
// NB: Squelch in this processor is only for the the audio output.
// Squelching will likely drop data "noise" and break processing.
if (squelch_.enabled()) {
bool has_audio = squelch_.execute(audio);
squelch_history = (squelch_history << 1) | (has_audio ? 1 : 0);
}
smooth.Process(audio.p, audio.count);
processDemodulatedSamples(audio.p, 16);
extractFrames();
samples_processed += buffer.count;
if (samples_processed >= stat_update_threshold) {
send_stats();
samples_processed = 0;
}
// Clear the output before sending to audio chip.
// Only clear the audio buffer when there hasn't been any audio for a while.
if (squelch_.enabled() && squelch_history == 0) {
for (size_t i = 0; i < audio.count; ++i) {
audio.p[i] = 0.0;
}
}
audio_output.write(audio);
}
// ====================================================================
@@ -71,8 +96,20 @@ int POCSAGProcessor::OnDataFrame(int len, int baud) {
}
void POCSAGProcessor::on_message(const Message* const message) {
if (message->id == Message::ID::POCSAGConfigure)
configure();
switch (message->id) {
case Message::ID::POCSAGConfigure:
configure();
break;
case Message::ID::NBFMConfigure: {
auto config = reinterpret_cast<const NBFMConfigureMessage*>(message);
squelch_.set_threshold(config->squelch_level / 100.0);
break;
}
default:
break;
}
}
void POCSAGProcessor::configure() {
@@ -87,13 +124,16 @@ void POCSAGProcessor::configure() {
const size_t demod_input_fs = channel_filter_output_fs;
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
decim_1.configure(taps_11k0_decim_1.taps, 131072);
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
channel_filter.configure(taps_11k0_channel.taps, 2);
demod.configure(demod_input_fs, 4500);
demod.configure(demod_input_fs, 4'500); // FSK +/- 4k5Hz.
// Smoothing should be roughly sample rate over max baud
// 24k / 3.2k is 7.5
// 24k / 3.2k = 7.5
smooth.SetSize(8);
// Don't have audio process the stream.
audio_output.configure(false);
// Set up the frame extraction, limits of baud
@@ -103,6 +143,11 @@ void POCSAGProcessor::configure() {
configured = true;
}
void POCSAGProcessor::send_stats() const {
POCSAGStatsMessage message(m_fifo.codeword, m_numCode, m_gotSync);
shared_memory.application_queue.push(message);
}
// -----------------------------
// Frame extractraction methods
// -----------------------------
+13 -3
View File
@@ -3,6 +3,7 @@
* Copyright (C) 2012-2014 Elias Oenal (multimon-ng@eliasoenal.com)
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -48,7 +49,7 @@ using namespace std;
template <class ValType, class CalcType>
class SmoothVals {
protected:
ValType* m_lastVals; // Previoius N values
ValType* m_lastVals; // Previous N values
int m_size; // The size N
CalcType m_sumVal; // Running sum of lastVals
int m_pos; // Current position in last vals ring buffer
@@ -106,10 +107,10 @@ class SmoothVals {
// Use a rolling smoothed value while processing the buffer
for (int buffPos = 0; buffPos < numVals; ++buffPos) {
m_pos = (m_pos + 1); // Increment the position in the stored values
m_pos++;
if (m_pos >= m_size) {
m_pos = 0;
} // loop if reached the end of the stored values
}
m_sumVal -= (CalcType)m_lastVals[m_pos]; // Subtract the oldest value
m_lastVals[m_pos] = valBuff[buffPos]; // Store the new value
@@ -136,6 +137,9 @@ class POCSAGProcessor : public BasebandProcessor {
private:
static constexpr size_t baseband_fs = 3072000;
static constexpr uint8_t stat_update_interval = 10;
static constexpr uint32_t stat_update_threshold =
baseband_fs / stat_update_interval;
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
@@ -157,7 +161,10 @@ class POCSAGProcessor : public BasebandProcessor {
bool configured = false;
pocsag::POCSAGPacket packet{};
uint32_t samples_processed = 0;
void configure();
void send_stats() const;
// ----------------------------------------
// Frame extractraction methods and members
@@ -215,6 +222,9 @@ class POCSAGProcessor : public BasebandProcessor {
int m_numCode{0};
bool m_inverted{false};
FMSquelch squelch_{};
uint64_t squelch_history = 0;
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
+501
View File
@@ -0,0 +1,501 @@
/*
* Copyright (C) 1996 Thomas Sailer (sailer@ife.ee.ethz.ch, hb9jnx@hb9w.che.eu)
* Copyright (C) 2012-2014 Elias Oenal (multimon-ng@eliasoenal.com)
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2016 Kyle Reed
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "proc_pocsag2.hpp"
#include "event_m4.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstddef>
using namespace std;
void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
// buffer has 2048 samples
// decim0 out: 2048/8 = 256 samples
// decim1 out: 256/8 = 32 samples
// channel out: 32/2 = 16 samples
// Get 24kHz audio
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 channel_out = channel_filter.execute(decim_1_out, dst_buffer);
auto audio = demod.execute(channel_out, audio_buffer);
// Check if there's any signal in the audio buffer.
bool has_audio = squelch.execute(audio);
squelch_history = (squelch_history << 1) | (has_audio ? 1 : 0);
// Has there been any signal?
if (squelch_history == 0) {
// No signal for a while, flush and reset.
if (!has_been_reset) {
OnDataFrame(m_numCode, getRate());
resetVals();
send_stats();
}
// Clear the audio stream before sending.
for (size_t i = 0; i < audio.count; ++i)
audio.p[i] = 0.0;
audio_output.write(audio);
return;
}
// Filter out high-frequency noise. TODO: compensate gain?
lpf.execute_in_place(audio);
normalizer.execute_in_place(audio);
audio_output.write(audio);
processDemodulatedSamples(audio.p, 16);
extractFrames();
samples_processed += buffer.count;
if (samples_processed >= stat_update_threshold) {
send_stats();
samples_processed = 0;
}
}
void POCSAGProcessor::on_message(const Message* const message) {
switch (message->id) {
case Message::ID::POCSAGConfigure:
configure();
break;
case Message::ID::NBFMConfigure: {
auto config = reinterpret_cast<const NBFMConfigureMessage*>(message);
squelch.set_threshold(config->squelch_level / 99.0);
break;
}
default:
break;
}
}
void POCSAGProcessor::configure() {
constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor;
constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor;
const size_t channel_filter_output_fs = decim_1_output_fs / 2;
const size_t demod_input_fs = channel_filter_output_fs;
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
channel_filter.configure(taps_11k0_channel.taps, 2);
demod.configure(demod_input_fs, 4'500); // FSK +/- 4k5Hz.
// Don't process the audio stream.
audio_output.configure(false);
// Set up the frame extraction, limits of baud.
setFrameExtractParams(demod_input_fs, 4000, 300, 32);
// Set ready to process data.
configured = true;
}
void POCSAGProcessor::send_stats() const {
POCSAGStatsMessage message(m_fifo.codeword, m_numCode, m_gotSync);
shared_memory.application_queue.push(message);
}
int POCSAGProcessor::OnDataWord(uint32_t word, int pos) {
packet.set(pos, word);
return 0;
}
int POCSAGProcessor::OnDataFrame(int len, int baud) {
if (len > 0) {
packet.set_bitrate(baud);
packet.set_flag(pocsag::PacketFlag::NORMAL);
packet.set_timestamp(Timestamp::now());
const POCSAGPacketMessage message(packet);
shared_memory.application_queue.push(message);
}
return 0;
}
#define BAUD_STABLE (104)
#define MAX_CONSEC_SAME (32)
#define MAX_WITHOUT_SINGLE (64)
#define MAX_BAD_TRANS (10)
#define M_SYNC (0x7cd215d8)
#define M_NOTSYNC (0x832dea27)
#define M_IDLE (0x7a89c197)
inline int bitsDiff(unsigned long left, unsigned long right) {
unsigned long xord = left ^ right;
int count = 0;
for (int i = 0; i < 32; i++) {
if ((xord & 0x01) != 0) ++count;
xord = xord >> 1;
}
return (count);
}
void POCSAGProcessor::initFrameExtraction() {
m_averageSymbolLen_1024 = m_maxSymSamples_1024;
m_lastStableSymbolLen_1024 = m_minSymSamples_1024;
m_badTransitions = 0;
m_bitsStart = 0;
m_bitsEnd = 0;
m_inverted = false;
resetVals();
}
void POCSAGProcessor::resetVals() {
if (has_been_reset) return;
// Reset the parameters
m_goodTransitions = 0;
m_badTransitions = 0;
m_averageSymbolLen_1024 = m_maxSymSamples_1024;
m_shortestGoodTrans_1024 = m_maxSymSamples_1024;
m_valMid = 0;
// And reset the counts
m_lastTransPos_1024 = 0;
m_lastBitPos_1024 = 0;
m_lastSample = 0;
m_sampleNo = 0;
m_nextBitPos_1024 = m_maxSymSamples_1024;
m_nextBitPosInt = (long)m_nextBitPos_1024;
// Extraction
m_fifo.numBits = 0;
m_fifo.codeword = 0;
m_gotSync = false;
m_numCode = 0;
has_been_reset = true;
samples_processed = 0;
}
void POCSAGProcessor::setFrameExtractParams(long a_samplesPerSec, long a_maxBaud, long a_minBaud, long maxRunOfSameValue) {
m_samplesPerSec = a_samplesPerSec;
m_minSymSamples_1024 = (uint32_t)(1024.0f * (float)a_samplesPerSec / (float)a_maxBaud);
m_maxSymSamples_1024 = (uint32_t)(1024.0f * (float)a_samplesPerSec / (float)a_minBaud);
m_maxRunOfSameValue = maxRunOfSameValue;
m_shortestGoodTrans_1024 = m_maxSymSamples_1024;
m_averageSymbolLen_1024 = m_maxSymSamples_1024;
m_lastStableSymbolLen_1024 = m_minSymSamples_1024;
m_nextBitPos_1024 = m_averageSymbolLen_1024 / 2;
m_nextBitPosInt = m_nextBitPos_1024 >> 10;
initFrameExtraction();
}
int POCSAGProcessor::processDemodulatedSamples(float* sampleBuff, int noOfSamples) {
bool transition = false;
uint32_t samplePos_1024 = 0;
uint32_t len_1024 = 0;
has_been_reset = false;
// Loop through the block of data
// ------------------------------
for (int pos = 0; pos < noOfSamples; ++pos) {
m_sample = sampleBuff[pos];
m_valMid += (m_sample - m_valMid) / 1024.0f;
++m_sampleNo;
// Detect Transition
// -----------------
transition = !((m_lastSample < m_valMid) ^ (m_sample >= m_valMid)); // use XOR for speed
// If this is a transition
// -----------------------
if (transition) {
// Calculate samples since last trans
// ----------------------------------
int32_t fractional_1024 = (int32_t)(((m_sample - m_valMid) * 1024) / (m_sample - m_lastSample));
if (fractional_1024 < 0) {
fractional_1024 = -fractional_1024;
}
samplePos_1024 = (m_sampleNo << 10) - fractional_1024;
len_1024 = samplePos_1024 - m_lastTransPos_1024;
m_lastTransPos_1024 = samplePos_1024;
// If symbol is large enough to be valid
// -------------------------------------
if (len_1024 > m_minSymSamples_1024) {
// Check for shortest good transition
// ----------------------------------
if ((len_1024 < m_shortestGoodTrans_1024) &&
(m_goodTransitions < BAUD_STABLE)) // detect change of symbol size
{
int32_t fractionOfShortest_1024 = (len_1024 << 10) / m_shortestGoodTrans_1024;
// If currently at half the baud rate
// ----------------------------------
if ((fractionOfShortest_1024 > 410) && (fractionOfShortest_1024 < 614)) // 0.4 and 0.6
{
m_averageSymbolLen_1024 /= 2;
m_shortestGoodTrans_1024 = len_1024;
}
// If currently at the wrong baud rate
// -----------------------------------
else if (fractionOfShortest_1024 < 768) // 0.75
{
m_averageSymbolLen_1024 = len_1024;
m_shortestGoodTrans_1024 = len_1024;
m_goodTransitions = 0;
m_lastSingleBitPos_1024 = samplePos_1024 - len_1024;
}
}
// Calc the number of bits since events
// ------------------------------------
int32_t halfSymbol_1024 = m_averageSymbolLen_1024 / 2;
int bitsSinceLastTrans = max((uint32_t)1, (len_1024 + halfSymbol_1024) / m_averageSymbolLen_1024);
int bitsSinceLastSingle = (((m_sampleNo << 10) - m_lastSingleBitPos_1024) + halfSymbol_1024) / m_averageSymbolLen_1024;
// Check for single bit
// --------------------
if (bitsSinceLastTrans == 1) {
m_lastSingleBitPos_1024 = samplePos_1024;
}
// If too long since last transition
// ---------------------------------
if (bitsSinceLastTrans > MAX_CONSEC_SAME) {
resetVals();
}
// If too long sice last single bit
// --------------------------------
else if (bitsSinceLastSingle > MAX_WITHOUT_SINGLE) {
resetVals();
} else {
// If this is a good transition
// ----------------------------
int32_t offsetFromExtectedTransition_1024 = len_1024 - (bitsSinceLastTrans * m_averageSymbolLen_1024);
if (offsetFromExtectedTransition_1024 < 0) {
offsetFromExtectedTransition_1024 = -offsetFromExtectedTransition_1024;
}
if (offsetFromExtectedTransition_1024 < ((int32_t)m_averageSymbolLen_1024 / 4)) // Has to be within 1/4 of symbol to be good
{
++m_goodTransitions;
uint32_t bitsCount = min((uint32_t)BAUD_STABLE, m_goodTransitions);
uint32_t propFromPrevious = m_averageSymbolLen_1024 * bitsCount;
uint32_t propFromCurrent = (len_1024 / bitsSinceLastTrans);
m_averageSymbolLen_1024 = (propFromPrevious + propFromCurrent) / (bitsCount + 1);
m_badTransitions = 0;
// if ( len < m_shortestGoodTrans ){m_shortestGoodTrans = len;}
// Store the old symbol size
if (m_goodTransitions >= BAUD_STABLE) {
m_lastStableSymbolLen_1024 = m_averageSymbolLen_1024;
}
}
}
// Set the point of the last bit if not yet stable
// -----------------------------------------------
if ((m_goodTransitions < BAUD_STABLE) || (m_badTransitions > 0)) {
m_lastBitPos_1024 = samplePos_1024 - (m_averageSymbolLen_1024 / 2);
}
// Calculate the exact positiom of the next bit
// --------------------------------------------
int32_t thisPlusHalfsymbol_1024 = samplePos_1024 + (m_averageSymbolLen_1024 / 2);
int32_t lastPlusSymbol = m_lastBitPos_1024 + m_averageSymbolLen_1024;
m_nextBitPos_1024 = lastPlusSymbol + ((thisPlusHalfsymbol_1024 - lastPlusSymbol) / 16);
// Check for bad pos error
// -----------------------
if (m_nextBitPos_1024 < samplePos_1024) m_nextBitPos_1024 += m_averageSymbolLen_1024;
// Calculate integer sample after next bit
// ---------------------------------------
m_nextBitPosInt = (m_nextBitPos_1024 >> 10) + 1;
} // symbol is large enough to be valid
else {
// Bad transition, so reset the counts
// -----------------------------------
++m_badTransitions;
if (m_badTransitions > MAX_BAD_TRANS) {
resetVals();
}
}
} // end of if transition
// Reached the point of the next bit
// ---------------------------------
if (m_sampleNo >= m_nextBitPosInt) {
// Everything is good so extract a bit
// -----------------------------------
if (m_goodTransitions > 20) {
// Store value at the center of bit
// --------------------------------
storeBit();
}
// Check for long 1 or zero
// ------------------------
uint32_t bitsSinceLastTrans = ((m_sampleNo << 10) - m_lastTransPos_1024) / m_averageSymbolLen_1024;
if (bitsSinceLastTrans > m_maxRunOfSameValue) {
resetVals();
}
// Store the point of the last bit
// -------------------------------
m_lastBitPos_1024 = m_nextBitPos_1024;
// Calculate the exact point of the next bit
// -----------------------------------------
m_nextBitPos_1024 += m_averageSymbolLen_1024;
// Look for the bit after the next bit pos
// ---------------------------------------
m_nextBitPosInt = (m_nextBitPos_1024 >> 10) + 1;
} // Reached the point of the next bit
m_lastSample = m_sample;
} // Loop through the block of data
return getNoOfBits();
}
void POCSAGProcessor::storeBit() {
if (++m_bitsStart >= BIT_BUF_SIZE) {
m_bitsStart = 0;
}
// Calculate the bit value
float sample = (m_sample + m_lastSample) / 2;
// int32_t sample_1024 = m_sample_1024;
bool bit = sample > m_valMid;
// If buffer not full
if (m_bitsStart != m_bitsEnd) {
// Decide on output val
if (bit) {
m_bits[m_bitsStart] = 0;
} else {
m_bits[m_bitsStart] = 1;
}
}
// Throw away bits if the buffer is full
else {
if (--m_bitsStart <= -1) {
m_bitsStart = BIT_BUF_SIZE - 1;
}
}
}
int POCSAGProcessor::extractFrames() {
int msgCnt = 0;
// While there is unread data in the bits buffer
//----------------------------------------------
while (getNoOfBits() > 0) {
m_fifo.codeword = (m_fifo.codeword << 1) + getBit();
m_fifo.numBits++;
// If number of bits in fifo equals 32
//------------------------------------
if (m_fifo.numBits >= 32) {
// Not got sync
// ------------
if (!m_gotSync) {
if (bitsDiff(m_fifo.codeword, M_SYNC) <= 2) {
m_inverted = false;
m_gotSync = true;
m_numCode = -1;
m_fifo.numBits = 0;
} else if (bitsDiff(m_fifo.codeword, M_NOTSYNC) <= 2) {
m_inverted = true;
m_gotSync = true;
m_numCode = -1;
m_fifo.numBits = 0;
} else {
// Cause it to load one more bit
m_fifo.numBits = 31;
}
} // Not got sync
else {
// Increment the word count
// ------------------------
++m_numCode; // It got set to -1 when a sync was found, now count the 16 words
uint32_t val = m_inverted ? ~m_fifo.codeword : m_fifo.codeword;
OnDataWord(val, m_numCode);
// If at the end of a 16 word block
// --------------------------------
if (m_numCode >= 15) {
msgCnt += OnDataFrame(m_numCode + 1, getRate());
m_gotSync = false;
m_numCode = -1;
}
m_fifo.numBits = 0;
}
} // If number of bits in fifo equals 32
} // While there is unread data in the bits buffer
return msgCnt;
} // extractFrames
short POCSAGProcessor::getBit() {
if (m_bitsEnd != m_bitsStart) {
if (++m_bitsEnd >= BIT_BUF_SIZE) {
m_bitsEnd = 0;
}
return m_bits[m_bitsEnd];
} else {
return -1;
}
}
int POCSAGProcessor::getNoOfBits() {
int bits = m_bitsEnd - m_bitsStart;
if (bits < 0) {
bits += BIT_BUF_SIZE;
}
return bits;
}
uint32_t POCSAGProcessor::getRate() {
return ((m_samplesPerSec << 10) + 512) / m_lastStableSymbolLen_1024;
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<POCSAGProcessor>()};
event_dispatcher.run();
return 0;
}
+231
View File
@@ -0,0 +1,231 @@
/*
* Copyright (C) 1996 Thomas Sailer (sailer@ife.ee.ethz.ch, hb9jnx@hb9w.che.eu)
* Copyright (C) 2012-2014 Elias Oenal (multimon-ng@eliasoenal.com)
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* 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 __PROC_POCSAG2_H__
#define __PROC_POCSAG2_H__
#include "audio_output.hpp"
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "dsp_iir_config.hpp"
#include "message.hpp"
#include "pocsag.hpp"
#include "pocsag_packet.hpp"
#include "portapack_shared_memory.hpp"
#include "rssi_thread.hpp"
#include <cstdint>
/* Takes audio stream and automatically normalizes it to +/-1.0f */
class AudioNormalizer {
public:
void execute_in_place(const buffer_f32_t& audio) {
// Decay min/max every second (@24kHz).
if (counter_ >= 24'000) {
// 90% decay factor seems to work well.
// This keeps large transients from wrecking the filter.
max_ *= 0.9f;
min_ *= 0.9f;
counter_ = 0;
calculate_thresholds();
}
counter_ += audio.count;
for (size_t i = 0; i < audio.count; ++i) {
auto& val = audio.p[i];
if (val > max_) {
max_ = val;
calculate_thresholds();
}
if (val < min_) {
min_ = val;
calculate_thresholds();
}
if (val >= t_hi_)
val = 1.0f;
else if (val <= t_lo_)
val = -1.0f;
else
val = 0.0;
}
}
private:
void calculate_thresholds() {
auto center = (max_ + min_) / 2.0f;
auto range = (max_ - min_) / 2.0f;
// 10% off center force either +/-1.0f.
// Higher == larger dead zone.
// Lower == more false positives.
auto threshold = range * 0.1;
t_hi_ = center + threshold;
t_lo_ = center - threshold;
}
uint32_t counter_ = 0;
float min_ = 99.0f;
float max_ = -99.0f;
float t_hi_ = 1.0;
float t_lo_ = 1.0;
};
// How to detect clock signal across baud rates?
// Maybe have a bit extraction state machine that reset
// then watches for the clocks, but there are multiple
// clock and the last one is the right one.
// So keep updating clock until a sync?
class BitExtractor {};
class WordExtractor {};
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);
int OnDataWord(uint32_t word, int pos);
private:
static constexpr size_t baseband_fs = 3072000;
static constexpr uint8_t stat_update_interval = 10;
static constexpr uint32_t stat_update_threshold =
baseband_fs / stat_update_interval;
void configure();
void send_stats() const;
// Set once app is ready to receive messages.
bool configured = false;
// Buffer for decimated IQ data.
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{dst.data(), dst.size()};
// Buffer for demodulated audio.
std::array<float, 32> audio{};
const buffer_f32_t audio_buffer{audio.data(), audio.size()};
// Decimate to 48kHz.
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
// Filter to 24kHz and demodulate.
dsp::decimate::FIRAndDecimateComplex channel_filter{};
dsp::demodulate::FM demod{};
// LPF to reduce noise.
// scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False)
IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f},
{1.00000000f, -1.34896775f, 0.51398189f}}};
// Squelch to ignore noise.
FMSquelch squelch{};
uint64_t squelch_history = 0;
// Attempts to de-noise signal and normalize to +/- 1.0f.
AudioNormalizer normalizer{};
// Handles writing audio stream to hardware.
AudioOutput audio_output{};
// Holds the data sent to the app.
pocsag::POCSAGPacket packet{};
bool has_been_reset = true;
uint32_t samples_processed = 0;
//--------------------------------------------------
// ----------------------------------------
// Frame extractraction methods and members
// ----------------------------------------
void initFrameExtraction();
struct FIFOStruct {
unsigned long codeword;
int numBits;
};
void resetVals();
void setFrameExtractParams(long a_samplesPerSec, long a_maxBaud = 8000, long a_minBaud = 200, long maxRunOfSameValue = 32);
int processDemodulatedSamples(float* sampleBuff, int noOfSamples);
int extractFrames();
void storeBit();
short getBit();
int getNoOfBits();
uint32_t getRate();
uint32_t m_averageSymbolLen_1024{0};
uint32_t m_lastStableSymbolLen_1024{0};
uint32_t m_samplesPerSec{0};
uint32_t m_goodTransitions{0};
uint32_t m_badTransitions{0};
uint32_t m_sampleNo{0};
float m_sample{0};
float m_valMid{0.0f};
float m_lastSample{0.0f};
uint32_t m_lastTransPos_1024{0};
uint32_t m_lastSingleBitPos_1024{0};
uint32_t m_nextBitPosInt{0}; // Integer rounded up version to save on ops
uint32_t m_nextBitPos_1024{0};
uint32_t m_lastBitPos_1024{0};
uint32_t m_shortestGoodTrans_1024{0};
uint32_t m_minSymSamples_1024{0};
uint32_t m_maxSymSamples_1024{0};
uint32_t m_maxRunOfSameValue{0};
static constexpr long BIT_BUF_SIZE = 64;
std::bitset<64> m_bits{0};
long m_bitsStart{0};
long m_bitsEnd{0};
FIFOStruct m_fifo{0, 0};
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_POCSAG2_H__*/
+63 -29
View File
@@ -41,43 +41,75 @@ ReplayProcessor::ReplayProcessor() {
baseband_thread.start();
}
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
/* 4MHz, 2048 samples */
// Change to 1 to enable buffer assertions in replay.
#define BUFFER_SIZE_ASSERT 0
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
if (!configured || !stream) return;
buffer_c16_t iq_buffer{iq.data(), iq.size(), baseband_fs / 8};
// Because this is actually adding samples, alias
// oversample_rate so the math below is more clear.
const size_t interpolation_factor = toUType(oversample_rate);
// 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
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);
// Wrap the IQ data array in a buffer with the correct sample_rate.
buffer_c16_t iq_buffer{iq.data(), iq.size(), baseband_fs / interpolation_factor};
bytes_read += bytes_read_this_iteration;
// The IQ data in stream is C16 format and needs to be converted to C8 (N * 2).
// The data also needs to be interpolated so the effective sample rate is closer
// to 4Mhz. Because interpolation repeats a sample multiple times, fewer bytes
// are needed from the source stream in order to fill the buffer (count / oversample).
// Together the C16->C8 conversion and the interpolation give the number of
// bytes that need to be read from the source stream.
const size_t samples_to_read = buffer.count / interpolation_factor;
const size_t bytes_to_read = samples_to_read * sizeof(buffer_c16_t::Type);
// Fill and "stretch"
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;
auto im_out = iq_buffer.p[i >> 3].imag() >> 8;
buffer.p[i] = {(int8_t)re_out, (int8_t)im_out};
#if BUFFER_SIZE_ASSERT
// Verify the output buffer size is divisible by the interpolation factor.
if (samples_to_read * interpolation_factor != buffer.count)
chDbgPanic("Output not div.");
// Is the input smaple buffer big enough?
if (samples_to_read > iq_buffer.count)
chDbgPanic("IQ buf ovf.");
#endif
// Read the C16 IQ data from the source stream.
size_t current_bytes_read = stream->read(iq_buffer.p, bytes_to_read);
// Compute the number of samples were actually read from the source.
size_t samples_read = current_bytes_read / sizeof(buffer_c16_t::Type);
// Write converted source samples to the output buffer with interpolation.
for (auto i = 0u; i < samples_read; ++i) {
int8_t re_out = iq_buffer.p[i].real() >> 8;
int8_t im_out = iq_buffer.p[i].imag() >> 8;
auto out_value = buffer_c8_t::Type{re_out, im_out};
// Interpolate sample.
for (auto j = 0u; j < interpolation_factor; ++j) {
size_t index = i * interpolation_factor + j;
buffer.p[index] = out_value;
#if BUFFER_SIZE_ASSERT
// Verify the index is within bounds.
if (index >= buffer.count)
chDbgPanic("Output bounds");
#endif
}
}
spectrum_samples += oversamples_this_iteration;
// Update tracking stats.
bytes_read += current_bytes_read;
spectrum_samples += samples_read * interpolation_factor;
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);
channel_spectrum.feed(
iq_buffer, channel_filter_low_f,
channel_filter_high_f, channel_filter_transition);
txprogress_message.progress = bytes_read; // Inform UI about progress
// Inform UI about progress.
txprogress_message.progress = bytes_read;
txprogress_message.done = false;
shared_memory.application_queue.push(txprogress_message);
}
@@ -90,8 +122,8 @@ void ReplayProcessor::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:
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
break;
case Message::ID::ReplayConfig:
@@ -110,9 +142,11 @@ void ReplayProcessor::on_message(const Message* const message) {
}
}
void ReplayProcessor::samplerate_config(const SamplerateConfigMessage& message) {
baseband_fs = message.sample_rate;
void ReplayProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
baseband_fs = message.sample_rate * toUType(message.oversample_rate);
oversample_rate = message.oversample_rate;
baseband_thread.set_sampling_rate(baseband_fs);
spectrum_interval_samples = baseband_fs / spectrum_rate_hz;
}
+4 -2
View File
@@ -44,7 +44,8 @@ class ReplayProcessor : public BasebandProcessor {
size_t baseband_fs = 3072000;
static constexpr auto spectrum_rate_hz = 50.0f;
std::array<complex16_t, 256> iq{};
// Holds the read IQ data chunk from the file to send.
std::array<complex16_t, 512> iq{};
int32_t channel_filter_low_f = 0;
int32_t channel_filter_high_f = 0;
@@ -58,8 +59,9 @@ class ReplayProcessor : public BasebandProcessor {
bool configured{false};
uint32_t bytes_read{0};
OversampleRate oversample_rate = OversampleRate::x8;
void samplerate_config(const SamplerateConfigMessage& message);
void sample_rate_config(const SampleRateConfigMessage& message);
void replay_config(const ReplayConfigMessage& message);
TXProgressMessage txprogress_message{};
+2 -2
View File
@@ -29,8 +29,8 @@
#include "audio_output.hpp"
SondeProcessor::SondeProcessor() {
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
decim_1.configure(taps_11k0_decim_1.taps, 131072);
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
audio_output.configure(false);
+16 -17
View File
@@ -25,10 +25,12 @@
#include "random.hpp"
#include <cstdint>
#include <memory>
#include <utility>
// TODO move to class members SpectrumPainterProcessor
complex16_t* current_line_data = nullptr;
complex16_t* volatile next_line_data = nullptr;
std::unique_ptr<complex16_t[]> current_line_data;
std::unique_ptr<complex16_t[]> next_line_data;
uint32_t current_line_index = 0;
uint32_t current_line_width = 0;
int32_t current_bw = 0;
@@ -41,20 +43,17 @@ int max_val = 127;
// This is called at 3072000/2048 = 1500Hz
void SpectrumPainterProcessor::execute(const buffer_c8_t& buffer) {
for (uint32_t i = 0; i < buffer.count; i++) {
if (current_line_data != nullptr) {
if (current_line_data) {
auto data = current_line_data[(current_line_index++ * current_bw / 3072) % current_line_width];
buffer.p[i] = {(int8_t)data.real(), (int8_t)data.imag()};
} else
buffer.p[i] = {0, 0};
}
// collect new data
if (next_line_data != nullptr) {
if (current_line_data != nullptr)
delete current_line_data;
current_line_data = next_line_data;
next_line_data = nullptr;
// Move "next line" into "current line" if set.
if (next_line_data) {
current_line_data = std::move(next_line_data);
next_line_data.reset();
}
}
@@ -65,7 +64,7 @@ void SpectrumPainterProcessor::run() {
init_genrand(22267);
while (true) {
if (fifo.is_empty() == false && next_line_data == nullptr) {
if (fifo.is_empty() == false && !next_line_data) {
std::vector<uint8_t> data;
fifo.out(data);
@@ -74,8 +73,9 @@ void SpectrumPainterProcessor::run() {
auto fft_width = picture_width * 2;
auto qu = fft_width / 4;
complex16_t* v = new complex16_t[fft_width];
complex16_t* tmp = new complex16_t[fft_width];
// TODO: can these be statically allocated?
auto v = std::make_unique<complex16_t[]>(fft_width);
auto tmp = std::make_unique<complex16_t[]>(fft_width);
for (uint32_t fft_index = 0; fft_index < fft_width; fft_index++) {
if (fft_index < qu) {
@@ -103,10 +103,10 @@ void SpectrumPainterProcessor::run() {
}
}
ifft<complex16_t>(v, fft_width, tmp);
ifft<complex16_t>(v.get(), fft_width, tmp.get());
// normalize
volatile int32_t maximum = 1;
int32_t maximum = 1;
for (uint32_t i = 0; i < fft_width; i++) {
if (v[i].real() > maximum)
maximum = v[i].real();
@@ -127,8 +127,7 @@ void SpectrumPainterProcessor::run() {
}
}
delete tmp;
next_line_data = v;
next_line_data = std::move(v);
ui++;
} else {
chThdSleepMilliseconds(1);
+2 -2
View File
@@ -27,8 +27,8 @@
#include "event_m4.hpp"
TestProcessor::TestProcessor() {
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
decim_1.configure(taps_11k0_decim_1.taps, 131072);
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
baseband_thread.start();
}
+2 -2
View File
@@ -26,8 +26,8 @@
#include "event_m4.hpp"
TPMSProcessor::TPMSProcessor() {
decim_0.configure(taps_200k_decim_0.taps, 33554432);
decim_1.configure(taps_200k_decim_1.taps, 131072);
decim_0.configure(taps_200k_decim_0.taps);
decim_1.configure(taps_200k_decim_1.taps);
baseband_thread.start();
}
+2 -2
View File
@@ -165,8 +165,8 @@ void WidebandFMAudio::configure(const WFMConfigureMessage& message) {
spectrum_interval_samples = decim_1_output_fs / spectrum_rate_hz;
spectrum_samples = 0;
decim_0.configure(message.decim_0_filter.taps, 33554432);
decim_1.configure(message.decim_1_filter.taps, 131072);
decim_0.configure(message.decim_0_filter.taps);
decim_1.configure(message.decim_1_filter.taps);
channel_filter_low_f = message.decim_1_filter.low_frequency_normalized * decim_1_input_fs;
channel_filter_high_f = message.decim_1_filter.high_frequency_normalized * decim_1_input_fs;
channel_filter_transition = message.decim_1_filter.transition_normalized * decim_1_input_fs;
+8 -7
View File
@@ -21,9 +21,10 @@
#include "rssi_dma.hpp"
#include <array>
#include <cstdint>
#include <cstddef>
#include <array>
#include <memory>
#include "hal.h"
#include "gpdma.hpp"
@@ -98,8 +99,8 @@ struct buffers_config_t {
static buffers_config_t buffers_config;
static sample_t* samples{nullptr};
static gpdma::channel::LLI* lli{nullptr};
static std::unique_ptr<sample_t[]> samples;
static std::unique_ptr<gpdma::channel::LLI[]> lli;
static ThreadWait thread_wait;
@@ -128,8 +129,8 @@ void allocate(size_t buffer_count, size_t items_per_buffer) {
const auto peripheral = reinterpret_cast<uint32_t>(&LPC_ADC1->DR[portapack::adc1_rssi_input]) + 1;
const auto control_value = control(gpdma::buffer_words(buffers_config.items_per_buffer, 1));
samples = new sample_t[buffers_config.count * buffers_config.items_per_buffer];
lli = new gpdma::channel::LLI[buffers_config.count];
samples = std::make_unique<sample_t[]>(buffers_config.count * buffers_config.items_per_buffer);
lli = std::make_unique<gpdma::channel::LLI[]>(buffers_config.count);
for (size_t i = 0; i < buffers_config.count; i++) {
const auto memory = reinterpret_cast<uint32_t>(&samples[i * buffers_config.items_per_buffer]);
@@ -141,8 +142,8 @@ void allocate(size_t buffer_count, size_t items_per_buffer) {
}
void free() {
delete samples;
delete lli;
samples.reset();
lli.reset();
}
void enable() {
-4
View File
@@ -97,8 +97,6 @@ void init() {
}
void allocate() {
// samples = new sample_t[channel_samples_per_frame];
// lli = new gpdma::channel::LLI;
lli.srcaddr = reinterpret_cast<uint32_t>(&LPC_ADC0->DR[0]);
lli.destaddr = reinterpret_cast<uint32_t>(&shared_memory.touch_adc_frame.dr[0]);
lli.lli = lli_pointer(&lli);
@@ -106,8 +104,6 @@ void allocate() {
}
void free() {
// delete samples;
// delete lli;
}
void enable() {
+1
View File
@@ -49,6 +49,7 @@ using Timestamp = lpc43xx::rtc::RTC;
template <typename T>
struct buffer_t {
using Type = T;
T* const p;
const size_t count;
const uint32_t sampling_rate;
-24
View File
@@ -36,24 +36,12 @@ struct complex<int8_t> {
typedef int8_t value_type;
typedef uint16_t rep_type;
// constexpr complex(
// rep_type r
// ) : _rep { r }
// {
// }
constexpr complex(
int8_t re = 0,
int8_t im = 0)
: _v{re, im} {
}
// constexpr complex(
// const complex& o
// ) : _rep { o._rep }
// {
// }
constexpr int8_t real() const { return _v[0]; }
constexpr int8_t imag() const { return _v[1]; }
@@ -77,24 +65,12 @@ struct complex<int16_t> {
typedef int16_t value_type;
typedef uint32_t rep_type;
// constexpr complex(
// rep_type r
// ) : _rep { r }
// {
// }
constexpr complex(
int16_t re = 0,
int16_t im = 0)
: _v{re, im} {
}
// constexpr complex(
// const complex& o
// ) : _rep { o._rep }
// {
// }
constexpr int16_t real() const { return _v[0]; }
constexpr int16_t imag() const { return _v[1]; }
+1 -3
View File
@@ -38,9 +38,7 @@ constexpr iir_biquad_config_t audio_48k_hpf_300hz_config{
// scipy.signal.butter(2, 300 / 12000.0, 'highpass', analog=False)
constexpr iir_biquad_config_t audio_24k_hpf_300hz_config{
{0.94597686f, -1.89195371f, 0.94597686f},
{1.00000000f, -1.88903308f, 0.89487434f}
};
{1.00000000f, -1.88903308f, 0.89487434f}};
// scipy.signal.butter(2, 30 / 12000.0, 'highpass', analog=False)
constexpr iir_biquad_config_t audio_24k_hpf_30hz_config{
+3
View File
@@ -456,6 +456,9 @@ bool ILI9341::drawBMP2(const ui::Point p, const std::filesystem::path& file) {
width = bmp_header.width;
height = bmp_header.height;
if (width != 240)
return false;
file_pos = bmp_header.image_data;
py = height + 16;

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