Compare commits

...

35 Commits

Author SHA1 Message Date
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
80 changed files with 1808 additions and 1154 deletions
+1 -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)
+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);
}
+192 -99
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,93 @@ 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_log,
&check_log_raw,
&check_small_font,
&check_hide_bad,
&check_hide_addr_only,
&check_ignore,
&field_ignore,
&button_save});
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_.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);
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,
&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 +139,133 @@ 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*) {
}
} /* namespace ui */
+144 -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,73 @@ class POCSAGLogger {
namespace ui {
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;
};
class POCSAGSettingsView : public View {
public:
POCSAGSettingsView(NavigationView& nav, POCSAGSettings& settings);
std::string title() const override { return "POCSAG Config"; };
private:
POCSAGSettings& settings_;
Checkbox check_log{
{2 * 8, 2 * 16},
10,
"Enable Log",
false};
Checkbox check_log_raw{
{2 * 8, 4 * 16},
12,
"Log Raw Data",
false};
Checkbox check_small_font{
{2 * 8, 6 * 16},
4,
"Use Small Font",
false};
Checkbox check_hide_bad{
{2 * 8, 8 * 16},
22,
"Hide Bad Data",
false};
Checkbox check_hide_addr_only{
{2 * 8, 10 * 16},
22,
"Hide Addr Only",
false};
Checkbox check_ignore{
{2 * 8, 12 * 16},
22,
"Enable Ignored Address",
false};
NumberField field_ignore{
{7 * 8, 13 * 16 + 8},
7,
{0, 9999999},
1,
'0'};
Button button_save{
{12 * 8, 16 * 16, 10 * 8, 2 * 16},
"Save"};
};
class POCSAGAppView : public View {
public:
POCSAGAppView(NavigationView& nav);
@@ -58,60 +128,86 @@ 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},
}};
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,
' '};
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"};
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 +215,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 {
+13 -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,10 @@ 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(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 +279,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 +342,6 @@ FileSaveView::FileSaveView(
file_{ file }
{
add_children({
&labels,
&text_path,
&button_edit_path,
&text_name,
@@ -546,7 +545,6 @@ FileManagerView::FileManagerView(
add_children({
&menu_view,
&labels,
&text_date,
&button_rename,
&button_delete,
@@ -560,10 +558,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{
-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);
+49 -127
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
{
@@ -1234,10 +1156,10 @@ size_t ReconView::change_mode(freqman_index_t new_mod) {
break;
case SPEC_MODULATION:
freqman_set_bandwidth_option(new_mod, field_bw);
// bw 200k (0) default
// bw 12k5 (0) default
field_bw.set_by_value(0);
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);
+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},
+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);
+72 -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,7 +47,7 @@ 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);
static bool error_shown = false;
@@ -67,8 +68,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({12, 320 - 32}, Styles::white, "Press DFU to try Stack Dump");
}
if (source == CORTEX_M4)
painter.draw_string({48 + 8, 24}, Styles::white, "4");
@@ -83,11 +86,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) {
@@ -117,11 +116,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 +126,10 @@ 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) {
bool dumped{false};
LED led = (source == CORTEX_M0) ? hackrf::one::led_rx : hackrf::one::led_tx;
led.off();
while (true) {
@@ -139,9 +137,70 @@ void runtime_error(LED led) {
while (n--)
;
led.toggle();
// Stack dump is not guaranteed to work in this state, so wait for DFU button press to attempt it
if (!dumped && (swizzled_switches() & (1 << (int)Switch::Dfu))) {
dumped = true;
stack_dump();
}
}
}
// TODO: Fix this function to work after a fault; might need to write to screen instead or to Flash memory.
// Using the stack while dumping the stack isn't ideal, but hopefully anything imporant is still on the call stack.
bool stack_dump() {
ui::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;
bool data_found;
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__, n = 0, data_found = false; p < &__process_stack_end__; p++) {
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 bytes " + to_string_dec_uint(stack_space_left * sizeof(uint32_t)));
// align subsequent lines to start on 16-byte boundaries
p -= (stack_space_left & 3);
}
}
if (n++ == 0) {
str = to_string_hex((uint32_t)p, 8) + ":";
stack_dump_file.write(str.data(), 9);
}
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 */
+12 -9
View File
@@ -76,16 +76,19 @@ 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) */
{"600k", 600000}, /* We doubled x2 previous REC BW limit , now extended BW from 600k to 1M with fast enough SD card in C16 or C8 format .*/
{"650k", 650000},
{"750k", 750000}, /* From this BW onwards, the LCD is ok, but the recorded file is decimated, (not real file size) */
{"1100k", 1100000},
{"750k", 750000},
{"1000k", 1000000}, /* New limit bandwith option for recording in C16 (in fast SD card) or in C8 */
{"1500k", 1500000}, /* From this BW onwards, the LCD is ok, but M4 CPU is having periodical sample rec dropps, (not real file size, accelerated replay) */
{"1750k", 1750000},
{"2000k", 2000000},
{"2500k", 2500000},
@@ -267,18 +270,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();
+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);
+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 {
+23 -18
View File
@@ -380,33 +380,38 @@ void WaterfallView::on_audio_spectrum() {
} /* namespace spectrum */
// TODO: Comments below refer to a fixed oversample rate (8x), cleanup.
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 (0 <= 250kHz max) fs = 8x250 kHz =2000., 16x150 khz =2400, 32x100 khz =3200, (32x75k = 2400), (future 64x40 khz =2400)
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 ... 6'000'000: // 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 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 8'000'000: // 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. writting 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: // BW capture 1,5Mhz, fs = 8 x 1,5Mhz = 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 14'000'000: // BW capture 1,75Mhz, fs = 8 x 1,75Mhz = 14Mhz
// Good BPF, good matching, we have some periodical M4 % samples drop.
return 5'000'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 16'000'000: // BW capture 2Mhz, fs = 8 x 2Mhz = 16Mhz
// Good BPF, good matching, we have some periodical M4 % samples drop.
return 6'000'000;
case 20'000'000: // BW capture 2,5Mhz, fs = 8 x 2,5 Mhz = 20Mhz
// Good BPF, good matching, we have some periodical M4 % samples drop.
return 7'000'000;
default: // BW capture 2,75Mhz, fs = 8 x 2,75Mhz = 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;
}
}
+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();
}
+34 -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,28 @@ 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)
/* We are changing "REC" icon background to yellow in BW rec Options >1Mhz
* > 1Mhz BW options , we are NOT recording full IQ .C16 files (those files has some periodical missing dropped samples).
* Those recorded files, has not the full IQ samples information, looks like decimated in file size.
* They are ok as recorded spectrum indication, but they should not be used by Replay app. (the voice speed will be accelerated)
* We keep original black background in all the correct IQ .C16 files BW's Options. */
if (actual_sampling_rate > 8'000'000) { // yellow REC button means not ok for REC, BW >1Mhz (BW from 12k5 till 1Mhz OK for REC and Replay)
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 +133,24 @@ 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;
auto rate = ::get_oversample_rate(sample_rate);
// Currently proc_capture only supports /8, /16, /32 for decimation.
if (rate < OversampleRate::x8) // clipping while /4 is not implemented yet (it will be used >1Mhz onwards when available)
rate = OversampleRate::x8;
else if (rate > OversampleRate::x64) // clipping while /128 is not implemented yet , (but it is not necessary for 12k5)
rate = OversampleRate::x64;
return rate;
}
// Setter for datetime and frequency filename
@@ -202,8 +222,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 +297,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{
+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);
+98 -25
View File
@@ -27,9 +27,12 @@
#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);
decim_0_4.configure(taps_200k_decim_0.taps, 33554432); // to be used with decim1 (/2), then total two stages decim (/8)
decim_0_8.configure(taps_200k_decim_0.taps, 33554432); // to be used with decim1 (/2), then total two stages decim (/16)
decim_0_8_180k.configure(taps_180k_wfm_decim_0.taps, 33554432); // to be used alone - no additional decim1 (/2), then total single stage decim (/8)
decim_1_2.configure(taps_200k_decim_1.taps, 131072);
decim_1_8.configure(taps_16k0_decim_1.taps, 131072); // tentative decim1 /8 and taps, pending to be optimized.
channel_spectrum.set_decimation_factor(1);
baseband_thread.start();
@@ -37,8 +40,17 @@ CaptureProcessor::CaptureProcessor() {
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 decim_0_out = decim_0_execute(buffer, dst_buffer); // selectable 3 possible decim_0, (/4. /8 200k soft filter , /8 180k sharp )
const auto decim_1_out = decim_1_execute(decim_0_out, dst_buffer); // selectable 3 possible decim_1, (/8. /2 200k or bypassed /1 )
/* this code was valid when we had only 2 decim1 cases.
const auto decim_1_out = baseband_fs < 4800'000
? decim_1_2.execute(decim_0_out, dst_buffer) // < 600khz double decim. stage , means 500khz and lower bit rates.
// ? decim_1_8.execute(decim_0_out, dst_buffer) // < 600khz double decim. stage , means 500khz and lower bit rates.
: decim_0_out; // >= 600khz single decim. stage , means 600khz and upper bit rates.
} */
const auto& decimator_out = decim_1_out;
const auto& channel = decimator_out;
@@ -66,19 +78,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,17 +91,54 @@ void CaptureProcessor::on_message(const Message* const message) {
}
}
void CaptureProcessor::update_for_rate_change() {
void CaptureProcessor::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);
auto decim_0_factor = oversample_rate == OversampleRate::Rate8x
// Current fw , we are using only 2 decim_0 modes, /4 , /8
auto decim_0_factor = oversample_rate == OversampleRate::x8
? decim_0_4.decimation_factor
: decim_0_8.decimation_factor;
size_t decim_0_output_fs = baseband_fs / decim_0_factor;
size_t decim_1_input_fs = decim_0_output_fs;
size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
size_t decim_1_factor;
switch (oversample_rate) { // we are using 3 decim_1 modes, /1 , /2 , /8
case OversampleRate::x8:
if (baseband_fs < 4800'000) {
decim_1_factor = decim_1_2.decimation_factor; // /8 = /4x2
} else {
decim_1_factor = 2 * 1; // 600khz and onwards, single decim /8 = /8x1 (we applied additional *2 correction to speed up waterfall, no effect to scale spectrum)
}
break;
case OversampleRate::x16:
decim_1_factor = 2 * decim_1_2.decimation_factor; // /16 = /8x2 (we applied additional *2 correction to increase waterfall spped >=600k and smooth & avoid abnormal motion >1M5 )
break;
case OversampleRate::x32:
decim_1_factor = 2 * decim_1_8.decimation_factor; // /32 = /4x8 (we applied additional *2 correction to speed up waterfall, no effect to scale spectrum)
break;
case OversampleRate::x64:
decim_1_factor = 8 * decim_1_8.decimation_factor; // /64 = /8x8 (we applied additional *8 correction to speed up waterfall, no effect to scale spectrum)
break;
default:
decim_1_factor = 2; // just default initial value to remove compile warning.
break;
}
/*
auto decim_1_factor = oversample_rate == OversampleRate::x32 // that was ok, when we had only 2 oversampling x8 , x16
? decim_1_8.decimation_factor
: decim_1_2.decimation_factor;
*/
size_t decim_1_output_fs = decim_1_input_fs / decim_1_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;
@@ -119,11 +158,45 @@ void CaptureProcessor::capture_config(const CaptureConfigMessage& message) {
buffer_c16_t CaptureProcessor::decim_0_execute(const buffer_c8_t& src, const buffer_c16_t& dst) {
switch (oversample_rate) {
case OversampleRate::Rate8x:
return decim_0_4.execute(src, dst);
case OversampleRate::x8: // we can get /8 by two means , decim0 (/4) + decim1 (/2) . or just decim0 (/8)
if (baseband_fs < 4800'000) { // 600khz (600k x 8)
return decim_0_4.execute(src, dst); // decim_0 , /4 with double decim stage
} else {
return decim_0_8_180k.execute(src, dst); // decim_0 /8 with single decim stage
}
case OversampleRate::Rate16x:
return decim_0_8.execute(src, dst);
case OversampleRate::x16:
return decim_0_8.execute(src, dst); // decim_0 , /8 with double decim stage
case OversampleRate::x32:
return decim_0_4.execute(src, dst); // decim_0 , /4 with double decim stage
case OversampleRate::x64:
return decim_0_8.execute(src, dst); // decim_0 , /8 with double decim stage
default:
chDbgPanic("Unhandled OversampleRate");
return {};
}
}
buffer_c16_t CaptureProcessor::decim_1_execute(const buffer_c16_t& src, const buffer_c16_t& dst) {
switch (oversample_rate) {
case OversampleRate::x8: // we can get /8 by two means , decim0 (/4) + decim1 (/2) . or just decim0 (/8)
if (baseband_fs < 4800'000) { // 600khz (600k x 8)
return decim_1_2.execute(src, dst);
} else {
return src;
}
case OversampleRate::x16:
return decim_1_2.execute(src, dst); // total decim /16 = /8x2, applied to 100khz and 150khz
case OversampleRate::x32:
return decim_1_8.execute(src, dst); // total decim /32 = /4x8, appled to 75k , 50k, 32k
case OversampleRate::x64:
return decim_1_8.execute(src, dst); // total decim /64 = /8x8, appled to 16k and 12k5
default:
chDbgPanic("Unhandled OversampleRate");
+9 -5
View File
@@ -42,7 +42,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{};
@@ -56,8 +56,10 @@ class CaptureProcessor : public BasebandProcessor {
* use decim_0_8 for an overall decimation factor of 16. */
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0_4{};
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_8{};
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_8_180k{};
dsp::decimate::FIRC16xR16x16Decim2 decim_1_2{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1_8{};
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 +69,21 @@ class CaptureProcessor : public BasebandProcessor {
SpectrumCollector channel_spectrum{};
size_t spectrum_interval_samples = 0;
size_t spectrum_samples = 0;
OversampleRate oversample_rate{OversampleRate::Rate8x};
OversampleRate oversample_rate{OversampleRate::x8};
/* 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);
/* Dispatch to the correct decim_1 based on oversample rate. */
buffer_c16_t decim_1_execute(const buffer_c16_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{};
+28 -8
View File
@@ -24,12 +24,13 @@
#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
@@ -41,9 +42,13 @@ 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
// Output audio pre-smoothing so squelch actually works.
// NB: It's useful to output *after* when debugging the smoothing filter.
audio_output.write(audio);
// Smooth the data to make decoding more accurate.
smooth.Process(audio.p, audio.count);
processDemodulatedSamples(audio.p, 16);
extractFrames();
}
@@ -71,8 +76,23 @@ 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);
audio_output.configure(
audio_24k_hpf_300hz_config,
audio_8k_deemph_300_6_config,
config->squelch_level / 100.0);
break;
}
default:
break;
}
}
void POCSAGProcessor::configure() {
@@ -90,11 +110,11 @@ void POCSAGProcessor::configure() {
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
decim_1.configure(taps_11k0_decim_1.taps, 131072);
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);
audio_output.configure(false);
// Set up the frame extraction, limits of baud
setFrameExtractParams(demod_input_fs, 4000, 300, 32);
+3 -3
View File
@@ -48,7 +48,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 +106,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
+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.size())
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;
}
+3 -1
View File
@@ -44,6 +44,7 @@ class ReplayProcessor : public BasebandProcessor {
size_t baseband_fs = 3072000;
static constexpr auto spectrum_rate_hz = 50.0f;
// Holds the read IQ data chunk from the file to send.
std::array<complex16_t, 256> iq{};
int32_t channel_filter_low_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{};
+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);
+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;
+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;
+42 -20
View File
@@ -78,7 +78,7 @@ class Message {
ReplayThreadDone = 21,
AFSKRxConfigure = 22,
StatusRefresh = 23,
SamplerateConfig = 24,
SampleRateConfig = 24,
BTLERxConfigure = 25,
NRFRxConfigure = 26,
TXProgress = 27,
@@ -111,7 +111,7 @@ class Message {
APRSRxConfigure = 54,
SpectrumPainterBufferRequestConfigure = 55,
SpectrumPainterBufferResponseConfigure = 56,
OversampleRateConfig = 57,
POCSAGStats = 57,
MAX
};
@@ -341,6 +341,17 @@ class POCSAGPacketMessage : public Message {
pocsag::POCSAGPacket packet;
};
class POCSAGStatsMessage : public Message {
public:
constexpr POCSAGStatsMessage(
uint16_t baud_rate)
: Message{ID::POCSAGStats},
baud_rate_{baud_rate} {
}
uint16_t baud_rate_;
};
class ACARSPacketMessage : public Message {
public:
constexpr ACARSPacketMessage(
@@ -799,32 +810,43 @@ class RetuneMessage : public Message {
uint32_t range = 0;
};
class SamplerateConfigMessage : public Message {
public:
constexpr SamplerateConfigMessage(
const uint32_t sample_rate)
: Message{ID::SamplerateConfig},
sample_rate(sample_rate) {
}
const uint32_t sample_rate = 0;
};
/* Controls decimation handling in proc_capture. */
/* Oversample/Interpolation sample rate multipliers. */
enum class OversampleRate : uint8_t {
Rate8x = 8,
Rate16x = 16,
/* Use either to indicate there's no oversampling needed. */
None = 1,
x1 = None,
// 4x would make sense to have, but need to ensure it doesn't
// overrun the IQ read buffer in proc_replay.
/* Oversample rate of 8 times the sample rate. */
x8 = 8,
/* Oversample rate of 16 times the sample rate. */
x16 = 16,
/* Oversample rate of 32 times the sample rate. */
x32 = 32,
/* Oversample rate of 64 times the sample rate. */
x64 = 64,
/* Oversample rate of 128 times the sample rate. */
x128 = 128,
};
class OversampleRateConfigMessage : public Message {
class SampleRateConfigMessage : public Message {
public:
constexpr OversampleRateConfigMessage(
constexpr SampleRateConfigMessage(
uint32_t sample_rate,
OversampleRate oversample_rate)
: Message{ID::OversampleRateConfig},
: Message{ID::SampleRateConfig},
sample_rate(sample_rate),
oversample_rate(oversample_rate) {
}
const OversampleRate oversample_rate{OversampleRate::Rate8x};
const uint32_t sample_rate = 0;
const OversampleRate oversample_rate = OversampleRate::None;
};
class AudioLevelReportMessage : public Message {
+73
View File
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2023 Kyle Reed, zxkmm
*
* 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.
*/
/* Helpers for handling oversampling and interpolation. */
#ifndef __OVERSAMPLE_H__
#define __OVERSAMPLE_H__
#include "message.hpp"
#include "utility.hpp"
/* TODO:
* The decision to oversample/interpolate should only be a baseband concern.
* However, the baseband can't set up the radio (M0 code), so the apps also
* need to know about the "actual" sample rate so the radio settings can be
* applied correctly. Ideally the baseband would tell the apps what the
* actual sample rate is. Currently the apps are telling the baseband and
* that feels like a separation of concerns problem. */
/* HackRF suggests a minimum sample rate of 2M so a oversample rate is applied
* to the sample rate (pre-scale) to get the sample rate closer to that target.
* The baseband needs to know how to correctly decimate (or interpolate) so
* the set of allowed scalars is fixed (See OversampleRate enum).
* In testing, a minimum rate of 400kHz seems to the functional minimum.
*
* There are several different concepts or terms related to Capture and Replay,
* (1) oversampling (x8, x16 ,...) / decimation (/8, /16...)
* In Capture App , when ADC can not handle directly a requiered low sample rates ,
* we need to apply oversampling (x8. x16 ex) , getting more real samples than needed) by "x_number" ,
* and later to write it to SD card with the proper needed real sample rate , we apply Decimation , "/ number" .
*
* (2) up-sampling or re-escale or interpolation. (x8, x16, ...)
* In Replay-list App, when we got too low bit rate data for Hackrf ,
* we need to upsampling or interpolate or resampling to increase those low bit rates
* to a proper sample rate higher than the min. to be able to be transmitted by Hackrf.
*/
/* Gets the oversample rate for a given sample rate.
* The oversample rate is used to increase the sample rate to improve SNR and quality.
* This is also used as the interpolation rate when replaying captures. */
inline OversampleRate get_oversample_rate(uint32_t sample_rate) {
if (sample_rate < 30'000) return OversampleRate::x64; // 25k, 16k, 12k5.
if (sample_rate < 80'000) return OversampleRate::x32; // 75k, 50k, 32k.
if (sample_rate < 250'000) return OversampleRate::x16; // 100k and 150k.
return OversampleRate::x8; // 250k .. 1Mhz, that decim x8 , is already applied.(OVerSampling and decim OK)
}
/* Gets the actual sample rate for a given sample rate.
* This is the rate with the correct oversampling rate applied. */
inline uint32_t get_actual_sample_rate(uint32_t sample_rate) {
return sample_rate * toUType(get_oversample_rate(sample_rate));
}
#endif /*__OVERSAMPLE_H__*/
+77 -94
View File
@@ -225,27 +225,14 @@ void pocsag_encode(const MessageType type, BCHCode& BCH_code, const uint32_t fun
} while (char_idx < message_size);
}
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
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);
// ----------------------------------------------------------------------------
// EccContainer
// ----------------------------------------------------------------------------
EccContainer::EccContainer() {
setup_ecc();
}
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
static uint32_t ecs[32]; /* error correction sequence */
static uint32_t bch[1025];
static int eccSetup = 0;
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
void setupecc() {
void EccContainer::setup_ecc() {
unsigned int srr = 0x3b4;
unsigned int i, n, j, k;
@@ -308,36 +295,24 @@ void setupecc() {
}
}
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
inline int errorCorrection(uint32_t* val) {
// Set up the tables the first time
if (eccSetup == 0) {
setupecc();
eccSetup = 1;
}
int EccContainer::error_correct(uint32_t& val) {
int i, synd, errl, acc, pari, ecc, b1, b2;
errl = 0;
pari = 0;
/* run through error detection and correction routine */
// for (i=0; i<=20; i++)
ecc = 0;
for (i = 31; i >= 11; --i) {
if ((*val & (1 << i))) {
if (val & (1 << i)) {
ecc = ecc ^ ecs[31 - i];
pari = pari ^ 0x01;
}
}
// for (i=21; i<=30; i++)
acc = 0;
for (i = 10; i >= 1; --i) {
acc = acc << 1;
if ((*val & (1 << i))) {
if (val & (1 << i)) {
acc = acc ^ 0x01;
}
}
@@ -355,12 +330,12 @@ inline int errorCorrection(uint32_t* val) {
b2 = b2 & 0x1f;
if (b2 != 0x1f) {
*val ^= 0x01 << (31 - b2);
val ^= 0x01 << (31 - b2);
ecc = ecc ^ ecs[b2];
}
if (b1 != 0x1f) {
*val ^= 0x01 << (31 - b1);
val ^= 0x01 << (31 - b1);
ecc = ecc ^ ecs[b1];
}
@@ -377,78 +352,86 @@ inline int errorCorrection(uint32_t* val) {
return errl;
}
void pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState* const state) {
int errors = 0;
uint32_t codeword;
char ascii_char;
std::string output_text = "";
bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state) {
constexpr uint8_t codeword_max = 16;
state.output.clear();
state->out_type = EMPTY;
while (state.codeword_index < codeword_max) {
auto codeword = batch[state.codeword_index];
bool is_address = (codeword & 0x80000000U) == 0;
// For each codeword...
for (size_t i = 0; i < 16; i++) {
codeword = batch[i];
// Error correct twice. First time to fix any errors it can,
// second time to count number of errors that couldn't be fixed.
state.ecc->error_correct(codeword);
auto error_count = state.ecc->error_correct(codeword);
errorCorrection(&codeword);
errors = errorCorrection(&codeword);
switch (state.mode) {
case STATE_CLEAR:
if (is_address && codeword != POCSAG_IDLEWORD) {
state.function = (codeword >> 11) & 3;
state.address = (codeword >> 10) & 0x1FFFF8U; // 18 MSBs are transmitted
state.mode = STATE_HAVE_ADDRESS;
state.out_type = ADDRESS;
state.errors = error_count;
if (!(codeword & 0x80000000U)) {
// Address codeword
if (state->mode == STATE_CLEAR) {
// if (codeword != POCSAG_IDLEWORD) {
if (!(bitsDiff(codeword, POCSAG_IDLEWORD) < 1)) {
state->function = (codeword >> 11) & 3;
state->address = (codeword >> 10) & 0x1FFFF8U; // 18 MSBs are transmitted
state->mode = STATE_HAVE_ADDRESS;
state->out_type = ADDRESS;
state->errors = errors;
state->ascii_idx = 0;
state->ascii_data = 0;
state.ascii_idx = 0;
state.ascii_data = 0;
} else if (codeword == POCSAG_IDLEWORD) {
state.out_type = IDLE;
}
} else {
state->mode = STATE_CLEAR; // New address = new message
}
} else {
state->errors += errors;
// Message codeword
if (state->mode == STATE_HAVE_ADDRESS) {
// First message codeword: complete address
state->address |= (i >> 1); // Add in the 3 LSBs (frame #)
state->mode = STATE_GETTING_MSG;
}
break;
state->out_type = MESSAGE;
case STATE_HAVE_ADDRESS:
if (is_address) {
// Got another address, return the current state.
state.mode = STATE_CLEAR;
return true;
}
state->ascii_data |= ((codeword >> 11) & 0xFFFFF); // Get 20 message bits
state->ascii_idx += 20;
// First message codeword, complete the address.
state.address |= (state.codeword_index >> 1); // Add in the 3 LSBs (frame #).
state.mode = STATE_GETTING_MSG;
[[fallthrough]];
// Raw 20 bits to 7 bit reversed ASCII
while (state->ascii_idx >= 7) {
state->ascii_idx -= 7;
ascii_char = ((state->ascii_data) >> (state->ascii_idx)) & 0x7F;
case STATE_GETTING_MSG:
if (is_address) {
// Codeword isn't a message, return the current state.
state.mode = STATE_CLEAR;
return true;
}
// Bottom's up
ascii_char = (ascii_char & 0xF0) >> 4 | (ascii_char & 0x0F) << 4; // 01234567 -> 45670123
ascii_char = (ascii_char & 0xCC) >> 2 | (ascii_char & 0x33) << 2; // 45670123 -> 67452301
ascii_char = (ascii_char & 0xAA) >> 2 | (ascii_char & 0x55); // 67452301 -> *7654321
state.out_type = MESSAGE;
state.errors += error_count;
state.ascii_data |= (codeword >> 11) & 0xFFFFF; // Get 20 message bits.
state.ascii_idx += 20;
// Translate non-printable chars
if ((ascii_char < 32) || (ascii_char > 126)) {
// output_text += "[" + to_string_dec_uint(ascii_char) + "]";
output_text += ".";
} else
output_text += ascii_char;
}
// Raw 20 bits to 7 bit reversed ASCII.
// NB: This is processed MSB first, any remaining bits are shifted
// up so a whole 7 bits are processed with the next codeword.
while (state.ascii_idx >= 7) {
state.ascii_idx -= 7;
char ascii_char = (state.ascii_data >> state.ascii_idx) & 0x7F;
state->ascii_data <<= 20; // Remaining bits are for next time...
// Bottom's up (reverse the bits).
ascii_char = (ascii_char & 0xF0) >> 4 | (ascii_char & 0x0F) << 4; // 01234567 -> 45670123
ascii_char = (ascii_char & 0xCC) >> 2 | (ascii_char & 0x33) << 2; // 45670123 -> 67452301
ascii_char = (ascii_char & 0xAA) >> 2 | (ascii_char & 0x55); // 67452301 -> 76543210
// Translate non-printable chars. TODO: Leave CRLF?
if (ascii_char < 32 || ascii_char > 126)
state.output += ".";
else
state.output += ascii_char;
}
state.ascii_data <<= 20; // Remaining bits are for next iteration...
break;
}
state.codeword_index++;
}
state->output = output_text;
if (state->mode == STATE_HAVE_ADDRESS)
state->mode = STATE_CLEAR;
return false;
}
} /* namespace pocsag */
+36 -9
View File
@@ -35,7 +35,7 @@
namespace pocsag {
// Todo: these enums suck, make a better decode_batch
// TODO: these enums suck, make a better decode_batch
enum Mode : uint32_t {
STATE_CLEAR,
@@ -45,6 +45,7 @@ enum Mode : uint32_t {
enum OutputType : uint32_t {
EMPTY,
IDLE,
ADDRESS,
MESSAGE
};
@@ -55,22 +56,46 @@ enum MessageType : uint32_t {
ALPHANUMERIC
};
/* Holds error correction arrays. This allows the arrays to
* be freed when POCSAG is not running, saving ~4kb of RAM. */
class EccContainer {
public:
EccContainer();
EccContainer(const EccContainer&) = delete;
EccContainer(EccContainer&&) = delete;
EccContainer& operator=(const EccContainer&) = delete;
EccContainer& operator=(EccContainer&&) = delete;
int error_correct(uint32_t& val);
private:
void setup_ecc();
/* error correction sequence */
uint32_t ecs[32];
uint32_t bch[1025];
};
struct POCSAGState {
uint32_t function;
uint32_t address;
EccContainer* ecc = nullptr;
uint8_t codeword_index = 0;
uint32_t function = 0;
uint32_t address = 0;
Mode mode = STATE_CLEAR;
OutputType out_type = EMPTY;
uint32_t ascii_data;
uint32_t ascii_idx;
uint32_t errors;
std::string output;
uint32_t ascii_data = 0;
uint32_t ascii_idx = 0;
uint32_t errors = 0;
std::string output{};
};
const pocsag::BitRate pocsag_bitrates[4] = {
pocsag::BitRate::FSK512,
pocsag::BitRate::FSK1200,
pocsag::BitRate::FSK2400,
pocsag::BitRate::FSK3200};
pocsag::BitRate::FSK3200,
};
std::string bitrate_str(BitRate bitrate);
std::string flag_str(PacketFlag packetflag);
@@ -78,7 +103,9 @@ std::string flag_str(PacketFlag packetflag);
void insert_BCH(BCHCode& BCH_code, uint32_t* codeword);
uint32_t get_digit_code(char code);
void pocsag_encode(const MessageType type, BCHCode& BCH_code, const uint32_t function, const std::string message, const uint32_t address, std::vector<uint32_t>& codewords);
void pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState* const state);
// Returns true if the batch has more to process.
bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state);
} /* namespace pocsag */
@@ -364,6 +364,7 @@ void defaults() {
set_config_backlight_timer(backlight_config_t{});
set_config_splash(true);
set_encoder_dial_sensitivity(DIAL_SENSITIVITY_NORMAL);
set_config_speaker_disable(true); // Disable AK4951 speaker by default (in case of OpenSourceSDRLab H2)
// Default values for recon app.
set_recon_autosave_freqs(false);
+1
View File
@@ -29,6 +29,7 @@
namespace ui {
// CGA palette
// Index into this table should match STR_COLOR_ escape string in ui.hpp
Color term_colors[16] = {
Color::black(),
Color::dark_blue(),
+19
View File
@@ -26,6 +26,25 @@
namespace ui {
// Escape sequences for colored text; second character is index into term_colors[]
#define STR_COLOR_BLACK "\x1B\x00"
#define STR_COLOR_DARK_BLUE "\x1B\x01"
#define STR_COLOR_DARK_GREEN "\x1B\x02"
#define STR_COLOR_DARK_CYAN "\x1B\x03"
#define STR_COLOR_DARK_RED "\x1B\x04"
#define STR_COLOR_DARK_MAGENTA "\x1B\x05"
#define STR_COLOR_DARK_YELLOW "\x1B\x06"
#define STR_COLOR_LIGHT_GREY "\x1B\x07"
#define STR_COLOR_DARK_GREY "\x1B\x08"
#define STR_COLOR_BLUE "\x1B\x09"
#define STR_COLOR_GREEN "\x1B\x0A"
#define STR_COLOR_CYAN "\x1B\x0B"
#define STR_COLOR_RED "\x1B\x0C"
#define STR_COLOR_MAGENTA "\x1B\x0D"
#define STR_COLOR_YELLOW "\x1B\x0E"
#define STR_COLOR_WHITE "\x1B\x0F"
#define STR_COLOR_FOREGROUND "\x1B\x10"
#define DEG_TO_RAD(d) (d * (2 * pi) / 360.0)
using Coord = int16_t;
+2 -2
View File
@@ -57,8 +57,8 @@ int Painter::draw_string(
for (auto c : text) {
if (escape) {
if (c <= 15)
pen = term_colors[c & 15];
if (c < std::size(term_colors))
pen = term_colors[(uint8_t)c];
else
pen = foreground;
escape = false;
+25 -2
View File
@@ -365,13 +365,18 @@ void Text::set(std::string_view value) {
void Text::paint(Painter& painter) {
const auto rect = screen_rect();
auto s = has_focus() ? style().invert() : style();
auto max_len = (unsigned)rect.width() / s.font.char_width();
auto text_view = std::string_view{text};
painter.fill_rectangle(rect, s.background);
if (text_view.length() > max_len)
text_view = text_view.substr(0, max_len);
painter.draw_string(
rect.location(),
s,
text);
text_view);
}
/* Labels ****************************************************************/
@@ -612,7 +617,7 @@ void Console::write(std::string message) {
for (const auto c : message) {
if (escape) {
if (c <= 15)
if (c < std::size(term_colors))
pen_color = term_colors[(uint8_t)c];
else
pen_color = s.foreground;
@@ -1760,6 +1765,24 @@ bool TextField::on_key(KeyEvent key) {
return false;
}
bool TextField::on_encoder(EncoderEvent delta) {
if (on_encoder_change) {
on_encoder_change(*this, delta);
return true;
}
return false;
}
bool TextField::on_touch(TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
return true;
}
return false;
}
/* NumberField ***********************************************************/
NumberField::NumberField(
+7
View File
@@ -208,6 +208,10 @@ class Text : public Widget {
void paint(Painter& painter) override;
protected:
// NB: Don't truncate this string. The UI will only render
// as many characters as will fit in its rectange.
// Apps expect to be able to retrieve this string to avoid
// needing to hold additional copies in memory.
std::string text;
};
@@ -694,6 +698,7 @@ class TextField : public Text {
public:
std::function<void(TextField&)> on_select{};
std::function<void(TextField&)> on_change{};
std::function<void(TextField&, EncoderEvent)> on_encoder_change{};
TextField(Rect parent_rect, std::string text);
@@ -701,6 +706,8 @@ class TextField : public Text {
void set_text(std::string_view value);
bool on_key(KeyEvent key) override;
bool on_encoder(EncoderEvent delta) override;
bool on_touch(TouchEvent event) override;
private:
using Text::set;
+3
View File
@@ -24,6 +24,9 @@
#include <cstdint>
#define STR_DEGREES_C "\xB0\x43" // ºC - 0xB0 is degree ° symbol in our 8x16 font, 0x43 is "C"
#define STR_DEGREES_F "\xB0\x46" // ºF - 0xB0 is degree ° symbol in our 8x16 font, 0x46 is "F"
namespace units {
class Pressure {
@@ -99,4 +99,10 @@ TEST_CASE("It should convert 8-bit.") {
CHECK_EQ(val, 123);
}
TEST_CASE("It should convert negative.") {
int8_t val = 0;
REQUIRE(parse_int("-64", val));
CHECK_EQ(val, -64);
}
TEST_SUITE_END();
@@ -24,12 +24,27 @@
/* TODO: Tests for all string_format functions. */
TEST_CASE("to_string_dec_uint64 returns correct value.") {
CHECK_EQ(to_string_dec_uint64(0), "0");
CHECK_EQ(to_string_dec_uint64(1), "1");
CHECK_EQ(to_string_dec_uint64(1'000'000), "1000000");
CHECK_EQ(to_string_dec_uint64(1'234'567'890), "1234567890");
CHECK_EQ(to_string_dec_uint64(1'234'567'891), "1234567891");
TEST_CASE("to_string_dec_int returns correct value.") {
CHECK_EQ(to_string_dec_int(0), "0");
CHECK_EQ(to_string_dec_int(1), "1");
CHECK_EQ(to_string_dec_int(-1), "-1");
CHECK_EQ(to_string_dec_int(1'000'000), "1000000");
CHECK_EQ(to_string_dec_int(-1'000'000), "-1000000");
CHECK_EQ(to_string_dec_int(1'234'567'890), "1234567890");
CHECK_EQ(to_string_dec_int(-1'234'567'890), "-1234567890");
CHECK_EQ(to_string_dec_int(1'234'567'891), "1234567891");
CHECK_EQ(to_string_dec_int(-1'234'567'891), "-1234567891");
CHECK_EQ(to_string_dec_int(9'876'543'210), "9876543210");
CHECK_EQ(to_string_dec_int(-9'876'543'210), "-9876543210");
}
TEST_CASE("to_string_dec_uint returns correct value.") {
CHECK_EQ(to_string_dec_uint(0), "0");
CHECK_EQ(to_string_dec_uint(1), "1");
CHECK_EQ(to_string_dec_uint(1'000'000), "1000000");
CHECK_EQ(to_string_dec_uint(1'234'567'890), "1234567890");
CHECK_EQ(to_string_dec_uint(1'234'567'891), "1234567891");
CHECK_EQ(to_string_dec_uint(9'876'543'210), "9876543210");
}
TEST_CASE("to_string_freq returns correct value.") {