mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-13 18:19:33 +00:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7e1bedcad | |||
| d8930db8af | |||
| 014db9e233 | |||
| 933920edfd | |||
| cf25d85d51 | |||
| 9af1308e29 | |||
| f4496d8f45 | |||
| 966d1c938b | |||
| f537c7896e | |||
| 4a1479957c | |||
| dc9a16c54b | |||
| a476647d70 | |||
| 95a48e5693 | |||
| 09404ca198 | |||
| 564f76b47d | |||
| c6424f1623 | |||
| a4325ac20d | |||
| d8a6422d37 | |||
| cc963c3562 | |||
| 63f99742fc | |||
| a4636d7872 | |||
| bd2ee03e44 | |||
| deeb81c183 | |||
| 12dbf957b3 | |||
| e1cc0b1ad0 | |||
| f079d57fc6 | |||
| 09b9ed642f | |||
| e74a9f3b41 | |||
| ff7a9d10cb | |||
| 5cfd7f1dc2 | |||
| 853ca2ef53 | |||
| cb0a4854f5 | |||
| 1188157a3e | |||
| 2ae639412d | |||
| 37386c29cb | |||
| e2ad0a1b1a | |||
| 96cdb2e9e0 | |||
| 002ef720ee | |||
| 2214533894 | |||
| 06b7a0419e | |||
| d24ff7b3bc | |||
| a24b3ad3de | |||
| 91c6e3fc30 | |||
| 411f6c0a34 | |||
| 0a3aa706ef | |||
| 5ca74db2f9 | |||
| f24523c2f1 | |||
| e7c5a862da | |||
| b27c738b69 | |||
| 8c565bbf15 | |||
| 37aa9c046f | |||
| 195a6224a0 | |||
| ea238f4988 | |||
| 3514a9a608 | |||
| e2bca9aebb | |||
| 6ae164e59b | |||
| e6ad5efbb7 | |||
| 7bd370b5bc | |||
| 828eb67a52 | |||
| c4df2e66be | |||
| 47e95c0c47 | |||
| 3b5890d0aa | |||
| bee2dc17af | |||
| d6b0173e7a |
@@ -29,7 +29,7 @@ def print_nightly_changelog():
|
||||
|
||||
|
||||
def handle_get_request(path, offset=None):
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
headers = {} if token == None else {"Authorization": f"Bearer {token}"}
|
||||
params = {"since": offset}
|
||||
url_base = f"https://api.github.com/repos/{repo_owner}/{repo_name}/"
|
||||
response = requests.get(url_base + path, headers=headers, params=params)
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.7.2
|
||||
v1.7.3
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.7.3
|
||||
v1.7.4
|
||||
|
||||
@@ -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)
|
||||
@@ -179,6 +179,7 @@ set(CPPSRC
|
||||
file.cpp
|
||||
freqman_db.cpp
|
||||
freqman.cpp
|
||||
io_convert.cpp
|
||||
io_file.cpp
|
||||
io_wave.cpp
|
||||
irq_controls.cpp
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ class AMOptionsView : public View {
|
||||
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
6, // number of blanking characters
|
||||
6, // Max option length
|
||||
{
|
||||
// using common messages from freqman.cpp
|
||||
// Using common messages from freqman_ui.cpp
|
||||
}};
|
||||
};
|
||||
|
||||
@@ -65,9 +65,9 @@ class NBFMOptionsView : public View {
|
||||
};
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
4,
|
||||
3, // Max option length
|
||||
{
|
||||
// using common messages from freqman.cpp
|
||||
// Using common messages from freqman_ui.cpp
|
||||
}};
|
||||
|
||||
Text text_squelch{
|
||||
@@ -93,9 +93,9 @@ class WFMOptionsView : public View {
|
||||
};
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
4,
|
||||
4, // Max option length
|
||||
{
|
||||
// using common messages from freqman.cpp
|
||||
// Using common messages from freqman_ui.cpp
|
||||
}};
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ namespace ui {
|
||||
|
||||
CaptureAppView::CaptureAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_capture);
|
||||
|
||||
add_children({
|
||||
@@ -44,43 +43,56 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&option_bandwidth,
|
||||
&option_format,
|
||||
&record_view,
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
/* THE ONE LINE BELOW IS A TEMPORARY KLUDGE WORKAROUND FOR A MYSTERY M4 BASEBAND HANG ISSUE WHEN THE CAPTURE
|
||||
APP IS THE FIRST APP STARTED AFTER POWER-UP, OR THE CAPTURE APP IS RUN AFTER RUNNING REPLAY WITH A HIGH
|
||||
SAMPLE RATE. IT SHOULD NOT BE NECESSARY SINCE sampling_rate IS ALREADY INITIALIZED BY RADIOSTATE BUT
|
||||
APPARENTLY IS ADDING JUST THE RIGHT AMOUNT OF DELAY. ISSUE DOES NOT AFFECT ALL PORTAPACK UNITS.
|
||||
INVESTIGATION IS ONGOING, SEE ISSUE #1283. */
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
/* END MYSTERY HANG WORKAROUND. */
|
||||
|
||||
field_frequency_step.set_by_value(receiver_model.frequency_step());
|
||||
field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
receiver_model.set_frequency_step(v);
|
||||
this->field_frequency.set_step(v);
|
||||
};
|
||||
|
||||
option_bandwidth.on_change = [this](size_t, uint32_t base_rate) {
|
||||
sampling_rate = 8 * base_rate; // Decimation by 8 done on baseband side
|
||||
/* base_rate is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card */
|
||||
waterfall.on_hide();
|
||||
|
||||
/* Set up proper anti aliasing BPF bandwith in MAX2837 before ADC sampling according to the new added BW Options. */
|
||||
auto anti_alias_baseband_bandwidth_filter = filter_bandwidth_for_sampling_rate(sampling_rate);
|
||||
|
||||
record_view.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
|
||||
|
||||
waterfall.on_show();
|
||||
option_format.set_selected_index(0); // Default to C16
|
||||
option_format.on_change = [this](size_t, uint32_t file_type) {
|
||||
record_view.set_file_type((RecordView::FileType)file_type);
|
||||
};
|
||||
|
||||
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
|
||||
option_bandwidth.on_change = [this](size_t, uint32_t bandwidth) {
|
||||
/* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
|
||||
* provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */
|
||||
auto sample_rate = bandwidth;
|
||||
|
||||
/* base_rate (bandwidth) is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */
|
||||
|
||||
waterfall.stop();
|
||||
|
||||
// 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();
|
||||
};
|
||||
|
||||
option_bandwidth.set_selected_index(7); // Preselected default option 500kHz.
|
||||
receiver_model.enable();
|
||||
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);
|
||||
@@ -92,11 +104,6 @@ CaptureAppView::~CaptureAppView() {
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void CaptureAppView::on_hide() {
|
||||
waterfall.on_hide();
|
||||
View::on_hide();
|
||||
}
|
||||
|
||||
void CaptureAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ class CaptureAppView : public View {
|
||||
CaptureAppView(NavigationView& nav);
|
||||
~CaptureAppView();
|
||||
|
||||
void on_hide() override;
|
||||
void focus() override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
@@ -47,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};
|
||||
@@ -54,10 +54,9 @@ class CaptureAppView : public View {
|
||||
"rx_capture", app_settings::Mode::RX,
|
||||
app_settings::Options::UseGlobalTargetFrequency};
|
||||
|
||||
uint32_t sampling_rate = 0;
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "Rate:", Color::light_grey()},
|
||||
{{11 * 8, 1 * 16}, "Format:", Color::light_grey()},
|
||||
};
|
||||
|
||||
RSSI rssi{
|
||||
@@ -87,6 +86,12 @@ class CaptureAppView : public View {
|
||||
5,
|
||||
{}};
|
||||
|
||||
OptionsField option_format{
|
||||
{18 * 8, 1 * 16},
|
||||
3,
|
||||
{{"C16", RecordView::FileType::RawS16},
|
||||
{"C8", RecordView::FileType::RawS8}}};
|
||||
|
||||
RecordView record_view{
|
||||
{0 * 8, 2 * 16, 30 * 8, 1 * 16},
|
||||
u"BBD_????.*",
|
||||
|
||||
@@ -63,17 +63,15 @@ void GpsSimAppView::on_file_changed(const fs::path& new_file_path) {
|
||||
|
||||
if (metadata) {
|
||||
field_frequency.set_value(metadata->center_frequency);
|
||||
sample_rate = metadata->sample_rate;
|
||||
} else {
|
||||
sample_rate = 2600000;
|
||||
transmitter_model.set_sampling_rate(metadata->sample_rate);
|
||||
}
|
||||
|
||||
// UI Fixup.
|
||||
text_sample_rate.set(unit_auto_scale(sample_rate, 3, 1) + "Hz");
|
||||
text_sample_rate.set(unit_auto_scale(transmitter_model.sampling_rate(), 3, 1) + "Hz");
|
||||
progressbar.set_max(file_size);
|
||||
text_filename.set(truncate(file_path.filename().string(), 12));
|
||||
|
||||
auto duration = ms_duration(file_size, sample_rate, 2);
|
||||
auto duration = ms_duration(file_size, transmitter_model.sampling_rate(), 2);
|
||||
text_duration.set(to_string_time_ms(duration));
|
||||
|
||||
button_play.focus();
|
||||
@@ -129,7 +127,6 @@ void GpsSimAppView::start() {
|
||||
});
|
||||
}
|
||||
|
||||
transmitter_model.set_sampling_rate(sample_rate);
|
||||
transmitter_model.enable();
|
||||
}
|
||||
|
||||
@@ -176,6 +173,11 @@ GpsSimAppView::GpsSimAppView(
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
if (!settings_.loaded()) {
|
||||
field_frequency.set_value(initial_target_frequency);
|
||||
transmitter_model.set_sampling_rate(2600000);
|
||||
}
|
||||
|
||||
field_frequency.set_step(5000);
|
||||
|
||||
button_play.on_select = [this](ImageButton&) {
|
||||
|
||||
@@ -54,6 +54,8 @@ class GpsSimAppView : public View {
|
||||
std::string title() const override { return "GPS Sim TX"; };
|
||||
|
||||
private:
|
||||
static constexpr uint32_t initial_target_frequency = 1575420000;
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
3000000 /* bandwidth */,
|
||||
@@ -64,7 +66,6 @@ class GpsSimAppView : public View {
|
||||
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
|
||||
uint32_t sample_rate = 0;
|
||||
int32_t tx_gain{47};
|
||||
bool rf_amp{true}; // aux private var to store temporal, same as Replay App rf_amp user selection.
|
||||
static constexpr uint32_t baseband_bandwidth = 3000000; // filter bandwidth
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "io_convert.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "metadata_file.hpp"
|
||||
#include "portapack.hpp"
|
||||
@@ -75,7 +77,8 @@ void ReplayAppView::on_file_changed(const fs::path& new_file_path) {
|
||||
progressbar.set_max(file_size);
|
||||
text_filename.set(truncate(file_path.filename().string(), 12));
|
||||
|
||||
auto duration = ms_duration(file_size, sample_rate, 4);
|
||||
uint8_t sample_size = capture_file_sample_size(current()->path);
|
||||
auto duration = ms_duration(file_size, sample_rate, sample_size);
|
||||
text_duration.set(to_string_time_ms(duration));
|
||||
|
||||
button_play.focus();
|
||||
@@ -110,7 +113,7 @@ void ReplayAppView::start() {
|
||||
|
||||
std::unique_ptr<stream::Reader> reader;
|
||||
|
||||
auto p = std::make_unique<FileReader>();
|
||||
auto p = std::make_unique<FileConvertReader>();
|
||||
auto open_error = p->open(file_path);
|
||||
if (open_error.is_valid()) {
|
||||
file_error();
|
||||
@@ -121,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),
|
||||
@@ -133,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();
|
||||
|
||||
@@ -191,7 +194,7 @@ ReplayAppView::ReplayAppView(
|
||||
};
|
||||
|
||||
button_open.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".C16");
|
||||
auto open_view = nav.push<FileLoadView>(".C*");
|
||||
open_view->on_changed = [this](fs::path new_file_path) {
|
||||
on_file_changed(new_file_path);
|
||||
};
|
||||
|
||||
@@ -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}};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) +
|
||||
|
||||
@@ -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(), ' ');
|
||||
|
||||
@@ -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(
|
||||
@@ -251,7 +252,7 @@ void ControlsSwitchesWidget::on_show() {
|
||||
|
||||
bool ControlsSwitchesWidget::on_key(const KeyEvent key) {
|
||||
key_event_mask = 1 << toUType(key);
|
||||
long_press_key_event_mask = switch_long_press_occurred((size_t)key) ? key_event_mask : 0;
|
||||
long_press_key_event_mask = key_is_long_pressed(key) ? key_event_mask : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -264,9 +265,9 @@ void ControlsSwitchesWidget::paint(Painter& painter) {
|
||||
{32, 64, 16, 16}, // Down
|
||||
{32, 0, 16, 16}, // Up
|
||||
{32, 32, 16, 16}, // Select
|
||||
{96, 0, 16, 16}, // Dfu
|
||||
{16, 96, 16, 16}, // Encoder phase 0
|
||||
{48, 96, 16, 16}, // Encoder phase 1
|
||||
{96, 0, 16, 16}, // Dfu
|
||||
{96, 64, 16, 16}, // Touch
|
||||
}};
|
||||
|
||||
@@ -283,9 +284,9 @@ void ControlsSwitchesWidget::paint(Painter& painter) {
|
||||
{32 + 1, 64 + 1, 16 - 2, 16 - 2}, // Down
|
||||
{32 + 1, 0 + 1, 16 - 2, 16 - 2}, // Up
|
||||
{32 + 1, 32 + 1, 16 - 2, 16 - 2}, // Select
|
||||
{96 + 1, 0 + 1, 16 - 2, 16 - 2}, // Dfu
|
||||
{16 + 1, 96 + 1, 16 - 2, 16 - 2}, // Encoder phase 0
|
||||
{48 + 1, 96 + 1, 16 - 2, 16 - 2}, // Encoder phase 1
|
||||
{96 + 1, 0 + 1, 16 - 2, 16 - 2}, // Dfu
|
||||
}};
|
||||
|
||||
auto switches_raw = control::debug::switches();
|
||||
@@ -354,13 +355,13 @@ DebugControlsView::DebugControlsView(NavigationView& nav) {
|
||||
});
|
||||
|
||||
button_done.on_select = [&nav](Button&) {
|
||||
switches_long_press_enable(0);
|
||||
set_switches_long_press_config(0);
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
options_switches_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
(void)v;
|
||||
switches_long_press_enable(options_switches_mode.selected_index_value());
|
||||
set_switches_long_press_config(options_switches_mode.selected_index_value());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -403,9 +404,11 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
|
||||
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
|
||||
{"Temperature", ui::Color::dark_cyan(), &bitmap_icon_temperature, [&nav]() { nav.push<TemperatureView>(); }},
|
||||
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
|
||||
{"Touch Test", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugScreenTest>(); }},
|
||||
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
|
||||
{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }},
|
||||
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
|
||||
{"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
|
||||
}
|
||||
@@ -501,6 +504,56 @@ DebugFontsView::DebugFontsView(NavigationView& nav)
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
/* DebugScreenTest ****************************************************/
|
||||
|
||||
DebugScreenTest::DebugScreenTest(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
bool DebugScreenTest::on_key(const KeyEvent key) {
|
||||
Painter painter;
|
||||
switch (key) {
|
||||
case KeyEvent::Select:
|
||||
nav_.pop();
|
||||
break;
|
||||
case KeyEvent::Down:
|
||||
painter.fill_rectangle({0, 0, screen_width, screen_height}, semirand());
|
||||
break;
|
||||
case KeyEvent::Left:
|
||||
pen_color = semirand();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DebugScreenTest::on_encoder(EncoderEvent delta) {
|
||||
pen_size = clip<int32_t>(pen_size + delta, 1, screen_width);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DebugScreenTest::on_touch(const TouchEvent event) {
|
||||
Painter painter;
|
||||
pen_pos = event.point;
|
||||
painter.fill_rectangle({pen_pos.x() - pen_size / 2, pen_pos.y() - pen_size / 2, pen_size, pen_size}, pen_color);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t DebugScreenTest::semirand() {
|
||||
static uint64_t seed{0x0102030405060708};
|
||||
seed = seed * 137;
|
||||
seed = (seed >> 1) | ((seed & 0x01) << 63);
|
||||
return (uint16_t)seed;
|
||||
}
|
||||
|
||||
void DebugScreenTest::paint(Painter& painter) {
|
||||
painter.fill_rectangle({0, 16, screen_width, screen_height - 16}, Color::white());
|
||||
painter.draw_string({10 * 8, screen_height / 2}, Styles::white, "Use Stylus");
|
||||
pen_color = semirand();
|
||||
}
|
||||
|
||||
/* DebugLCRView *******************************************************/
|
||||
|
||||
/*DebugLCRView::DebugLCRView(NavigationView& nav, std::string lcr_string) {
|
||||
|
||||
@@ -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 {
|
||||
@@ -321,6 +321,22 @@ class DebugFontsView : public View {
|
||||
NavigationView& nav_;
|
||||
};
|
||||
|
||||
class DebugScreenTest : public View {
|
||||
public:
|
||||
DebugScreenTest(NavigationView& nav);
|
||||
bool on_key(KeyEvent key) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
bool on_touch(TouchEvent event) override;
|
||||
uint16_t semirand();
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
Point pen_pos{};
|
||||
Color pen_color{0};
|
||||
int16_t pen_size{10};
|
||||
};
|
||||
|
||||
/*class DebugLCRView : public View {
|
||||
public:
|
||||
DebugLCRView(NavigationView& nav, std::string lcrstring);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -39,7 +39,9 @@ namespace fs = std::filesystem;
|
||||
namespace ui {
|
||||
static const fs::path txt_ext{u".TXT"};
|
||||
static const fs::path ppl_ext{u".PPL"};
|
||||
static const fs::path c8_ext{u".C8"};
|
||||
static const fs::path c16_ext{u".C16"};
|
||||
static const fs::path cxx_ext{u".C*"};
|
||||
static const fs::path png_ext{u".PNG"};
|
||||
static const fs::path bmp_ext{u".BMP"};
|
||||
} // namespace ui
|
||||
@@ -78,14 +80,15 @@ fs::path get_partner_file(fs::path path) {
|
||||
return {};
|
||||
auto ext = path.extension();
|
||||
|
||||
if (path_iequal(ext, txt_ext))
|
||||
ext = c16_ext;
|
||||
else if (path_iequal(ext, c16_ext))
|
||||
ext = txt_ext;
|
||||
else
|
||||
if (is_cxx_capture_file(path))
|
||||
path.replace_extension(txt_ext);
|
||||
else if (path_iequal(ext, txt_ext)) {
|
||||
path.replace_extension(c8_ext);
|
||||
if (!fs::file_exists(path))
|
||||
path.replace_extension(c16_ext);
|
||||
} else
|
||||
return {};
|
||||
|
||||
path.replace_extension(ext);
|
||||
return fs::file_exists(path) && !fs::is_directory(path) ? path : fs::path{};
|
||||
}
|
||||
|
||||
@@ -141,6 +144,7 @@ void FileManBaseView::load_directory_contents(const fs::path& dir_path) {
|
||||
current_path = dir_path;
|
||||
entry_list.clear();
|
||||
auto filtering = !extension_filter.empty();
|
||||
bool cxx_file = path_iequal(cxx_ext, extension_filter);
|
||||
|
||||
text_current.set(dir_path.empty() ? "(sd root)" : truncate(dir_path, 24));
|
||||
|
||||
@@ -150,7 +154,7 @@ void FileManBaseView::load_directory_contents(const fs::path& dir_path) {
|
||||
continue;
|
||||
|
||||
if (fs::is_regular_file(entry.status())) {
|
||||
if (!filtering || path_iequal(entry.path().extension(), extension_filter))
|
||||
if (!filtering || path_iequal(entry.path().extension(), extension_filter) || (cxx_file && is_cxx_capture_file(entry.path())))
|
||||
insert_sorted(entry_list, {entry.path(), (uint32_t)entry.size(), false});
|
||||
} else if (fs::is_directory(entry.status())) {
|
||||
insert_sorted(entry_list, {entry.path(), 0, true});
|
||||
@@ -181,7 +185,6 @@ FileManBaseView::FileManBaseView(
|
||||
extension_filter{filter} {
|
||||
add_children({&labels,
|
||||
&text_current,
|
||||
&text_info,
|
||||
&button_exit});
|
||||
|
||||
button_exit.on_select = [this, &nav](Button&) {
|
||||
@@ -246,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) {
|
||||
@@ -274,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);
|
||||
}
|
||||
|
||||
@@ -338,7 +342,6 @@ FileSaveView::FileSaveView(
|
||||
file_{ file }
|
||||
{
|
||||
add_children({
|
||||
&labels,
|
||||
&text_path,
|
||||
&button_edit_path,
|
||||
&text_name,
|
||||
@@ -399,11 +402,17 @@ void FileManagerView::refresh_widgets(const bool v) {
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void FileManagerView::on_rename() {
|
||||
void FileManagerView::on_rename(std::string_view hint) {
|
||||
auto& entry = get_selected_entry();
|
||||
name_buffer = entry.path.filename().string();
|
||||
uint32_t cursor_pos = (uint32_t)name_buffer.length();
|
||||
|
||||
// Append the hint to the filename stem as a rename suggestion.
|
||||
name_buffer = entry.path.stem().string();
|
||||
if (!hint.empty())
|
||||
name_buffer += "_" + std::string{hint};
|
||||
name_buffer += entry.path.extension().string();
|
||||
|
||||
// Set the rename cursor to before the extension to make renaming simpler.
|
||||
uint32_t cursor_pos = (uint32_t)name_buffer.length();
|
||||
if (auto pos = name_buffer.find_last_of(".");
|
||||
pos != name_buffer.npos && !entry.is_directory)
|
||||
cursor_pos = pos;
|
||||
@@ -430,6 +439,11 @@ void FileManagerView::on_rename() {
|
||||
}
|
||||
|
||||
void FileManagerView::on_delete() {
|
||||
if (is_directory(get_selected_full_path()) && !is_empty_directory(get_selected_full_path())) {
|
||||
nav_.display_modal("Delete", "Directory not empty!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = get_selected_entry().path.filename().string();
|
||||
nav_.push<ModalMessageView>(
|
||||
"Delete", "Delete " + name + "\nAre you sure?", YESNO,
|
||||
@@ -497,7 +511,7 @@ bool FileManagerView::handle_file_open() {
|
||||
if (path_iequal(txt_ext, ext)) {
|
||||
nav_.push<TextEditorView>(path);
|
||||
return true;
|
||||
} else if (path_iequal(c16_ext, ext) || path_iequal(ppl_ext, ext)) {
|
||||
} else if (is_cxx_capture_file(path) || path_iequal(ppl_ext, ext)) {
|
||||
// TODO: Enough memory to push?
|
||||
nav_.push<PlaylistView>(path);
|
||||
return true;
|
||||
@@ -531,7 +545,6 @@ FileManagerView::FileManagerView(
|
||||
|
||||
add_children({
|
||||
&menu_view,
|
||||
&labels,
|
||||
&text_date,
|
||||
&button_rename,
|
||||
&button_delete,
|
||||
@@ -541,13 +554,20 @@ FileManagerView::FileManagerView(
|
||||
&button_new_dir,
|
||||
&button_new_file,
|
||||
&button_open_notepad,
|
||||
&button_rename_timestamp,
|
||||
});
|
||||
|
||||
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();
|
||||
@@ -564,7 +584,7 @@ FileManagerView::FileManagerView(
|
||||
|
||||
button_rename.on_select = [this]() {
|
||||
if (selected_is_valid())
|
||||
on_rename();
|
||||
on_rename("");
|
||||
};
|
||||
|
||||
button_delete.on_select = [this]() {
|
||||
@@ -610,6 +630,13 @@ FileManagerView::FileManagerView(
|
||||
} else
|
||||
nav_.display_modal("Open in Notepad", "Can't open that in Notepad.");
|
||||
};
|
||||
|
||||
button_rename_timestamp.on_select = [this]() {
|
||||
if (selected_is_valid() && !get_selected_entry().is_directory) {
|
||||
on_rename(::truncate(to_string_timestamp(rtc_time::now()), 8));
|
||||
} else
|
||||
nav_.display_modal("Timestamp Rename", "Can't rename that.");
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -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"};
|
||||
@@ -207,7 +202,7 @@ class FileManagerView : public FileManBaseView {
|
||||
ClipboardMode clipboard_mode{ClipboardMode::None};
|
||||
|
||||
void refresh_widgets(const bool v);
|
||||
void on_rename();
|
||||
void on_rename(std::string_view hint);
|
||||
void on_delete();
|
||||
void on_paste();
|
||||
void on_new_dir();
|
||||
@@ -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{
|
||||
@@ -272,6 +264,12 @@ class FileManagerView : public FileManBaseView {
|
||||
{},
|
||||
&bitmap_icon_notepad,
|
||||
Color::orange()};
|
||||
|
||||
NewButton button_rename_timestamp{
|
||||
{4 * 8, 34 * 8, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_options_datetime,
|
||||
Color::orange()};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -487,7 +487,7 @@ void FrequencyEditView::populate_bandwidth_options() {
|
||||
|
||||
if (entry_.modulation < std::size(freqman_bandwidths)) {
|
||||
auto& bandwidths = freqman_bandwidths[entry_.modulation];
|
||||
for (auto i = 1u; i < bandwidths.size(); ++i) {
|
||||
for (auto i = 0u; i < bandwidths.size(); ++i) {
|
||||
auto& item = bandwidths[i];
|
||||
options.push_back({item.first, (OptionsField::value_t)i});
|
||||
}
|
||||
@@ -500,7 +500,7 @@ void FrequencyEditView::populate_step_options() {
|
||||
OptionsField::options_t options;
|
||||
options.push_back({"None", -1});
|
||||
|
||||
for (auto i = 1u; i < freqman_steps.size(); ++i) {
|
||||
for (auto i = 0u; i < freqman_steps.size(); ++i) {
|
||||
auto& item = freqman_steps[i];
|
||||
options.push_back({item.first, (OptionsField::value_t)i});
|
||||
}
|
||||
@@ -513,7 +513,7 @@ void FrequencyEditView::populate_tone_options() {
|
||||
OptionsField::options_t options;
|
||||
options.push_back({"None", -1});
|
||||
|
||||
for (auto i = 1u; i < tone_keys.size(); ++i) {
|
||||
for (auto i = 0u; i < tone_keys.size(); ++i) {
|
||||
auto& item = tone_keys[i];
|
||||
options.push_back({fx100_string(item.second), (OptionsField::value_t)i});
|
||||
}
|
||||
|
||||
@@ -219,12 +219,15 @@ class FrequencyEditView : public View {
|
||||
{{0 * 8, 10 * 16}, "Description:", Color::light_grey()},
|
||||
};
|
||||
|
||||
OptionsField field_type{{13 * 8, 3 * 16}, 8, {
|
||||
{"Single", 0},
|
||||
{"Range", 1},
|
||||
{"HamRadio", 2},
|
||||
{"Raw", 3},
|
||||
}};
|
||||
OptionsField field_type{
|
||||
{13 * 8, 3 * 16},
|
||||
8,
|
||||
{
|
||||
{"Single", 0},
|
||||
{"Range", 1},
|
||||
{"HamRadio", 2},
|
||||
{"Raw", 3},
|
||||
}};
|
||||
|
||||
FrequencyField field_freq_a{{13 * 8, 4 * 16}};
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -24,16 +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>
|
||||
|
||||
@@ -48,7 +50,6 @@ namespace fs = std::filesystem;
|
||||
namespace ui {
|
||||
|
||||
// TODO: consolidate extesions into a shared header?
|
||||
static const fs::path c16_ext = u".C16";
|
||||
static const fs::path ppl_ext = u".PPL";
|
||||
|
||||
void PlaylistView::load_file(const fs::path& playlist_path) {
|
||||
@@ -258,24 +259,23 @@ void PlaylistView::send_current_track() {
|
||||
chThdSleepMilliseconds(current()->ms_delay);
|
||||
|
||||
// Open the sample file to send.
|
||||
auto reader = std::make_unique<FileReader>();
|
||||
auto reader = std::make_unique<FileConvertReader>();
|
||||
auto error = reader->open(current()->path);
|
||||
if (error) {
|
||||
show_file_error(current()->path, "Can't open file to send.");
|
||||
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);
|
||||
|
||||
@@ -323,9 +323,10 @@ void PlaylistView::update_ui() {
|
||||
chDbgAssert(!at_end(), "update_ui #1", "current_index_ invalid");
|
||||
|
||||
text_filename.set(current()->path.filename().string());
|
||||
text_sample_rate.set(unit_auto_scale(current()->metadata.sample_rate, 3, 0) + "Hz");
|
||||
text_sample_rate.set(unit_auto_scale(current()->metadata.sample_rate, 3, (current()->metadata.sample_rate > 1000000) ? 2 : 0) + "Hz");
|
||||
|
||||
auto duration = ms_duration(current()->file_size, current()->metadata.sample_rate, 4);
|
||||
uint8_t sample_size = capture_file_sample_size(current()->path);
|
||||
auto duration = ms_duration(current()->file_size, current()->metadata.sample_rate, sample_size);
|
||||
text_duration.set(to_string_time_ms(duration));
|
||||
field_frequency.set_value(current()->metadata.center_frequency);
|
||||
|
||||
@@ -336,7 +337,7 @@ void PlaylistView::update_ui() {
|
||||
|
||||
progressbar_track.set_max(playlist_db_.size() - 1);
|
||||
progressbar_track.set_value(current_index_);
|
||||
progressbar_transmit.set_max(current()->file_size);
|
||||
progressbar_transmit.set_max(current()->file_size * sizeof(complex16_t) / sample_size);
|
||||
}
|
||||
|
||||
button_play.set_bitmap(is_active() ? &bitmap_stop : &bitmap_play);
|
||||
@@ -406,7 +407,7 @@ PlaylistView::PlaylistView(
|
||||
button_add.on_select = [this, &nav]() {
|
||||
if (is_active())
|
||||
return;
|
||||
auto open_view = nav_.push<FileLoadView>(".C16");
|
||||
auto open_view = nav_.push<FileLoadView>(".C*");
|
||||
open_view->push_dir(u"CAPTURES");
|
||||
open_view->on_changed = [this](fs::path path) {
|
||||
add_entry(std::move(path));
|
||||
@@ -459,7 +460,7 @@ PlaylistView::PlaylistView(
|
||||
auto ext = path.extension();
|
||||
if (path_iequal(ext, ppl_ext))
|
||||
on_file_changed(path);
|
||||
else if (path_iequal(ext, c16_ext))
|
||||
else if (is_cxx_capture_file(path))
|
||||
add_entry(fs::path{path});
|
||||
}
|
||||
|
||||
|
||||
@@ -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, recon_match_mode);
|
||||
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
|
||||
@@ -890,10 +813,8 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
if (recon) {
|
||||
if (!timer) {
|
||||
status = 0;
|
||||
continuous_lock = false;
|
||||
freq_lock = 0;
|
||||
timer = local_recon_lock_duration;
|
||||
big_display.set_style(&Styles::white);
|
||||
}
|
||||
if (freq_lock < recon_lock_nb_match) // LOCKING
|
||||
{
|
||||
@@ -907,22 +828,19 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
}
|
||||
if (db > squelch) // MATCHING LEVEL
|
||||
{
|
||||
continuous_lock = true;
|
||||
freq_lock++;
|
||||
timer += time_interval; // give some more time for next lock
|
||||
} else {
|
||||
// continuous, direct cut it if not consecutive match after 1 first match
|
||||
if (recon_match_mode == RECON_MATCH_CONTINUOUS) {
|
||||
if (freq_lock > 0) {
|
||||
timer = 0;
|
||||
continuous_lock = false;
|
||||
}
|
||||
freq_lock = 0;
|
||||
timer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (freq_lock >= recon_lock_nb_match) // LOCKED
|
||||
{
|
||||
if (status != 2) {
|
||||
continuous_lock = false;
|
||||
status = 2;
|
||||
// FREQ IS STRONG: GREEN and recon will pause when on_statistics_update()
|
||||
if ((!scanner_mode) && autosave && frequency_list.size() > 0) {
|
||||
@@ -963,10 +881,9 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
last_timer = timer;
|
||||
text_timer.set("TIMER: " + to_string_dec_int(timer));
|
||||
}
|
||||
if (timer) {
|
||||
if (!continuous_lock || recon_match_mode == RECON_MATCH_SPARSE) {
|
||||
timer -= time_interval;
|
||||
}
|
||||
|
||||
if (timer != 0) {
|
||||
timer -= time_interval;
|
||||
if (timer < 0) {
|
||||
timer = 0;
|
||||
}
|
||||
@@ -1126,7 +1043,6 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
void ReconView::recon_pause() {
|
||||
timer = 0;
|
||||
freq_lock = 0;
|
||||
continuous_lock = false;
|
||||
recon = false;
|
||||
|
||||
if (field_mode.selected_index_value() != SPEC_MODULATION)
|
||||
@@ -1139,7 +1055,6 @@ void ReconView::recon_pause() {
|
||||
void ReconView::recon_resume() {
|
||||
timer = 0;
|
||||
freq_lock = 0;
|
||||
continuous_lock = false;
|
||||
recon = true;
|
||||
|
||||
if (field_mode.selected_index_value() != SPEC_MODULATION)
|
||||
@@ -1241,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);
|
||||
@@ -1258,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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -69,10 +69,11 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
|
||||
|
||||
button_save_freqs.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".TXT");
|
||||
open_view->push_dir(freqman_dir);
|
||||
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
|
||||
if (new_file_path.native().find(freqman_dir.native()) == 0) {
|
||||
_output_file = new_file_path.stem().string();
|
||||
text_input_file.set(_output_file);
|
||||
button_output_file.set_text(_output_file);
|
||||
} else {
|
||||
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,10 +21,13 @@
|
||||
*/
|
||||
|
||||
#include "ui_scanner.hpp"
|
||||
|
||||
#include "optional.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -34,7 +37,7 @@ ScannerThread::ScannerThread(std::vector<rf::Frequency> frequency_list)
|
||||
create_thread();
|
||||
}
|
||||
|
||||
ScannerThread::ScannerThread(const jammer::jammer_range_t& frequency_range, size_t def_step_hz)
|
||||
ScannerThread::ScannerThread(const scanner_range_t& frequency_range, size_t def_step_hz)
|
||||
: frequency_range_(frequency_range), def_step_hz_(def_step_hz) {
|
||||
_manual_search = true;
|
||||
create_thread();
|
||||
@@ -80,7 +83,7 @@ void ScannerThread::set_freq_del(const rf::Frequency v) {
|
||||
}
|
||||
|
||||
// Force a one-time forward or reverse frequency index change; OK to do this without pausing scan thread
|
||||
//(used when rotary encoder is turned)
|
||||
// (used when rotary encoder is turned)
|
||||
void ScannerThread::set_index_stepper(const int32_t v) {
|
||||
_index_stepper = v;
|
||||
}
|
||||
@@ -228,17 +231,37 @@ void ScannerView::handle_retune(int64_t freq, uint32_t freq_idx) {
|
||||
}
|
||||
|
||||
if (!manual_search) {
|
||||
if (frequency_list.size() > 0) {
|
||||
text_current_index.set(to_string_dec_uint(freq_idx + 1, 3));
|
||||
}
|
||||
if (entries.size() > 0)
|
||||
field_current_index.set_text(to_string_dec_uint(freq_idx + 1, 3));
|
||||
|
||||
if (freq_idx < description_list.size() && description_list[freq_idx].size() > 1)
|
||||
desc_current_index.set(description_list[freq_idx]); // Show description from file
|
||||
if (freq_idx < entries.size() && entries[freq_idx].description.size() > 1)
|
||||
text_current_desc.set(entries[freq_idx].description); // Show description from file
|
||||
else
|
||||
desc_current_index.set(desc_freq_list_scan); // Show Scan file name (no description in file)
|
||||
text_current_desc.set(loaded_filename()); // Show Scan file name (no description in file)
|
||||
}
|
||||
}
|
||||
|
||||
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 = freqman_file;
|
||||
if (filename.length() > 23) { // Truncate long file name.
|
||||
filename.resize(22);
|
||||
filename = filename + "+";
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
void ScannerView::focus() {
|
||||
button_load.focus();
|
||||
}
|
||||
@@ -252,48 +275,48 @@ ScannerView::~ScannerView() {
|
||||
}
|
||||
|
||||
void ScannerView::show_max_index() { // show total number of freqs to scan
|
||||
text_current_index.set("---");
|
||||
field_current_index.set_text("---");
|
||||
|
||||
if (frequency_list.size() == FREQMAN_MAX_PER_FILE) {
|
||||
if (entries.size() == FREQMAN_MAX_PER_FILE) {
|
||||
text_max_index.set_style(&Styles::red);
|
||||
text_max_index.set("/ " + to_string_dec_uint(FREQMAN_MAX_PER_FILE) + " (DB MAX!)");
|
||||
} else {
|
||||
text_max_index.set_style(&Styles::grey);
|
||||
text_max_index.set("/ " + to_string_dec_uint(frequency_list.size()));
|
||||
text_max_index.set("/ " + to_string_dec_uint(entries.size()));
|
||||
}
|
||||
}
|
||||
|
||||
ScannerView::ScannerView(
|
||||
NavigationView& nav)
|
||||
: nav_{nav}, loaded_file_name{"SCANNER"} {
|
||||
add_children({&labels,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_rf_amp,
|
||||
&field_volume,
|
||||
&field_bw,
|
||||
&field_squelch,
|
||||
&field_browse_wait,
|
||||
&field_lock_wait,
|
||||
&button_load,
|
||||
&button_clear,
|
||||
&rssi,
|
||||
&text_current_index,
|
||||
&text_max_index,
|
||||
&desc_current_index,
|
||||
&big_display,
|
||||
&button_manual_start,
|
||||
&button_manual_end,
|
||||
&field_mode,
|
||||
&field_step,
|
||||
&button_manual_search,
|
||||
&button_pause,
|
||||
&button_dir,
|
||||
&button_audio_app,
|
||||
&button_mic_app,
|
||||
&button_add,
|
||||
&button_remove
|
||||
|
||||
: nav_{nav} {
|
||||
add_children({
|
||||
&labels,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_rf_amp,
|
||||
&field_volume,
|
||||
&field_bw,
|
||||
&field_squelch,
|
||||
&field_browse_wait,
|
||||
&field_lock_wait,
|
||||
&button_load,
|
||||
&button_clear,
|
||||
&rssi,
|
||||
&field_current_index,
|
||||
&text_max_index,
|
||||
&text_current_desc,
|
||||
&big_display,
|
||||
&button_manual_start,
|
||||
&button_manual_end,
|
||||
&field_mode,
|
||||
&field_step,
|
||||
&button_manual_search,
|
||||
&button_pause,
|
||||
&button_dir,
|
||||
&button_audio_app,
|
||||
&button_mic_app,
|
||||
&button_add,
|
||||
&button_remove,
|
||||
});
|
||||
|
||||
// Populate option text for these fields
|
||||
@@ -305,44 +328,39 @@ 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 txt files from the FREQMAN folder
|
||||
// Button to load a Freqman file.
|
||||
button_load.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".TXT");
|
||||
open_view->push_dir(freqman_dir);
|
||||
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
|
||||
if (new_file_path.native().find(freqman_dir.native()) == 0) {
|
||||
scan_pause();
|
||||
frequency_file_load(new_file_path.stem().string(), true);
|
||||
frequency_file_load(new_file_path);
|
||||
} else {
|
||||
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Button to clear in-memory frequency list
|
||||
// Button to clear in-memory frequency list.
|
||||
button_clear.on_select = [this, &nav](Button&) {
|
||||
if (scan_thread && frequency_list.size()) {
|
||||
if (scan_thread && entries.size()) {
|
||||
scan_thread->stop(); // STOP SCANNER THREAD
|
||||
frequency_list.clear();
|
||||
description_list.clear();
|
||||
entries.clear();
|
||||
|
||||
show_max_index(); // UPDATE new list size on screen
|
||||
text_current_index.set("");
|
||||
desc_current_index.set(desc_freq_list_scan);
|
||||
field_current_index.set_text("");
|
||||
text_current_desc.set(loaded_filename());
|
||||
scan_thread->set_freq_lock(0); // Reset the scanner lock
|
||||
|
||||
// FUTURE: Consider switching to manual search mode automatically after clear (but would need to validate freq range)
|
||||
}
|
||||
};
|
||||
|
||||
// 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) {
|
||||
@@ -351,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) {
|
||||
@@ -360,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();
|
||||
@@ -371,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&) {
|
||||
@@ -404,16 +417,15 @@ ScannerView::ScannerView(
|
||||
|
||||
// Button to delete current frequency from scan Freq List
|
||||
button_remove.on_select = [this](Button&) {
|
||||
if (scan_thread && (frequency_list.size() > current_index)) {
|
||||
if (scan_thread && (entries.size() > current_index)) {
|
||||
scan_thread->set_scanning(false); // PAUSE Scanning if necessary
|
||||
|
||||
// Remove frequency from the Freq List in memory (it is not removed from the file)
|
||||
scan_thread->set_freq_del(frequency_list[current_index]);
|
||||
description_list.erase(description_list.begin() + current_index);
|
||||
frequency_list.erase(frequency_list.begin() + current_index);
|
||||
// Remove frequency from the Freq List in memory (it is not removed from the file).
|
||||
scan_thread->set_freq_del(entries[current_index].freq);
|
||||
entries.erase(entries.begin() + current_index);
|
||||
|
||||
show_max_index(); // UPDATE new list size on screen
|
||||
desc_current_index.set(""); // Clean up description (cosmetic detail)
|
||||
text_current_desc.set(""); // Clean up description (cosmetic detail)
|
||||
scan_thread->set_freq_lock(0); // Reset the scanner lock
|
||||
}
|
||||
};
|
||||
@@ -432,15 +444,7 @@ ScannerView::ScannerView(
|
||||
manual_search = false; // Switch to List Scan mode
|
||||
}
|
||||
|
||||
audio::output::stop();
|
||||
|
||||
if (scan_thread)
|
||||
scan_thread->stop(); // STOP SCANNER THREAD
|
||||
|
||||
if (userpause) // If user-paused, resume
|
||||
user_resume();
|
||||
|
||||
start_scan_thread(); // RESTART SCANNER THREAD in selected mode
|
||||
restart_scan();
|
||||
};
|
||||
|
||||
// Mode field was changed (AM/NFM/WFM)
|
||||
@@ -467,17 +471,8 @@ ScannerView::ScannerView(
|
||||
field_step.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
receiver_model.set_frequency_step(v);
|
||||
|
||||
if (manual_search && scan_thread) {
|
||||
// Restart scan thread with new step value
|
||||
scan_thread->stop(); // STOP SCANNER THREAD
|
||||
|
||||
// Resuming pause automatically
|
||||
// FUTURE: Consider whether we should stay in Pause mode...
|
||||
if (userpause) // If user-paused, resume
|
||||
user_resume();
|
||||
|
||||
start_scan_thread(); // RESTART SCANNER THREAD in Manual Search mode
|
||||
}
|
||||
if (manual_search && scan_thread)
|
||||
restart_scan();
|
||||
};
|
||||
|
||||
// Button to toggle Forward/Reverse
|
||||
@@ -491,181 +486,123 @@ ScannerView::ScannerView(
|
||||
bigdisplay_update(BDC_GREY); // Back to grey color
|
||||
};
|
||||
|
||||
// TODO: remove this parsing?
|
||||
// Button to add current frequency (found during Search) to the Scan Frequency List
|
||||
button_add.on_select = [this](Button&) {
|
||||
File scanner_file;
|
||||
const std::string freq_file_path = "FREQMAN/" + loaded_file_name + ".TXT";
|
||||
auto result = scanner_file.open(freq_file_path); // First search if freq is already in txt
|
||||
FreqmanDB db;
|
||||
if (db.open(get_freqman_path(freqman_file), /*create*/ true)) {
|
||||
freqman_entry entry{
|
||||
.frequency_a = current_frequency,
|
||||
.type = freqman_type::Single,
|
||||
};
|
||||
|
||||
if (!result.is_valid()) {
|
||||
const std::string frequency_to_add = "f=" + to_string_dec_uint(current_frequency / 1000) + to_string_dec_uint(current_frequency % 1000UL, 3, '0');
|
||||
|
||||
bool found = false;
|
||||
constexpr size_t buffer_size = 1024;
|
||||
char buffer[buffer_size];
|
||||
|
||||
for (size_t pointer = 0, freq_str_idx = 0; pointer < scanner_file.size(); pointer += buffer_size) {
|
||||
size_t adjusted_buffer_size;
|
||||
if (pointer + buffer_size >= scanner_file.size()) {
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
adjusted_buffer_size = scanner_file.size() - pointer;
|
||||
} else {
|
||||
adjusted_buffer_size = buffer_size;
|
||||
}
|
||||
|
||||
scanner_file.seek(pointer);
|
||||
scanner_file.read(buffer, adjusted_buffer_size);
|
||||
|
||||
for (size_t i = 0; i < adjusted_buffer_size; ++i) {
|
||||
if (buffer[i] == frequency_to_add.data()[freq_str_idx]) {
|
||||
++freq_str_idx;
|
||||
if (freq_str_idx >= frequency_to_add.size()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
freq_str_idx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Look for existing entry with same frequency.
|
||||
auto it = db.find_entry([&entry](const auto& e) {
|
||||
return e.frequency_a == entry.frequency_a;
|
||||
});
|
||||
auto found = (it != db.end());
|
||||
|
||||
if (found) {
|
||||
nav_.display_modal("Error", "Frequency already exists");
|
||||
bigdisplay_update(-1); // After showing an error
|
||||
bigdisplay_update(-1); // Need to poke this control after displaying modal?
|
||||
} else {
|
||||
scanner_file.append(freq_file_path); // Second: append if it is not there
|
||||
scanner_file.write_line(frequency_to_add);
|
||||
|
||||
db.append_entry(entry);
|
||||
// Add to frequency_list in memory too, since we can now switch back from manual mode
|
||||
// Note that we are allowing freqs to be added to file (code above) that exceed the max count we can load into memory
|
||||
if (frequency_list.size() < FREQMAN_MAX_PER_FILE) {
|
||||
frequency_list.push_back(current_frequency);
|
||||
description_list.push_back("");
|
||||
|
||||
// Note that we are allowing freqs to be added to file (code above) that exceed the
|
||||
// max count we can load into memory.
|
||||
if (entries.size() < FREQMAN_MAX_PER_FILE) {
|
||||
entries.push_back({current_frequency, ""});
|
||||
show_max_index(); // Display updated frequency list size
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nav_.display_modal("Error", "Cannot open " + loaded_file_name + ".TXT\nfor appending freq.");
|
||||
bigdisplay_update(-1); // After showing an error
|
||||
nav_.display_modal("Error", "Cannot open " + 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(-10);
|
||||
field_squelch.set_value(squelch);
|
||||
|
||||
// LEARN FREQUENCIES
|
||||
std::string scanner_txt = "SCANNER";
|
||||
frequency_file_load(scanner_txt);
|
||||
// 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(get_freqman_path(freqman_file));
|
||||
}
|
||||
|
||||
void ScannerView::frequency_file_load(std::string file_name, bool stop_all_before) {
|
||||
bool found_range{false};
|
||||
bool found_single{false};
|
||||
void ScannerView::frequency_file_load(const fs::path& path) {
|
||||
freqman_index_t def_mod_index{freqman_invalid_index};
|
||||
freqman_index_t def_bw_index{freqman_invalid_index};
|
||||
freqman_index_t def_step_index{freqman_invalid_index};
|
||||
|
||||
// stop everything running now if required
|
||||
if (stop_all_before) {
|
||||
scan_thread->stop();
|
||||
frequency_list.clear(); // clear the existing frequency list (expected behavior)
|
||||
description_list.clear();
|
||||
FreqmanDB db;
|
||||
if (!db.open(path)) {
|
||||
text_current_desc.set("NO " + path.filename().string());
|
||||
return;
|
||||
}
|
||||
|
||||
if (load_freqman_file(file_name, database, {})) {
|
||||
loaded_file_name = file_name;
|
||||
for (auto& entry_ptr : database) {
|
||||
if (frequency_list.size() >= FREQMAN_MAX_PER_FILE)
|
||||
entries.clear();
|
||||
freqman_file = path.stem().string();
|
||||
Optional<scanner_range_t> range;
|
||||
|
||||
for (auto entry : db) {
|
||||
if (is_invalid(def_mod_index))
|
||||
def_mod_index = entry.modulation;
|
||||
|
||||
if (is_invalid(def_bw_index))
|
||||
def_bw_index = entry.bandwidth;
|
||||
|
||||
if (is_invalid(def_step_index))
|
||||
def_step_index = entry.step;
|
||||
|
||||
switch (entry.type) {
|
||||
case freqman_type::Single:
|
||||
entries.push_back({entry.frequency_a, entry.description});
|
||||
break;
|
||||
case freqman_type::HamRadio:
|
||||
entries.push_back({entry.frequency_a, "R: " + entry.description});
|
||||
entries.push_back({entry.frequency_b, "T: " + entry.description});
|
||||
break;
|
||||
case freqman_type::Range:
|
||||
// NB: Only the first range will be loaded.
|
||||
if (!range)
|
||||
range = {entry.frequency_a, entry.frequency_b};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
auto& entry = *entry_ptr;
|
||||
|
||||
// Get modulation & bw & step from file if specified
|
||||
// Note these values could be different for each line in the file, but we only look at the first one
|
||||
//
|
||||
// Note that freqman requires a very specific string for these parameters,
|
||||
// so check syntax in frequency file if specified value isn't being loaded
|
||||
//
|
||||
if (is_invalid(def_mod_index))
|
||||
def_mod_index = entry.modulation;
|
||||
|
||||
if (is_invalid(def_bw_index))
|
||||
def_bw_index = entry.bandwidth;
|
||||
|
||||
if (is_invalid(def_step_index))
|
||||
def_step_index = entry.step;
|
||||
|
||||
// Get frequency
|
||||
if (entry.type == freqman_type::Range) {
|
||||
if (!found_range) {
|
||||
// Set Start & End Search Range instead of populating the small in-memory frequency table
|
||||
// NOTE: There may be multiple single frequencies in file, but only one search range is supported.
|
||||
found_range = true;
|
||||
frequency_range.min = entry.frequency_a;
|
||||
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
|
||||
frequency_range.max = entry.frequency_b;
|
||||
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
|
||||
}
|
||||
} else if (entry.type == freqman_type::Single) {
|
||||
found_single = true;
|
||||
frequency_list.push_back(entry.frequency_a);
|
||||
description_list.push_back(entry.description);
|
||||
} else if (entry.type == freqman_type::HamRadio) {
|
||||
// For HAM repeaters, add both receive & transmit frequencies to scan list and modify description
|
||||
// (FUTURE fw versions might handle these differently)
|
||||
found_single = true;
|
||||
frequency_list.push_back(entry.frequency_a);
|
||||
description_list.push_back("R:" + entry.description);
|
||||
|
||||
if ((entry.frequency_a != entry.frequency_b) &&
|
||||
(frequency_list.size() < FREQMAN_MAX_PER_FILE)) {
|
||||
frequency_list.push_back(entry.frequency_b);
|
||||
description_list.push_back("T:" + entry.description);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loaded_file_name = "SCANNER"; // back to the default frequency file
|
||||
desc_current_index.set(" NO " + file_name + ".TXT FILE ...");
|
||||
}
|
||||
|
||||
desc_freq_list_scan = loaded_file_name + ".TXT";
|
||||
if (desc_freq_list_scan.length() > 23) { // Truncate description and add ellipses if long file name
|
||||
desc_freq_list_scan.resize(20);
|
||||
desc_freq_list_scan = desc_freq_list_scan + "...";
|
||||
if (entries.size() >= FREQMAN_MAX_PER_FILE)
|
||||
break;
|
||||
}
|
||||
|
||||
if (is_valid(def_mod_index) && def_mod_index != (freqman_index_t)field_mode.selected_index_value())
|
||||
field_mode.set_by_value(def_mod_index); // Update mode (also triggers a change callback that disables & reenables RF background)
|
||||
field_mode.set_by_value(def_mod_index);
|
||||
|
||||
if (is_valid(def_bw_index)) // Update BW if specified in file
|
||||
if (is_valid(def_bw_index))
|
||||
field_bw.set_selected_index(def_bw_index);
|
||||
|
||||
if (is_valid(def_step_index)) // Update step if specified in file
|
||||
if (is_valid(def_step_index))
|
||||
field_step.set_selected_index(def_step_index);
|
||||
|
||||
audio::output::stop();
|
||||
// Found range, set it and update UI.
|
||||
if (range) {
|
||||
frequency_range = *range;
|
||||
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
|
||||
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
|
||||
}
|
||||
|
||||
if (userpause) // If user-paused, resume
|
||||
user_resume();
|
||||
|
||||
// Scan list if we found one, otherwise do manual range search
|
||||
manual_search = !found_single;
|
||||
|
||||
start_scan_thread();
|
||||
// Scan entries if any, otherwise do manual range search.
|
||||
manual_search = entries.empty();
|
||||
restart_scan();
|
||||
}
|
||||
|
||||
void ScannerView::update_squelch_while_paused(int32_t max_db) {
|
||||
@@ -754,7 +691,8 @@ void ScannerView::user_resume() {
|
||||
userpause = false; // Resume scanning
|
||||
}
|
||||
|
||||
void ScannerView::change_mode(freqman_index_t new_mod) { // Before this, do a scan_thread->stop(); After this do a start_scan_thread()
|
||||
// Before this, do a scan_thread->stop(); After this do a start_scan_thread()
|
||||
void ScannerView::change_mode(freqman_index_t new_mod) {
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
options_t bw;
|
||||
@@ -791,22 +729,38 @@ void ScannerView::change_mode(freqman_index_t new_mod) { // Before this, do a s
|
||||
|
||||
void ScannerView::start_scan_thread() {
|
||||
receiver_model.enable();
|
||||
receiver_model.set_squelch_level(0);
|
||||
show_max_index();
|
||||
|
||||
// Start Scanner Thread
|
||||
// FUTURE: Consider passing additional control flags (fwd,userpause,etc) to scanner thread at start (perhaps in a data structure)
|
||||
if (manual_search) {
|
||||
button_manual_search.set_text("SCAN"); // Update meaning of Manual Scan button
|
||||
desc_current_index.set(desc_freq_range_search);
|
||||
text_current_desc.set("SEARCHING...");
|
||||
scan_thread = std::make_unique<ScannerThread>(frequency_range, field_step.selected_index_value());
|
||||
} else {
|
||||
button_manual_search.set_text("SRCH"); // Update meaning of Manual Scan button
|
||||
desc_current_index.set(desc_freq_list_scan);
|
||||
scan_thread = std::make_unique<ScannerThread>(frequency_list);
|
||||
text_current_desc.set(loaded_filename());
|
||||
|
||||
// TODO: just pass ref to the thread?
|
||||
std::vector<rf::Frequency> frequency_list;
|
||||
frequency_list.reserve(entries.size());
|
||||
for (const auto& entry : entries)
|
||||
frequency_list.push_back(entry.freq);
|
||||
|
||||
scan_thread = std::make_unique<ScannerThread>(std::move(frequency_list));
|
||||
}
|
||||
|
||||
scan_thread->set_scanning_direction(fwd);
|
||||
}
|
||||
|
||||
void ScannerView::restart_scan() {
|
||||
audio::output::stop();
|
||||
if (scan_thread) // STOP SCANNER THREAD
|
||||
scan_thread->stop();
|
||||
|
||||
if (userpause) // If user-paused, resume
|
||||
user_resume();
|
||||
|
||||
start_scan_thread(); // RESTART SCANNER THREAD in selected mode
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -20,30 +20,49 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "file.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "freqman_db.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_mictx.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "ui_mictx.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "file.hpp"
|
||||
|
||||
#define SCANNER_SLEEP_MS 50 // ms that Scanner Thread sleeps per loop
|
||||
#define STATISTICS_UPDATES_PER_SEC 10
|
||||
#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 {
|
||||
|
||||
// TODO: There is too much duplicated data in these classes.
|
||||
// ScannerThread should just use more from the View.
|
||||
// Or perhaps ScannerThread should just be in the View.
|
||||
|
||||
// TODO: Too many functions mix work and UI update.
|
||||
// Consolidate UI fixup to a single function.
|
||||
|
||||
// TODO: Just use freqman_entry.
|
||||
struct scanner_entry_t {
|
||||
rf::Frequency freq;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
struct scanner_range_t {
|
||||
int64_t min;
|
||||
int64_t max;
|
||||
};
|
||||
|
||||
class ScannerThread {
|
||||
public:
|
||||
ScannerThread(std::vector<rf::Frequency> frequency_list);
|
||||
ScannerThread(const jammer::jammer_range_t& frequency_range, size_t def_step_hz);
|
||||
ScannerThread(const scanner_range_t& frequency_range, size_t def_step_hz);
|
||||
~ScannerThread();
|
||||
|
||||
void set_scanning(const bool v);
|
||||
@@ -65,7 +84,7 @@ class ScannerThread {
|
||||
|
||||
private:
|
||||
std::vector<rf::Frequency> frequency_list_{};
|
||||
jammer::jammer_range_t frequency_range_{false, 0, 0};
|
||||
scanner_range_t frequency_range_{0, 0};
|
||||
size_t def_step_hz_{0};
|
||||
Thread* thread{nullptr};
|
||||
|
||||
@@ -89,43 +108,57 @@ class ScannerView : public View {
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Scanner"; };
|
||||
std::vector<rf::Frequency> frequency_list{};
|
||||
std::vector<std::string> description_list{};
|
||||
|
||||
// void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
private:
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_scanner", app_settings::Mode::RX};
|
||||
static constexpr const char* default_freqman_file = "SCANNER";
|
||||
|
||||
NavigationView& nav_;
|
||||
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"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_;
|
||||
|
||||
void start_scan_thread();
|
||||
void restart_scan();
|
||||
void change_mode(freqman_index_t mod_type);
|
||||
void show_max_index();
|
||||
void scan_pause();
|
||||
void scan_resume();
|
||||
void user_resume();
|
||||
void frequency_file_load(std::string file_name, bool stop_all_before = false);
|
||||
void frequency_file_load(const std::filesystem::path& path);
|
||||
void bigdisplay_update(int32_t);
|
||||
void update_squelch_while_paused(int32_t max_db);
|
||||
void on_statistics_update(const ChannelStatistics& statistics);
|
||||
void handle_retune(int64_t freq, uint32_t freq_idx);
|
||||
void handle_encoder(EncoderEvent delta);
|
||||
std::string loaded_filename() const;
|
||||
|
||||
jammer::jammer_range_t frequency_range{false, 0, 0}; // perfect for manual scan task too...
|
||||
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};
|
||||
freqman_db database{};
|
||||
std::string loaded_file_name;
|
||||
|
||||
std::vector<scanner_entry_t> entries{};
|
||||
uint32_t current_index{0};
|
||||
rf::Frequency current_frequency{0};
|
||||
|
||||
bool userpause{false};
|
||||
bool manual_search{false};
|
||||
bool fwd{true}; // to preserve direction setting even if scan_thread restarted
|
||||
@@ -137,9 +170,6 @@ class ScannerView : public View {
|
||||
BDC_RED
|
||||
};
|
||||
|
||||
std::string desc_freq_range_search = "SEARCHING...";
|
||||
std::string desc_freq_list_scan = "";
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 16}, "LNA: VGA: AMP: VOL:", Color::light_grey()},
|
||||
{{0 * 8, 1 * 16}, "BW: SQ: Wsa: Wsl:", Color::light_grey()},
|
||||
@@ -196,21 +226,22 @@ 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{
|
||||
{4 * 8, 3 * 16, 18 * 8, 16},
|
||||
};
|
||||
|
||||
Text desc_current_index{
|
||||
Text text_current_desc{
|
||||
{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},
|
||||
|
||||
@@ -480,12 +480,10 @@ class SetEncoderDialView : public View {
|
||||
|
||||
OptionsField field_encoder_dial_sensitivity{
|
||||
{20 * 8, 3 * 16},
|
||||
7,
|
||||
{{"LOWEST", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOWEST},
|
||||
{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
|
||||
6,
|
||||
{{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
|
||||
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_NORMAL},
|
||||
{"HIGH", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGH},
|
||||
{"HIGHEST", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGHEST}}};
|
||||
{"HIGH", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGH}}};
|
||||
|
||||
Button button_save{
|
||||
{2 * 8, 16 * 16, 12 * 8, 32},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -72,6 +72,7 @@ void TextViewer::paint(Painter& painter) {
|
||||
paint_state_.first_line = first_line;
|
||||
paint_state_.first_col = first_col;
|
||||
paint_state_.redraw_text = true;
|
||||
paint_state_.line = UINT32_MAX; // forget old cursor position when overwritten
|
||||
}
|
||||
|
||||
if (paint_state_.redraw_text) {
|
||||
@@ -221,6 +222,10 @@ void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) {
|
||||
r.top() + (int)i * char_height,
|
||||
clear_width * char_width, char_height},
|
||||
style().background);
|
||||
|
||||
// if cursor line got overwritten, disable XOR of old cursor when displaying new cursor
|
||||
if (i == paint_state_.line)
|
||||
paint_state_.line = UINT32_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,21 +233,32 @@ void TextViewer::paint_cursor(Painter& painter) {
|
||||
if (!has_focus())
|
||||
return;
|
||||
|
||||
auto draw_cursor = [this, &painter](uint32_t line, uint16_t col, Color c) {
|
||||
auto r = screen_rect();
|
||||
line = line - paint_state_.first_line;
|
||||
col = col - paint_state_.first_col;
|
||||
auto xor_cursor = [this, &painter](int32_t line, uint16_t col) {
|
||||
int cursor_width = char_width + 1;
|
||||
int x = (col - paint_state_.first_col) * char_width - 1;
|
||||
if (x < 0) { // cursor is one pixel narrower when in left column
|
||||
cursor_width--;
|
||||
x = 0;
|
||||
}
|
||||
int y = screen_rect().top() + (line - paint_state_.first_line) * char_height;
|
||||
|
||||
painter.draw_rectangle(
|
||||
{(int)col * char_width - 1,
|
||||
r.top() + (int)line * char_height,
|
||||
char_width + 1, char_height},
|
||||
c);
|
||||
// Converting one row at a time to reduce buffer size
|
||||
auto pbuf8 = cursor_.pixel_buffer8;
|
||||
auto pbuf = cursor_.pixel_buffer;
|
||||
for (auto col = 0; col < char_height; col++) {
|
||||
// Annoyingly, read_pixels uses a 24-bit pixel format vs draw_pixels which uses 16-bit
|
||||
portapack::display.read_pixels({x, y + col, cursor_width, 1}, pbuf8, cursor_width);
|
||||
for (auto i = 0; i < cursor_width; i++)
|
||||
pbuf[i] = Color(pbuf8[i].r, pbuf8[i].g, pbuf8[i].b).v ^ 0xFFFF;
|
||||
portapack::display.draw_pixels({x, y + col, cursor_width, 1}, pbuf, cursor_width);
|
||||
}
|
||||
};
|
||||
|
||||
// Clear old cursor. CONSIDER: XOR cursor?
|
||||
draw_cursor(paint_state_.line, paint_state_.col, style().background);
|
||||
draw_cursor(cursor_.line, cursor_.col, style().foreground);
|
||||
if (paint_state_.line != UINT32_MAX) // only XOR old cursor if it still appears on the screen
|
||||
xor_cursor(paint_state_.line, paint_state_.col);
|
||||
|
||||
xor_cursor(cursor_.line, cursor_.col);
|
||||
|
||||
paint_state_.line = cursor_.line;
|
||||
paint_state_.col = cursor_.col;
|
||||
}
|
||||
@@ -325,7 +341,7 @@ static void show_save_prompt(
|
||||
std::function<void()> on_save,
|
||||
std::function<void()> continuation) {
|
||||
nav.display_modal(
|
||||
"Save?", "Save changes?", YESNO,
|
||||
"Save?", " Save changes?", YESNO,
|
||||
[on_save](bool choice) {
|
||||
if (choice && on_save)
|
||||
on_save();
|
||||
@@ -344,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)
|
||||
@@ -366,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);
|
||||
};
|
||||
@@ -458,7 +476,13 @@ void TextEditorView::open_file(const fs::path& path) {
|
||||
path_ = {};
|
||||
file_dirty_ = false;
|
||||
has_temp_file_ = false;
|
||||
auto result = FileWrapper::open(path);
|
||||
auto result = FileWrapper::open(
|
||||
path, false, [](uint32_t value, uint32_t total) {
|
||||
Painter p;
|
||||
auto percent = (value * 100) / total;
|
||||
auto width = (percent * screen_width) / 100;
|
||||
p.draw_hline({0, 16}, width, Color::yellow());
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
nav_.display_modal("Read Error", "Cannot open file:\n" + result.error().what());
|
||||
@@ -566,6 +590,10 @@ void TextEditorView::prepare_for_write() {
|
||||
if (has_temp_file_)
|
||||
return;
|
||||
|
||||
// TODO: This would be nice to have but it causes a stack overflow in an ISR?
|
||||
// Painter p;
|
||||
// p.draw_string({2, 48}, Styles::yellow, "Creating temporary file...");
|
||||
|
||||
// Copy to temp file on write.
|
||||
has_temp_file_ = true;
|
||||
delete_temp_file(path_);
|
||||
|
||||
@@ -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{};
|
||||
@@ -117,6 +121,10 @@ class TextViewer : public Widget {
|
||||
uint32_t line{};
|
||||
uint16_t col{};
|
||||
ScrollDirection dir{ScrollDirection::Vertical};
|
||||
|
||||
// Pixel buffer used for cursor XOR'ing - Max cursor width = Max char width + 1
|
||||
ColorRGB888 pixel_buffer8[ui::char_width + 1]{};
|
||||
Color pixel_buffer[ui::char_width + 1]{};
|
||||
} cursor_{};
|
||||
};
|
||||
|
||||
@@ -216,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_{};
|
||||
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
|
||||
#include "core_control.hpp"
|
||||
|
||||
/* Set true to enable additional checks to ensure
|
||||
* M4 and M0 are synchronized before passing messages. */
|
||||
static constexpr bool enforce_core_sync = true;
|
||||
|
||||
/* Set true to enable check for baseband messages getting stuck.
|
||||
* This implies the baseband thread is not dequeuing and has probably stalled.
|
||||
* NB: This check adds a small amout of overhead to the message sending code
|
||||
* and may impact application perf if it is sending a lot of messages. */
|
||||
static constexpr bool check_for_message_hang = true;
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace baseband {
|
||||
@@ -40,8 +50,18 @@ static void send_message(const Message* const message) {
|
||||
// another message is present before setting new message.
|
||||
shared_memory.baseband_message = message;
|
||||
creg::m0apptxevent::assert_event();
|
||||
while (shared_memory.baseband_message)
|
||||
;
|
||||
|
||||
if constexpr (check_for_message_hang) {
|
||||
auto count = UINT32_MAX;
|
||||
while (shared_memory.baseband_message && --count)
|
||||
/* spin */;
|
||||
|
||||
if (count == 0)
|
||||
chDbgPanic("Baseband Send Fail");
|
||||
} else {
|
||||
while (shared_memory.baseband_message)
|
||||
/* spin */;
|
||||
}
|
||||
}
|
||||
|
||||
void AMConfig::apply() const {
|
||||
@@ -276,17 +296,28 @@ void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bo
|
||||
|
||||
static bool baseband_image_running = false;
|
||||
|
||||
void run_image(const portapack::spi_flash::image_tag_t image_tag) {
|
||||
void run_image(const spi_flash::image_tag_t image_tag) {
|
||||
if (baseband_image_running) {
|
||||
chDbgPanic("BBRunning");
|
||||
}
|
||||
|
||||
creg::m4txevent::clear();
|
||||
shared_memory.clear_baseband_ready();
|
||||
|
||||
m4_init(image_tag, portapack::memory::map::m4_code, false);
|
||||
m4_init(image_tag, memory::map::m4_code, false);
|
||||
baseband_image_running = true;
|
||||
|
||||
creg::m4txevent::enable();
|
||||
|
||||
if constexpr (enforce_core_sync) {
|
||||
// Wait up to 3 seconds for baseband to start handling events.
|
||||
auto count = 3'000u;
|
||||
while (!shared_memory.baseband_ready && --count)
|
||||
chThdSleepMilliseconds(1);
|
||||
|
||||
if (count == 0)
|
||||
chDbgPanic("Baseband Sync Fail");
|
||||
}
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
@@ -316,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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__*/
|
||||
|
||||
@@ -20,21 +20,20 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "core_control.hpp"
|
||||
|
||||
#include "hal.h"
|
||||
|
||||
#include "lpc43xx_cpp.hpp"
|
||||
using namespace lpc43xx;
|
||||
|
||||
#include "message.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "core_control.hpp"
|
||||
#include "hal.h"
|
||||
#include "lpc43xx_cpp.hpp"
|
||||
#include "lz4.h"
|
||||
#include "message.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
void m4_init(const portapack::spi_flash::image_tag_t image_tag, const portapack::memory::region_t to, const bool full_reset) {
|
||||
const portapack::spi_flash::chunk_t* chunk = reinterpret_cast<const portapack::spi_flash::chunk_t*>(portapack::spi_flash::images.base());
|
||||
using namespace lpc43xx;
|
||||
using namespace portapack;
|
||||
|
||||
void m4_init(const spi_flash::image_tag_t image_tag, const memory::region_t to, const bool full_reset) {
|
||||
const spi_flash::chunk_t* chunk = reinterpret_cast<const spi_flash::chunk_t*>(spi_flash::images.base());
|
||||
while (chunk->tag) {
|
||||
if (chunk->tag == image_tag) {
|
||||
const void* src = &chunk->data[0];
|
||||
@@ -49,9 +48,8 @@ void m4_init(const portapack::spi_flash::image_tag_t image_tag, const portapack:
|
||||
LPC_CREG->M4MEMMAP = to.base();
|
||||
|
||||
/* Reset M4 core and optionally all peripherals */
|
||||
LPC_RGU->RESET_CTRL[0] = (full_reset) ? (1 << 1) // PERIPH_RST
|
||||
: (1 << 13) // M4_RST
|
||||
;
|
||||
LPC_RGU->RESET_CTRL[0] = (full_reset) ? (1 << 1) // PERIPH_RST
|
||||
: (1 << 13); // M4_RST
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -47,4 +47,6 @@ inline uint32_t get_free_stack_space() {
|
||||
return stack_space_left;
|
||||
}
|
||||
|
||||
bool stack_dump();
|
||||
|
||||
#endif /*__DEBUG_H__*/
|
||||
|
||||
@@ -21,12 +21,17 @@
|
||||
*/
|
||||
|
||||
#include "file.hpp"
|
||||
#include "complex.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <codecvt>
|
||||
#include <cstring>
|
||||
#include <locale>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
static const fs::path c8_ext{u".C8"};
|
||||
static const fs::path c16_ext{u".C16"};
|
||||
|
||||
Optional<File::Error> File::open_fatfs(const std::filesystem::path& filename, BYTE mode) {
|
||||
auto result = f_open(&f, reinterpret_cast<const TCHAR*>(filename.c_str()), mode);
|
||||
if (result == FR_OK) {
|
||||
@@ -507,6 +512,19 @@ bool path_iequal(
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_cxx_capture_file(const path& filename) {
|
||||
auto ext = filename.extension();
|
||||
return path_iequal(c8_ext, ext) || path_iequal(c16_ext, ext);
|
||||
}
|
||||
|
||||
uint8_t capture_file_sample_size(const path& filename) {
|
||||
if (path_iequal(filename.extension(), c8_ext))
|
||||
return sizeof(complex8_t);
|
||||
if (path_iequal(filename.extension(), c16_ext))
|
||||
return sizeof(complex16_t);
|
||||
return 0;
|
||||
}
|
||||
|
||||
directory_iterator::directory_iterator(
|
||||
const std::filesystem::path& path,
|
||||
const std::filesystem::path& wild)
|
||||
@@ -550,6 +568,28 @@ bool is_directory(const path& file_path) {
|
||||
return fr == FR_OK && is_directory(static_cast<file_status>(filinfo.fattrib));
|
||||
}
|
||||
|
||||
bool is_empty_directory(const path& file_path) {
|
||||
DIR dir;
|
||||
FILINFO filinfo;
|
||||
|
||||
if (!is_directory(file_path))
|
||||
return false;
|
||||
|
||||
auto result = f_findfirst(&dir, &filinfo, reinterpret_cast<const TCHAR*>(file_path.c_str()), (const TCHAR*)u"*");
|
||||
return !((result == FR_OK) && (filinfo.fname[0] != (TCHAR)'\0'));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -168,6 +168,8 @@ path operator/(const path& lhs, const path& rhs);
|
||||
|
||||
/* Case insensitive path equality on underlying "native" string. */
|
||||
bool path_iequal(const path& lhs, const path& rhs);
|
||||
bool is_cxx_capture_file(const path& filename);
|
||||
uint8_t capture_file_sample_size(const path& filename);
|
||||
|
||||
using file_status = BYTE;
|
||||
|
||||
@@ -246,6 +248,9 @@ bool is_directory(const file_status s);
|
||||
bool is_regular_file(const file_status s);
|
||||
bool file_exists(const path& file_path);
|
||||
bool is_directory(const path& file_path);
|
||||
bool is_empty_directory(const path& file_path);
|
||||
|
||||
int file_count(const path& dir_path);
|
||||
|
||||
space_info space(const path& p);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "file.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
@@ -75,6 +76,8 @@ class BufferWrapper {
|
||||
}
|
||||
virtual ~BufferWrapper() {}
|
||||
|
||||
std::function<void(Size, Size)> on_read_progress{};
|
||||
|
||||
/* Prevent copies */
|
||||
BufferWrapper(const BufferWrapper&) = delete;
|
||||
BufferWrapper& operator=(const BufferWrapper&) = delete;
|
||||
@@ -234,13 +237,23 @@ class BufferWrapper {
|
||||
|
||||
line_count_ = start_line_;
|
||||
Offset offset = start_offset_;
|
||||
|
||||
// Report progress every N lines.
|
||||
constexpr auto report_interval = 100u;
|
||||
auto result = next_newline(offset);
|
||||
auto next_report = report_interval;
|
||||
|
||||
while (result) {
|
||||
++line_count_;
|
||||
if (newlines_.size() < max_newlines)
|
||||
newlines_.push_back(*result);
|
||||
offset = *result + 1;
|
||||
|
||||
if (on_read_progress && line_count_ > next_report) {
|
||||
on_read_progress(offset, size());
|
||||
next_report = line_count_ + report_interval;
|
||||
}
|
||||
|
||||
result = next_newline(offset);
|
||||
}
|
||||
}
|
||||
@@ -397,6 +410,9 @@ class BufferWrapper {
|
||||
// Number of bytes left to shift.
|
||||
Offset remaining = size() - src;
|
||||
Offset offset = size();
|
||||
Size report_total = remaining;
|
||||
Size report_interval = report_total / 8;
|
||||
Size next_report = remaining - report_interval;
|
||||
|
||||
while (remaining > 0) {
|
||||
offset -= std::min(remaining, buffer_size);
|
||||
@@ -413,6 +429,11 @@ class BufferWrapper {
|
||||
break;
|
||||
|
||||
remaining -= *result;
|
||||
|
||||
if (on_read_progress && remaining <= next_report) {
|
||||
on_read_progress(report_total - remaining, report_total);
|
||||
next_report = remaining > report_interval ? remaining - report_interval : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,6 +445,9 @@ class BufferWrapper {
|
||||
|
||||
char buffer[buffer_size];
|
||||
auto offset = src;
|
||||
Size report_total = size();
|
||||
Size report_interval = report_total / 8;
|
||||
Size next_report = offset + report_interval;
|
||||
|
||||
while (true) {
|
||||
wrapped_->seek(offset);
|
||||
@@ -438,6 +462,11 @@ class BufferWrapper {
|
||||
break;
|
||||
|
||||
offset += *result;
|
||||
|
||||
if (on_read_progress && offset >= next_report) {
|
||||
on_read_progress(offset, report_total);
|
||||
next_report = offset + report_interval;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the extra bytes at the end of the file.
|
||||
@@ -463,13 +492,19 @@ class FileWrapper : public BufferWrapper<File, 64> {
|
||||
template <typename T>
|
||||
using Result = File::Result<T>;
|
||||
using Error = File::Error;
|
||||
static Result<std::unique_ptr<FileWrapper>> open(const std::filesystem::path& path, bool create = false) {
|
||||
static Result<std::unique_ptr<FileWrapper>> open(
|
||||
const std::filesystem::path& path,
|
||||
bool create = false,
|
||||
std::function<void(Size, Size)> on_read_progress = nullptr) {
|
||||
auto fw = std::unique_ptr<FileWrapper>(new FileWrapper());
|
||||
auto error = fw->file_.open(path, /*read_only*/ false, create);
|
||||
|
||||
if (error)
|
||||
return *error;
|
||||
|
||||
if (on_read_progress)
|
||||
fw->on_read_progress = on_read_progress;
|
||||
|
||||
fw->initialize();
|
||||
return fw;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2023 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -23,86 +24,128 @@
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
uint8_t Debounce::state() {
|
||||
bool v = state_to_report_;
|
||||
simulated_pulse_ = false;
|
||||
return v;
|
||||
}
|
||||
|
||||
void Debounce::enable_repeat() {
|
||||
repeat_enabled_ = true;
|
||||
}
|
||||
|
||||
bool Debounce::get_long_press_enabled() const {
|
||||
return long_press_enabled_;
|
||||
}
|
||||
|
||||
void Debounce::set_long_press_enabled(bool v) {
|
||||
long_press_enabled_ = v;
|
||||
}
|
||||
|
||||
bool Debounce::long_press_occurred() {
|
||||
bool v = long_press_occurred_;
|
||||
long_press_occurred_ = false;
|
||||
return v;
|
||||
}
|
||||
|
||||
// Returns TRUE if button state changed (after debouncing)
|
||||
bool Debounce::feed(const uint8_t bit) {
|
||||
history_ = (history_ << 1) | (bit & 1);
|
||||
|
||||
// "Repeat" handling - simulated button release
|
||||
if (repeat_ctr_) {
|
||||
// Make sure the button is still being held continuously
|
||||
if ((history_ == 0xFF) && !long_press_enabled_) {
|
||||
// Simulate button press every REPEAT_SUBSEQUENT_DELAY ticks
|
||||
if (--repeat_ctr_ == 0) {
|
||||
state_ = !state_;
|
||||
repeat_ctr_ = REPEAT_SUBSEQUENT_DELAY / 2;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// It's a real button release; stop simulating
|
||||
repeat_ctr_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (state_ == 0) {
|
||||
// Previous button state was 0 (released);
|
||||
// Has button been held for DEBOUNCE_COUNT ticks?
|
||||
if ((history_ & DEBOUNCE_MASK) == DEBOUNCE_MASK) {
|
||||
// Has button been held for DEBOUNCE_COUNT ticks?
|
||||
if ((history_ & DEBOUNCE_MASK) == DEBOUNCE_MASK) {
|
||||
//
|
||||
// Button is currently pressed;
|
||||
// Was previous button state 0 (released)?
|
||||
//
|
||||
if (state_ == 0) {
|
||||
//
|
||||
// Button has been pressed (after filtering glitches), transition 0->1
|
||||
//
|
||||
state_ = 1;
|
||||
held_time_ = 0;
|
||||
|
||||
// If long_press_enabled_, state() function masks the button press until it's released
|
||||
// or until LONG_PRESS_DELAY is reached
|
||||
if (long_press_enabled_) {
|
||||
pulse_upon_release_ = true;
|
||||
state_to_report_ = 0;
|
||||
return false;
|
||||
}
|
||||
state_to_report_ = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// "Repeat" handling - simulated button release
|
||||
if (repeat_ctr_ && !long_press_enabled_) {
|
||||
// Simulate button press every REPEAT_SUBSEQUENT_DELAY ticks
|
||||
// (by toggling reported state every 1/2 of the delay time)
|
||||
if (--repeat_ctr_ == 0) {
|
||||
state_to_report_ = !state_to_report_;
|
||||
repeat_ctr_ = REPEAT_SUBSEQUENT_DELAY / 2;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep track of how long button has been held
|
||||
held_time_++;
|
||||
if (pulse_upon_release_) {
|
||||
// Button is being held down and long_press support is enabled for this key:
|
||||
// if LONG_PRESS_DELAY is reached then finally report that switch is pressed and set flag
|
||||
// indicating it was a LONG press
|
||||
// (note that repeat_support and long_press support are mutually exclusive)
|
||||
if (held_time_ >= LONG_PRESS_DELAY) {
|
||||
pulse_upon_release_ = false;
|
||||
long_press_occurred_ = true;
|
||||
state_to_report_ = 1;
|
||||
return true;
|
||||
}
|
||||
} else if (repeat_enabled_ && !long_press_enabled_) {
|
||||
// Repeat support -- 4 directional buttons only (unless long_press is enabled)
|
||||
if (held_time_ >= REPEAT_INITIAL_DELAY) {
|
||||
// Delay reached; trigger repeat code on NEXT tick
|
||||
repeat_ctr_ = 1;
|
||||
held_time_ = 0;
|
||||
}
|
||||
}
|
||||
} else if ((history_ & DEBOUNCE_MASK) == 0) { // Has button been released for at least DEBOUNCE_COUNT ticks?
|
||||
//
|
||||
// Button is released;
|
||||
// Was previous button state 1 (pressed)?
|
||||
//
|
||||
if (state_ == 1) {
|
||||
//
|
||||
// Button has been released (after filtering glitches), transition 1->0
|
||||
//
|
||||
state_ = 0;
|
||||
long_press_occurred_ = false;
|
||||
|
||||
// If button released when long_press_enabled_ and before LONG_PRESS_DELAY was reached;
|
||||
// allow state() function to finally return a single press indication (simulated pulse).
|
||||
// Note: In long press mode, apps won't see button press until the button is released.
|
||||
if (pulse_upon_release_) {
|
||||
pulse_upon_release_ = false;
|
||||
simulated_pulse_ = true;
|
||||
state_to_report_ = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
state_to_report_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reset reported state after application/event has cleared simulated_pulse_
|
||||
if (state_to_report_ == 1 && !simulated_pulse_) {
|
||||
state_to_report_ = 0;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Previous button state was 1 (pressed);
|
||||
// Has button been released for DEBOUNCE_COUNT ticks?
|
||||
if ((history_ & DEBOUNCE_MASK) == 0) {
|
||||
// Button has been released when long_press_enabled_ and before LONG_PRESS_DELAY was reached;
|
||||
// allow state() function to finally return a single press indication
|
||||
// (in long press mode, apps won't see button press until the button is released)
|
||||
if (pulse_upon_release_) {
|
||||
// leaving state_==1 for one cycle
|
||||
pulse_upon_release_ = 0;
|
||||
} else {
|
||||
state_ = 0;
|
||||
}
|
||||
|
||||
// Reset long_press_occurred_ flag after button is released
|
||||
long_press_occurred_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Has button been held continuously?
|
||||
if (history_ == 0xFF) {
|
||||
held_time_++;
|
||||
if (pulse_upon_release_) {
|
||||
// Button is being held down and long_press support is enabled for this key:
|
||||
// if LONG_PRESS_DELAY is reached then finally report that switch is pressed and set flag
|
||||
// indicating it was a LONG press
|
||||
// (note that repease_support and long_press support are mutually exclusive)
|
||||
if (held_time_ == LONG_PRESS_DELAY) {
|
||||
long_press_occurred_ = true;
|
||||
pulse_upon_release_ = 0;
|
||||
held_time_ = 0;
|
||||
return true;
|
||||
}
|
||||
} else if (repeat_enabled_ && !long_press_enabled_) {
|
||||
// Repeat support -- 4 directional buttons only (unless long_press is enabled)
|
||||
if (held_time_ == REPEAT_INITIAL_DELAY) {
|
||||
// Delay reached; trigger repeat code on NEXT tick
|
||||
repeat_ctr_ = 1;
|
||||
held_time_ = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Button not continuously pressed; reset counter
|
||||
held_time_ = 0;
|
||||
}
|
||||
//
|
||||
// Button is in transition between states;
|
||||
// Reset counters until button inputs are stable.
|
||||
//
|
||||
held_time_ = 0;
|
||||
repeat_ctr_ = 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -31,39 +31,37 @@
|
||||
// # of timer0 ticks before a held button starts being counted as repeated presses
|
||||
#define REPEAT_INITIAL_DELAY 250
|
||||
#define REPEAT_SUBSEQUENT_DELAY 92
|
||||
#define LONG_PRESS_DELAY 1000
|
||||
#define LONG_PRESS_DELAY 800
|
||||
|
||||
class Debounce {
|
||||
public:
|
||||
bool feed(const uint8_t bit);
|
||||
|
||||
uint8_t state() const {
|
||||
return (pulse_upon_release_) ? 0 : state_;
|
||||
}
|
||||
|
||||
void enable_repeat() {
|
||||
repeat_enabled_ = true;
|
||||
}
|
||||
|
||||
void set_long_press_support(bool v) {
|
||||
long_press_enabled_ = v;
|
||||
}
|
||||
|
||||
bool long_press_occurred() {
|
||||
bool v = long_press_occurred_;
|
||||
long_press_occurred_ = false;
|
||||
return v;
|
||||
}
|
||||
uint8_t state();
|
||||
void enable_repeat();
|
||||
bool get_long_press_enabled() const;
|
||||
void set_long_press_enabled(bool v);
|
||||
bool long_press_occurred();
|
||||
|
||||
private:
|
||||
uint8_t history_{0};
|
||||
uint8_t state_{0};
|
||||
bool repeat_enabled_{0};
|
||||
uint16_t repeat_ctr_{0};
|
||||
uint16_t held_time_{0};
|
||||
bool pulse_upon_release_{0};
|
||||
bool long_press_enabled_{0};
|
||||
bool long_press_occurred_{0};
|
||||
uint8_t history_{0}; // shift register of last 8 reads from button hardware state bit
|
||||
|
||||
uint8_t state_{0}; // actual button hardware state (after debounce logic), 1=pressed
|
||||
|
||||
uint8_t state_to_report_{0}; // pseudo button state reported by state() function (may be masked off or simulated presses)
|
||||
|
||||
bool repeat_enabled_{false}; // TRUE if this button is enabled to auto-repeat when held down (ignored if long_press_enabled)
|
||||
|
||||
uint16_t repeat_ctr_{0}; // used for timing auto-repeat simulated button presses when button is held down and repeat_enabled
|
||||
|
||||
uint16_t held_time_{0}; // number of ticks that the button has been held down (compared against REPEAT and LONG_PRESS delays)
|
||||
|
||||
bool pulse_upon_release_{false}; // TRUE when button is being held down when long_press_enabled and LONG_PRESS_DELAY hasn't been reached yet
|
||||
|
||||
bool simulated_pulse_{false}; // TRUE if a simulated button press is active following a short button press (only when long_press_enabled)
|
||||
|
||||
bool long_press_enabled_{false}; // TRUE when button is in long-press mode (takes precedence over the repeat_enabled flag)
|
||||
|
||||
bool long_press_occurred_{false}; // TRUE when button is being held down and LONG_PRESS_DELAY has been reached (only when long_press_enabled)
|
||||
};
|
||||
|
||||
#endif /*__DEBOUNCE_H__*/
|
||||
|
||||
@@ -32,24 +32,66 @@
|
||||
// 12 degrees of rotation.
|
||||
//
|
||||
// For each encoder "pulse" there are 4 state transitions, and we can choose
|
||||
// how many transitions are needed before movement is registered
|
||||
static const int8_t transition_map[16] = {
|
||||
0, // 0000: noop
|
||||
-1, // 0001: ccw start
|
||||
1, // 0010: cw start
|
||||
0, // 0011: rate
|
||||
1, // 0100: cw end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
-1, // 0111: ccw end
|
||||
-1, // 1000: ccw end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
1, // 1011: cw end
|
||||
0, // 1100: rate
|
||||
1, // 1101: cw start
|
||||
-1, // 1110: ccw start
|
||||
0, // 1111: noop
|
||||
// between looking at all of them (high sensitivity), half of them (medium/default),
|
||||
// or one quarter of them (low sensitivity).
|
||||
static const int8_t transition_map[][16] = {
|
||||
// Normal (Medium) Sensitivity -- default
|
||||
{
|
||||
0, // 0000: noop
|
||||
0, // 0001: ccw start
|
||||
0, // 0010: cw start
|
||||
0, // 0011: rate
|
||||
1, // 0100: cw end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
-1, // 0111: ccw end
|
||||
-1, // 1000: ccw end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
1, // 1011: cw end
|
||||
0, // 1100: rate
|
||||
0, // 1101: cw start
|
||||
0, // 1110: ccw start
|
||||
0, // 1111: noop
|
||||
},
|
||||
// Low Sensitivity
|
||||
{
|
||||
0, // 0000: noop
|
||||
0, // 0001: ccw start
|
||||
0, // 0010: cw start
|
||||
0, // 0011: rate
|
||||
1, // 0100: cw end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
0, // 0111: ccw end
|
||||
-1, // 1000: ccw end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
0, // 1011: cw end
|
||||
0, // 1100: rate
|
||||
0, // 1101: cw start
|
||||
0, // 1110: ccw start
|
||||
0, // 1111: noop
|
||||
},
|
||||
// High Sensitivity
|
||||
{
|
||||
0, // 0000: noop
|
||||
-1, // 0001: ccw start
|
||||
1, // 0010: cw start
|
||||
0, // 0011: rate
|
||||
1, // 0100: cw end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
-1, // 0111: ccw end
|
||||
-1, // 1000: ccw end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
1, // 1011: cw end
|
||||
0, // 1100: rate
|
||||
1, // 1101: cw start
|
||||
-1, // 1110: ccw start
|
||||
0, // 1111: noop
|
||||
},
|
||||
};
|
||||
|
||||
int_fast8_t Encoder::update(
|
||||
@@ -60,13 +102,6 @@ int_fast8_t Encoder::update(
|
||||
state <<= 1;
|
||||
state |= phase_1;
|
||||
|
||||
int_fast8_t retval = transition_map[state & 0xf];
|
||||
|
||||
transition_count += retval;
|
||||
if (abs(transition_count) > portapack::persistent_memory::config_encoder_dial_sensitivity())
|
||||
transition_count = 0;
|
||||
else
|
||||
retval = 0;
|
||||
|
||||
return retval;
|
||||
// dial sensitivity setting is stored in pmem
|
||||
return transition_map[portapack::persistent_memory::config_encoder_dial_sensitivity()][state & 0xf];
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ class Encoder {
|
||||
|
||||
private:
|
||||
uint_fast8_t state{0};
|
||||
int_fast8_t transition_count{0};
|
||||
};
|
||||
|
||||
#endif /*__ENCODER_H__*/
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "io_convert.hpp"
|
||||
#include "complex.hpp"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
static const fs::path c8_ext = u".C8";
|
||||
|
||||
namespace file_convert {
|
||||
|
||||
// Convert buffer contents from c16 to c8.
|
||||
// Same buffer used for input & output; input size is bytes; output size is bytes/2.
|
||||
void c16_to_c8(const void* buffer, File::Size bytes) {
|
||||
complex16_t* src = (complex16_t*)buffer;
|
||||
complex8_t* dest = (complex8_t*)buffer;
|
||||
|
||||
// Shift isn't used here because it would amplify noise at center freq since it's a signed number
|
||||
// i.e. ((-1 >> 8) << 8) = -256, whereas (-1 / 256) * 256 = 0
|
||||
for (File::Size i = 0; i < bytes / sizeof(complex16_t); i++) {
|
||||
auto re_out = src[i].real() / 256;
|
||||
auto im_out = src[i].imag() / 256;
|
||||
dest[i] = {(int8_t)re_out, (int8_t)im_out};
|
||||
}
|
||||
}
|
||||
|
||||
// Convert c8 buffer to c16 buffer.
|
||||
// Same buffer used for input & output; input size is bytes; output size is 2*bytes.
|
||||
void c8_to_c16(const void* buffer, File::Size bytes) {
|
||||
complex8_t* src = (complex8_t*)buffer;
|
||||
complex16_t* dest = (complex16_t*)buffer;
|
||||
uint32_t i = bytes / sizeof(complex8_t);
|
||||
|
||||
if (i != 0) {
|
||||
do {
|
||||
i--;
|
||||
auto re_out = (int16_t)src[i].real() * 256; // C8 to C16 conversion;
|
||||
auto im_out = (int16_t)src[i].imag() * 256; // Can't shift signed numbers left, so using multiply
|
||||
dest[i] = {(int16_t)re_out, (int16_t)im_out};
|
||||
} while (i != 0);
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace file_convert */
|
||||
|
||||
// Automatically enables C8/C16 conversion based on file extension
|
||||
Optional<File::Error> FileConvertReader::open(const std::filesystem::path& filename) {
|
||||
convert_c8_to_c16 = path_iequal(filename.extension(), c8_ext);
|
||||
return file_.open(filename);
|
||||
}
|
||||
|
||||
// If C8 conversion enabled, half the number of bytes are read from the file & expanded to fill the whole buffer.
|
||||
File::Result<File::Size> FileConvertReader::read(void* const buffer, const File::Size bytes) {
|
||||
auto read_result = file_.read(buffer, convert_c8_to_c16 ? bytes / 2 : bytes);
|
||||
if (read_result.is_ok()) {
|
||||
if (convert_c8_to_c16) {
|
||||
file_convert::c8_to_c16(buffer, read_result.value());
|
||||
read_result = read_result.value() * 2;
|
||||
}
|
||||
bytes_read_ += read_result.value();
|
||||
}
|
||||
return read_result;
|
||||
}
|
||||
|
||||
// Automatically enables C8/C16 conversion based on file extension
|
||||
Optional<File::Error> FileConvertWriter::create(const std::filesystem::path& filename) {
|
||||
convert_c16_to_c8 = path_iequal(filename.extension(), c8_ext);
|
||||
return file_.create(filename);
|
||||
}
|
||||
|
||||
// If C8 conversion is enabled, half the number of bytes are written to the file.
|
||||
File::Result<File::Size> FileConvertWriter::write(const void* const buffer, const File::Size bytes) {
|
||||
if (convert_c16_to_c8) {
|
||||
file_convert::c16_to_c8(buffer, bytes);
|
||||
}
|
||||
auto write_result = file_.write(buffer, convert_c16_to_c8 ? bytes / 2 : bytes);
|
||||
if (write_result.is_ok()) {
|
||||
if (convert_c16_to_c8) {
|
||||
write_result = write_result.value() * 2;
|
||||
}
|
||||
bytes_written_ += write_result.value();
|
||||
}
|
||||
return write_result;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "io_file.hpp"
|
||||
|
||||
#include "io.hpp"
|
||||
#include "file.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace file_convert {
|
||||
|
||||
void c8_to_c16(const void* buffer, File::Size bytes);
|
||||
void c16_to_c8(const void* buffer, File::Size bytes);
|
||||
|
||||
} /* namespace file_convert */
|
||||
|
||||
class FileConvertReader : public stream::Reader {
|
||||
public:
|
||||
FileConvertReader() = default;
|
||||
|
||||
FileConvertReader(const FileConvertReader&) = delete;
|
||||
FileConvertReader& operator=(const FileConvertReader&) = delete;
|
||||
FileConvertReader(FileConvertReader&& file) = delete;
|
||||
FileConvertReader& operator=(FileConvertReader&&) = delete;
|
||||
|
||||
Optional<File::Error> open(const std::filesystem::path& filename);
|
||||
|
||||
File::Result<File::Size> read(void* const buffer, const File::Size bytes) override;
|
||||
const File& file() const& { return file_; }
|
||||
|
||||
bool convert_c8_to_c16{};
|
||||
|
||||
protected:
|
||||
File file_{};
|
||||
uint64_t bytes_read_{0};
|
||||
};
|
||||
|
||||
class FileConvertWriter : public stream::Writer {
|
||||
public:
|
||||
FileConvertWriter() = default;
|
||||
|
||||
FileConvertWriter(const FileConvertWriter&) = delete;
|
||||
FileConvertWriter& operator=(const FileConvertWriter&) = delete;
|
||||
FileConvertWriter(FileConvertWriter&& file) = delete;
|
||||
FileConvertWriter& operator=(FileConvertWriter&&) = delete;
|
||||
|
||||
Optional<File::Error> create(const std::filesystem::path& filename);
|
||||
|
||||
File::Result<File::Size> write(const void* const buffer, const File::Size bytes) override;
|
||||
const File& file() const& { return file_; }
|
||||
|
||||
bool convert_c16_to_c8{};
|
||||
|
||||
protected:
|
||||
File file_{};
|
||||
uint64_t bytes_written_{0};
|
||||
};
|
||||
@@ -22,32 +22,33 @@
|
||||
#include "irq_controls.hpp"
|
||||
|
||||
#include "ch.h"
|
||||
#include "hal.h"
|
||||
|
||||
#include "debounce.hpp"
|
||||
#include "encoder.hpp"
|
||||
#include "event_m0.hpp"
|
||||
|
||||
#include "hal.h"
|
||||
#include "touch.hpp"
|
||||
#include "touch_adc.hpp"
|
||||
#include "encoder.hpp"
|
||||
#include "debounce.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
|
||||
#include "portapack_io.hpp"
|
||||
|
||||
#include "hackrf_hal.hpp"
|
||||
|
||||
using namespace hackrf::one;
|
||||
using namespace portapack;
|
||||
|
||||
static Thread* thread_controls_event = NULL;
|
||||
|
||||
static std::array<Debounce, 8> switch_debounce;
|
||||
// Index with the Switch enum.
|
||||
static std::array<Debounce, 6> switch_debounce;
|
||||
static std::array<Debounce, 2> encoder_debounce;
|
||||
|
||||
static_assert(std::size(switch_debounce) == toUType(Switch::Dfu) + 1);
|
||||
|
||||
static Encoder encoder;
|
||||
|
||||
static volatile uint32_t encoder_position{0};
|
||||
|
||||
static volatile uint32_t touch_phase{0};
|
||||
|
||||
/* TODO: Change how touch scanning works. It produces a decent amount of noise
|
||||
@@ -60,11 +61,11 @@ static volatile uint32_t touch_phase{0};
|
||||
* Noise will only occur when the panel is being touched. Not ideal, but
|
||||
* an acceptable improvement.
|
||||
*/
|
||||
static std::array<portapack::IO::TouchPinsConfig, 3> touch_pins_configs{
|
||||
static std::array<IO::TouchPinsConfig, 3> touch_pins_configs{
|
||||
/* State machine will pause here until touch is detected. */
|
||||
portapack::IO::TouchPinsConfig::SensePressure,
|
||||
portapack::IO::TouchPinsConfig::SenseX,
|
||||
portapack::IO::TouchPinsConfig::SenseY,
|
||||
IO::TouchPinsConfig::SensePressure,
|
||||
IO::TouchPinsConfig::SenseX,
|
||||
IO::TouchPinsConfig::SenseY,
|
||||
};
|
||||
|
||||
static touch::Frame temp_frame;
|
||||
@@ -80,7 +81,7 @@ static bool touch_update() {
|
||||
const auto current_phase = touch_pins_configs[touch_phase];
|
||||
|
||||
switch (current_phase) {
|
||||
case portapack::IO::TouchPinsConfig::SensePressure: {
|
||||
case IO::TouchPinsConfig::SensePressure: {
|
||||
const auto z1 = samples.xp - samples.xn;
|
||||
const auto z2 = samples.yp - samples.yn;
|
||||
const auto touch_raw = (z1 > touch::touch_threshold) || (z2 > touch::touch_threshold);
|
||||
@@ -94,11 +95,11 @@ static bool touch_update() {
|
||||
}
|
||||
} break;
|
||||
|
||||
case portapack::IO::TouchPinsConfig::SenseX:
|
||||
case IO::TouchPinsConfig::SenseX:
|
||||
temp_frame.x += samples;
|
||||
break;
|
||||
|
||||
case portapack::IO::TouchPinsConfig::SenseY:
|
||||
case IO::TouchPinsConfig::SenseY:
|
||||
temp_frame.y += samples;
|
||||
break;
|
||||
|
||||
@@ -122,25 +123,59 @@ static bool touch_update() {
|
||||
|
||||
static uint8_t switches_raw = 0;
|
||||
|
||||
/* The raw data is not packed in a way that makes looping over it easy.
|
||||
* One option would be an accessor helper (RawSwitch). Another option
|
||||
* is to swizzle the bits into a friendlier order. */
|
||||
// /* Type to access the bits in the raw switch data. */
|
||||
// struct RawSwitch {
|
||||
// const uint8_t raw_{0};
|
||||
|
||||
// uint8_t right() const { return (raw_ >> 0) & 1; }
|
||||
// uint8_t left() const { return (raw_ >> 1) & 1; }
|
||||
// uint8_t down() const { return (raw_ >> 2) & 1; }
|
||||
// uint8_t up() const { return (raw_ >> 3) & 1; }
|
||||
// uint8_t select() const { return (raw_ >> 4) & 1; }
|
||||
// uint8_t rot_a() const { return (raw_ >> 5) & 1; }
|
||||
// uint8_t rot_b() const { return (raw_ >> 6) & 1; }
|
||||
// uint8_t dfu() const { return (raw_ >> 7) & 1; }};
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
static bool switches_update(const uint8_t raw) {
|
||||
// TODO: Only fire event on press, not release?
|
||||
bool switch_changed = false;
|
||||
for (size_t i = 0; i < switch_debounce.size(); i++) {
|
||||
switch_changed |= switch_debounce[i].feed((raw >> i) & 1);
|
||||
|
||||
for (size_t i = 0; i < switch_debounce.size(); ++i) {
|
||||
uint8_t bit = (raw >> i) & 0x01;
|
||||
switch_changed |= switch_debounce[i].feed(bit);
|
||||
}
|
||||
|
||||
return switch_changed;
|
||||
}
|
||||
|
||||
static bool encoder_update(const uint8_t raw) {
|
||||
bool encoder_changed = false;
|
||||
|
||||
encoder_changed |= encoder_debounce[0].feed((raw >> 6) & 0x01);
|
||||
encoder_changed |= encoder_debounce[1].feed((raw >> 7) & 0x01);
|
||||
|
||||
return encoder_changed;
|
||||
}
|
||||
|
||||
static bool encoder_read() {
|
||||
const auto delta = encoder.update(
|
||||
switch_debounce[5].state(),
|
||||
switch_debounce[6].state());
|
||||
encoder_debounce[0].state(),
|
||||
encoder_debounce[1].state());
|
||||
|
||||
if (delta != 0) {
|
||||
encoder_position += delta;
|
||||
return true;
|
||||
;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -149,11 +184,13 @@ static bool encoder_read() {
|
||||
void timer0_callback(GPTDriver* const) {
|
||||
eventmask_t event_mask = 0;
|
||||
if (touch_update()) event_mask |= EVT_MASK_TOUCH;
|
||||
switches_raw = portapack::io.io_update(touch_pins_configs[touch_phase]);
|
||||
if (switches_update(switches_raw)) {
|
||||
|
||||
switches_raw = swizzled_switches();
|
||||
if (switches_update(switches_raw))
|
||||
event_mask |= EVT_MASK_SWITCHES;
|
||||
if (encoder_read()) event_mask |= EVT_MASK_ENCODER;
|
||||
}
|
||||
|
||||
if (encoder_update(switches_raw) && encoder_read())
|
||||
event_mask |= EVT_MASK_ENCODER;
|
||||
|
||||
/* Signal event loop */
|
||||
if (event_mask) {
|
||||
@@ -189,37 +226,40 @@ void controls_init() {
|
||||
gptStartContinuous(&GPTD1, timer0_match_count);
|
||||
|
||||
// Enable repeat for directional switches only
|
||||
for (size_t i = 0; i < 4; i++)
|
||||
switch_debounce[i].enable_repeat();
|
||||
for (auto i = Switch::Right; i <= Switch::Up; incr(i))
|
||||
switch_debounce[toUType(i)].enable_repeat();
|
||||
}
|
||||
|
||||
// Note: Called by event handler or apps, not in ISR, so some presses might be missed during high CPU utilization
|
||||
SwitchesState get_switches_state() {
|
||||
SwitchesState result;
|
||||
|
||||
// Right, Left, Down, Up, & Select switches
|
||||
for (size_t i = 0; i < result.size() - 1; i++) {
|
||||
// TODO: Ignore multiple keys at the same time?
|
||||
// TODO: Ignore multiple keys at the same time?
|
||||
for (size_t i = 0; i < result.size(); i++)
|
||||
result[i] = switch_debounce[i].state();
|
||||
}
|
||||
|
||||
// Grab Dfu switch from bit 7 and return in bit 5 so that all switches are grouped together in this 6-bit result
|
||||
result[(size_t)Switch::Dfu] = switch_debounce[7].state();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Configure which switches support long press (note those switches will not support Repeat function)
|
||||
void switches_long_press_enable(SwitchesState switches_long_press_enabled) {
|
||||
// Right, Left, Down, Up, & Select switches
|
||||
for (size_t i = 0; i < switches_long_press_enabled.size() - 1; i++) {
|
||||
switch_debounce[i].set_long_press_support(switches_long_press_enabled[i]);
|
||||
}
|
||||
/* Gets the long press enabled state for all the switches. */
|
||||
SwitchesState get_switches_long_press_config() {
|
||||
SwitchesState result;
|
||||
|
||||
// Dfu switch
|
||||
switch_debounce[7].set_long_press_support(switches_long_press_enabled[(size_t)Switch::Dfu]);
|
||||
for (size_t i = 0; i < result.size(); i++)
|
||||
result[i] = switch_debounce[i].get_long_press_enabled();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool switch_long_press_occurred(size_t v) {
|
||||
return (v == (size_t)Switch::Dfu) ? switch_debounce[7].long_press_occurred() : switch_debounce[v].long_press_occurred();
|
||||
/* Configures which switches support long press.
|
||||
* NB: those switches will not support Repeat function. */
|
||||
void set_switches_long_press_config(SwitchesState switch_config) {
|
||||
for (size_t i = 0; i < switch_config.size(); i++)
|
||||
switch_debounce[i].set_long_press_enabled(switch_config[i]);
|
||||
}
|
||||
|
||||
bool switch_is_long_pressed(Switch s) {
|
||||
return switch_debounce[toUType(s)].long_press_occurred();
|
||||
}
|
||||
|
||||
EncoderPosition get_encoder_position() {
|
||||
|
||||
@@ -28,25 +28,29 @@
|
||||
#include "touch.hpp"
|
||||
|
||||
// Must be same values as in ui::KeyEvent
|
||||
enum class Switch {
|
||||
// TODO: Why not just reuse one or the other?
|
||||
enum class Switch : uint8_t {
|
||||
Right = 0,
|
||||
Left = 1,
|
||||
Down = 2,
|
||||
Up = 3,
|
||||
Sel = 4,
|
||||
Dfu = 5,
|
||||
Dfu = 5
|
||||
};
|
||||
|
||||
// Index with the Switch enum.
|
||||
using SwitchesState = std::bitset<6>;
|
||||
|
||||
using EncoderPosition = uint32_t;
|
||||
|
||||
void controls_init();
|
||||
uint8_t swizzled_switches();
|
||||
SwitchesState get_switches_state();
|
||||
EncoderPosition get_encoder_position();
|
||||
touch::Frame get_touch_frame();
|
||||
void switches_long_press_enable(SwitchesState v);
|
||||
bool switch_long_press_occurred(size_t v);
|
||||
|
||||
SwitchesState get_switches_long_press_config();
|
||||
void set_switches_long_press_config(SwitchesState switch_config);
|
||||
bool switch_is_long_pressed(Switch s);
|
||||
|
||||
namespace control {
|
||||
namespace debug {
|
||||
|
||||
@@ -238,6 +238,7 @@ void set_baseband_filter_bandwidth(const uint32_t bandwidth_minimum) {
|
||||
|
||||
void set_baseband_rate(const uint32_t rate) {
|
||||
portapack::clock_manager.set_sampling_frequency(rate);
|
||||
// TODO: actually set baseband too?
|
||||
}
|
||||
|
||||
void set_antenna_bias(const bool on) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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},
|
||||
""};
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -32,6 +32,7 @@ using namespace portapack;
|
||||
#include "string_format.hpp"
|
||||
#include "complex.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "ui_font_fixed_5x8.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -243,6 +244,7 @@ void GeoMap::paint(Painter& painter) {
|
||||
|
||||
// Draw the other markers
|
||||
draw_markers(painter);
|
||||
draw_scale(painter);
|
||||
markerListUpdated = false;
|
||||
set_clean();
|
||||
}
|
||||
@@ -283,6 +285,10 @@ void GeoMap::move(const float lon, const float lat) {
|
||||
x_pos = map_width - map_rect.width();
|
||||
if (y_pos > (map_height + map_rect.height()))
|
||||
y_pos = map_height - map_rect.height();
|
||||
|
||||
// Scale calculation
|
||||
float km_per_deg_lon = cos(lat * pi / 180) * 111.321; // 111.321 km/deg longitude at equator, and 0 km at poles
|
||||
pixels_per_km = (map_rect.width() / 2) / km_per_deg_lon;
|
||||
}
|
||||
|
||||
bool GeoMap::init() {
|
||||
@@ -318,6 +324,24 @@ bool GeoMap::manual_panning() {
|
||||
return manual_panning_;
|
||||
}
|
||||
|
||||
void GeoMap::draw_scale(Painter& painter) {
|
||||
uint16_t km = 100;
|
||||
uint16_t scale_width = km * pixels_per_km * map_zoom;
|
||||
|
||||
while (scale_width > screen_width / 2) {
|
||||
scale_width /= 2;
|
||||
km /= 2;
|
||||
}
|
||||
|
||||
std::string km_string = to_string_dec_uint(km) + " km";
|
||||
|
||||
display.fill_rectangle({{screen_width - 5 - scale_width, screen_height - 4}, {scale_width, 2}}, Color::black());
|
||||
display.fill_rectangle({{screen_width - 5, screen_height - 8}, {2, 6}}, Color::black());
|
||||
display.fill_rectangle({{screen_width - 5 - scale_width, screen_height - 8}, {2, 6}}, Color::black());
|
||||
|
||||
painter.draw_string({(uint16_t)(screen_width - 25 - scale_width - km_string.length() * 5 / 2), screen_height - 10}, ui::font::fixed_5x8, Color::black(), Color::white(), km_string);
|
||||
}
|
||||
|
||||
void GeoMap::draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color) {
|
||||
Point arrow_a, arrow_b, arrow_c;
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ class GeoMap : public Widget {
|
||||
MapMarkerStored store_marker(GeoMarker& marker);
|
||||
|
||||
private:
|
||||
void draw_scale(Painter& painter);
|
||||
void draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color);
|
||||
void draw_marker(Painter& painter, const ui::Point itemPoint, const uint16_t itemAngle, const std::string itemTag, const Color color = Color::red(), const Color fontColor = Color::white(), const Color backColor = Color::black());
|
||||
void draw_markers(Painter& painter);
|
||||
@@ -199,6 +200,7 @@ class GeoMap : public Widget {
|
||||
int32_t prev_x_pos{0xFFFF}, prev_y_pos{0xFFFF};
|
||||
float lat_{};
|
||||
float lon_{};
|
||||
float pixels_per_km{};
|
||||
uint16_t angle_{};
|
||||
std::string tag_{};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()};
|
||||
|
||||
@@ -20,16 +20,15 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "irq_controls.hpp"
|
||||
#include "max283x.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
using namespace portapack;
|
||||
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "max283x.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* FrequencyField ********************************************************/
|
||||
@@ -38,15 +37,26 @@ FrequencyField::FrequencyField(
|
||||
const Point parent_pos)
|
||||
: Widget{{parent_pos, {8 * 10, 16}}},
|
||||
length_{11},
|
||||
range(rf::tuning_range) {
|
||||
range_{rf::tuning_range} {
|
||||
initial_switch_config_ = get_switches_long_press_config();
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
FrequencyField::~FrequencyField() {
|
||||
reset_switch_config();
|
||||
}
|
||||
|
||||
rf::Frequency FrequencyField::value() const {
|
||||
return value_;
|
||||
}
|
||||
|
||||
void FrequencyField::set_value(rf::Frequency new_value) {
|
||||
// While in digit mode, don't update if the value is
|
||||
// out of range. That way going out of range doesn't
|
||||
// cause all the other digits to reset.
|
||||
if (digit_mode_ && !range_.contains_inc(new_value))
|
||||
return;
|
||||
|
||||
new_value = clamp_value(new_value);
|
||||
|
||||
if (new_value != value_) {
|
||||
@@ -59,48 +69,98 @@ void FrequencyField::set_value(rf::Frequency new_value) {
|
||||
}
|
||||
|
||||
void FrequencyField::set_step(rf::Frequency new_value) {
|
||||
step = new_value;
|
||||
// TODO: Quantize current frequency to a step of the new size?
|
||||
step_ = new_value;
|
||||
}
|
||||
|
||||
void FrequencyField::set_allow_digit_mode(bool allowed) {
|
||||
allow_digit_mode_ = allowed;
|
||||
|
||||
if (!allowed && digit_mode_) {
|
||||
digit_mode_ = false;
|
||||
reset_switch_config();
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void FrequencyField::paint(Painter& painter) {
|
||||
const std::string str_value = to_string_short_freq(value_);
|
||||
|
||||
const auto str_value = to_string_short_freq(value_);
|
||||
const auto paint_style = has_focus() ? style().invert() : style();
|
||||
|
||||
painter.draw_string(
|
||||
screen_pos(),
|
||||
paint_style,
|
||||
str_value);
|
||||
|
||||
// Highlight current digit in digit_mode.
|
||||
if (digit_mode_) {
|
||||
auto p = screen_pos();
|
||||
p += {digit_ * char_width, 0};
|
||||
painter.draw_char(p, Styles::bg_blue, str_value[digit_]);
|
||||
}
|
||||
}
|
||||
|
||||
bool FrequencyField::on_key(const ui::KeyEvent event) {
|
||||
if (event == ui::KeyEvent::Select) {
|
||||
bool FrequencyField::on_key(KeyEvent event) {
|
||||
if (event == KeyEvent::Select) {
|
||||
// Toggle 'digit' mode with long-press.
|
||||
if (digit_mode_ || key_is_long_pressed(event)) {
|
||||
digit_mode_ = !digit_mode_;
|
||||
set_dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (on_edit) {
|
||||
on_edit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (digit_mode_) {
|
||||
constexpr uint8_t decimal_pos = 4;
|
||||
int8_t delta = 0;
|
||||
|
||||
switch (event) {
|
||||
case KeyEvent::Up:
|
||||
set_value(value_ + digit_step());
|
||||
break;
|
||||
case KeyEvent::Down:
|
||||
set_value(value_ - digit_step());
|
||||
break;
|
||||
case KeyEvent::Left:
|
||||
delta = -1;
|
||||
break;
|
||||
case KeyEvent::Right:
|
||||
delta = 1;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (delta != 0) {
|
||||
digit_ += delta;
|
||||
|
||||
// If on decimal, skip it.
|
||||
if (digit_ == decimal_pos)
|
||||
digit_ += delta;
|
||||
// Otherwise ensure in bounds.
|
||||
else
|
||||
digit_ = clip<int8_t>(digit_, 0, 8);
|
||||
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FrequencyField::on_encoder(const EncoderEvent delta) {
|
||||
if (step == 0) { // 'Auto' mode.'
|
||||
auto ms = RTT2MS(halGetCounterValue());
|
||||
auto delta_ms = last_ms_ <= ms ? ms - last_ms_ : ms;
|
||||
last_ms_ = ms;
|
||||
if (digit_mode_)
|
||||
set_value(value_ + (delta * digit_step()));
|
||||
else
|
||||
set_value(value_ + (delta * step_));
|
||||
|
||||
// The goal is to map 'scale' to a range of about 10 to 10M.
|
||||
// The faster the encoder is rotated, the larger the step.
|
||||
// Linear doesn't feel right. Hyperbolic felt better.
|
||||
// To get these magic numbers, I graphed the function until the
|
||||
// curve shape seemed about right then tested on device.
|
||||
delta_ms = std::min(145ull, delta_ms) + 5; // Prevent DIV/0
|
||||
int64_t scale = 200'000'000 / (0.001'55 * std::pow(delta_ms, 5.45)) + 8;
|
||||
set_value(value() + (delta * scale));
|
||||
} else {
|
||||
set_value(value() + (delta * step));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -112,13 +172,49 @@ bool FrequencyField::on_touch(const TouchEvent event) {
|
||||
}
|
||||
|
||||
void FrequencyField::on_focus() {
|
||||
if (on_show_options) {
|
||||
if (on_show_options)
|
||||
on_show_options();
|
||||
}
|
||||
|
||||
enable_switch_config();
|
||||
}
|
||||
|
||||
void FrequencyField::on_blur() {
|
||||
reset_switch_config();
|
||||
}
|
||||
|
||||
rf::Frequency FrequencyField::digit_step() const {
|
||||
constexpr rf::Frequency steps[] = {
|
||||
1'000'000'000,
|
||||
100'000'000,
|
||||
10'000'000,
|
||||
1'000'000,
|
||||
0, // Decimal point position.
|
||||
100'000,
|
||||
10'000,
|
||||
1'000,
|
||||
100,
|
||||
};
|
||||
|
||||
return digit_ < std::size(steps) ? steps[digit_] : 0;
|
||||
}
|
||||
|
||||
rf::Frequency FrequencyField::clamp_value(rf::Frequency value) {
|
||||
return range.clip(value);
|
||||
return range_.clip(value);
|
||||
}
|
||||
|
||||
void FrequencyField::enable_switch_config() {
|
||||
// Don't enable long press testing when not allowed.
|
||||
if (!allow_digit_mode_)
|
||||
return;
|
||||
|
||||
// Enable long press on "Select".
|
||||
SwitchesState config;
|
||||
config[toUType(Switch::Sel)] = true;
|
||||
set_switches_long_press_config(config);
|
||||
}
|
||||
|
||||
void FrequencyField::reset_switch_config() {
|
||||
set_switches_long_press_config(initial_switch_config_);
|
||||
}
|
||||
|
||||
/* FrequencyKeypadView ***************************************************/
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
#include "irq_controls.hpp"
|
||||
#include "rf_path.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
@@ -34,6 +35,8 @@
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
|
||||
#define MAX_UFREQ 7200000000 // maximum usable frequency
|
||||
|
||||
namespace ui {
|
||||
|
||||
class FrequencyField : public Widget {
|
||||
@@ -45,27 +48,41 @@ class FrequencyField : public Widget {
|
||||
using range_t = rf::FrequencyRange;
|
||||
|
||||
FrequencyField(Point parent_pos);
|
||||
~FrequencyField();
|
||||
|
||||
rf::Frequency value() const;
|
||||
|
||||
void set_value(rf::Frequency new_value);
|
||||
void set_step(rf::Frequency new_value);
|
||||
void set_allow_digit_mode(bool allowed);
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
bool on_key(ui::KeyEvent event) override;
|
||||
bool on_key(KeyEvent event) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
bool on_touch(TouchEvent event) override;
|
||||
void on_focus() override;
|
||||
void on_blur() override;
|
||||
|
||||
private:
|
||||
const size_t length_;
|
||||
const range_t range;
|
||||
const range_t range_;
|
||||
|
||||
rf::Frequency value_{0};
|
||||
rf::Frequency step{25000};
|
||||
rf::Frequency step_{25000};
|
||||
uint64_t last_ms_{0};
|
||||
|
||||
uint8_t digit_{3};
|
||||
bool digit_mode_{false};
|
||||
bool allow_digit_mode_{true};
|
||||
SwitchesState initial_switch_config_{};
|
||||
|
||||
/* Gets the step value for the given digit when in digit_mode. */
|
||||
rf::Frequency digit_step() const;
|
||||
rf::Frequency clamp_value(rf::Frequency value);
|
||||
|
||||
void enable_switch_config();
|
||||
void reset_switch_config();
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
@@ -242,7 +259,6 @@ class FrequencyStepView : public OptionsField {
|
||||
parent_pos,
|
||||
5,
|
||||
{
|
||||
{" Auto", 0}, /* Faster == larger step. */
|
||||
{" 10", 10}, /* Fine tuning SSB voice pitch,in HF and QO-100 sat #669 */
|
||||
{" 50", 50}, /* added 50Hz/10Hz,but we do not increase GUI digit decimal */
|
||||
{" 100", 100},
|
||||
|
||||
@@ -308,13 +308,25 @@ WaterfallView::WaterfallView(const bool cursor) {
|
||||
}
|
||||
|
||||
void WaterfallView::on_show() {
|
||||
// TODO: Assert that baseband is not shutdown.
|
||||
baseband::spectrum_streaming_start();
|
||||
start();
|
||||
}
|
||||
|
||||
void WaterfallView::on_hide() {
|
||||
// TODO: Assert that baseband is not shutdown.
|
||||
baseband::spectrum_streaming_stop();
|
||||
stop();
|
||||
}
|
||||
|
||||
void WaterfallView::start() {
|
||||
if (!running_) {
|
||||
baseband::spectrum_streaming_start();
|
||||
running_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void WaterfallView::stop() {
|
||||
if (running_) {
|
||||
baseband::spectrum_streaming_stop();
|
||||
running_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void WaterfallView::show_audio_spectrum_view(const bool show) {
|
||||
@@ -368,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -131,11 +131,14 @@ class WaterfallView : public View {
|
||||
WaterfallView& operator=(const WaterfallView&) = delete;
|
||||
WaterfallView& operator=(WaterfallView&&) = delete;
|
||||
|
||||
// TODO: remove these, use start/stop directly instead.
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void show_audio_spectrum_view(const bool show);
|
||||
|
||||
private:
|
||||
@@ -147,6 +150,7 @@ class WaterfallView : public View {
|
||||
|
||||
WaterfallWidget waterfall_widget{};
|
||||
FrequencyScale frequency_scale{};
|
||||
bool running_{false};
|
||||
|
||||
ChannelSpectrumFIFO* channel_fifo{nullptr};
|
||||
AudioSpectrum* audio_spectrum_data{nullptr};
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
*/
|
||||
|
||||
#include "ui_textentry.hpp"
|
||||
//#include "portapack_persistent_memory.hpp"
|
||||
#include "ui_alphanum.hpp"
|
||||
//#include "ui_handwrite.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
|
||||
@@ -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"};
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -497,13 +498,8 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) {
|
||||
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
||||
}
|
||||
add_items({
|
||||
{
|
||||
"ADS-B",
|
||||
Color::green(),
|
||||
&bitmap_icon_adsb,
|
||||
[&nav]() { nav.push<ADSBRxView>(); },
|
||||
},
|
||||
//{ "ACARS", Color::yellow(), &bitmap_icon_adsb, [&nav](){ nav.push<ACARSAppView>(); }, },
|
||||
{"ADS-B", Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBRxView>(); }},
|
||||
//{ "ACARS", Color::yellow(), &bitmap_icon_adsb, [&nav](){ nav.push<ACARSAppView>(); }},
|
||||
{"AIS Boats", Color::green(), &bitmap_icon_ais, [&nav]() { nav.push<AISAppView>(); }},
|
||||
{"AFSK", Color::yellow(), &bitmap_icon_modem, [&nav]() { nav.push<AFSKRxView>(); }},
|
||||
{"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push<BTLERxView>(); }},
|
||||
|
||||
@@ -26,9 +26,11 @@ using namespace portapack;
|
||||
|
||||
#include "io_file.hpp"
|
||||
#include "io_wave.hpp"
|
||||
#include "io_convert.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "metadata_file.hpp"
|
||||
#include "oversample.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "utility.hpp"
|
||||
@@ -100,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);
|
||||
@@ -127,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
|
||||
@@ -161,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";
|
||||
@@ -194,18 +219,17 @@ void RecordView::start() {
|
||||
}
|
||||
} break;
|
||||
|
||||
case FileType::RawS8:
|
||||
case FileType::RawS16: {
|
||||
const auto metadata_file_error =
|
||||
write_metadata_file(get_metadata_path(base_path),
|
||||
{receiver_model.target_frequency(), sampling_rate / 8});
|
||||
// Not sure why sample_rate is div. 8, but stored value matches rate settings.
|
||||
const auto metadata_file_error = write_metadata_file(
|
||||
get_metadata_path(base_path), {receiver_model.target_frequency(), sampling_rate});
|
||||
if (metadata_file_error.is_valid()) {
|
||||
handle_error(metadata_file_error.value());
|
||||
return;
|
||||
}
|
||||
|
||||
auto p = std::make_unique<RawFileWriter>();
|
||||
auto create_error = p->create(base_path.replace_extension(u".C16"));
|
||||
auto p = std::make_unique<FileConvertWriter>();
|
||||
auto create_error = p->create(base_path.replace_extension((file_type == FileType::RawS8) ? u".C8" : u".C16"));
|
||||
if (create_error.is_valid()) {
|
||||
handle_error(create_error.value());
|
||||
} else {
|
||||
@@ -261,13 +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;
|
||||
|
||||
@@ -40,6 +40,7 @@ class RecordView : public View {
|
||||
std::function<void(std::string)> on_error{};
|
||||
|
||||
enum FileType {
|
||||
RawS8 = 1,
|
||||
RawS16 = 2,
|
||||
WAV = 3,
|
||||
};
|
||||
@@ -55,7 +56,13 @@ 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; }
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
@@ -75,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
|
||||
@@ -83,10 +92,10 @@ class RecordView : public View {
|
||||
|
||||
const std::filesystem::path filename_stem_pattern;
|
||||
const std::filesystem::path folder;
|
||||
const FileType file_type;
|
||||
FileType file_type;
|
||||
const size_t write_size;
|
||||
const size_t buffer_count;
|
||||
size_t sampling_rate{0};
|
||||
uint32_t sampling_rate{0};
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
Rectangle rect_background{
|
||||
|
||||
@@ -46,24 +46,34 @@ Thread* BasebandThread::thread = nullptr;
|
||||
BasebandThread::BasebandThread(
|
||||
uint32_t sampling_rate,
|
||||
BasebandProcessor* const baseband_processor,
|
||||
const tprio_t priority,
|
||||
baseband::Direction direction)
|
||||
: baseband_processor{baseband_processor},
|
||||
_direction{direction},
|
||||
sampling_rate{sampling_rate} {
|
||||
thread = chThdCreateStatic(baseband_thread_wa, sizeof(baseband_thread_wa),
|
||||
priority, ThreadBase::fn,
|
||||
this);
|
||||
baseband::Direction direction,
|
||||
bool auto_start,
|
||||
tprio_t priority)
|
||||
: baseband_processor_{baseband_processor},
|
||||
direction_{direction},
|
||||
sampling_rate_{sampling_rate},
|
||||
priority_{priority} {
|
||||
if (auto_start) start();
|
||||
}
|
||||
|
||||
BasebandThread::~BasebandThread() {
|
||||
chThdTerminate(thread);
|
||||
chThdWait(thread);
|
||||
thread = nullptr;
|
||||
if (thread) {
|
||||
chThdTerminate(thread);
|
||||
chThdWait(thread);
|
||||
thread = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void BasebandThread::start() {
|
||||
if (!thread) {
|
||||
thread = chThdCreateStatic(
|
||||
baseband_thread_wa, sizeof(baseband_thread_wa),
|
||||
priority_, ThreadBase::fn, this);
|
||||
}
|
||||
}
|
||||
|
||||
void BasebandThread::set_sampling_rate(uint32_t new_sampling_rate) {
|
||||
sampling_rate = new_sampling_rate;
|
||||
sampling_rate_ = new_sampling_rate;
|
||||
}
|
||||
|
||||
void BasebandThread::run() {
|
||||
@@ -71,9 +81,7 @@ void BasebandThread::run() {
|
||||
baseband::dma::init();
|
||||
|
||||
const auto baseband_buffer = std::make_unique<std::array<baseband::sample_t, 8192>>();
|
||||
baseband::dma::configure(
|
||||
baseband_buffer->data(),
|
||||
direction());
|
||||
baseband::dma::configure(baseband_buffer->data(), direction());
|
||||
// baseband::dma::allocate(4, 2048);
|
||||
|
||||
baseband_sgpio.configure(direction());
|
||||
@@ -85,10 +93,10 @@ void BasebandThread::run() {
|
||||
const auto buffer_tmp = baseband::dma::wait_for_buffer();
|
||||
if (buffer_tmp) {
|
||||
buffer_c8_t buffer{
|
||||
buffer_tmp.p, buffer_tmp.count, sampling_rate};
|
||||
buffer_tmp.p, buffer_tmp.count, sampling_rate_};
|
||||
|
||||
if (baseband_processor) {
|
||||
baseband_processor->execute(buffer);
|
||||
if (baseband_processor_) {
|
||||
baseband_processor_->execute(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,20 @@
|
||||
|
||||
#include <ch.h>
|
||||
|
||||
/* NB: Because ThreadBase threads start when then are initialized (by default),
|
||||
* they should be the last members in a Processor class to ensure the rest of the
|
||||
* members are fully initialized before data handling starts. If the Procressor
|
||||
* needs to do additional initialization (in its ctor), set 'auto_start' to false
|
||||
* and manually call 'start()' on the thread. */
|
||||
|
||||
class BasebandThread : public ThreadBase {
|
||||
public:
|
||||
BasebandThread(
|
||||
uint32_t sampling_rate,
|
||||
BasebandProcessor* const baseband_processor,
|
||||
const tprio_t priority,
|
||||
const baseband::Direction direction = baseband::Direction::Receive);
|
||||
baseband::Direction direction,
|
||||
bool auto_start = true,
|
||||
tprio_t priority = (NORMALPRIO + 20));
|
||||
~BasebandThread();
|
||||
|
||||
BasebandThread(const BasebandThread&) = delete;
|
||||
@@ -42,10 +49,12 @@ class BasebandThread : public ThreadBase {
|
||||
BasebandThread& operator=(const BasebandThread&) = delete;
|
||||
BasebandThread& operator=(BasebandThread&&) = delete;
|
||||
|
||||
void start() override;
|
||||
|
||||
// This getter should die, it's just here to leak information to code that
|
||||
// isn't in the right place to begin with.
|
||||
baseband::Direction direction() const {
|
||||
return _direction;
|
||||
return direction_;
|
||||
}
|
||||
|
||||
void set_sampling_rate(uint32_t new_sampling_rate);
|
||||
@@ -53,9 +62,10 @@ class BasebandThread : public ThreadBase {
|
||||
private:
|
||||
static Thread* thread;
|
||||
|
||||
BasebandProcessor* baseband_processor{nullptr};
|
||||
baseband::Direction _direction{baseband::Direction::Receive};
|
||||
uint32_t sampling_rate{0};
|
||||
BasebandProcessor* baseband_processor_;
|
||||
baseband::Direction direction_;
|
||||
uint32_t sampling_rate_;
|
||||
const tprio_t priority_;
|
||||
|
||||
void run() override;
|
||||
};
|
||||
|
||||
@@ -19,21 +19,18 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "event_m4.hpp"
|
||||
#include "debug.hpp"
|
||||
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "message_queue.hpp"
|
||||
|
||||
#include "ch.h"
|
||||
|
||||
#include "debug.hpp"
|
||||
#include "event_m4.hpp"
|
||||
#include "lpc43xx_cpp.hpp"
|
||||
using namespace lpc43xx;
|
||||
#include "message_queue.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
|
||||
using namespace lpc43xx;
|
||||
|
||||
extern "C" {
|
||||
|
||||
CH_IRQ_HANDLER(MAPP_IRQHandler) {
|
||||
@@ -61,6 +58,10 @@ void EventDispatcher::run() {
|
||||
|
||||
lpc43xx::creg::m0apptxevent::enable();
|
||||
|
||||
// Indicate to the M0 thread that
|
||||
// M4 is ready to receive message events.
|
||||
shared_memory.set_baseband_ready();
|
||||
|
||||
while (is_running) {
|
||||
const auto events = wait();
|
||||
dispatch(events);
|
||||
|
||||
@@ -32,6 +32,7 @@ ACARSProcessor::ACARSProcessor() {
|
||||
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_11k0_decim_1.taps, 131072);
|
||||
packet.clear();
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
void ACARSProcessor::execute(const buffer_c8_t& buffer) {
|
||||
|
||||
@@ -110,9 +110,6 @@ class ACARSProcessor : public BasebandProcessor {
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 2457600;
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{NORMALPRIO + 10};
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
@@ -138,6 +135,11 @@ class ACARSProcessor : public BasebandProcessor {
|
||||
};*/
|
||||
baseband::Packet packet{};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
void consume_symbol(const float symbol);
|
||||
void payload_handler(const baseband::Packet& packet);
|
||||
};
|
||||
|
||||
@@ -36,15 +36,11 @@ using namespace adsb;
|
||||
class ADSBRXProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 2000000;
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{NORMALPRIO + 10};
|
||||
|
||||
ADSBFrame frame{};
|
||||
bool configured{false};
|
||||
uint32_t prev_mag{0};
|
||||
@@ -58,6 +54,10 @@ class ADSBRXProcessor : public BasebandProcessor {
|
||||
uint32_t sample{0};
|
||||
int32_t re{}, im{};
|
||||
int32_t amp{0};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -29,14 +29,11 @@
|
||||
class ADSBTXProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const p) override;
|
||||
|
||||
private:
|
||||
bool configured = false;
|
||||
|
||||
BasebandThread baseband_thread{4000000, this, NORMALPRIO + 20, baseband::Direction::Transmit};
|
||||
|
||||
const complex8_t am_lut[4] = {
|
||||
{127, 0},
|
||||
{0, 127},
|
||||
@@ -48,6 +45,9 @@ class ADSBTXProcessor : public BasebandProcessor {
|
||||
uint32_t phase{0};
|
||||
|
||||
TXProgressMessage txprogress_message{};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{4000000, this, baseband::Direction::Transmit};
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -32,14 +32,11 @@
|
||||
class AFSKProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const msg) override;
|
||||
|
||||
private:
|
||||
bool configured = false;
|
||||
|
||||
BasebandThread baseband_thread{AFSK_SAMPLERATE, this, NORMALPRIO + 20, baseband::Direction::Transmit};
|
||||
|
||||
uint32_t afsk_samples_per_bit{0};
|
||||
uint32_t afsk_phase_inc_mark{0};
|
||||
uint32_t afsk_phase_inc_space{0};
|
||||
@@ -59,6 +56,9 @@ class AFSKProcessor : public BasebandProcessor {
|
||||
int8_t re{0}, im{0};
|
||||
|
||||
TXProgressMessage txprogress_message{};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{AFSK_SAMPLERATE, this, baseband::Direction::Transmit};
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
class AFSKRxProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
@@ -53,9 +52,6 @@ class AFSKRxProcessor : public BasebandProcessor {
|
||||
RECEIVE
|
||||
};
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{NORMALPRIO + 10};
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
@@ -93,9 +89,13 @@ class AFSKRxProcessor : public BasebandProcessor {
|
||||
bool trigger_word{};
|
||||
bool triggered{};
|
||||
|
||||
void configure(const AFSKRxConfigureMessage& message);
|
||||
|
||||
AFSKDataMessage data_message{false, 0};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
void configure(const AFSKRxConfigureMessage& message);
|
||||
};
|
||||
|
||||
#endif /*__PROC_TPMS_H__*/
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
AISProcessor::AISProcessor() {
|
||||
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_11k0_decim_1.taps, 131072);
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
void AISProcessor::execute(const buffer_c8_t& buffer) {
|
||||
|
||||
@@ -51,9 +51,6 @@ class AISProcessor : public BasebandProcessor {
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 2457600;
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{NORMALPRIO + 10};
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
@@ -77,6 +74,11 @@ class AISProcessor : public BasebandProcessor {
|
||||
this->payload_handler(packet);
|
||||
}};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
void consume_symbol(const float symbol);
|
||||
void payload_handler(const baseband::Packet& packet);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
class NarrowbandAMAudio : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
@@ -46,9 +45,6 @@ class NarrowbandAMAudio : public BasebandProcessor {
|
||||
static constexpr size_t decim_2_decimation_factor = 4;
|
||||
static constexpr size_t channel_filter_decimation_factor = 1;
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{NORMALPRIO + 10};
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
@@ -65,6 +61,7 @@ class NarrowbandAMAudio : public BasebandProcessor {
|
||||
int32_t channel_filter_low_f = 0;
|
||||
int32_t channel_filter_high_f = 0;
|
||||
int32_t channel_filter_transition = 0;
|
||||
bool configured{false};
|
||||
|
||||
bool modulation_ssb = false;
|
||||
dsp::demodulate::AM demod_am{};
|
||||
@@ -74,7 +71,10 @@ class NarrowbandAMAudio : public BasebandProcessor {
|
||||
|
||||
SpectrumCollector channel_spectrum{};
|
||||
|
||||
bool configured{false};
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
void configure(const AMConfigureMessage& message);
|
||||
void capture_config(const CaptureConfigMessage& message);
|
||||
|
||||
|
||||
@@ -38,15 +38,11 @@
|
||||
class WidebandFMAudio : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 2000000;
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{NORMALPRIO + 10};
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
@@ -55,8 +51,12 @@ class WidebandFMAudio : public BasebandProcessor {
|
||||
AudioSpectrum audio_spectrum{};
|
||||
TvCollector channel_spectrum{};
|
||||
std::array<complex16_t, 256> spectrum{};
|
||||
|
||||
bool configured{false};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
void configure(const WFMConfigureMessage& message);
|
||||
};
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ static uint16_t crc_ccitt_tab[256] = {
|
||||
class APRSRxProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
@@ -90,9 +89,6 @@ class APRSRxProcessor : public BasebandProcessor {
|
||||
IN_FRAME
|
||||
};
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{NORMALPRIO + 10};
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
@@ -135,6 +131,10 @@ class APRSRxProcessor : public BasebandProcessor {
|
||||
|
||||
aprs::APRSPacket aprs_packet{};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
void configure(const APRSRxConfigureMessage& message);
|
||||
void capture_config(const CaptureConfigMessage& message);
|
||||
void parse_packet();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,6 @@ class AudioTXProcessor : public BasebandProcessor {
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 1536000;
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Transmit};
|
||||
|
||||
std::unique_ptr<StreamOutput> stream{};
|
||||
|
||||
ToneGen tone_gen{};
|
||||
@@ -55,12 +53,15 @@ 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);
|
||||
|
||||
TXProgressMessage txprogress_message{};
|
||||
RequestSignalMessage sig_message{RequestSignalMessage::Signal::FillRequest};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Transmit};
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -39,16 +39,12 @@
|
||||
class BTLERxProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 4000000;
|
||||
static constexpr size_t audio_fs = baseband_fs / 8 / 8 / 2;
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{NORMALPRIO + 10};
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
@@ -81,10 +77,13 @@ class BTLERxProcessor : public BasebandProcessor {
|
||||
int RB_SIZE{1000};
|
||||
|
||||
bool configured{false};
|
||||
AFSKDataMessage data_message{false, 0};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
void configure(const BTLERxConfigureMessage& message);
|
||||
|
||||
AFSKDataMessage data_message{false, 0};
|
||||
};
|
||||
|
||||
#endif /*__PROC_BTLERX_H__*/
|
||||
|
||||
@@ -23,22 +23,34 @@
|
||||
#include "proc_capture.hpp"
|
||||
|
||||
#include "dsp_fir_taps.hpp"
|
||||
|
||||
#include "event_m4.hpp"
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
CaptureProcessor::CaptureProcessor() {
|
||||
decim_0.configure(taps_200k_decim_0.taps, 33554432);
|
||||
decim_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();
|
||||
}
|
||||
|
||||
void CaptureProcessor::execute(const buffer_c8_t& buffer) {
|
||||
/* 2.4576MHz, 2048 samples */
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
const auto decim_0_out = decim_0_execute(buffer, dst_buffer); // selectable 3 possible decim_0, (/4. /8 200k soft filter , /8 180k sharp )
|
||||
const auto decim_1_out = decim_1_execute(decim_0_out, dst_buffer); // selectable 3 possible decim_1, (/8. /2 200k or bypassed /1 )
|
||||
|
||||
/* this code was valid when we had only 2 decim1 cases.
|
||||
const auto decim_1_out = baseband_fs < 4800'000
|
||||
? decim_1_2.execute(decim_0_out, dst_buffer) // < 600khz double decim. stage , means 500khz and lower bit rates.
|
||||
// ? decim_1_8.execute(decim_0_out, dst_buffer) // < 600khz double decim. stage , means 500khz and lower bit rates.
|
||||
: decim_0_out; // >= 600khz single decim. stage , means 600khz and upper bit rates.
|
||||
|
||||
} */
|
||||
|
||||
const auto& decimator_out = decim_1_out;
|
||||
const auto& channel = decimator_out;
|
||||
|
||||
@@ -66,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:
|
||||
@@ -79,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;
|
||||
@@ -104,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();
|
||||
|
||||
@@ -28,9 +28,7 @@
|
||||
#include "rssi_thread.hpp"
|
||||
|
||||
#include "dsp_decimate.hpp"
|
||||
|
||||
#include "spectrum_collector.hpp"
|
||||
|
||||
#include "stream_input.hpp"
|
||||
|
||||
#include <array>
|
||||
@@ -41,24 +39,27 @@ class CaptureProcessor : public BasebandProcessor {
|
||||
CaptureProcessor();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
// TODO: Repeated value needs to be transmitted from application side.
|
||||
size_t baseband_fs = 0;
|
||||
size_t baseband_fs = 3072000; // aka: sample_rate
|
||||
static constexpr auto spectrum_rate_hz = 50.0f;
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{NORMALPRIO + 10};
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
|
||||
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;
|
||||
@@ -68,9 +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};
|
||||
|
||||
void samplerate_config(const SamplerateConfigMessage& message);
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
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__*/
|
||||
|
||||
@@ -67,9 +67,6 @@ class ERTProcessor : public BasebandProcessor {
|
||||
const size_t samples_per_symbol = channel_sampling_rate / symbol_rate;
|
||||
const float clock_recovery_rate = symbol_rate * 2;
|
||||
|
||||
BasebandThread baseband_thread{baseband_sampling_rate, this, NORMALPRIO + 20, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{NORMALPRIO + 10};
|
||||
|
||||
clock_recovery::ClockRecovery<clock_recovery::FixedErrorFilter> clock_recovery{
|
||||
clock_recovery_rate,
|
||||
symbol_rate,
|
||||
@@ -116,6 +113,10 @@ class ERTProcessor : public BasebandProcessor {
|
||||
float offset_i{0.0f};
|
||||
float offset_q{0.0f};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_sampling_rate, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
float abs(const complex8_t& v);
|
||||
};
|
||||
|
||||
|
||||
@@ -29,14 +29,11 @@
|
||||
class FSKProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const p) override;
|
||||
|
||||
private:
|
||||
bool configured = false;
|
||||
|
||||
BasebandThread baseband_thread{2280000, this, NORMALPRIO + 20, baseband::Direction::Transmit};
|
||||
|
||||
uint32_t samples_per_bit{0};
|
||||
uint32_t length{0};
|
||||
|
||||
@@ -48,6 +45,9 @@ class FSKProcessor : public BasebandProcessor {
|
||||
uint32_t phase{0}, sphase{0};
|
||||
|
||||
TXProgressMessage txprogress_message{};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{2280000, this, baseband::Direction::Transmit};
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user