Compare commits

...

42 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
jLynx e2ad0a1b1a Stable 1.7.4 release (#1340)
* Update version.txt

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

* Slightly wider "123" button and smaller OK button

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

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

* Better shift colors

* Better keybaord layout for symbols

* Start ShiftLocked for consistency with previous UX.

* Fix number layout

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

* Resolve button responsiveness

* Clang

* Clang

* Clang try again

* Removed unnecessary lines

* Address review comments

* Add comments per reviewer suggestion

* Clang test

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

* second_draft_dynamic_decim

* Add support for Oversample Rate to capture.

---------

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

* Removed unsupported RawS32 type

* Clang
2023-07-31 08:02:11 -07:00
90 changed files with 2090 additions and 1228 deletions
+1 -1
View File
@@ -1 +1 @@
v1.7.2
v1.7.3
+1 -1
View File
@@ -1 +1 @@
v1.7.3
v1.7.4
+1 -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 -11
View File
@@ -60,24 +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. */
/* 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 */
auto sampling_rate = 8 * base_rate; // Decimation by 8 done on baseband side.
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;
/* Set up proper anti aliasing BPF bandwith in MAX2837 before ADC sampling according to the new added BW Options. */
auto anti_alias_baseband_bandwidth_filter = filter_bandwidth_for_sampling_rate(sampling_rate);
/* 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. */
waterfall.stop();
record_view.set_sampling_rate(sampling_rate);
receiver_model.set_sampling_rate(sampling_rate);
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
// record_view determines the correct oversampling to apply and returns the actual sample rate.
// 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);
+51 -128
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);
@@ -1251,8 +1173,9 @@ size_t ReconView::change_mode(freqman_index_t new_mod) {
}
if (new_mod != SPEC_MODULATION) {
button_audio_app.set_text("AUDIO");
// TODO: Oversampling.
record_view->set_sampling_rate(recording_sampling_rate);
// reset receiver model to fix bug when going from SPEC to audio, the sound is distorded
// reset receiver model to fix bug when going from SPEC to audio, the sound is distorted
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
} else {
+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 -2
View File
@@ -347,8 +347,8 @@ void spectrum_streaming_stop() {
send_message(&message);
}
void set_sample_rate(const uint32_t sample_rate) {
SamplerateConfigMessage message{sample_rate};
void set_sample_rate(uint32_t sample_rate, OversampleRate oversample_rate) {
SampleRateConfigMessage message{sample_rate, oversample_rate};
send_message(&message);
}
+2 -1
View File
@@ -94,7 +94,8 @@ void shutdown();
void spectrum_streaming_start();
void spectrum_streaming_stop();
void set_sample_rate(const uint32_t sample_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);
+38
View File
@@ -5607,6 +5607,44 @@ static constexpr Bitmap bitmap_icon_speaker_and_headphones_mute{
{16, 16},
bitmap_icon_speaker_and_headphones_mute_data};
static constexpr uint8_t bitmap_icon_shift_data[] = {
0x00,
0x00,
0x80,
0x00,
0xC0,
0x01,
0xE0,
0x03,
0xF0,
0x07,
0xF8,
0x0F,
0xFC,
0x1F,
0xE0,
0x03,
0xE0,
0x03,
0xE0,
0x03,
0x20,
0x02,
0xE0,
0x03,
0x20,
0x02,
0xE0,
0x03,
0x00,
0x00,
0x00,
0x00,
};
static constexpr Bitmap bitmap_icon_shift{
{16, 16},
bitmap_icon_shift_data};
} /* namespace ui */
#endif /*__BITMAP_HPP__*/
+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 */
+13 -10
View File
@@ -74,18 +74,21 @@ options_t freqman_bandwidths[4] = {
},
{
// SPEC -- TODO: these should be indexes.
{"8k5", 8500},
{"11k", 11000},
{"12k5", 12500},
{"16k", 16000},
{"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));
+77 -61
View File
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2023 Mark Thompson
*
* This file is part of PortaPack.
*
@@ -24,9 +25,8 @@
#include "utility.hpp"
uint8_t Debounce::state() {
bool v = !pulse_upon_release_ && (state_ || simulated_pulse_);
if (simulated_pulse_)
simulated_pulse_ = false;
bool v = state_to_report_;
simulated_pulse_ = false;
return v;
}
@@ -52,84 +52,100 @@ bool Debounce::long_press_occurred() {
bool Debounce::feed(const uint8_t bit) {
history_ = (history_ << 1) | (bit & 1);
// "Repeat" handling - simulated button release
if (repeat_ctr_) {
// Make sure the button is still being held continuously
if ((history_ == 0xFF) && !long_press_enabled_) {
// Simulate button press every REPEAT_SUBSEQUENT_DELAY ticks
if (--repeat_ctr_ == 0) {
state_ = !state_;
repeat_ctr_ = REPEAT_SUBSEQUENT_DELAY / 2;
return true;
}
} else {
// It's a real button release; stop simulating
repeat_ctr_ = 0;
}
}
if (state_ == 0) {
// Previous button state was 0 (released);
// Has button been held for DEBOUNCE_COUNT ticks?
if ((history_ & DEBOUNCE_MASK) == DEBOUNCE_MASK) {
// Has button been held for DEBOUNCE_COUNT ticks?
if ((history_ & DEBOUNCE_MASK) == DEBOUNCE_MASK) {
//
// Button is currently pressed;
// Was previous button state 0 (released)?
//
if (state_ == 0) {
//
// Button has been pressed (after filtering glitches), transition 0->1
//
state_ = 1;
held_time_ = 0;
// If long_press_enabled_, state() function masks the button press until it's released
// or until LONG_PRESS_DELAY is reached
if (long_press_enabled_) {
pulse_upon_release_ = true;
state_to_report_ = 0;
return false;
}
state_to_report_ = 1;
return true;
}
} else {
// Previous button state was 1 (pressed);
// Has button been released for DEBOUNCE_COUNT ticks?
if ((history_ & DEBOUNCE_MASK) == 0) {
// Button has been released when long_press_enabled_ and before LONG_PRESS_DELAY was reached;
// "Repeat" handling - simulated button release
if (repeat_ctr_ && !long_press_enabled_) {
// Simulate button press every REPEAT_SUBSEQUENT_DELAY ticks
// (by toggling reported state every 1/2 of the delay time)
if (--repeat_ctr_ == 0) {
state_to_report_ = !state_to_report_;
repeat_ctr_ = REPEAT_SUBSEQUENT_DELAY / 2;
return true;
}
}
// Keep track of how long button has been held
held_time_++;
if (pulse_upon_release_) {
// Button is being held down and long_press support is enabled for this key:
// if LONG_PRESS_DELAY is reached then finally report that switch is pressed and set flag
// indicating it was a LONG press
// (note that repeat_support and long_press support are mutually exclusive)
if (held_time_ >= LONG_PRESS_DELAY) {
pulse_upon_release_ = false;
long_press_occurred_ = true;
state_to_report_ = 1;
return true;
}
} else if (repeat_enabled_ && !long_press_enabled_) {
// Repeat support -- 4 directional buttons only (unless long_press is enabled)
if (held_time_ >= REPEAT_INITIAL_DELAY) {
// Delay reached; trigger repeat code on NEXT tick
repeat_ctr_ = 1;
held_time_ = 0;
}
}
} else if ((history_ & DEBOUNCE_MASK) == 0) { // Has button been released for at least DEBOUNCE_COUNT ticks?
//
// Button is released;
// Was previous button state 1 (pressed)?
//
if (state_ == 1) {
//
// Button has been released (after filtering glitches), transition 1->0
//
state_ = 0;
long_press_occurred_ = false;
// If button released when long_press_enabled_ and before LONG_PRESS_DELAY was reached;
// allow state() function to finally return a single press indication (simulated pulse).
// Note: In long press mode, apps won't see button press until the button is released.
if (pulse_upon_release_) {
// force state() function (called by EventDispatcher) to return simulated press for one cycle
simulated_pulse_ = true;
pulse_upon_release_ = false;
} else {
state_ = 0;
simulated_pulse_ = true;
state_to_report_ = 1;
return true;
}
// Reset long_press_occurred_ flag after button is released
long_press_occurred_ = false;
state_to_report_ = 0;
return true;
}
// Has button been held continuously?
if (history_ == 0xFF) {
held_time_++;
if (pulse_upon_release_) {
// Button is being held down and long_press support is enabled for this key:
// if LONG_PRESS_DELAY is reached then finally report that switch is pressed and set flag
// indicating it was a LONG press
// (note that repeat_support and long_press support are mutually exclusive)
if (held_time_ >= LONG_PRESS_DELAY) {
long_press_occurred_ = true;
simulated_pulse_ = true;
pulse_upon_release_ = false;
held_time_ = 0;
return true;
}
} else if (repeat_enabled_ && !long_press_enabled_) {
// Repeat support -- 4 directional buttons only (unless long_press is enabled)
if (held_time_ == REPEAT_INITIAL_DELAY) {
// Delay reached; trigger repeat code on NEXT tick
repeat_ctr_ = 1;
held_time_ = 0;
}
}
} else {
// Button not continuously pressed; reset counter
held_time_ = 0;
// Reset reported state after application/event has cleared simulated_pulse_
if (state_to_report_ == 1 && !simulated_pulse_) {
state_to_report_ = 0;
return true;
}
} else {
//
// Button is in transition between states;
// Reset counters until button inputs are stable.
//
held_time_ = 0;
repeat_ctr_ = 0;
}
return false;
}
+19 -9
View File
@@ -43,15 +43,25 @@ class Debounce {
bool long_press_occurred();
private:
uint8_t history_{0};
uint8_t state_{0};
bool repeat_enabled_{false};
uint16_t repeat_ctr_{0};
uint16_t held_time_{0};
bool pulse_upon_release_{false};
bool simulated_pulse_{false};
bool long_press_enabled_{false};
bool long_press_occurred_{false};
uint8_t history_{0}; // shift register of last 8 reads from button hardware state bit
uint8_t state_{0}; // actual button hardware state (after debounce logic), 1=pressed
uint8_t state_to_report_{0}; // pseudo button state reported by state() function (may be masked off or simulated presses)
bool repeat_enabled_{false}; // TRUE if this button is enabled to auto-repeat when held down (ignored if long_press_enabled)
uint16_t repeat_ctr_{0}; // used for timing auto-repeat simulated button presses when button is held down and repeat_enabled
uint16_t held_time_{0}; // number of ticks that the button has been held down (compared against REPEAT and LONG_PRESS delays)
bool pulse_upon_release_{false}; // TRUE when button is being held down when long_press_enabled and LONG_PRESS_DELAY hasn't been reached yet
bool simulated_pulse_{false}; // TRUE if a simulated button press is active following a short button press (only when long_press_enabled)
bool long_press_enabled_{false}; // TRUE when button is in long-press mode (takes precedence over the repeat_enabled flag)
bool long_press_occurred_{false}; // TRUE when button is being held down and LONG_PRESS_DELAY has been reached (only when long_press_enabled)
};
#endif /*__DEBOUNCE_H__*/
+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);
+52 -18
View File
@@ -22,11 +22,10 @@
#include "ui_alphanum.hpp"
#include "portapack.hpp"
#include "hackrf_hal.hpp"
#include "portapack.hpp"
#include "portapack_shared_memory.hpp"
#include <algorithm>
#include "utility.hpp"
namespace ui {
@@ -38,6 +37,7 @@ AlphanumView::AlphanumView(
size_t n;
add_children({
&button_shift,
&labels,
&field_raw,
&text_raw_to_char,
@@ -49,6 +49,19 @@ AlphanumView::AlphanumView(
this->on_button(button);
};
button_shift.set_vertical_center(true);
button_shift.on_select = [this]() {
incr(shift_mode);
// Disallow one-time shift in digits/symbols mode
if ((mode == 1) && (shift_mode == ShiftMode::Shift))
shift_mode = ShiftMode::ShiftLock;
else if (shift_mode > ShiftMode::ShiftLock)
shift_mode = ShiftMode::None;
refresh_keys();
};
n = 0;
for (auto& button : buttons) {
button.id = n;
@@ -56,9 +69,9 @@ AlphanumView::AlphanumView(
focused_button = button.id;
};
button.on_select = button_fn;
button.set_parent_rect({static_cast<Coord>((n % 5) * (240 / 5)),
button.set_parent_rect({static_cast<Coord>((n % 5) * (screen_width / 5)),
static_cast<Coord>((n / 5) * 38 + 24),
240 / 5, 38});
screen_width / 5, 38});
add_child(&button);
n++;
}
@@ -78,38 +91,59 @@ AlphanumView::AlphanumView(
char_add(field_raw.value());
};
// make text_raw_to_char widget display the char value from field_raw
field_raw.on_change = [this](auto) {
text_raw_to_char.set(std::string{static_cast<char>(field_raw.value())});
};
}
void AlphanumView::set_mode(const uint32_t new_mode) {
size_t n = 0;
if (new_mode < 3)
void AlphanumView::set_mode(uint32_t new_mode, ShiftMode new_shift_mode) {
if (new_mode < std::size(key_sets))
mode = new_mode;
else
mode = 0;
const char* key_list = key_sets[mode].second;
shift_mode = new_shift_mode;
refresh_keys();
if (mode + 1 < std::size(key_sets))
button_mode.set_text(key_sets[mode + 1].name);
else
button_mode.set_text(key_sets[0].name);
}
void AlphanumView::refresh_keys() {
auto key_list = shift_mode == ShiftMode::None
? key_sets[mode].normal
: key_sets[mode].shifted;
size_t n = 0;
for (auto& button : buttons) {
const std::string label{
key_list[n]};
button.set_text(label);
button.set_text(std::string{key_list[n]});
n++;
}
if (mode < 2)
button_mode.set_text(key_sets[mode + 1].first);
else
button_mode.set_text(key_sets[0].first);
switch (shift_mode) {
case ShiftMode::None:
button_shift.set_color(Color::dark_grey());
break;
case ShiftMode::Shift:
button_shift.set_color(Color::black());
break;
case ShiftMode::ShiftLock:
button_shift.set_color(Color::dark_blue());
break;
}
}
void AlphanumView::on_button(Button& button) {
const auto c = button.text()[0];
char_add(c);
// TODO: Consolidate shift handling.
if (shift_mode == ShiftMode::Shift) {
shift_mode = ShiftMode::None;
refresh_keys();
}
}
bool AlphanumView::on_encoder(const EncoderEvent delta) {
+36 -12
View File
@@ -29,6 +29,10 @@
#include "ui_textentry.hpp"
#include "ui_menu.hpp"
// TODO: Building this as a custom widget instead of using
// all the Button controls would save a considerable amount of RAM.
// The Buttons each have a backing string but these only need one char.
namespace ui {
class AlphanumView : public TextEntryView {
@@ -43,22 +47,42 @@ class AlphanumView : public TextEntryView {
bool on_encoder(const EncoderEvent delta) override;
private:
const char* const keys_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ, ._";
const char* const keys_lower = "abcdefghijklmnopqrstuvwxyz, ._";
const char* const keys_digit = "0123456789!\"#'()*+-/:;=<>@[\\]?";
enum class ShiftMode : uint8_t {
None,
Shift,
ShiftLock,
};
const std::pair<std::string, const char*> key_sets[3] = {
{"ABC", keys_upper},
{"abc", keys_lower},
{"123", keys_digit}};
const char* const keys_lower = "abcdefghijklmnopqrstuvwxyz, .";
const char* const keys_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ, .";
const char* const keys_digit = "1234567890()'`\"+-*/=<>_\\!?, .";
const char* const keys_symbl = "!@#$%^&*()[]'`\"{}|:;<>-_~?, .";
struct key_set_t {
const char* name;
const char* normal;
const char* shifted;
};
const key_set_t key_sets[2] = {
{"abc", keys_lower, keys_upper},
{"123", keys_digit, keys_symbl}};
int16_t focused_button = 0;
uint32_t mode = 0; // Uppercase
uint32_t mode = 0; // Letters.
ShiftMode shift_mode = ShiftMode::None;
void set_mode(const uint32_t new_mode);
void set_mode(uint32_t new_mode, ShiftMode new_shift_mode = ShiftMode::None);
void refresh_keys();
void on_button(Button& button);
std::array<Button, 30> buttons{};
std::array<Button, 29> buttons{};
NewButton button_shift{
{192, 214, screen_width / 5, 38},
{},
&bitmap_icon_shift,
Color::dark_grey()};
Labels labels{
{{1 * 8, 33 * 8}, "Raw:", Color::light_grey()},
@@ -76,11 +100,11 @@ class AlphanumView : public TextEntryView {
"0"};
Button button_delete{
{9 * 8, 33 * 8, 6 * 8, 32},
{9 * 8, 32 * 8 - 3, 7 * 8, 3 * 16 + 3},
"<DEL"};
Button button_mode{
{16 * 8, 33 * 8, 5 * 8, 32},
{16 * 8, 32 * 8 - 3, 6 * 8, 3 * 16 + 3},
""};
};
+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;
}
}
-2
View File
@@ -21,9 +21,7 @@
*/
#include "ui_textentry.hpp"
//#include "portapack_persistent_memory.hpp"
#include "ui_alphanum.hpp"
//#include "ui_handwrite.hpp"
using namespace portapack;
+1 -1
View File
@@ -50,7 +50,7 @@ class TextEntryView : public View {
TextEdit text_input;
Button button_ok{
{22 * 8, 33 * 8, 7 * 8, 32},
{22 * 8, 32 * 8 - 3, 8 * 8, 3 * 16 + 3},
"OK"};
};
+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();
}
+52 -24
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,24 +102,28 @@ void RecordView::focus() {
button_record.focus();
}
void RecordView::set_sampling_rate(const size_t new_sampling_rate) {
/* We are changing "REC" icon background to yellow in BW rec Options >600kHz
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 > 4800000) { // > 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) {
if (sampling_rate != new_sampling_rate) {
stop();
sampling_rate = new_sampling_rate;
baseband::set_sample_rate(sampling_rate);
baseband::set_sample_rate(sampling_rate, oversample_rate);
button_record.hidden(sampling_rate == 0);
text_record_filename.hidden(sampling_rate == 0);
@@ -128,6 +133,24 @@ void RecordView::set_sampling_rate(const size_t new_sampling_rate) {
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
@@ -162,12 +185,13 @@ void RecordView::start() {
rtcGetTime(&RTCD1, &datetime);
// ISO 8601
std::string date_time = to_string_dec_uint(datetime.year(), 4, '0') +
to_string_dec_uint(datetime.month(), 2, '0') +
to_string_dec_uint(datetime.day(), 2, '0') + "T" +
to_string_dec_uint(datetime.hour()) +
to_string_dec_uint(datetime.minute()) +
to_string_dec_uint(datetime.second());
std::string date_time =
to_string_dec_uint(datetime.year(), 4, '0') +
to_string_dec_uint(datetime.month(), 2, '0') +
to_string_dec_uint(datetime.day(), 2, '0') + "T" +
to_string_dec_uint(datetime.hour()) +
to_string_dec_uint(datetime.minute()) +
to_string_dec_uint(datetime.second());
base_path = filename_stem_pattern.string() + "_" + date_time + "_" +
trim(to_string_freq(receiver_model.target_frequency())) + "Hz";
@@ -197,10 +221,8 @@ 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 / 8});
// Not sure why sample_rate is div. 8, but stored value matches rate settings.
const auto metadata_file_error = write_metadata_file(
get_metadata_path(base_path), {receiver_model.target_frequency(), sampling_rate});
if (metadata_file_error.is_valid()) {
handle_error(metadata_file_error.value());
return;
@@ -263,13 +285,19 @@ void RecordView::update_status_display() {
text_record_dropped.set(s);
}
/*if (pitch_rssi_enabled) {
button_pitch_rssi.invert_colors();
}*/
/*
if (pitch_rssi_enabled) {
button_pitch_rssi.invert_colors();
}
*/
if (sampling_rate) {
if (sampling_rate > 0) {
const auto space_info = std::filesystem::space(u"");
const uint32_t bytes_per_second = file_type == FileType::WAV ? (sampling_rate * 2) : (sampling_rate / 8 * 4); // TODO: Why 8/4??
// - Audio is 1 int16_t per sample or '2' bytes per sample.
// - C8 captures 2 (I,Q) int8_t per sample or '2' bytes per sample.
// - C16 captures 2 (I,Q) int16_t per sample or '4' bytes per sample.
const auto bytes_per_sample = file_type == FileType::RawS16 ? 4 : 2;
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;
+9 -4
View File
@@ -42,8 +42,7 @@ class RecordView : public View {
enum FileType {
RawS8 = 1,
RawS16 = 2,
RawS32 = 3,
WAV = 4,
WAV = 3,
};
RecordView(
@@ -57,7 +56,11 @@ class RecordView : public View {
void focus() override;
void set_sampling_rate(const size_t new_sampling_rate);
/* 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; }
@@ -79,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
@@ -90,7 +95,7 @@ class RecordView : public View {
FileType file_type;
const size_t write_size;
const size_t buffer_count;
size_t sampling_rate{0};
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);
+111 -10
View File
@@ -27,8 +27,12 @@
#include "utility.hpp"
CaptureProcessor::CaptureProcessor() {
decim_0.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();
@@ -36,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;
@@ -65,8 +78,8 @@ void CaptureProcessor::on_message(const Message* const message) {
channel_spectrum.on_message(message);
break;
case Message::ID::SamplerateConfig:
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message));
case Message::ID::SampleRateConfig:
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
break;
case Message::ID::CaptureConfig:
@@ -78,14 +91,54 @@ void CaptureProcessor::on_message(const Message* const message) {
}
}
void CaptureProcessor::samplerate_config(const SamplerateConfigMessage& message) {
baseband_fs = message.sample_rate;
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);
size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor;
// 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;
@@ -103,6 +156,54 @@ 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::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::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");
return {};
}
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<CaptureProcessor>()};
event_dispatcher.run();
+19 -4
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{};
@@ -50,8 +50,16 @@ class CaptureProcessor : public BasebandProcessor {
dst.data(),
dst.size()};
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
dsp::decimate::FIRC16xR16x16Decim2 decim_1{};
/* NB: There are two decimation passes: 0 and 1. In pass 0, one of
* the following will be selected based on the oversample rate.
* use decim_0_4 for an overall decimation factor of 8.
* use decim_0_8 for an overall decimation factor of 16. */
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0_4{};
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_8{};
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_8_180k{};
dsp::decimate::FIRC16xR16x16Decim2 decim_1_2{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1_8{};
int32_t channel_filter_low_f = 0;
int32_t channel_filter_high_f = 0;
int32_t channel_filter_transition = 0;
@@ -61,14 +69,21 @@ class CaptureProcessor : public BasebandProcessor {
SpectrumCollector channel_spectrum{};
size_t spectrum_interval_samples = 0;
size_t spectrum_samples = 0;
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{};
void samplerate_config(const SamplerateConfigMessage& message);
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
+64 -28
View File
@@ -41,41 +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;
// 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);
// Because this is actually adding samples, alias
// oversample_rate so the math below is more clear.
const size_t interpolation_factor = toUType(oversample_rate);
bytes_read += bytes_read_this_iteration;
// 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};
// 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};
// 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);
#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);
}
@@ -88,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:
@@ -108,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 -5
View File
@@ -44,11 +44,8 @@ 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{};
const buffer_c16_t iq_buffer{
iq.data(),
iq.size(),
baseband_fs / 8};
int32_t channel_filter_low_f = 0;
int32_t channel_filter_high_f = 0;
@@ -62,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;
+46 -6
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,6 +111,7 @@ class Message {
APRSRxConfigure = 54,
SpectrumPainterBufferRequestConfigure = 55,
SpectrumPainterBufferResponseConfigure = 56,
POCSAGStats = 57,
MAX
};
@@ -340,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(
@@ -798,15 +810,43 @@ class RetuneMessage : public Message {
uint32_t range = 0;
};
class SamplerateConfigMessage : public Message {
/* Oversample/Interpolation sample rate multipliers. */
enum class OversampleRate : uint8_t {
/* 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 SampleRateConfigMessage : public Message {
public:
constexpr SamplerateConfigMessage(
const uint32_t sample_rate)
: Message{ID::SamplerateConfig},
sample_rate(sample_rate) {
constexpr SampleRateConfigMessage(
uint32_t sample_rate,
OversampleRate oversample_rate)
: Message{ID::SampleRateConfig},
sample_rate(sample_rate),
oversample_rate(oversample_rate) {
}
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;
+32 -12
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;
@@ -1188,7 +1193,7 @@ void NewButton::paint(Painter& painter) {
painter.draw_bitmap(
bmp_pos,
*bitmap_,
color_, // Color::green(), //fg,
color_,
bg);
}
@@ -1667,8 +1672,6 @@ void TextEdit::char_delete() {
}
void TextEdit::paint(Painter& painter) {
constexpr int char_width = 8;
auto rect = screen_rect();
auto text_style = has_focus() ? style().invert() : style();
auto offset = 0;
@@ -1677,15 +1680,14 @@ void TextEdit::paint(Painter& painter) {
if (cursor_pos_ >= char_count_)
offset = cursor_pos_ - char_count_ + 1;
// Clear the control.
painter.fill_rectangle(rect, text_style.background);
// Draw the text starting at the offset.
for (uint32_t i = 0; i < char_count_ && i + offset < text_.length(); i++) {
for (uint32_t i = 0; i < char_count_; i++) {
// Using draw_char to blank the rest of the line with spaces produces less flicker.
auto c = (i + offset < text_.length()) ? text_[i + offset] : ' ';
painter.draw_char(
{rect.location().x() + (static_cast<int>(i) * char_width), rect.location().y()},
text_style,
text_[i + offset]);
text_style, c);
}
// Determine cursor position on screen (either the cursor position or the last char).
@@ -1698,7 +1700,7 @@ void TextEdit::paint(Painter& painter) {
painter.draw_char(cursor_point, cursor_style, text_[cursor_pos_]);
// Draw the cursor.
Rect cursor_box{cursor_point, {char_width, 16}};
Rect cursor_box{cursor_point, {char_width, char_height}};
painter.draw_rectangle(cursor_box, cursor_style.background);
}
@@ -1763,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 {
Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

@@ -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.") {