mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-31 11:59:03 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8a6422d37 | |||
| cc963c3562 | |||
| 63f99742fc | |||
| a4636d7872 | |||
| bd2ee03e44 | |||
| deeb81c183 | |||
| 12dbf957b3 | |||
| e1cc0b1ad0 | |||
| f079d57fc6 | |||
| 09b9ed642f | |||
| e74a9f3b41 | |||
| ff7a9d10cb | |||
| 5cfd7f1dc2 | |||
| 853ca2ef53 | |||
| cb0a4854f5 | |||
| 1188157a3e | |||
| 2ae639412d | |||
| 37386c29cb |
@@ -23,7 +23,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 +36,131 @@
|
||||
|
||||
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>() = std::string{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} {
|
||||
load_settings(store_name_, bindings_);
|
||||
}
|
||||
|
||||
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_settings(store_name_, bindings_);
|
||||
}
|
||||
|
||||
ResultCode save_settings(const std::string& app_name, AppSettings& settings) {
|
||||
if (!portapack::persistent_memory::save_app_settings())
|
||||
return ResultCode::SettingsDisabled;
|
||||
bool load_settings(std::string_view store_name, SettingBindings& bindings) {
|
||||
File f;
|
||||
auto path = get_settings_path(std::string{store_name});
|
||||
|
||||
File settings_file;
|
||||
auto file_path = get_settings_path(app_name);
|
||||
ensure_directory(file_path.parent_path());
|
||||
|
||||
auto error = settings_file.create(file_path);
|
||||
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 +218,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,82 @@
|
||||
#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();
|
||||
|
||||
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 +117,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 +141,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 +169,9 @@ class SettingsManager {
|
||||
AppSettings& raw() { return settings_; }
|
||||
|
||||
private:
|
||||
std::string app_name_;
|
||||
std::string_view app_name_;
|
||||
AppSettings settings_;
|
||||
SettingBindings bindings_;
|
||||
bool loaded_;
|
||||
};
|
||||
|
||||
|
||||
@@ -60,30 +60,33 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
|
||||
};
|
||||
|
||||
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
|
||||
option_bandwidth.on_change = [this](size_t, uint32_t base_rate) {
|
||||
/* base_rate is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
option_bandwidth.on_change = [this](size_t, uint32_t bandwidth) {
|
||||
/* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
|
||||
* provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */
|
||||
auto sample_rate = bandwidth;
|
||||
|
||||
/* base_rate (bandwidth) is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card */
|
||||
|
||||
// For lower bandwidths, (12k5, 16k, 20k), increase the oversample rate to get a higher sample rate.
|
||||
OversampleRate oversample_rate = base_rate >= 25'000 ? OversampleRate::Rate8x : OversampleRate::Rate16x;
|
||||
|
||||
// HackRF suggests a minimum sample rate of 2M.
|
||||
// Oversampling helps get to higher sample rates when recording lower bandwidths.
|
||||
uint32_t sampling_rate = toUType(oversample_rate) * base_rate;
|
||||
|
||||
// Set up proper anti aliasing BPF bandwidth in MAX2837 before ADC sampling according to the new added BW Options.
|
||||
auto anti_alias_baseband_bandwidth_filter = filter_bandwidth_for_sampling_rate(sampling_rate);
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */
|
||||
|
||||
waterfall.stop();
|
||||
record_view.set_sampling_rate(sampling_rate, oversample_rate); // NB: Actually updates the baseband.
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
|
||||
|
||||
// record_view determines the correct oversampling to apply and returns the actual sample rate.
|
||||
// NB: record_view is what actually updates proc_capture baseband settings.
|
||||
auto actual_sample_rate = record_view.set_sampling_rate(sample_rate);
|
||||
|
||||
// Update the radio model with the actual sampling rate.
|
||||
receiver_model.set_sampling_rate(actual_sample_rate);
|
||||
|
||||
// Get suitable anti-aliasing BPF bandwidth for MAX2837 given the actual sample rate.
|
||||
auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth);
|
||||
|
||||
waterfall.start();
|
||||
};
|
||||
|
||||
receiver_model.enable();
|
||||
option_bandwidth.set_selected_index(7); // Preselected default option 500kHz.
|
||||
option_bandwidth.set_by_value(500000); // better by_value than by option_bandwidth.set_selected_index(4), Preselected default option 500kHz.
|
||||
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
|
||||
@@ -71,6 +71,8 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
if (!settings_.loaded())
|
||||
field_frequency.set_value(initial_target_frequency);
|
||||
|
||||
check_log.set_value(enable_logging);
|
||||
|
||||
receiver_model.enable();
|
||||
|
||||
// TODO: app setting instead?
|
||||
@@ -88,7 +90,6 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
logger->append(LOG_ROOT_DIR "/POCSAG.TXT");
|
||||
|
||||
audio::output::start();
|
||||
|
||||
baseband::set_pocsag();
|
||||
}
|
||||
|
||||
@@ -99,8 +100,9 @@ void POCSAGAppView::focus() {
|
||||
POCSAGAppView::~POCSAGAppView() {
|
||||
audio::output::stop();
|
||||
|
||||
// Save ignored address
|
||||
// Save settings.
|
||||
persistent_memory::set_pocsag_ignore_address(sym_ignore.value_dec_u32());
|
||||
enable_logging = check_log.value();
|
||||
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
@@ -64,8 +64,12 @@ class POCSAGAppView : public View {
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{};
|
||||
// Settings
|
||||
bool enable_logging = false;
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_pocsag", app_settings::Mode::RX};
|
||||
"rx_pocsag"sv,
|
||||
app_settings::Mode::RX,
|
||||
{{"enable_logging"sv, &enable_logging}}};
|
||||
|
||||
uint32_t last_address = 0xFFFFFFFF;
|
||||
pocsag::POCSAGState pocsag_state{};
|
||||
|
||||
@@ -124,7 +124,7 @@ void ReplayAppView::start() {
|
||||
|
||||
if (reader) {
|
||||
button_play.set_bitmap(&bitmap_stop);
|
||||
baseband::set_sample_rate(sample_rate * 8);
|
||||
baseband::set_sample_rate(sample_rate, OversampleRate::x8);
|
||||
|
||||
replay_thread = std::make_unique<ReplayThread>(
|
||||
std::move(reader),
|
||||
@@ -136,7 +136,7 @@ void ReplayAppView::start() {
|
||||
});
|
||||
}
|
||||
|
||||
transmitter_model.set_sampling_rate(sample_rate * 8);
|
||||
transmitter_model.set_sampling_rate(sample_rate * toUType(OversampleRate::x8));
|
||||
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
transmitter_model.enable();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -185,7 +185,6 @@ FileManBaseView::FileManBaseView(
|
||||
extension_filter{filter} {
|
||||
add_children({&labels,
|
||||
&text_current,
|
||||
&text_info,
|
||||
&button_exit});
|
||||
|
||||
button_exit.on_select = [this, &nav](Button&) {
|
||||
@@ -250,8 +249,10 @@ void FileManBaseView::refresh_list() {
|
||||
auto entry_name = truncate(entry.path, 20);
|
||||
|
||||
if (entry.is_directory) {
|
||||
auto size_str = (entry.path == parent_dir_path) ? "" : to_string_dec_uint(file_count(entry.path));
|
||||
|
||||
menu_view.add_item(
|
||||
{entry_name,
|
||||
{entry_name + std::string(21 - entry_name.length(), ' ') + size_str,
|
||||
ui::Color::yellow(),
|
||||
&bitmap_icon_dir,
|
||||
[this](KeyEvent key) {
|
||||
@@ -278,7 +279,6 @@ void FileManBaseView::refresh_list() {
|
||||
break;
|
||||
}
|
||||
|
||||
text_info.set(menu_view.item_count() >= max_items_shown ? "Too many files!" : "");
|
||||
menu_view.set_highlighted(prev_highlight);
|
||||
}
|
||||
|
||||
@@ -342,7 +342,6 @@ FileSaveView::FileSaveView(
|
||||
file_{ file }
|
||||
{
|
||||
add_children({
|
||||
&labels,
|
||||
&text_path,
|
||||
&button_edit_path,
|
||||
&text_name,
|
||||
@@ -546,7 +545,6 @@ FileManagerView::FileManagerView(
|
||||
|
||||
add_children({
|
||||
&menu_view,
|
||||
&labels,
|
||||
&text_date,
|
||||
&button_rename,
|
||||
&button_delete,
|
||||
@@ -560,10 +558,16 @@ FileManagerView::FileManagerView(
|
||||
});
|
||||
|
||||
menu_view.on_highlight = [this]() {
|
||||
if (selected_is_valid())
|
||||
text_date.set(to_string_FAT_timestamp(file_created_date(get_selected_full_path())));
|
||||
else
|
||||
text_date.set("");
|
||||
if (menu_view.highlighted_index() >= max_items_shown - 1) {
|
||||
text_date.set_style(&Styles::red);
|
||||
text_date.set("Too many files!");
|
||||
} else {
|
||||
text_date.set_style(&Styles::grey);
|
||||
if (selected_is_valid())
|
||||
text_date.set((is_directory(get_selected_full_path()) ? "Created " : "Modified ") + to_string_FAT_timestamp(file_created_date(get_selected_full_path())));
|
||||
else
|
||||
text_date.set("");
|
||||
}
|
||||
};
|
||||
|
||||
refresh_list();
|
||||
|
||||
@@ -115,11 +115,6 @@ class FileManBaseView : public View {
|
||||
{0, 2 * 8, 240, 26 * 8},
|
||||
true};
|
||||
|
||||
// HACK: for item count limit.
|
||||
Text text_info{
|
||||
{1 * 8, 35 * 8, 15 * 8, 16},
|
||||
""};
|
||||
|
||||
Button button_exit{
|
||||
{22 * 8, 34 * 8, 8 * 8, 32},
|
||||
"Exit"};
|
||||
@@ -218,11 +213,8 @@ class FileManagerView : public FileManBaseView {
|
||||
// True if the selected entry is a real file item.
|
||||
bool selected_is_valid() const;
|
||||
|
||||
Labels labels{
|
||||
{{0, 26 * 8}, "Created ", Color::light_grey()}};
|
||||
|
||||
Text text_date{
|
||||
{8 * 8, 26 * 8, 19 * 8, 16},
|
||||
{0 * 8, 26 * 8, 28 * 8, 16},
|
||||
""};
|
||||
|
||||
NewButton button_rename{
|
||||
|
||||
@@ -24,18 +24,18 @@
|
||||
|
||||
#include "ui_playlist.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "convert.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "io_convert.hpp"
|
||||
|
||||
#include "oversample.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include <unistd.h>
|
||||
#include <fstream>
|
||||
|
||||
@@ -266,17 +266,16 @@ void PlaylistView::send_current_track() {
|
||||
return;
|
||||
}
|
||||
|
||||
// ReplayThread starts immediately on construction so
|
||||
// these need to be set before creating the ReplayThread.
|
||||
// Update the sample rate in proc_replay baseband.
|
||||
baseband::set_sample_rate(current()->metadata.sample_rate,
|
||||
get_oversample_rate(current()->metadata.sample_rate));
|
||||
|
||||
// ReplayThread starts immediately on construction; must be set before creating.
|
||||
transmitter_model.set_target_frequency(current()->metadata.center_frequency);
|
||||
transmitter_model.set_sampling_rate(current()->metadata.sample_rate * 8);
|
||||
transmitter_model.set_sampling_rate(get_actual_sample_rate(current()->metadata.sample_rate));
|
||||
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
transmitter_model.enable();
|
||||
|
||||
// Set baseband sample rate too for waterfall to be correct.
|
||||
// TODO: Why doesn't the transmitter_model just handle this?
|
||||
baseband::set_sample_rate(transmitter_model.sampling_rate());
|
||||
|
||||
// Reset the transmit progress bar.
|
||||
progressbar_transmit.set_value(0);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -193,8 +209,23 @@ bool ReconView::recon_load_config_from_sd() {
|
||||
break;
|
||||
case 6:
|
||||
parse_int(line, wait);
|
||||
break;
|
||||
case 7:
|
||||
if (!update_ranges) {
|
||||
parse_int(line, frequency_range.min);
|
||||
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
if (!update_ranges) {
|
||||
parse_int(line, frequency_range.max);
|
||||
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
|
||||
}
|
||||
complete = true; // NB: Last entry.
|
||||
break;
|
||||
default:
|
||||
complete = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (complete) break;
|
||||
@@ -220,6 +251,9 @@ bool ReconView::recon_save_config_to_sd() {
|
||||
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));
|
||||
settings_file.write_line(to_string_dec_uint(frequency_range.min));
|
||||
settings_file.write_line(to_string_dec_uint(frequency_range.max));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -290,19 +324,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();
|
||||
}
|
||||
@@ -562,9 +584,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();
|
||||
|
||||
@@ -839,17 +866,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 +878,6 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
chrono_end = chTimeNow();
|
||||
if (field_mode.selected_index_value() == SPEC_MODULATION) {
|
||||
time_interval = chrono_end - chrono_start;
|
||||
// capping here to avoid double check a frequency because time_interval is more than exactly 100 by a few units
|
||||
// this is because we are making a measurement and not using a fixed value
|
||||
if (time_interval > 100)
|
||||
time_interval = 100;
|
||||
if (field_lock_wait.value() == 0) {
|
||||
if (time_interval <= 1) // capping here to avoid freeze because too quick
|
||||
local_recon_lock_duration = 2; // minimum working tested value
|
||||
@@ -892,7 +905,6 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
status = 0;
|
||||
freq_lock = 0;
|
||||
timer = local_recon_lock_duration;
|
||||
big_display.set_style(&Styles::white);
|
||||
}
|
||||
if (freq_lock < recon_lock_nb_match) // LOCKING
|
||||
{
|
||||
@@ -1234,10 +1246,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);
|
||||
|
||||
@@ -70,6 +70,7 @@ class ReconView : public View {
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_recon", 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();
|
||||
|
||||
@@ -110,11 +110,11 @@ class ScannerView : public View {
|
||||
std::string title() const override { return "Scanner"; };
|
||||
|
||||
private:
|
||||
RxRadioState radio_state_{};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_scanner", app_settings::Mode::RX};
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{};
|
||||
|
||||
void start_scan_thread();
|
||||
void restart_scan();
|
||||
|
||||
@@ -360,6 +360,8 @@ TextEditorView::TextEditorView(NavigationView& nav)
|
||||
&text_size,
|
||||
});
|
||||
|
||||
viewer.set_font_zoom(enable_zoom);
|
||||
|
||||
viewer.on_select = [this]() {
|
||||
// Treat as if menu button was pressed.
|
||||
if (button_menu.on_select)
|
||||
@@ -382,7 +384,7 @@ TextEditorView::TextEditorView(NavigationView& nav)
|
||||
};
|
||||
|
||||
menu.on_zoom() = [this]() {
|
||||
viewer.toggle_font_zoom();
|
||||
enable_zoom = viewer.toggle_font_zoom();
|
||||
refresh_ui();
|
||||
hide_menu(true);
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "ui_styles.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "file_wrapper.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
@@ -80,7 +81,10 @@ class TextViewer : public Widget {
|
||||
|
||||
const Style& style() { return *font_style; }
|
||||
void set_font_zoom(bool zoom);
|
||||
void toggle_font_zoom() { set_font_zoom(!font_zoom); };
|
||||
bool toggle_font_zoom() {
|
||||
set_font_zoom(!font_zoom);
|
||||
return font_zoom;
|
||||
};
|
||||
|
||||
private:
|
||||
bool font_zoom{};
|
||||
@@ -220,6 +224,12 @@ class TextEditorView : public View {
|
||||
void on_show() override;
|
||||
|
||||
private:
|
||||
// Settings
|
||||
bool enable_zoom = false;
|
||||
SettingsStore settings_store_{
|
||||
"notepad"sv,
|
||||
{{"enable_zoom"sv, &enable_zoom}}};
|
||||
|
||||
static constexpr size_t max_edit_length = 1024;
|
||||
std::string edit_line_buffer_{};
|
||||
|
||||
|
||||
@@ -347,13 +347,8 @@ void spectrum_streaming_stop() {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_sample_rate(const uint32_t sample_rate) {
|
||||
SamplerateConfigMessage message{sample_rate};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_oversample_rate(OversampleRate oversample_rate) {
|
||||
OversampleRateConfigMessage message{oversample_rate};
|
||||
void set_sample_rate(uint32_t sample_rate, OversampleRate oversample_rate) {
|
||||
SampleRateConfigMessage message{sample_rate, oversample_rate};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,8 +94,8 @@ void shutdown();
|
||||
void spectrum_streaming_start();
|
||||
void spectrum_streaming_stop();
|
||||
|
||||
void set_sample_rate(const uint32_t sample_rate);
|
||||
void set_oversample_rate(OversampleRate oversample_rate);
|
||||
/* NB: sample_rate should be desired rate. Don't pre-scale. */
|
||||
void set_sample_rate(uint32_t sample_rate, OversampleRate oversample_rate = OversampleRate::None);
|
||||
void capture_start(CaptureConfig* const config);
|
||||
void capture_stop();
|
||||
void replay_start(ReplayConfig* const config);
|
||||
|
||||
@@ -579,6 +579,17 @@ bool is_empty_directory(const path& file_path) {
|
||||
return !((result == FR_OK) && (filinfo.fname[0] != (TCHAR)'\0'));
|
||||
}
|
||||
|
||||
int file_count(const path& directory) {
|
||||
int count{0};
|
||||
|
||||
for (auto& entry : std::filesystem::directory_iterator(directory, (const TCHAR*)u"*")) {
|
||||
(void)entry; // avoid unused warning
|
||||
++count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
space_info space(const path& p) {
|
||||
DWORD free_clusters{0};
|
||||
FATFS* fs;
|
||||
|
||||
@@ -250,6 +250,8 @@ bool file_exists(const path& file_path);
|
||||
bool is_directory(const path& file_path);
|
||||
bool is_empty_directory(const path& file_path);
|
||||
|
||||
int file_count(const path& dir_path);
|
||||
|
||||
space_info space(const path& p);
|
||||
|
||||
} /* namespace filesystem */
|
||||
|
||||
@@ -76,16 +76,19 @@ options_t freqman_bandwidths[4] = {
|
||||
// SPEC -- TODO: these should be indexes.
|
||||
{"12k5", 12500},
|
||||
{"16k", 16000},
|
||||
{"20k", 20000},
|
||||
{"25k", 25000},
|
||||
{"32k", 32000},
|
||||
{"50k", 50000},
|
||||
{"75k", 75000},
|
||||
{"100k", 100000},
|
||||
{"150k", 150000},
|
||||
{"250k", 250000},
|
||||
{"500k", 500000}, /* Previous Limit bandwith Option with perfect micro SD write .C16 format operaton.*/
|
||||
{"600k", 600000}, /* That extended option is still possible to record with FW version Mayhem v1.41 (< 2,5MB/sec) */
|
||||
{"600k", 600000}, /* We doubled x2 previous REC BW limit , now extended BW from 600k to 1M with fast enough SD card in C16 or C8 format .*/
|
||||
{"650k", 650000},
|
||||
{"750k", 750000}, /* From this BW onwards, the LCD is ok, but the recorded file is decimated, (not real file size) */
|
||||
{"1100k", 1100000},
|
||||
{"750k", 750000},
|
||||
{"1000k", 1000000}, /* New limit bandwith option for recording in C16 (in fast SD card) or in C8 */
|
||||
{"1500k", 1500000}, /* From this BW onwards, the LCD is ok, but M4 CPU is having periodical sample rec dropps, (not real file size, accelerated replay) */
|
||||
{"1750k", 1750000},
|
||||
{"2000k", 2000000},
|
||||
{"2500k", 2500000},
|
||||
@@ -267,18 +270,18 @@ std::string to_freqman_string(const freqman_entry& entry) {
|
||||
|
||||
switch (entry.type) {
|
||||
case freqman_type::Single:
|
||||
append_field("f", to_string_dec_uint64(entry.frequency_a));
|
||||
append_field("f", to_string_dec_uint(entry.frequency_a));
|
||||
break;
|
||||
case freqman_type::Range:
|
||||
append_field("a", to_string_dec_uint64(entry.frequency_a));
|
||||
append_field("b", to_string_dec_uint64(entry.frequency_b));
|
||||
append_field("a", to_string_dec_uint(entry.frequency_a));
|
||||
append_field("b", to_string_dec_uint(entry.frequency_b));
|
||||
|
||||
if (is_valid(entry.step))
|
||||
append_field("s", freqman_entry_get_step_string_short(entry.step));
|
||||
break;
|
||||
case freqman_type::HamRadio:
|
||||
append_field("r", to_string_dec_uint64(entry.frequency_a));
|
||||
append_field("t", to_string_dec_uint64(entry.frequency_b));
|
||||
append_field("r", to_string_dec_uint(entry.frequency_a));
|
||||
append_field("t", to_string_dec_uint(entry.frequency_b));
|
||||
|
||||
if (is_valid(entry.tone))
|
||||
append_field("c", tonekey::tone_key_value_string(entry.tone));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -380,33 +380,38 @@ void WaterfallView::on_audio_spectrum() {
|
||||
|
||||
} /* namespace spectrum */
|
||||
|
||||
// TODO: Comments below refer to a fixed oversample rate (8x), cleanup.
|
||||
uint32_t filter_bandwidth_for_sampling_rate(int32_t sampling_rate) {
|
||||
switch (sampling_rate) { // Use the var fs (sampling_rate) to set up BPF aprox < fs_max / 2 by Nyquist theorem.
|
||||
case 0 ... 2000000: // BW Captured range (0 <= 250kHz max) fs = 8 x 250 kHz.
|
||||
return 1750000; // Minimum BPF MAX2837 for all those lower BW options.
|
||||
switch (sampling_rate) { // Use the var fs (sampling_rate) to set up BPF aprox < fs_max / 2 by Nyquist theorem.
|
||||
case 0 ... 3'500'000: // BW Captured range (0 <= 250kHz max) fs = 8x250 kHz =2000., 16x150 khz =2400, 32x100 khz =3200, (32x75k = 2400), (future 64x40 khz =2400)
|
||||
return 1'750'000; // Minimum BPF MAX2837 for all those lower BW options.
|
||||
|
||||
case 4000000 ... 6000000: // BW capture range (500k...750kHz max) fs_max = 8 x 750kHz = 6Mhz
|
||||
// BW 500k...750kHz, ex. 500kHz (fs = 8 x BW = 4Mhz), BW 600kHz (fs = 4,8Mhz), BW 750 kHz (fs = 6Mhz).
|
||||
return 2500000; // In some IC, MAX2837 appears as 2250000, but both work similarly.
|
||||
case 4'000'000 ... 6'000'000: // BW capture range (500k...750kHz max) fs_max = 8 x 750kHz = 6Mhz
|
||||
// BW 500k...750kHz, ex. 500kHz (fs = 8 x BW = 4Mhz), BW 600kHz (fs = 4,8Mhz), BW 750 kHz (fs = 6Mhz).
|
||||
return 2'500'000; // In some IC, MAX2837 appears as 2250000, but both work similarly.
|
||||
|
||||
case 8800000: // BW capture 1,1Mhz fs = 8 x 1,1Mhz = 8,8Mhz. (1Mhz showed slightly higher noise background).
|
||||
return 3500000;
|
||||
case 8'000'000: // BW capture 1Mhz fs = 8 x 1Mhz = 8Mhz. (1Mhz showed slightly higher noise background).
|
||||
return 3'500'000; // some low SD cards, if not showing avg. writting speed >4MB/sec, they will produce sammples drop at REC with 1MB and C16 format.
|
||||
|
||||
case 14000000: // BW capture 1,75Mhz, fs = 8 x 1,75Mhz = 14Mhz
|
||||
// Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture.
|
||||
return 5000000;
|
||||
case 12'000'000: // BW capture 1,5Mhz, fs = 8 x 1,5Mhz = 12Mhz
|
||||
// Good BPF, good matching, we have some periodical M4 % samples drop.
|
||||
return 5'000'000;
|
||||
|
||||
case 16000000: // BW capture 2Mhz, fs = 8 x 2Mhz = 16Mhz
|
||||
// Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture.
|
||||
return 6000000;
|
||||
case 14'000'000: // BW capture 1,75Mhz, fs = 8 x 1,75Mhz = 14Mhz
|
||||
// Good BPF, good matching, we have some periodical M4 % samples drop.
|
||||
return 5'000'000;
|
||||
|
||||
case 20000000: // BW capture 2,5Mhz, fs = 8 x 2,5 Mhz = 20Mhz
|
||||
// Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture.
|
||||
return 7000000;
|
||||
case 16'000'000: // BW capture 2Mhz, fs = 8 x 2Mhz = 16Mhz
|
||||
// Good BPF, good matching, we have some periodical M4 % samples drop.
|
||||
return 6'000'000;
|
||||
|
||||
case 20'000'000: // BW capture 2,5Mhz, fs = 8 x 2,5 Mhz = 20Mhz
|
||||
// Good BPF, good matching, we have some periodical M4 % samples drop.
|
||||
return 7'000'000;
|
||||
|
||||
default: // BW capture 2,75Mhz, fs = 8 x 2,75Mhz = 22Mhz max ADC sampling and others.
|
||||
// We tested also 9Mhz FPB slightly too much noise floor, better at 8Mhz.
|
||||
return 8000000;
|
||||
return 8'000'000;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ using namespace portapack;
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "metadata_file.hpp"
|
||||
#include "oversample.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "utility.hpp"
|
||||
@@ -101,27 +102,28 @@ void RecordView::focus() {
|
||||
button_record.focus();
|
||||
}
|
||||
|
||||
void RecordView::set_sampling_rate(size_t new_sampling_rate, OversampleRate new_oversample_rate) {
|
||||
/* We are changing "REC" icon background to yellow in BW rec Options >600kHz
|
||||
where we are NOT recording full IQ .C16 files (recorded files are decimated ones).
|
||||
Those decimated recorded files, has not the full IQ samples.
|
||||
are ok as recorded spectrum indication, but they should not be used by Replay app.
|
||||
uint32_t RecordView::set_sampling_rate(uint32_t new_sampling_rate) {
|
||||
// Determine the oversampling needed (if any) and the actual sampling rate.
|
||||
auto oversample_rate = get_oversample_rate(new_sampling_rate);
|
||||
auto actual_sampling_rate = new_sampling_rate * toUType(oversample_rate);
|
||||
|
||||
We keep original black background in all the correct IQ .C16 files BW's Options */
|
||||
if (new_sampling_rate > 4'800'000) { // > BW >600kHz (fs=8*BW), (750kHz...2750kHz)
|
||||
/* We are changing "REC" icon background to yellow in BW rec Options >1Mhz
|
||||
* > 1Mhz BW options , we are NOT recording full IQ .C16 files (those files has some periodical missing dropped samples).
|
||||
* Those recorded files, has not the full IQ samples information, looks like decimated in file size.
|
||||
* They are ok as recorded spectrum indication, but they should not be used by Replay app. (the voice speed will be accelerated)
|
||||
|
||||
* We keep original black background in all the correct IQ .C16 files BW's Options. */
|
||||
if (actual_sampling_rate > 8'000'000) { // yellow REC button means not ok for REC, BW >1Mhz (BW from 12k5 till 1Mhz OK for REC and Replay)
|
||||
button_record.set_background(ui::Color::yellow());
|
||||
} else {
|
||||
button_record.set_background(ui::Color::black());
|
||||
}
|
||||
|
||||
if (new_sampling_rate != sampling_rate ||
|
||||
new_oversample_rate != oversample_rate) {
|
||||
if (sampling_rate != new_sampling_rate) {
|
||||
stop();
|
||||
|
||||
sampling_rate = new_sampling_rate;
|
||||
oversample_rate = new_oversample_rate;
|
||||
baseband::set_sample_rate(sampling_rate);
|
||||
baseband::set_oversample_rate(oversample_rate);
|
||||
baseband::set_sample_rate(sampling_rate, oversample_rate);
|
||||
|
||||
button_record.hidden(sampling_rate == 0);
|
||||
text_record_filename.hidden(sampling_rate == 0);
|
||||
@@ -131,6 +133,24 @@ void RecordView::set_sampling_rate(size_t new_sampling_rate, OversampleRate new_
|
||||
|
||||
update_status_display();
|
||||
}
|
||||
|
||||
return actual_sampling_rate;
|
||||
}
|
||||
|
||||
OversampleRate RecordView::get_oversample_rate(uint32_t sample_rate) {
|
||||
// No oversampling necessary for baseband audio processors.
|
||||
if (file_type == FileType::WAV)
|
||||
return OversampleRate::None;
|
||||
|
||||
auto rate = ::get_oversample_rate(sample_rate);
|
||||
|
||||
// Currently proc_capture only supports /8, /16, /32 for decimation.
|
||||
if (rate < OversampleRate::x8) // clipping while /4 is not implemented yet (it will be used >1Mhz onwards when available)
|
||||
rate = OversampleRate::x8;
|
||||
else if (rate > OversampleRate::x64) // clipping while /128 is not implemented yet , (but it is not necessary for 12k5)
|
||||
rate = OversampleRate::x64;
|
||||
|
||||
return rate;
|
||||
}
|
||||
|
||||
// Setter for datetime and frequency filename
|
||||
@@ -202,8 +222,7 @@ void RecordView::start() {
|
||||
case FileType::RawS8:
|
||||
case FileType::RawS16: {
|
||||
const auto metadata_file_error = write_metadata_file(
|
||||
get_metadata_path(base_path),
|
||||
{receiver_model.target_frequency(), sampling_rate / toUType(oversample_rate)});
|
||||
get_metadata_path(base_path), {receiver_model.target_frequency(), sampling_rate});
|
||||
if (metadata_file_error.is_valid()) {
|
||||
handle_error(metadata_file_error.value());
|
||||
return;
|
||||
@@ -278,12 +297,7 @@ void RecordView::update_status_display() {
|
||||
// - C8 captures 2 (I,Q) int8_t per sample or '2' bytes per sample.
|
||||
// - C16 captures 2 (I,Q) int16_t per sample or '4' bytes per sample.
|
||||
const auto bytes_per_sample = file_type == FileType::RawS16 ? 4 : 2;
|
||||
// WAV files are not oversampled, but C8 and C16 are. Divide by the
|
||||
// oversample rate to get the effective sample rate.
|
||||
const auto effective_sampling_rate = file_type == FileType::WAV
|
||||
? sampling_rate
|
||||
: sampling_rate / toUType(oversample_rate);
|
||||
const uint32_t bytes_per_second = effective_sampling_rate * bytes_per_sample;
|
||||
const uint32_t bytes_per_second = sampling_rate * bytes_per_sample;
|
||||
const uint32_t available_seconds = space_info.free / bytes_per_second;
|
||||
const uint32_t seconds = available_seconds % 60;
|
||||
const uint32_t available_minutes = available_seconds / 60;
|
||||
|
||||
@@ -56,16 +56,11 @@ class RecordView : public View {
|
||||
|
||||
void focus() override;
|
||||
|
||||
/* Sets the sampling rate and the oversampling "decimation" rate.
|
||||
* These values are passed down to the baseband proc_capture. For
|
||||
* Audio (WAV) recording, the OversampleRate should not be
|
||||
* specified and the default will be used. */
|
||||
/* TODO: Currently callers are expected to have already multiplied the
|
||||
* sample_rate with the oversample rate. It would be better move that
|
||||
* logic to a single place. */
|
||||
void set_sampling_rate(
|
||||
size_t new_sampling_rate,
|
||||
OversampleRate new_oversample_rate = OversampleRate::Rate8x);
|
||||
/* Sets the sampling rate for the baseband.
|
||||
* NB: Do not pre-apply any oversampling. This function will determine
|
||||
* the correct amount of oversampling and return the actual sample rate
|
||||
* that can be used to configure the radio or other UI element. */
|
||||
uint32_t set_sampling_rate(uint32_t new_sampling_rate);
|
||||
|
||||
void set_file_type(const FileType v) { file_type = v; }
|
||||
|
||||
@@ -87,6 +82,8 @@ class RecordView : public View {
|
||||
void handle_capture_thread_done(const File::Error error);
|
||||
void handle_error(const File::Error error);
|
||||
|
||||
OversampleRate get_oversample_rate(uint32_t sample_rate);
|
||||
|
||||
// bool pitch_rssi_enabled = false;
|
||||
|
||||
// Time Stamp
|
||||
@@ -98,8 +95,7 @@ class RecordView : public View {
|
||||
FileType file_type;
|
||||
const size_t write_size;
|
||||
const size_t buffer_count;
|
||||
size_t sampling_rate{0};
|
||||
OversampleRate oversample_rate{OversampleRate::Rate8x};
|
||||
uint32_t sampling_rate{0};
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
Rectangle rect_background{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class AudioTXProcessor : public BasebandProcessor {
|
||||
bool configured{false};
|
||||
uint32_t bytes_read{0};
|
||||
|
||||
void samplerate_config(const SamplerateConfigMessage& message);
|
||||
void sample_rate_config(const SampleRateConfigMessage& message);
|
||||
void audio_config(const AudioTXConfigMessage& message);
|
||||
void replay_config(const ReplayConfigMessage& message);
|
||||
|
||||
|
||||
@@ -27,9 +27,12 @@
|
||||
#include "utility.hpp"
|
||||
|
||||
CaptureProcessor::CaptureProcessor() {
|
||||
decim_0_4.configure(taps_200k_decim_0.taps, 33554432);
|
||||
decim_0_8.configure(taps_200k_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_200k_decim_1.taps, 131072);
|
||||
decim_0_4.configure(taps_200k_decim_0.taps, 33554432); // to be used with decim1 (/2), then total two stages decim (/8)
|
||||
decim_0_8.configure(taps_200k_decim_0.taps, 33554432); // to be used with decim1 (/2), then total two stages decim (/16)
|
||||
decim_0_8_180k.configure(taps_180k_wfm_decim_0.taps, 33554432); // to be used alone - no additional decim1 (/2), then total single stage decim (/8)
|
||||
|
||||
decim_1_2.configure(taps_200k_decim_1.taps, 131072);
|
||||
decim_1_8.configure(taps_16k0_decim_1.taps, 131072); // tentative decim1 /8 and taps, pending to be optimized.
|
||||
|
||||
channel_spectrum.set_decimation_factor(1);
|
||||
baseband_thread.start();
|
||||
@@ -37,8 +40,17 @@ CaptureProcessor::CaptureProcessor() {
|
||||
|
||||
void CaptureProcessor::execute(const buffer_c8_t& buffer) {
|
||||
/* 2.4576MHz, 2048 samples */
|
||||
const auto decim_0_out = decim_0_execute(buffer, dst_buffer);
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
const auto decim_0_out = decim_0_execute(buffer, dst_buffer); // selectable 3 possible decim_0, (/4. /8 200k soft filter , /8 180k sharp )
|
||||
const auto decim_1_out = decim_1_execute(decim_0_out, dst_buffer); // selectable 3 possible decim_1, (/8. /2 200k or bypassed /1 )
|
||||
|
||||
/* this code was valid when we had only 2 decim1 cases.
|
||||
const auto decim_1_out = baseband_fs < 4800'000
|
||||
? decim_1_2.execute(decim_0_out, dst_buffer) // < 600khz double decim. stage , means 500khz and lower bit rates.
|
||||
// ? decim_1_8.execute(decim_0_out, dst_buffer) // < 600khz double decim. stage , means 500khz and lower bit rates.
|
||||
: decim_0_out; // >= 600khz single decim. stage , means 600khz and upper bit rates.
|
||||
|
||||
} */
|
||||
|
||||
const auto& decimator_out = decim_1_out;
|
||||
const auto& channel = decimator_out;
|
||||
|
||||
@@ -66,19 +78,9 @@ void CaptureProcessor::on_message(const Message* const message) {
|
||||
channel_spectrum.on_message(message);
|
||||
break;
|
||||
|
||||
case Message::ID::SamplerateConfig: {
|
||||
auto config = reinterpret_cast<const SamplerateConfigMessage*>(message);
|
||||
baseband_fs = config->sample_rate;
|
||||
update_for_rate_change();
|
||||
case Message::ID::SampleRateConfig:
|
||||
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
|
||||
break;
|
||||
}
|
||||
|
||||
case Message::ID::OversampleRateConfig: {
|
||||
auto config = reinterpret_cast<const OversampleRateConfigMessage*>(message);
|
||||
oversample_rate = config->oversample_rate;
|
||||
update_for_rate_change();
|
||||
break;
|
||||
}
|
||||
|
||||
case Message::ID::CaptureConfig:
|
||||
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
|
||||
@@ -89,17 +91,54 @@ void CaptureProcessor::on_message(const Message* const message) {
|
||||
}
|
||||
}
|
||||
|
||||
void CaptureProcessor::update_for_rate_change() {
|
||||
void CaptureProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
|
||||
baseband_fs = message.sample_rate * toUType(message.oversample_rate);
|
||||
oversample_rate = message.oversample_rate;
|
||||
|
||||
baseband_thread.set_sampling_rate(baseband_fs);
|
||||
|
||||
auto decim_0_factor = oversample_rate == OversampleRate::Rate8x
|
||||
// Current fw , we are using only 2 decim_0 modes, /4 , /8
|
||||
auto decim_0_factor = oversample_rate == OversampleRate::x8
|
||||
? decim_0_4.decimation_factor
|
||||
: decim_0_8.decimation_factor;
|
||||
|
||||
size_t decim_0_output_fs = baseband_fs / decim_0_factor;
|
||||
|
||||
size_t decim_1_input_fs = decim_0_output_fs;
|
||||
size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
|
||||
|
||||
size_t decim_1_factor;
|
||||
switch (oversample_rate) { // we are using 3 decim_1 modes, /1 , /2 , /8
|
||||
case OversampleRate::x8:
|
||||
if (baseband_fs < 4800'000) {
|
||||
decim_1_factor = decim_1_2.decimation_factor; // /8 = /4x2
|
||||
} else {
|
||||
decim_1_factor = 2 * 1; // 600khz and onwards, single decim /8 = /8x1 (we applied additional *2 correction to speed up waterfall, no effect to scale spectrum)
|
||||
}
|
||||
break;
|
||||
|
||||
case OversampleRate::x16:
|
||||
decim_1_factor = 2 * decim_1_2.decimation_factor; // /16 = /8x2 (we applied additional *2 correction to increase waterfall spped >=600k and smooth & avoid abnormal motion >1M5 )
|
||||
break;
|
||||
|
||||
case OversampleRate::x32:
|
||||
decim_1_factor = 2 * decim_1_8.decimation_factor; // /32 = /4x8 (we applied additional *2 correction to speed up waterfall, no effect to scale spectrum)
|
||||
break;
|
||||
|
||||
case OversampleRate::x64:
|
||||
decim_1_factor = 8 * decim_1_8.decimation_factor; // /64 = /8x8 (we applied additional *8 correction to speed up waterfall, no effect to scale spectrum)
|
||||
break;
|
||||
|
||||
default:
|
||||
decim_1_factor = 2; // just default initial value to remove compile warning.
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
auto decim_1_factor = oversample_rate == OversampleRate::x32 // that was ok, when we had only 2 oversampling x8 , x16
|
||||
? decim_1_8.decimation_factor
|
||||
: decim_1_2.decimation_factor;
|
||||
|
||||
*/
|
||||
size_t decim_1_output_fs = decim_1_input_fs / decim_1_factor;
|
||||
|
||||
channel_filter_low_f = taps_200k_decim_1.low_frequency_normalized * decim_1_input_fs;
|
||||
channel_filter_high_f = taps_200k_decim_1.high_frequency_normalized * decim_1_input_fs;
|
||||
@@ -119,11 +158,45 @@ void CaptureProcessor::capture_config(const CaptureConfigMessage& message) {
|
||||
|
||||
buffer_c16_t CaptureProcessor::decim_0_execute(const buffer_c8_t& src, const buffer_c16_t& dst) {
|
||||
switch (oversample_rate) {
|
||||
case OversampleRate::Rate8x:
|
||||
return decim_0_4.execute(src, dst);
|
||||
case OversampleRate::x8: // we can get /8 by two means , decim0 (/4) + decim1 (/2) . or just decim0 (/8)
|
||||
if (baseband_fs < 4800'000) { // 600khz (600k x 8)
|
||||
return decim_0_4.execute(src, dst); // decim_0 , /4 with double decim stage
|
||||
} else {
|
||||
return decim_0_8_180k.execute(src, dst); // decim_0 /8 with single decim stage
|
||||
}
|
||||
|
||||
case OversampleRate::Rate16x:
|
||||
return decim_0_8.execute(src, dst);
|
||||
case OversampleRate::x16:
|
||||
return decim_0_8.execute(src, dst); // decim_0 , /8 with double decim stage
|
||||
|
||||
case OversampleRate::x32:
|
||||
return decim_0_4.execute(src, dst); // decim_0 , /4 with double decim stage
|
||||
|
||||
case OversampleRate::x64:
|
||||
return decim_0_8.execute(src, dst); // decim_0 , /8 with double decim stage
|
||||
|
||||
default:
|
||||
chDbgPanic("Unhandled OversampleRate");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
buffer_c16_t CaptureProcessor::decim_1_execute(const buffer_c16_t& src, const buffer_c16_t& dst) {
|
||||
switch (oversample_rate) {
|
||||
case OversampleRate::x8: // we can get /8 by two means , decim0 (/4) + decim1 (/2) . or just decim0 (/8)
|
||||
if (baseband_fs < 4800'000) { // 600khz (600k x 8)
|
||||
return decim_1_2.execute(src, dst);
|
||||
} else {
|
||||
return src;
|
||||
}
|
||||
|
||||
case OversampleRate::x16:
|
||||
return decim_1_2.execute(src, dst); // total decim /16 = /8x2, applied to 100khz and 150khz
|
||||
|
||||
case OversampleRate::x32:
|
||||
return decim_1_8.execute(src, dst); // total decim /32 = /4x8, appled to 75k , 50k, 32k
|
||||
|
||||
case OversampleRate::x64:
|
||||
return decim_1_8.execute(src, dst); // total decim /64 = /8x8, appled to 16k and 12k5
|
||||
|
||||
default:
|
||||
chDbgPanic("Unhandled OversampleRate");
|
||||
|
||||
@@ -42,7 +42,7 @@ class CaptureProcessor : public BasebandProcessor {
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
size_t baseband_fs = 3072000;
|
||||
size_t baseband_fs = 3072000; // aka: sample_rate
|
||||
static constexpr auto spectrum_rate_hz = 50.0f;
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
@@ -56,8 +56,10 @@ class CaptureProcessor : public BasebandProcessor {
|
||||
* use decim_0_8 for an overall decimation factor of 16. */
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0_4{};
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_8{};
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_8_180k{};
|
||||
dsp::decimate::FIRC16xR16x16Decim2 decim_1_2{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1_8{};
|
||||
|
||||
dsp::decimate::FIRC16xR16x16Decim2 decim_1{};
|
||||
int32_t channel_filter_low_f = 0;
|
||||
int32_t channel_filter_high_f = 0;
|
||||
int32_t channel_filter_transition = 0;
|
||||
@@ -67,19 +69,21 @@ class CaptureProcessor : public BasebandProcessor {
|
||||
SpectrumCollector channel_spectrum{};
|
||||
size_t spectrum_interval_samples = 0;
|
||||
size_t spectrum_samples = 0;
|
||||
OversampleRate oversample_rate{OversampleRate::Rate8x};
|
||||
OversampleRate oversample_rate{OversampleRate::x8};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
/* Called to update members when the sample rate or oversample rate is changed. */
|
||||
void update_for_rate_change();
|
||||
void sample_rate_config(const SampleRateConfigMessage& message);
|
||||
void capture_config(const CaptureConfigMessage& message);
|
||||
|
||||
/* Dispatch to the correct decim_0 based on oversample rate. */
|
||||
buffer_c16_t decim_0_execute(const buffer_c8_t& src, const buffer_c16_t& dst);
|
||||
|
||||
/* Dispatch to the correct decim_1 based on oversample rate. */
|
||||
buffer_c16_t decim_1_execute(const buffer_c16_t& src, const buffer_c16_t& dst);
|
||||
};
|
||||
|
||||
#endif /*__PROC_CAPTURE_HPP__*/
|
||||
|
||||
@@ -83,8 +83,8 @@ void GPSReplayProcessor::on_message(const Message* const message) {
|
||||
channel_spectrum.on_message(message);
|
||||
break;
|
||||
|
||||
case Message::ID::SamplerateConfig:
|
||||
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message));
|
||||
case Message::ID::SampleRateConfig:
|
||||
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
|
||||
break;
|
||||
|
||||
case Message::ID::ReplayConfig:
|
||||
@@ -103,7 +103,7 @@ void GPSReplayProcessor::on_message(const Message* const message) {
|
||||
}
|
||||
}
|
||||
|
||||
void GPSReplayProcessor::samplerate_config(const SamplerateConfigMessage& message) {
|
||||
void GPSReplayProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
|
||||
baseband_fs = message.sample_rate;
|
||||
baseband_thread.set_sampling_rate(baseband_fs);
|
||||
spectrum_interval_samples = baseband_fs / spectrum_rate_hz;
|
||||
|
||||
@@ -64,7 +64,7 @@ class GPSReplayProcessor : public BasebandProcessor {
|
||||
bool configured{false};
|
||||
uint32_t bytes_read{0};
|
||||
|
||||
void samplerate_config(const SamplerateConfigMessage& message);
|
||||
void sample_rate_config(const SampleRateConfigMessage& message);
|
||||
void replay_config(const ReplayConfigMessage& message);
|
||||
|
||||
TXProgressMessage txprogress_message{};
|
||||
|
||||
@@ -41,43 +41,75 @@ ReplayProcessor::ReplayProcessor() {
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
|
||||
/* 4MHz, 2048 samples */
|
||||
// Change to 1 to enable buffer assertions in replay.
|
||||
#define BUFFER_SIZE_ASSERT 0
|
||||
|
||||
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured || !stream) return;
|
||||
|
||||
buffer_c16_t iq_buffer{iq.data(), iq.size(), baseband_fs / 8};
|
||||
// Because this is actually adding samples, alias
|
||||
// oversample_rate so the math below is more clear.
|
||||
const size_t interpolation_factor = toUType(oversample_rate);
|
||||
|
||||
// File data is in C16 format, we need C8
|
||||
// File samplerate is 500kHz, we're at 4MHz
|
||||
// iq_buffer can only be 512 C16 samples (RAM limitation)
|
||||
// To fill up the 2048-sample C8 buffer, we need:
|
||||
// 2048 samples * 2 bytes per sample = 4096 bytes
|
||||
// Since we're oversampling by 4M/500k = 8, we only need 2048/8 = 256 samples from the file and duplicate them 8 times each
|
||||
// So 256 * 4 bytes per sample (C16) = 1024 bytes from the file
|
||||
const size_t bytes_to_read = sizeof(*buffer.p) * 2 * (buffer.count / 8); // *2 (C16), /8 (oversampling) should be == 1024
|
||||
size_t bytes_read_this_iteration = stream->read(iq_buffer.p, bytes_to_read);
|
||||
size_t oversamples_this_iteration = bytes_read_this_iteration * 8 / (sizeof(*buffer.p) * 2);
|
||||
// Wrap the IQ data array in a buffer with the correct sample_rate.
|
||||
buffer_c16_t iq_buffer{iq.data(), iq.size(), baseband_fs / interpolation_factor};
|
||||
|
||||
bytes_read += bytes_read_this_iteration;
|
||||
// The IQ data in stream is C16 format and needs to be converted to C8 (N * 2).
|
||||
// The data also needs to be interpolated so the effective sample rate is closer
|
||||
// to 4Mhz. Because interpolation repeats a sample multiple times, fewer bytes
|
||||
// are needed from the source stream in order to fill the buffer (count / oversample).
|
||||
// Together the C16->C8 conversion and the interpolation give the number of
|
||||
// bytes that need to be read from the source stream.
|
||||
const size_t samples_to_read = buffer.count / interpolation_factor;
|
||||
const size_t bytes_to_read = samples_to_read * sizeof(buffer_c16_t::Type);
|
||||
|
||||
// Fill and "stretch"
|
||||
for (size_t i = 0; i < oversamples_this_iteration; i++) {
|
||||
if (i & 7) {
|
||||
buffer.p[i] = buffer.p[i - 1];
|
||||
} else {
|
||||
auto re_out = iq_buffer.p[i >> 3].real() >> 8;
|
||||
auto im_out = iq_buffer.p[i >> 3].imag() >> 8;
|
||||
buffer.p[i] = {(int8_t)re_out, (int8_t)im_out};
|
||||
#if BUFFER_SIZE_ASSERT
|
||||
// Verify the output buffer size is divisible by the interpolation factor.
|
||||
if (samples_to_read * interpolation_factor != buffer.count)
|
||||
chDbgPanic("Output not div.");
|
||||
|
||||
// Is the input smaple buffer big enough?
|
||||
if (samples_to_read > iq_buffer.size())
|
||||
chDbgPanic("IQ buf ovf.");
|
||||
#endif
|
||||
|
||||
// Read the C16 IQ data from the source stream.
|
||||
size_t current_bytes_read = stream->read(iq_buffer.p, bytes_to_read);
|
||||
|
||||
// Compute the number of samples were actually read from the source.
|
||||
size_t samples_read = current_bytes_read / sizeof(buffer_c16_t::Type);
|
||||
|
||||
// Write converted source samples to the output buffer with interpolation.
|
||||
for (auto i = 0u; i < samples_read; ++i) {
|
||||
int8_t re_out = iq_buffer.p[i].real() >> 8;
|
||||
int8_t im_out = iq_buffer.p[i].imag() >> 8;
|
||||
auto out_value = buffer_c8_t::Type{re_out, im_out};
|
||||
|
||||
// Interpolate sample.
|
||||
for (auto j = 0u; j < interpolation_factor; ++j) {
|
||||
size_t index = i * interpolation_factor + j;
|
||||
buffer.p[index] = out_value;
|
||||
|
||||
#if BUFFER_SIZE_ASSERT
|
||||
// Verify the index is within bounds.
|
||||
if (index >= buffer.count)
|
||||
chDbgPanic("Output bounds");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
spectrum_samples += oversamples_this_iteration;
|
||||
// Update tracking stats.
|
||||
bytes_read += current_bytes_read;
|
||||
spectrum_samples += samples_read * interpolation_factor;
|
||||
|
||||
if (spectrum_samples >= spectrum_interval_samples) {
|
||||
spectrum_samples -= spectrum_interval_samples;
|
||||
channel_spectrum.feed(iq_buffer, channel_filter_low_f, channel_filter_high_f, channel_filter_transition);
|
||||
channel_spectrum.feed(
|
||||
iq_buffer, channel_filter_low_f,
|
||||
channel_filter_high_f, channel_filter_transition);
|
||||
|
||||
txprogress_message.progress = bytes_read; // Inform UI about progress
|
||||
// Inform UI about progress.
|
||||
txprogress_message.progress = bytes_read;
|
||||
txprogress_message.done = false;
|
||||
shared_memory.application_queue.push(txprogress_message);
|
||||
}
|
||||
@@ -90,8 +122,8 @@ void ReplayProcessor::on_message(const Message* const message) {
|
||||
channel_spectrum.on_message(message);
|
||||
break;
|
||||
|
||||
case Message::ID::SamplerateConfig:
|
||||
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message));
|
||||
case Message::ID::SampleRateConfig:
|
||||
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
|
||||
break;
|
||||
|
||||
case Message::ID::ReplayConfig:
|
||||
@@ -110,9 +142,11 @@ void ReplayProcessor::on_message(const Message* const message) {
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayProcessor::samplerate_config(const SamplerateConfigMessage& message) {
|
||||
baseband_fs = message.sample_rate;
|
||||
void ReplayProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
|
||||
baseband_fs = message.sample_rate * toUType(message.oversample_rate);
|
||||
oversample_rate = message.oversample_rate;
|
||||
baseband_thread.set_sampling_rate(baseband_fs);
|
||||
|
||||
spectrum_interval_samples = baseband_fs / spectrum_rate_hz;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ class ReplayProcessor : public BasebandProcessor {
|
||||
size_t baseband_fs = 3072000;
|
||||
static constexpr auto spectrum_rate_hz = 50.0f;
|
||||
|
||||
// Holds the read IQ data chunk from the file to send.
|
||||
std::array<complex16_t, 256> iq{};
|
||||
|
||||
int32_t channel_filter_low_f = 0;
|
||||
@@ -58,8 +59,9 @@ class ReplayProcessor : public BasebandProcessor {
|
||||
|
||||
bool configured{false};
|
||||
uint32_t bytes_read{0};
|
||||
OversampleRate oversample_rate = OversampleRate::x8;
|
||||
|
||||
void samplerate_config(const SamplerateConfigMessage& message);
|
||||
void sample_rate_config(const SampleRateConfigMessage& message);
|
||||
void replay_config(const ReplayConfigMessage& message);
|
||||
|
||||
TXProgressMessage txprogress_message{};
|
||||
|
||||
@@ -49,6 +49,7 @@ using Timestamp = lpc43xx::rtc::RTC;
|
||||
|
||||
template <typename T>
|
||||
struct buffer_t {
|
||||
using Type = T;
|
||||
T* const p;
|
||||
const size_t count;
|
||||
const uint32_t sampling_rate;
|
||||
|
||||
@@ -456,6 +456,9 @@ bool ILI9341::drawBMP2(const ui::Point p, const std::filesystem::path& file) {
|
||||
width = bmp_header.width;
|
||||
height = bmp_header.height;
|
||||
|
||||
if (width != 240)
|
||||
return false;
|
||||
|
||||
file_pos = bmp_header.image_data;
|
||||
|
||||
py = height + 16;
|
||||
|
||||
+30
-20
@@ -78,7 +78,7 @@ class Message {
|
||||
ReplayThreadDone = 21,
|
||||
AFSKRxConfigure = 22,
|
||||
StatusRefresh = 23,
|
||||
SamplerateConfig = 24,
|
||||
SampleRateConfig = 24,
|
||||
BTLERxConfigure = 25,
|
||||
NRFRxConfigure = 26,
|
||||
TXProgress = 27,
|
||||
@@ -111,7 +111,6 @@ class Message {
|
||||
APRSRxConfigure = 54,
|
||||
SpectrumPainterBufferRequestConfigure = 55,
|
||||
SpectrumPainterBufferResponseConfigure = 56,
|
||||
OversampleRateConfig = 57,
|
||||
MAX
|
||||
};
|
||||
|
||||
@@ -799,32 +798,43 @@ class RetuneMessage : public Message {
|
||||
uint32_t range = 0;
|
||||
};
|
||||
|
||||
class SamplerateConfigMessage : public Message {
|
||||
public:
|
||||
constexpr SamplerateConfigMessage(
|
||||
const uint32_t sample_rate)
|
||||
: Message{ID::SamplerateConfig},
|
||||
sample_rate(sample_rate) {
|
||||
}
|
||||
|
||||
const uint32_t sample_rate = 0;
|
||||
};
|
||||
|
||||
/* Controls decimation handling in proc_capture. */
|
||||
/* Oversample/Interpolation sample rate multipliers. */
|
||||
enum class OversampleRate : uint8_t {
|
||||
Rate8x = 8,
|
||||
Rate16x = 16,
|
||||
/* Use either to indicate there's no oversampling needed. */
|
||||
None = 1,
|
||||
x1 = None,
|
||||
|
||||
// 4x would make sense to have, but need to ensure it doesn't
|
||||
// overrun the IQ read buffer in proc_replay.
|
||||
|
||||
/* Oversample rate of 8 times the sample rate. */
|
||||
x8 = 8,
|
||||
|
||||
/* Oversample rate of 16 times the sample rate. */
|
||||
x16 = 16,
|
||||
|
||||
/* Oversample rate of 32 times the sample rate. */
|
||||
x32 = 32,
|
||||
|
||||
/* Oversample rate of 64 times the sample rate. */
|
||||
x64 = 64,
|
||||
|
||||
/* Oversample rate of 128 times the sample rate. */
|
||||
x128 = 128,
|
||||
};
|
||||
|
||||
class OversampleRateConfigMessage : public Message {
|
||||
class SampleRateConfigMessage : public Message {
|
||||
public:
|
||||
constexpr OversampleRateConfigMessage(
|
||||
constexpr SampleRateConfigMessage(
|
||||
uint32_t sample_rate,
|
||||
OversampleRate oversample_rate)
|
||||
: Message{ID::OversampleRateConfig},
|
||||
: Message{ID::SampleRateConfig},
|
||||
sample_rate(sample_rate),
|
||||
oversample_rate(oversample_rate) {
|
||||
}
|
||||
|
||||
const OversampleRate oversample_rate{OversampleRate::Rate8x};
|
||||
const uint32_t sample_rate = 0;
|
||||
const OversampleRate oversample_rate = OversampleRate::None;
|
||||
};
|
||||
|
||||
class AudioLevelReportMessage : public Message {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Kyle Reed, zxkmm
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
/* Helpers for handling oversampling and interpolation. */
|
||||
|
||||
#ifndef __OVERSAMPLE_H__
|
||||
#define __OVERSAMPLE_H__
|
||||
|
||||
#include "message.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
/* TODO:
|
||||
* The decision to oversample/interpolate should only be a baseband concern.
|
||||
* However, the baseband can't set up the radio (M0 code), so the apps also
|
||||
* need to know about the "actual" sample rate so the radio settings can be
|
||||
* applied correctly. Ideally the baseband would tell the apps what the
|
||||
* actual sample rate is. Currently the apps are telling the baseband and
|
||||
* that feels like a separation of concerns problem. */
|
||||
|
||||
/* HackRF suggests a minimum sample rate of 2M so a oversample rate is applied
|
||||
* to the sample rate (pre-scale) to get the sample rate closer to that target.
|
||||
* The baseband needs to know how to correctly decimate (or interpolate) so
|
||||
* the set of allowed scalars is fixed (See OversampleRate enum).
|
||||
* In testing, a minimum rate of 400kHz seems to the functional minimum.
|
||||
*
|
||||
* There are several different concepts or terms related to Capture and Replay,
|
||||
* (1) oversampling (x8, x16 ,...) / decimation (/8, /16...)
|
||||
* In Capture App , when ADC can not handle directly a requiered low sample rates ,
|
||||
* we need to apply oversampling (x8. x16 ex) , getting more real samples than needed) by "x_number" ,
|
||||
* and later to write it to SD card with the proper needed real sample rate , we apply Decimation , "/ number" .
|
||||
*
|
||||
* (2) up-sampling or re-escale or interpolation. (x8, x16, ...)
|
||||
* In Replay-list App, when we got too low bit rate data for Hackrf ,
|
||||
* we need to upsampling or interpolate or resampling to increase those low bit rates
|
||||
* to a proper sample rate higher than the min. to be able to be transmitted by Hackrf.
|
||||
*/
|
||||
|
||||
/* Gets the oversample rate for a given sample rate.
|
||||
* The oversample rate is used to increase the sample rate to improve SNR and quality.
|
||||
* This is also used as the interpolation rate when replaying captures. */
|
||||
inline OversampleRate get_oversample_rate(uint32_t sample_rate) {
|
||||
if (sample_rate < 30'000) return OversampleRate::x64; // 25k, 16k, 12k5.
|
||||
if (sample_rate < 80'000) return OversampleRate::x32; // 75k, 50k, 32k.
|
||||
if (sample_rate < 250'000) return OversampleRate::x16; // 100k and 150k.
|
||||
|
||||
return OversampleRate::x8; // 250k .. 1Mhz, that decim x8 , is already applied.(OVerSampling and decim OK)
|
||||
}
|
||||
|
||||
/* Gets the actual sample rate for a given sample rate.
|
||||
* This is the rate with the correct oversampling rate applied. */
|
||||
inline uint32_t get_actual_sample_rate(uint32_t sample_rate) {
|
||||
return sample_rate * toUType(get_oversample_rate(sample_rate));
|
||||
}
|
||||
|
||||
#endif /*__OVERSAMPLE_H__*/
|
||||
@@ -365,9 +365,13 @@ void Text::set(std::string_view value) {
|
||||
void Text::paint(Painter& painter) {
|
||||
const auto rect = screen_rect();
|
||||
auto s = has_focus() ? style().invert() : style();
|
||||
auto max_len = (unsigned)rect.width() / s.font.char_width();
|
||||
|
||||
painter.fill_rectangle(rect, s.background);
|
||||
|
||||
if (text.length() > max_len)
|
||||
text.resize(max_len);
|
||||
|
||||
painter.draw_string(
|
||||
rect.location(),
|
||||
s,
|
||||
|
||||
@@ -99,4 +99,10 @@ TEST_CASE("It should convert 8-bit.") {
|
||||
CHECK_EQ(val, 123);
|
||||
}
|
||||
|
||||
TEST_CASE("It should convert negative.") {
|
||||
int8_t val = 0;
|
||||
REQUIRE(parse_int("-64", val));
|
||||
CHECK_EQ(val, -64);
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -24,12 +24,27 @@
|
||||
|
||||
/* TODO: Tests for all string_format functions. */
|
||||
|
||||
TEST_CASE("to_string_dec_uint64 returns correct value.") {
|
||||
CHECK_EQ(to_string_dec_uint64(0), "0");
|
||||
CHECK_EQ(to_string_dec_uint64(1), "1");
|
||||
CHECK_EQ(to_string_dec_uint64(1'000'000), "1000000");
|
||||
CHECK_EQ(to_string_dec_uint64(1'234'567'890), "1234567890");
|
||||
CHECK_EQ(to_string_dec_uint64(1'234'567'891), "1234567891");
|
||||
TEST_CASE("to_string_dec_int returns correct value.") {
|
||||
CHECK_EQ(to_string_dec_int(0), "0");
|
||||
CHECK_EQ(to_string_dec_int(1), "1");
|
||||
CHECK_EQ(to_string_dec_int(-1), "-1");
|
||||
CHECK_EQ(to_string_dec_int(1'000'000), "1000000");
|
||||
CHECK_EQ(to_string_dec_int(-1'000'000), "-1000000");
|
||||
CHECK_EQ(to_string_dec_int(1'234'567'890), "1234567890");
|
||||
CHECK_EQ(to_string_dec_int(-1'234'567'890), "-1234567890");
|
||||
CHECK_EQ(to_string_dec_int(1'234'567'891), "1234567891");
|
||||
CHECK_EQ(to_string_dec_int(-1'234'567'891), "-1234567891");
|
||||
CHECK_EQ(to_string_dec_int(9'876'543'210), "9876543210");
|
||||
CHECK_EQ(to_string_dec_int(-9'876'543'210), "-9876543210");
|
||||
}
|
||||
|
||||
TEST_CASE("to_string_dec_uint returns correct value.") {
|
||||
CHECK_EQ(to_string_dec_uint(0), "0");
|
||||
CHECK_EQ(to_string_dec_uint(1), "1");
|
||||
CHECK_EQ(to_string_dec_uint(1'000'000), "1000000");
|
||||
CHECK_EQ(to_string_dec_uint(1'234'567'890), "1234567890");
|
||||
CHECK_EQ(to_string_dec_uint(1'234'567'891), "1234567891");
|
||||
CHECK_EQ(to_string_dec_uint(9'876'543'210), "9876543210");
|
||||
}
|
||||
|
||||
TEST_CASE("to_string_freq returns correct value.") {
|
||||
|
||||
Reference in New Issue
Block a user