mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-16 03:22:59 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 537cf2e79b | |||
| af424aa5f8 | |||
| 70e0f2913f | |||
| b28283271b | |||
| 4926cf8df5 | |||
| b3312a704a | |||
| 31e8019642 | |||
| 9525738118 | |||
| 62310ad9a9 | |||
| 4819a2f4e2 | |||
| 2435ee780f |
@@ -316,6 +316,7 @@ set(CPPSRC
|
||||
# ui_replay_view.cpp
|
||||
# ui_script.cpp
|
||||
ui_sd_card_debug.cpp
|
||||
config_mode.cpp
|
||||
${CPLD_20150901_DATA_CPP}
|
||||
${CPLD_20170522_DATA_CPP}
|
||||
${HACKRF_CPLD_DATA_CPP}
|
||||
|
||||
@@ -244,55 +244,64 @@ SettingsManager::SettingsManager(
|
||||
: app_name_{app_name},
|
||||
settings_{},
|
||||
bindings_{},
|
||||
loaded_{false} {
|
||||
loaded_{false},
|
||||
radio_loaded_{false} {
|
||||
settings_.mode = mode;
|
||||
settings_.options = options;
|
||||
|
||||
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);
|
||||
}
|
||||
// Additional settings should always be loaded because apps now rely
|
||||
// on being able to store UI settings, config, etc. The radio settings
|
||||
// are only loaded if the global option has been enabled.
|
||||
// Add the radio setting bindings if either load or save are enabled.
|
||||
if (portapack::persistent_memory::load_app_settings() ||
|
||||
portapack::persistent_memory::save_app_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);
|
||||
}
|
||||
// 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);
|
||||
// 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_)
|
||||
// Only copy to the radio if load was successful.
|
||||
if (loaded_ && portapack::persistent_memory::load_app_settings()) {
|
||||
radio_loaded_ = true;
|
||||
copy_to_radio_model(settings_);
|
||||
}
|
||||
|
||||
SettingsManager::~SettingsManager() {
|
||||
if (portapack::persistent_memory::save_app_settings()) {
|
||||
copy_from_radio_model(settings_);
|
||||
save_settings(app_name_, bindings_);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsManager::~SettingsManager() {
|
||||
// Only save radio settings when the option is enabled.
|
||||
if (portapack::persistent_memory::save_app_settings())
|
||||
copy_from_radio_model(settings_);
|
||||
|
||||
save_settings(app_name_, bindings_);
|
||||
}
|
||||
|
||||
} // namespace app_settings
|
||||
|
||||
@@ -150,7 +150,9 @@ 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 radio settings for an app.
|
||||
/* RAII wrapper for automatically loading and saving settings for an app.
|
||||
* "Additional" settings are always loaded, but radio settings are conditionally
|
||||
* saved/loaded if the global app settings options are enabled.
|
||||
* 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 {
|
||||
@@ -167,6 +169,9 @@ class SettingsManager {
|
||||
|
||||
/* True if settings were successfully loaded from file. */
|
||||
bool loaded() const { return loaded_; }
|
||||
|
||||
/* True if radio settings were successfully loaded from file. */
|
||||
bool radio_loaded() const { return radio_loaded_; }
|
||||
Mode mode() const { return settings_.mode; }
|
||||
|
||||
AppSettings& raw() { return settings_; }
|
||||
@@ -176,6 +181,7 @@ class SettingsManager {
|
||||
AppSettings settings_;
|
||||
SettingBindings bindings_;
|
||||
bool loaded_;
|
||||
bool radio_loaded_;
|
||||
};
|
||||
|
||||
} // namespace app_settings
|
||||
|
||||
@@ -382,7 +382,7 @@ AISAppView::AISAppView(NavigationView& nav)
|
||||
|
||||
recent_entry_detail_view.hidden(true);
|
||||
|
||||
if (!settings_.loaded())
|
||||
if (!settings_.radio_loaded())
|
||||
receiver_model.set_target_frequency(initial_target_frequency);
|
||||
|
||||
receiver_model.enable();
|
||||
|
||||
@@ -110,7 +110,7 @@ ERTAppView::ERTAppView(NavigationView&) {
|
||||
&recent_entries_view,
|
||||
});
|
||||
|
||||
if (!settings_.loaded())
|
||||
if (!settings_.radio_loaded())
|
||||
receiver_model.set_target_frequency(initial_target_frequency);
|
||||
|
||||
receiver_model.enable();
|
||||
|
||||
@@ -173,7 +173,7 @@ GpsSimAppView::GpsSimAppView(
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
if (!settings_.loaded()) {
|
||||
if (!settings_.radio_loaded()) {
|
||||
field_frequency.set_value(initial_target_frequency);
|
||||
transmitter_model.set_sampling_rate(2600000);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
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&) {
|
||||
@@ -81,7 +82,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
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();
|
||||
settings_.address_to_ignore = field_ignore.to_integer();
|
||||
|
||||
nav.pop();
|
||||
};
|
||||
@@ -105,16 +106,21 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
&field_volume,
|
||||
&image_status,
|
||||
&text_packet_count,
|
||||
&widget_baud,
|
||||
&widget_bits,
|
||||
&widget_frames,
|
||||
&button_ignore_last,
|
||||
&button_config,
|
||||
&console});
|
||||
|
||||
// No app settings, use fallbacks.
|
||||
// No app settings, use fallbacks from pmem.
|
||||
if (!app_settings_.loaded()) {
|
||||
field_frequency.set_value(initial_target_frequency);
|
||||
settings_.address_to_ignore = pmem::pocsag_ignore_address();
|
||||
settings_.enable_ignore = settings_.address_to_ignore > 0;
|
||||
}
|
||||
if (!app_settings_.radio_loaded()) {
|
||||
field_frequency.set_value(initial_target_frequency);
|
||||
}
|
||||
|
||||
logger.append(LOG_ROOT_DIR "/POCSAG.TXT");
|
||||
|
||||
@@ -148,9 +154,9 @@ POCSAGAppView::~POCSAGAppView() {
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
// Save pmem settings. TODO: Even needed anymore?
|
||||
// Save pmem settings.
|
||||
pmem::set_pocsag_ignore_address(settings_.address_to_ignore);
|
||||
pmem::set_pocsag_last_address(pocsag_state.address); // For POCSAG TX.
|
||||
pmem::set_pocsag_last_address(last_address); // For POCSAG TX.
|
||||
}
|
||||
|
||||
void POCSAGAppView::refresh_ui() {
|
||||
@@ -271,7 +277,47 @@ void POCSAGAppView::on_packet(const POCSAGPacketMessage* message) {
|
||||
image_status.set_foreground(get_status_color(pocsag_state));
|
||||
}
|
||||
|
||||
void POCSAGAppView::on_stats(const POCSAGStatsMessage*) {
|
||||
void POCSAGAppView::on_stats(const POCSAGStatsMessage* stats) {
|
||||
widget_baud.set_rate(stats->baud_rate);
|
||||
widget_bits.set_bits(stats->current_bits);
|
||||
widget_frames.set_frames(stats->current_frames);
|
||||
widget_frames.set_sync(stats->has_sync);
|
||||
}
|
||||
|
||||
void BaudIndicator::paint(Painter& painter) {
|
||||
auto p = screen_pos();
|
||||
char top = '-';
|
||||
char bot = '-';
|
||||
|
||||
if (rate_ > 0) {
|
||||
auto r = rate_ / 100;
|
||||
top = (r / 10) + '0';
|
||||
bot = (r % 10) + '0';
|
||||
}
|
||||
|
||||
painter.draw_char(p, Styles::white_small, top);
|
||||
painter.draw_char({p.x(), p.y() + 8}, Styles::white_small, bot);
|
||||
}
|
||||
|
||||
void BitsIndicator::paint(Painter&) {
|
||||
auto p = screen_pos();
|
||||
for (size_t i = 0; i < sizeof(bits_) * 8; ++i) {
|
||||
auto is_set = ((bits_ >> i) & 0x1) == 1;
|
||||
|
||||
int x = p.x() + (i / height);
|
||||
int y = p.y() + (i % height);
|
||||
display.draw_pixel({x, y}, is_set ? Color::white() : Color::black());
|
||||
}
|
||||
}
|
||||
|
||||
void FrameIndicator::paint(Painter& painter) {
|
||||
auto p = screen_pos();
|
||||
painter.draw_rectangle({p, {2, height}}, has_sync_ ? Color::green() : Color::grey());
|
||||
|
||||
for (size_t i = 0; i < height; ++i) {
|
||||
auto p2 = p + Point{2, 15 - (int)i};
|
||||
painter.draw_hline(p2, 2, i < frame_count_ ? Color::white() : Color::black());
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -52,6 +52,68 @@ class POCSAGLogger {
|
||||
|
||||
namespace ui {
|
||||
|
||||
class BaudIndicator : public Widget {
|
||||
public:
|
||||
BaudIndicator(Point position)
|
||||
: Widget{{position, {5, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_rate(uint16_t rate) {
|
||||
if (rate != rate_) {
|
||||
rate_ = rate;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint16_t rate_ = 0;
|
||||
};
|
||||
|
||||
class BitsIndicator : public Widget {
|
||||
public:
|
||||
BitsIndicator(Point position)
|
||||
: Widget{{position, {2, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_bits(uint32_t bits) {
|
||||
if (bits != bits_) {
|
||||
bits_ = bits;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint32_t bits_ = 0;
|
||||
};
|
||||
|
||||
class FrameIndicator : public Widget {
|
||||
public:
|
||||
FrameIndicator(Point position)
|
||||
: Widget{{position, {4, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_frames(uint8_t frame_count) {
|
||||
if (frame_count != frame_count_) {
|
||||
frame_count_ = frame_count;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void set_sync(bool has_sync) {
|
||||
if (has_sync != has_sync_) {
|
||||
has_sync_ = has_sync;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint8_t frame_count_ = 0;
|
||||
bool has_sync_ = false;
|
||||
};
|
||||
|
||||
struct POCSAGSettings {
|
||||
bool enable_small_font = false;
|
||||
bool enable_logging = false;
|
||||
@@ -109,12 +171,11 @@ class POCSAGSettingsView : public View {
|
||||
22,
|
||||
"Enable Ignored Address"};
|
||||
|
||||
NumberField field_ignore{
|
||||
SymField field_ignore{
|
||||
{7 * 8, 13 * 16 + 8},
|
||||
7,
|
||||
{0, 9999999},
|
||||
1,
|
||||
'0'};
|
||||
SymField::Type::Dec,
|
||||
true /*explicit_edit*/};
|
||||
|
||||
Button button_save{
|
||||
{11 * 8, 16 * 16, 10 * 8, 2 * 16},
|
||||
@@ -148,6 +209,7 @@ class POCSAGAppView : public View {
|
||||
{
|
||||
{"small_font"sv, &settings_.enable_small_font},
|
||||
{"enable_logging"sv, &settings_.enable_logging},
|
||||
{"enable_raw_log"sv, &settings_.enable_raw_log},
|
||||
{"enable_ignore"sv, &settings_.enable_ignore},
|
||||
{"address_to_ignore"sv, &settings_.address_to_ignore},
|
||||
{"hide_bad_data"sv, &settings_.hide_bad_data},
|
||||
@@ -160,7 +222,7 @@ class POCSAGAppView : public View {
|
||||
void on_packet(const POCSAGPacketMessage* message);
|
||||
void on_stats(const POCSAGStatsMessage* stats);
|
||||
|
||||
uint32_t last_address = 0xFFFFFFFF;
|
||||
uint32_t last_address = 0;
|
||||
pocsag::EccContainer ecc{};
|
||||
pocsag::POCSAGState pocsag_state{&ecc};
|
||||
POCSAGLogger logger{};
|
||||
@@ -187,7 +249,8 @@ class POCSAGAppView : public View {
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' '};
|
||||
' ',
|
||||
true /*wrap*/};
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 0 * 16}};
|
||||
|
||||
@@ -201,6 +264,15 @@ class POCSAGAppView : public View {
|
||||
{3 * 8, 1 * 16 + 2, 5 * 8, 16},
|
||||
"0"};
|
||||
|
||||
BitsIndicator widget_bits{
|
||||
{8 * 8 + 1, 1 * 16 + 2}};
|
||||
|
||||
FrameIndicator widget_frames{
|
||||
{8 * 8 + 4, 1 * 16 + 2}};
|
||||
|
||||
BaudIndicator widget_baud{
|
||||
{8 * 9 + 1, 1 * 16 + 2}};
|
||||
|
||||
Button button_ignore_last{
|
||||
{10 * 8, 1 * 16, 12 * 8, 20},
|
||||
"Ignore Last"};
|
||||
@@ -209,8 +281,9 @@ class POCSAGAppView : public View {
|
||||
{22 * 8, 1 * 16, 8 * 8, 20},
|
||||
"Config"};
|
||||
|
||||
// 54 == status bar (16) + top controls (2 * 16 + 6).
|
||||
Console console{
|
||||
{0, 2 * 16 + 6, screen_width, screen_height - 56}};
|
||||
{0, 2 * 16 + 6, screen_width, screen_height - 54}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::POCSAGPacket,
|
||||
|
||||
@@ -157,7 +157,7 @@ TPMSAppView::TPMSAppView(NavigationView&) {
|
||||
&field_vga,
|
||||
&recent_entries_view});
|
||||
|
||||
if (!settings_.loaded())
|
||||
if (!settings_.radio_loaded())
|
||||
receiver_model.set_target_frequency(initial_target_frequency);
|
||||
|
||||
receiver_model.enable();
|
||||
|
||||
@@ -186,13 +186,12 @@ ADSBSquawkView::ADSBSquawkView(
|
||||
&field_squawk});
|
||||
}
|
||||
|
||||
void ADSBSquawkView::collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list) {
|
||||
void ADSBSquawkView::collect_frames(const uint32_t /*ICAO_address*/, std::vector<ADSBFrame>& frame_list) {
|
||||
if (!enabled) return;
|
||||
|
||||
ADSBFrame temp_frame;
|
||||
(void)ICAO_address;
|
||||
|
||||
encode_frame_squawk(temp_frame, field_squawk.concatenate_4_octal_u16());
|
||||
encode_frame_squawk(temp_frame, field_squawk.to_integer());
|
||||
|
||||
frame_list.emplace_back(temp_frame);
|
||||
}
|
||||
@@ -277,7 +276,7 @@ ADSBTxView::~ADSBTxView() {
|
||||
}
|
||||
|
||||
void ADSBTxView::generate_frames() {
|
||||
const uint32_t ICAO_address = sym_icao.value_hex_u64();
|
||||
const uint32_t ICAO_address = sym_icao.to_integer();
|
||||
|
||||
frames.clear();
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ class ADSBSquawkView : public OptionTabView {
|
||||
SymField field_squawk{
|
||||
{10 * 8, 2 * 16},
|
||||
4,
|
||||
SymField::SYMFIELD_OCT};
|
||||
SymField::Type::Oct};
|
||||
};
|
||||
|
||||
class ADSBTXThread {
|
||||
@@ -235,7 +235,7 @@ class ADSBTxView : public View {
|
||||
SymField sym_icao{
|
||||
{10 * 8, 4 * 8},
|
||||
6,
|
||||
SymField::SYMFIELD_HEX};
|
||||
SymField::Type::Hex};
|
||||
|
||||
Text text_frame{
|
||||
{1 * 8, 29 * 8, 14 * 8, 16},
|
||||
|
||||
@@ -94,15 +94,15 @@ class AFSKRxView : public View {
|
||||
false};
|
||||
|
||||
Text text_debug{
|
||||
{0 * 8, 12 + 2 * 16, 240, 16},
|
||||
{0 * 8, 12 + 2 * 16, screen_width, 16},
|
||||
"DEBUG"};
|
||||
|
||||
Button button_modem_setup{
|
||||
{240 - 12 * 8, 1 * 16, 96, 24},
|
||||
{screen_width - 12 * 8, 1 * 16, 96, 24},
|
||||
"Modem setup"};
|
||||
|
||||
Console console{
|
||||
{0, 4 * 16, 240, 240}};
|
||||
{0, 4 * 16, 240, screen_width}};
|
||||
|
||||
void on_data_afsk(const AFSKDataMessage& message);
|
||||
|
||||
|
||||
@@ -48,9 +48,10 @@ APRSTXView::~APRSTXView() {
|
||||
}
|
||||
|
||||
void APRSTXView::start_tx() {
|
||||
// TODO: Clean up this API to take string_views to avoid allocations.
|
||||
make_aprs_frame(
|
||||
sym_source.value_string().c_str(), num_ssid_source.value(),
|
||||
sym_dest.value_string().c_str(), num_ssid_dest.value(),
|
||||
sym_source.to_string().c_str(), num_ssid_source.value(),
|
||||
sym_dest.to_string().c_str(), num_ssid_dest.value(),
|
||||
payload);
|
||||
|
||||
// uint8_t * bb_data_ptr = shared_memory.bb_data.data;
|
||||
|
||||
@@ -68,7 +68,8 @@ class APRSTXView : public View {
|
||||
SymField sym_source{
|
||||
{7 * 8, 1 * 16},
|
||||
6,
|
||||
SymField::SYMFIELD_ALPHANUM};
|
||||
SymField::Type::Alpha};
|
||||
|
||||
NumberField num_ssid_source{
|
||||
{19 * 8, 1 * 16},
|
||||
2,
|
||||
@@ -79,7 +80,7 @@ class APRSTXView : public View {
|
||||
SymField sym_dest{
|
||||
{7 * 8, 2 * 16},
|
||||
6,
|
||||
SymField::SYMFIELD_ALPHANUM};
|
||||
SymField::Type::Alpha};
|
||||
|
||||
NumberField num_ssid_dest{
|
||||
{19 * 8, 2 * 16},
|
||||
|
||||
@@ -42,12 +42,12 @@ CoasterPagerView::~CoasterPagerView() {
|
||||
}
|
||||
|
||||
void CoasterPagerView::generate_frame() {
|
||||
uint8_t frame[19];
|
||||
uint32_t c;
|
||||
constexpr uint8_t frame_bytes = 19;
|
||||
uint8_t frame[frame_bytes];
|
||||
|
||||
// Preamble (8 bytes)
|
||||
for (c = 0; c < 8; c++)
|
||||
frame[c] = 0x55; // Isn't this 0xAA ?
|
||||
for (uint8_t c = 0; c < 8; c++)
|
||||
frame[c] = 0x55;
|
||||
|
||||
// Sync word
|
||||
frame[8] = 0x2D;
|
||||
@@ -57,11 +57,11 @@ void CoasterPagerView::generate_frame() {
|
||||
frame[10] = 8;
|
||||
|
||||
// Data
|
||||
for (c = 0; c < 8; c++)
|
||||
frame[c + 11] = (sym_data.get_sym(c * 2) << 4) | sym_data.get_sym(c * 2 + 1);
|
||||
auto data_bytes = to_byte_array(sym_data.to_integer());
|
||||
memcpy(&frame[11], data_bytes.data(), data_bytes.size());
|
||||
|
||||
// Copy for baseband
|
||||
memcpy(shared_memory.bb_data.data, frame, 19);
|
||||
memcpy(shared_memory.bb_data.data, frame, frame_bytes);
|
||||
}
|
||||
|
||||
void CoasterPagerView::start_tx() {
|
||||
@@ -72,12 +72,7 @@ void CoasterPagerView::start_tx() {
|
||||
baseband::set_fsk_data(19 * 8, 2280000 / 1000, 5000, 32);
|
||||
}
|
||||
|
||||
void CoasterPagerView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
(void)progress;
|
||||
|
||||
uint16_t address = 0;
|
||||
uint32_t c;
|
||||
|
||||
void CoasterPagerView::on_tx_progress(const uint32_t /*progress*/, const bool done) {
|
||||
if (done) {
|
||||
if (tx_mode == SINGLE) {
|
||||
transmitter_model.disable();
|
||||
@@ -85,18 +80,12 @@ void CoasterPagerView::on_tx_progress(const uint32_t progress, const bool done)
|
||||
tx_view.set_transmitting(false);
|
||||
} else if (tx_mode == SCAN) {
|
||||
// Increment address
|
||||
|
||||
for (c = 0; c < 4; c++) {
|
||||
address <<= 4;
|
||||
address |= sym_data.get_sym(12 + c);
|
||||
}
|
||||
uint64_t data = sym_data.to_integer();
|
||||
uint16_t address = data & 0xFFFF;
|
||||
|
||||
address++;
|
||||
|
||||
for (c = 0; c < 4; c++) {
|
||||
sym_data.set_sym(15 - c, address & 0x0F);
|
||||
address >>= 4;
|
||||
}
|
||||
data = (data & 0xFFFFFFFFFFFF0000) + address;
|
||||
sym_data.set_value(data);
|
||||
|
||||
start_tx();
|
||||
}
|
||||
@@ -104,9 +93,6 @@ void CoasterPagerView::on_tx_progress(const uint32_t progress, const bool done)
|
||||
}
|
||||
|
||||
CoasterPagerView::CoasterPagerView(NavigationView& nav) {
|
||||
const uint8_t data_init[8] = {0x44, 0x01, 0x3B, 0x30, 0x30, 0x30, 0x34, 0xBC};
|
||||
uint32_t c;
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_fsktx);
|
||||
|
||||
add_children({&labels,
|
||||
@@ -115,9 +101,7 @@ CoasterPagerView::CoasterPagerView(NavigationView& nav) {
|
||||
&text_message,
|
||||
&tx_view});
|
||||
|
||||
// Bytes to nibbles
|
||||
for (c = 0; c < 16; c++)
|
||||
sym_data.set_sym(c, (data_init[c >> 1] >> ((c & 1) ? 0 : 4)) & 0x0F);
|
||||
sym_data.set_value(0x44013B30303034BC);
|
||||
|
||||
checkbox_scan.set_value(false);
|
||||
|
||||
|
||||
@@ -68,17 +68,19 @@ class CoasterPagerView : public View {
|
||||
|
||||
SymField sym_data{
|
||||
{7 * 8, 8 * 8},
|
||||
16, // 14 ? 12 ?
|
||||
SymField::SYMFIELD_HEX};
|
||||
16,
|
||||
SymField::Type::Hex};
|
||||
|
||||
Checkbox checkbox_scan{
|
||||
{10 * 8, 14 * 8},
|
||||
4,
|
||||
"Scan"};
|
||||
|
||||
/*ProgressBar progressbar {
|
||||
{ 5 * 8, 12 * 16, 20 * 8, 16 },
|
||||
};*/
|
||||
/*
|
||||
ProgressBar progressbar {
|
||||
{ 5 * 8, 12 * 16, 20 * 8, 16 }};
|
||||
*/
|
||||
|
||||
Text text_message{
|
||||
{5 * 8, 13 * 16, 20 * 8, 16},
|
||||
""};
|
||||
|
||||
@@ -49,7 +49,6 @@ EncodersConfigView::EncodersConfigView(
|
||||
&field_clk_step,
|
||||
&field_frameduration,
|
||||
&field_frameduration_step,
|
||||
&symfield_word,
|
||||
&text_format,
|
||||
&waveform});
|
||||
|
||||
@@ -64,10 +63,6 @@ EncodersConfigView::EncodersConfigView(
|
||||
options_enctype.set_options(enc_options);
|
||||
options_enctype.set_selected_index(0);
|
||||
|
||||
symfield_word.on_change = [this]() {
|
||||
generate_frame();
|
||||
};
|
||||
|
||||
// Selecting input clock changes symbol and word duration
|
||||
field_clk.on_change = [this](int32_t value) {
|
||||
// value is in kHz, new_value is in us
|
||||
@@ -98,33 +93,46 @@ void EncodersConfigView::focus() {
|
||||
}
|
||||
|
||||
void EncodersConfigView::on_type_change(size_t index) {
|
||||
std::string format_string = "";
|
||||
size_t word_length;
|
||||
char symbol_type;
|
||||
// Remove existing SymField controls.
|
||||
for (auto& symfield : symfields_word)
|
||||
remove_child(symfield.get());
|
||||
symfields_word.clear();
|
||||
|
||||
encoder_def = &encoder_defs[index];
|
||||
|
||||
field_clk.set_value(encoder_def->default_speed / 1000);
|
||||
field_repeat_min.set_value(encoder_def->repeat_min);
|
||||
|
||||
// SymField setup
|
||||
word_length = encoder_def->word_length;
|
||||
symfield_word.set_length(word_length);
|
||||
size_t n = 0, i = 0;
|
||||
while (n < word_length) {
|
||||
symbol_type = encoder_def->word_format[i++];
|
||||
if (symbol_type == 'A') {
|
||||
symfield_word.set_symbol_list(n++, encoder_def->address_symbols);
|
||||
format_string += 'A';
|
||||
} else if (symbol_type == 'D') {
|
||||
symfield_word.set_symbol_list(n++, encoder_def->data_symbols);
|
||||
format_string += 'D';
|
||||
// Add new SymFields.
|
||||
Point pos{2 * 8, 9 * 8};
|
||||
std::string format_string;
|
||||
uint8_t word_length = encoder_def->word_length;
|
||||
auto on_change_handler = [this](SymField&) {
|
||||
generate_frame();
|
||||
};
|
||||
|
||||
for (uint8_t i = 0; i < word_length; i++) {
|
||||
auto symbol_type = encoder_def->word_format[i];
|
||||
symfields_word.push_back(std::make_unique<SymField>(pos, 1));
|
||||
auto& symfield = symfields_word.back();
|
||||
symfield->on_change = on_change_handler;
|
||||
|
||||
switch (symbol_type) {
|
||||
case 'A':
|
||||
symfield->set_symbol_list(encoder_def->address_symbols);
|
||||
format_string += 'A';
|
||||
break;
|
||||
case 'D':
|
||||
symfield->set_symbol_list(encoder_def->data_symbols);
|
||||
format_string += 'D';
|
||||
break;
|
||||
}
|
||||
|
||||
add_child(symfield.get());
|
||||
pos += Point{8, 0};
|
||||
}
|
||||
|
||||
// Ugly :( Pad to erase
|
||||
format_string.append(24 - format_string.size(), ' ');
|
||||
|
||||
text_format.set(format_string);
|
||||
|
||||
generate_frame();
|
||||
@@ -146,17 +154,18 @@ void EncodersConfigView::draw_waveform() {
|
||||
}
|
||||
|
||||
void EncodersConfigView::generate_frame() {
|
||||
size_t i = 0;
|
||||
|
||||
frame_fragments.clear();
|
||||
|
||||
size_t i = 0;
|
||||
for (auto c : encoder_def->word_format) {
|
||||
if (c == 'S')
|
||||
frame_fragments += encoder_def->sync;
|
||||
else if (!c)
|
||||
else if (c == '\0')
|
||||
break;
|
||||
else
|
||||
frame_fragments += encoder_def->bit_format[symfield_word.get_sym(i++)];
|
||||
else {
|
||||
auto offset = symfields_word[i++]->get_offset(0);
|
||||
frame_fragments += encoder_def->bit_format[offset];
|
||||
}
|
||||
}
|
||||
|
||||
draw_waveform();
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
using namespace encoders;
|
||||
|
||||
namespace ui {
|
||||
@@ -113,10 +116,7 @@ class EncodersConfigView : public View {
|
||||
{"100", 100},
|
||||
{"1000", 1000}}};
|
||||
|
||||
SymField symfield_word{
|
||||
{2 * 8, 9 * 8},
|
||||
20,
|
||||
SymField::SYMFIELD_DEF};
|
||||
std::vector<std::unique_ptr<SymField>> symfields_word{};
|
||||
|
||||
Text text_format{
|
||||
{2 * 8, 11 * 8, 24 * 8, 16},
|
||||
|
||||
@@ -97,12 +97,12 @@ size_t KeyfobView::generate_frame() {
|
||||
uint64_t payload;
|
||||
|
||||
// Symfield word to frame
|
||||
payload = field_payload_a.value_hex_u64();
|
||||
payload = field_payload_a.to_integer();
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
frame[4 - i] = payload & 0xFF;
|
||||
payload >>= 8;
|
||||
}
|
||||
payload = field_payload_b.value_hex_u64();
|
||||
payload = field_payload_b.to_integer();
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
frame[9 - i] = payload & 0xFF;
|
||||
payload >>= 8;
|
||||
@@ -136,9 +136,6 @@ void KeyfobView::focus() {
|
||||
}
|
||||
|
||||
KeyfobView::~KeyfobView() {
|
||||
// save app settings
|
||||
settings.save("tx_keyfob", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
@@ -167,12 +164,12 @@ void KeyfobView::on_make_change(size_t index) {
|
||||
// DEBUG
|
||||
void KeyfobView::update_symfields() {
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
field_payload_a.set_sym(i << 1, frame[i] >> 4);
|
||||
field_payload_a.set_sym((i << 1) + 1, frame[i] & 0x0F);
|
||||
field_payload_a.set_offset(i << 1, frame[i] >> 4);
|
||||
field_payload_a.set_offset((i << 1) + 1, frame[i] & 0x0F);
|
||||
}
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
field_payload_b.set_sym(i << 1, frame[5 + i] >> 4);
|
||||
field_payload_b.set_sym((i << 1) + 1, frame[5 + i] & 0x0F);
|
||||
field_payload_b.set_offset(i << 1, frame[5 + i] >> 4);
|
||||
field_payload_b.set_offset((i << 1) + 1, frame[5 + i] & 0x0F);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,13 +209,6 @@ KeyfobView::KeyfobView(
|
||||
&progressbar,
|
||||
&tx_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_keyfob", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
frame[0] = 0x55;
|
||||
update_symfields();
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class KeyfobView : public View {
|
||||
OOK_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_keyfob", , app_settings::Mode::TX};
|
||||
"tx_keyfob", app_settings::Mode::TX};
|
||||
|
||||
// 1013210ns / bit
|
||||
static constexpr uint32_t subaru_samples_per_bit = (OOK_SAMPLERATE * 0.00101321);
|
||||
@@ -98,11 +98,11 @@ class KeyfobView : public View {
|
||||
SymField field_payload_a{
|
||||
{2 * 8, 5 * 16},
|
||||
10,
|
||||
SymField::SYMFIELD_HEX};
|
||||
SymField::Type::Hex};
|
||||
SymField field_payload_b{
|
||||
{13 * 8, 5 * 16},
|
||||
10,
|
||||
SymField::SYMFIELD_HEX};
|
||||
SymField::Type::Hex};
|
||||
|
||||
Text text_status{
|
||||
{2 * 8, 13 * 16, 128, 16},
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
@@ -41,15 +40,20 @@ ModemSetupView::ModemSetupView(
|
||||
using options_t = std::vector<option_t>;
|
||||
options_t modem_options;
|
||||
|
||||
add_children({&labels,
|
||||
&field_baudrate,
|
||||
&field_mark,
|
||||
&field_space,
|
||||
&field_repeat,
|
||||
&options_modem,
|
||||
&button_set_modem,
|
||||
&sym_format,
|
||||
&button_save});
|
||||
add_children({
|
||||
&labels,
|
||||
&field_baudrate,
|
||||
&field_mark,
|
||||
&field_space,
|
||||
&field_repeat,
|
||||
&options_modem,
|
||||
&button_set_modem,
|
||||
&sym_format_data,
|
||||
&sym_format_parity,
|
||||
&sym_format_stop,
|
||||
&sym_format_msb,
|
||||
&button_save,
|
||||
});
|
||||
|
||||
// Only list AFSK modems for now
|
||||
for (size_t i = 0; i < MODEM_DEF_COUNT; i++) {
|
||||
@@ -59,15 +63,10 @@ ModemSetupView::ModemSetupView(
|
||||
options_modem.set_options(modem_options);
|
||||
options_modem.set_selected_index(0);
|
||||
|
||||
sym_format.set_symbol_list(0, "6789"); // Data bits
|
||||
sym_format.set_symbol_list(1, "NEo"); // Parity
|
||||
sym_format.set_symbol_list(2, "012"); // Stop bits
|
||||
sym_format.set_symbol_list(3, "ML"); // MSB/LSB first
|
||||
|
||||
sym_format.set_sym(0, persistent_memory::serial_format().data_bits - 6);
|
||||
sym_format.set_sym(1, persistent_memory::serial_format().parity);
|
||||
sym_format.set_sym(2, persistent_memory::serial_format().stop_bits);
|
||||
sym_format.set_sym(3, persistent_memory::serial_format().bit_order);
|
||||
sym_format_data.set_offset(0, persistent_memory::serial_format().data_bits - 6);
|
||||
sym_format_parity.set_offset(0, persistent_memory::serial_format().parity);
|
||||
sym_format_stop.set_offset(0, persistent_memory::serial_format().stop_bits);
|
||||
sym_format_msb.set_offset(0, persistent_memory::serial_format().bit_order);
|
||||
|
||||
field_mark.set_value(persistent_memory::afsk_mark_freq());
|
||||
field_space.set_value(persistent_memory::afsk_space_freq());
|
||||
@@ -84,18 +83,17 @@ ModemSetupView::ModemSetupView(
|
||||
};
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
serial_format_t serial_format;
|
||||
|
||||
persistent_memory::set_afsk_mark(field_mark.value());
|
||||
persistent_memory::set_afsk_space(field_space.value());
|
||||
|
||||
persistent_memory::set_modem_baudrate(field_baudrate.value());
|
||||
persistent_memory::set_modem_repeat(field_repeat.value());
|
||||
|
||||
serial_format.data_bits = sym_format.get_sym(0) + 6;
|
||||
serial_format.parity = (parity_enum)sym_format.get_sym(1);
|
||||
serial_format.stop_bits = sym_format.get_sym(2);
|
||||
serial_format.bit_order = (order_enum)sym_format.get_sym(3);
|
||||
serial_format_t serial_format{};
|
||||
serial_format.data_bits = sym_format_data.get_offset(0) + 6;
|
||||
serial_format.parity = (parity_enum)sym_format_parity.get_offset(0);
|
||||
serial_format.stop_bits = sym_format_stop.get_offset(0);
|
||||
serial_format.bit_order = (order_enum)sym_format_msb.get_offset(0);
|
||||
|
||||
persistent_memory::set_serial_format(serial_format);
|
||||
|
||||
|
||||
@@ -79,10 +79,25 @@ class ModemSetupView : public View {
|
||||
7,
|
||||
{}};
|
||||
|
||||
SymField sym_format{
|
||||
SymField sym_format_data{
|
||||
{16 * 8, 22 * 8},
|
||||
4,
|
||||
SymField::SYMFIELD_DEF};
|
||||
1,
|
||||
"6789"};
|
||||
|
||||
SymField sym_format_parity{
|
||||
{17 * 8, 22 * 8},
|
||||
1,
|
||||
"NEo"};
|
||||
|
||||
SymField sym_format_stop{
|
||||
{18 * 8, 22 * 8},
|
||||
1,
|
||||
"012"};
|
||||
|
||||
SymField sym_format_msb{
|
||||
{19 * 8, 22 * 8},
|
||||
1,
|
||||
"ML"};
|
||||
|
||||
Button button_set_modem{
|
||||
{23 * 8, 6 * 8 - 4, 6 * 8, 24},
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
|
||||
#include "ui_numbers.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
@@ -75,7 +76,7 @@ void NumbersStationView::prepare_audio() {
|
||||
return;
|
||||
}
|
||||
|
||||
code = symfield_code.get_sym(code_index);
|
||||
code = symfield_code.get_offset(code_index);
|
||||
|
||||
if (code >= 10) {
|
||||
memset(audio_buffer, 0, 1024);
|
||||
@@ -144,7 +145,7 @@ void NumbersStationView::on_tick_second() {
|
||||
armed_blink = not armed_blink;
|
||||
|
||||
if (armed_blink)
|
||||
check_armed.set_style(&style_red);
|
||||
check_armed.set_style(&Styles::red);
|
||||
else
|
||||
check_armed.set_style(&style());
|
||||
|
||||
@@ -152,14 +153,12 @@ void NumbersStationView::on_tick_second() {
|
||||
}
|
||||
|
||||
void NumbersStationView::on_voice_changed(size_t index) {
|
||||
std::string code_list = "";
|
||||
std::string code_list;
|
||||
|
||||
for (const auto& wavs : voices[index].available_wavs)
|
||||
code_list += wavs.code;
|
||||
|
||||
for (uint32_t c = 0; c < 25; c++)
|
||||
symfield_code.set_symbol_list(c, code_list);
|
||||
|
||||
symfield_code.set_symbol_list(code_list);
|
||||
current_voice = &voices[index];
|
||||
}
|
||||
|
||||
@@ -265,17 +264,17 @@ NumbersStationView::NumbersStationView(
|
||||
};
|
||||
|
||||
// DEBUG
|
||||
symfield_code.set_sym(0, 10);
|
||||
symfield_code.set_sym(1, 3);
|
||||
symfield_code.set_sym(2, 4);
|
||||
symfield_code.set_sym(3, 11);
|
||||
symfield_code.set_sym(4, 6);
|
||||
symfield_code.set_sym(5, 1);
|
||||
symfield_code.set_sym(6, 9);
|
||||
symfield_code.set_sym(7, 7);
|
||||
symfield_code.set_sym(8, 8);
|
||||
symfield_code.set_sym(9, 0);
|
||||
symfield_code.set_sym(10, 12); // End
|
||||
symfield_code.set_offset(0, 10);
|
||||
symfield_code.set_offset(1, 3);
|
||||
symfield_code.set_offset(2, 4);
|
||||
symfield_code.set_offset(3, 11);
|
||||
symfield_code.set_offset(4, 6);
|
||||
symfield_code.set_offset(5, 1);
|
||||
symfield_code.set_offset(6, 9);
|
||||
symfield_code.set_offset(7, 7);
|
||||
symfield_code.set_offset(8, 8);
|
||||
symfield_code.set_offset(9, 0);
|
||||
symfield_code.set_offset(10, 12); // End
|
||||
|
||||
/*
|
||||
rtc::RTC datetime;
|
||||
|
||||
@@ -142,16 +142,19 @@ class NumbersStationView : public View {
|
||||
SymField symfield_code{
|
||||
{1 * 8, 10 * 8},
|
||||
25,
|
||||
SymField::SYMFIELD_DEF};
|
||||
SymField::Type::Custom};
|
||||
|
||||
Checkbox check_armed{
|
||||
{2 * 8, 13 * 16},
|
||||
5,
|
||||
"Armed"};
|
||||
/*Button button_tx_now {
|
||||
{ 18 * 8, 13 * 16, 10 * 8, 32 },
|
||||
"TX now"
|
||||
};*/
|
||||
|
||||
/*
|
||||
Button button_tx_now {
|
||||
{ 18 * 8, 13 * 16, 10 * 8, 32 },
|
||||
"TX now"};
|
||||
*/
|
||||
|
||||
Button button_exit{
|
||||
{21 * 8, 16 * 16, 64, 32},
|
||||
"Exit"};
|
||||
|
||||
@@ -56,7 +56,7 @@ bool POCSAGTXView::start_tx() {
|
||||
pocsag::BitRate bitrate;
|
||||
std::vector<uint32_t> codewords;
|
||||
|
||||
address = field_address.value_dec_u32();
|
||||
address = field_address.to_integer();
|
||||
if (address > 0x1FFFFFU) {
|
||||
nav_.display_modal("Bad address", "Address must be less\nthan 2097152.");
|
||||
return false;
|
||||
@@ -137,12 +137,7 @@ POCSAGTXView::POCSAGTXView(
|
||||
options_bitrate.set_selected_index(1); // 1200bps
|
||||
options_type.set_selected_index(0); // Address only
|
||||
|
||||
// TODO: set_value for whole symfield
|
||||
uint32_t reload_address = persistent_memory::pocsag_last_address();
|
||||
for (uint32_t c = 0; c < 7; c++) {
|
||||
field_address.set_sym(6 - c, reload_address % 10);
|
||||
reload_address /= 10;
|
||||
}
|
||||
field_address.set_value(persistent_memory::pocsag_last_address());
|
||||
|
||||
options_type.on_change = [this](size_t, int32_t i) {
|
||||
if (i == 2)
|
||||
|
||||
@@ -94,8 +94,7 @@ class POCSAGTXView : public View {
|
||||
|
||||
SymField field_address{
|
||||
{11 * 8, 6 * 8},
|
||||
7,
|
||||
SymField::SYMFIELD_DEC};
|
||||
7};
|
||||
|
||||
OptionsField options_type{
|
||||
{11 * 8, 8 * 8},
|
||||
|
||||
@@ -164,7 +164,7 @@ RDSView::~RDSView() {
|
||||
}
|
||||
|
||||
void RDSView::start_tx() {
|
||||
rds_flags.PI_code = sym_pi_code.value_hex_u64();
|
||||
rds_flags.PI_code = sym_pi_code.to_integer();
|
||||
rds_flags.PTY = options_pty.selected_index_value();
|
||||
rds_flags.DI = view_PSN.mono_stereo ? 1 : 0;
|
||||
rds_flags.TP = check_TP.value();
|
||||
@@ -212,12 +212,9 @@ RDSView::RDSView(
|
||||
|
||||
check_TP.set_value(true);
|
||||
|
||||
sym_pi_code.set_sym(0, 0xF);
|
||||
sym_pi_code.set_sym(1, 0x3);
|
||||
sym_pi_code.set_sym(2, 0xE);
|
||||
sym_pi_code.set_sym(3, 0x0);
|
||||
sym_pi_code.on_change = [this]() {
|
||||
rds_flags.PI_code = sym_pi_code.value_hex_u64();
|
||||
sym_pi_code.set_value(0xF3E0);
|
||||
sym_pi_code.on_change = [this](SymField&) {
|
||||
rds_flags.PI_code = sym_pi_code.to_integer();
|
||||
};
|
||||
|
||||
options_pty.set_selected_index(0); // None
|
||||
|
||||
@@ -287,7 +287,7 @@ class RDSView : public View {
|
||||
SymField sym_pi_code{
|
||||
{13 * 8, 28 + 16},
|
||||
4,
|
||||
SymField::SYMFIELD_HEX};
|
||||
SymField::Type::Hex};
|
||||
|
||||
/*OptionsField options_coverage {
|
||||
{ 17 * 8, 32 + 8 },
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -125,8 +126,30 @@ SetRadioView::SetRadioView(
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
add_children({
|
||||
&label_source,
|
||||
&value_source,
|
||||
&value_source_frequency,
|
||||
&check_clkout,
|
||||
&field_clkout_freq,
|
||||
&labels_clkout_khz,
|
||||
&value_freq_step,
|
||||
&labels_bias,
|
||||
&check_bias,
|
||||
&disable_external_tcxo, // TODO: always show?
|
||||
&button_save,
|
||||
&button_cancel,
|
||||
});
|
||||
|
||||
const auto reference = clock_manager.get_reference();
|
||||
|
||||
if (reference.source == ClockManager::ReferenceSource::Xtal) {
|
||||
add_children({
|
||||
&labels_correction,
|
||||
&field_ppm,
|
||||
});
|
||||
}
|
||||
|
||||
std::string source_name("---");
|
||||
switch (reference.source) {
|
||||
case ClockManager::ReferenceSource::Xtal:
|
||||
@@ -141,34 +164,15 @@ SetRadioView::SetRadioView(
|
||||
}
|
||||
|
||||
value_source.set(source_name);
|
||||
value_source_frequency.set(to_string_dec_uint(reference.frequency / 1000000, 2) + "." + to_string_dec_uint((reference.frequency % 1000000) / 100, 4, '0') + " MHz");
|
||||
value_source_frequency.set(
|
||||
to_string_dec_uint(reference.frequency / 1000000, 2) + "." +
|
||||
to_string_dec_uint((reference.frequency % 1000000) / 100, 4, '0') + " MHz");
|
||||
|
||||
// Make these Text controls look like Labels.
|
||||
label_source.set_style(&Styles::light_grey);
|
||||
value_source.set_style(&Styles::light_grey);
|
||||
value_source_frequency.set_style(&Styles::light_grey);
|
||||
|
||||
add_children({
|
||||
&label_source,
|
||||
&value_source,
|
||||
&value_source_frequency,
|
||||
});
|
||||
|
||||
if (reference.source == ClockManager::ReferenceSource::Xtal) {
|
||||
add_children({
|
||||
&labels_correction,
|
||||
&field_ppm,
|
||||
});
|
||||
}
|
||||
|
||||
add_children({&check_clkout,
|
||||
&field_clkout_freq,
|
||||
&labels_clkout_khz,
|
||||
&value_freq_step,
|
||||
&labels_bias,
|
||||
&check_bias,
|
||||
&button_save,
|
||||
&button_cancel});
|
||||
|
||||
SetFrequencyCorrectionModel model{
|
||||
static_cast<int8_t>(pmem::correction_ppb() / 1000), 0};
|
||||
|
||||
@@ -221,10 +225,13 @@ SetRadioView::SetRadioView(
|
||||
send_system_refresh();
|
||||
};
|
||||
|
||||
disable_external_tcxo.set_value(pmem::config_disable_external_tcxo());
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
const auto model = this->form_collect();
|
||||
pmem::set_correction_ppb(model.ppm * 1000);
|
||||
pmem::set_clkout_freq(model.freq);
|
||||
pmem::set_config_disable_external_tcxo(disable_external_tcxo.value());
|
||||
clock_manager.enable_clock_output(pmem::clkout_enabled());
|
||||
nav.pop();
|
||||
};
|
||||
@@ -339,10 +346,13 @@ void SetUIView::focus() {
|
||||
/* SetAppSettingsView ************************************/
|
||||
|
||||
SetAppSettingsView::SetAppSettingsView(NavigationView& nav) {
|
||||
add_children({&checkbox_load_app_settings,
|
||||
&checkbox_save_app_settings,
|
||||
&button_save,
|
||||
&button_cancel});
|
||||
add_children({
|
||||
&labels,
|
||||
&checkbox_load_app_settings,
|
||||
&checkbox_save_app_settings,
|
||||
&button_save,
|
||||
&button_cancel,
|
||||
});
|
||||
|
||||
checkbox_load_app_settings.set_value(pmem::load_app_settings());
|
||||
checkbox_save_app_settings.set_value(pmem::save_app_settings());
|
||||
@@ -364,11 +374,14 @@ void SetAppSettingsView::focus() {
|
||||
/* SetConverterSettingsView ******************************/
|
||||
|
||||
SetConverterSettingsView::SetConverterSettingsView(NavigationView& nav) {
|
||||
add_children({&check_show_converter,
|
||||
&check_converter,
|
||||
&converter_mode,
|
||||
&button_converter_freq,
|
||||
&button_return});
|
||||
add_children({
|
||||
&labels,
|
||||
&check_show_converter,
|
||||
&check_converter,
|
||||
&opt_converter_mode,
|
||||
&field_converter_freq,
|
||||
&button_return,
|
||||
});
|
||||
|
||||
check_show_converter.set_value(!pmem::ui_hide_converter());
|
||||
check_show_converter.on_select = [this](Checkbox&, bool v) {
|
||||
@@ -378,7 +391,7 @@ SetConverterSettingsView::SetConverterSettingsView(NavigationView& nav) {
|
||||
}
|
||||
// Retune to take converter change in account.
|
||||
receiver_model.set_target_frequency(receiver_model.target_frequency());
|
||||
// Refresh status bar with/out converter
|
||||
// Refresh status bar converter icon.
|
||||
send_system_refresh();
|
||||
};
|
||||
|
||||
@@ -389,27 +402,30 @@ SetConverterSettingsView::SetConverterSettingsView(NavigationView& nav) {
|
||||
pmem::set_ui_hide_converter(false);
|
||||
}
|
||||
pmem::set_config_converter(v);
|
||||
// Retune to take converter change in account
|
||||
// Retune to take converter change in account.
|
||||
receiver_model.set_target_frequency(receiver_model.target_frequency());
|
||||
// Refresh status bar with/out converter
|
||||
// Refresh status bar converter icon.
|
||||
send_system_refresh();
|
||||
};
|
||||
|
||||
converter_mode.set_by_value(pmem::config_updown_converter());
|
||||
converter_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
opt_converter_mode.set_by_value(pmem::config_updown_converter());
|
||||
opt_converter_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
pmem::set_config_updown_converter(v);
|
||||
// Refresh status bar with icon up or down
|
||||
// Refresh status bar with up or down icon.
|
||||
send_system_refresh();
|
||||
};
|
||||
|
||||
button_converter_freq.set_text(to_string_short_freq(pmem::config_converter_freq()) + "MHz");
|
||||
button_converter_freq.on_select = [this, &nav](Button& button) {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(pmem::config_converter_freq());
|
||||
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||
pmem::set_config_converter_freq(f);
|
||||
// Retune to take converter change in account
|
||||
receiver_model.set_target_frequency(receiver_model.target_frequency());
|
||||
button_converter_freq.set_text("<" + to_string_short_freq(f) + " MHz>");
|
||||
field_converter_freq.set_step(1'000'000);
|
||||
field_converter_freq.set_value(pmem::config_converter_freq());
|
||||
field_converter_freq.on_change = [this](rf::Frequency f) {
|
||||
pmem::set_config_converter_freq(f);
|
||||
// Retune to take converter change in account.
|
||||
receiver_model.set_target_frequency(receiver_model.target_frequency());
|
||||
};
|
||||
field_converter_freq.on_edit = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(field_converter_freq.value());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
field_converter_freq.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -425,46 +441,50 @@ void SetConverterSettingsView::focus() {
|
||||
/* SetFrequencyCorrectionView ****************************/
|
||||
|
||||
SetFrequencyCorrectionView::SetFrequencyCorrectionView(NavigationView& nav) {
|
||||
add_children({&text_freqCorrection_about,
|
||||
&frequency_rx_correction_mode,
|
||||
&frequency_tx_correction_mode,
|
||||
&button_freq_rx_correction,
|
||||
&button_freq_tx_correction,
|
||||
&button_return});
|
||||
add_children({
|
||||
&labels,
|
||||
&opt_rx_correction_mode,
|
||||
&field_rx_correction,
|
||||
&opt_tx_correction_mode,
|
||||
&field_tx_correction,
|
||||
&button_return,
|
||||
});
|
||||
|
||||
frequency_rx_correction_mode.set_by_value(pmem::config_freq_rx_correction_updown());
|
||||
frequency_rx_correction_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
opt_rx_correction_mode.set_by_value(pmem::config_freq_rx_correction_updown());
|
||||
opt_rx_correction_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
pmem::set_freq_rx_correction_updown(v);
|
||||
};
|
||||
|
||||
frequency_tx_correction_mode.set_by_value(pmem::config_freq_rx_correction_updown());
|
||||
frequency_tx_correction_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
opt_tx_correction_mode.set_by_value(pmem::config_freq_rx_correction_updown());
|
||||
opt_tx_correction_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
pmem::set_freq_tx_correction_updown(v);
|
||||
};
|
||||
|
||||
button_freq_rx_correction.set_text(to_string_short_freq(pmem::config_freq_rx_correction()) + "MHz (Rx)");
|
||||
button_freq_rx_correction.on_select = [this, &nav](Button& button) {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(pmem::config_converter_freq());
|
||||
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||
if (f >= MAX_FREQ_CORRECTION)
|
||||
f = MAX_FREQ_CORRECTION;
|
||||
pmem::set_config_freq_rx_correction(f);
|
||||
// Retune to take converter change in account
|
||||
receiver_model.set_target_frequency(receiver_model.target_frequency());
|
||||
button_freq_rx_correction.set_text("<" + to_string_short_freq(f) + " MHz>");
|
||||
field_rx_correction.set_step(100'000);
|
||||
field_rx_correction.set_value(pmem::config_freq_rx_correction());
|
||||
field_rx_correction.on_change = [this](rf::Frequency f) {
|
||||
pmem::set_config_freq_rx_correction(f);
|
||||
// Retune to take converter change in account.
|
||||
receiver_model.set_target_frequency(receiver_model.target_frequency());
|
||||
};
|
||||
field_rx_correction.on_edit = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(field_rx_correction.value());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
field_rx_correction.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
button_freq_tx_correction.set_text(to_string_short_freq(pmem::config_freq_tx_correction()) + "MHz (Tx)");
|
||||
button_freq_tx_correction.on_select = [this, &nav](Button& button) {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(pmem::config_converter_freq());
|
||||
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||
if (f >= MAX_FREQ_CORRECTION)
|
||||
f = MAX_FREQ_CORRECTION;
|
||||
pmem::set_config_freq_tx_correction(f);
|
||||
// Retune to take converter change in account
|
||||
receiver_model.set_target_frequency(receiver_model.target_frequency());
|
||||
button_freq_tx_correction.set_text("<" + to_string_short_freq(f) + " MHz>");
|
||||
field_tx_correction.set_step(100'000);
|
||||
field_tx_correction.set_value(pmem::config_freq_tx_correction());
|
||||
field_tx_correction.on_change = [this](rf::Frequency f) {
|
||||
pmem::set_config_freq_tx_correction(f);
|
||||
// Retune to take converter change in account. NB: receiver_model.
|
||||
receiver_model.set_target_frequency(receiver_model.target_frequency());
|
||||
};
|
||||
field_tx_correction.on_edit = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(field_tx_correction.value());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
field_tx_correction.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -480,49 +500,52 @@ void SetFrequencyCorrectionView::focus() {
|
||||
/* SetPersistentMemoryView *******************************/
|
||||
|
||||
SetPersistentMemoryView::SetPersistentMemoryView(NavigationView& nav) {
|
||||
add_children({&text_pmem_about,
|
||||
&text_pmem_informations,
|
||||
&text_pmem_status,
|
||||
&check_use_sdcard_for_pmem,
|
||||
&button_save_mem_to_file,
|
||||
&button_load_mem_from_file,
|
||||
&button_load_mem_defaults,
|
||||
&button_return});
|
||||
add_children({
|
||||
&labels,
|
||||
&text_pmem_status,
|
||||
&check_use_sdcard_for_pmem,
|
||||
&button_save_mem_to_file,
|
||||
&button_load_mem_from_file,
|
||||
&button_load_mem_defaults,
|
||||
&button_return,
|
||||
});
|
||||
|
||||
text_pmem_status.set_style(&Styles::yellow);
|
||||
|
||||
check_use_sdcard_for_pmem.set_value(pmem::should_use_sdcard_for_pmem());
|
||||
check_use_sdcard_for_pmem.on_select = [this](Checkbox&, bool v) {
|
||||
File pmem_flag_file_handle;
|
||||
if (v) {
|
||||
if (fs::file_exists(PMEM_FILEFLAG)) {
|
||||
text_pmem_status.set("pmem flag already present");
|
||||
text_pmem_status.set("P.Mem flag file present.");
|
||||
} else {
|
||||
auto error = pmem_flag_file_handle.create(PMEM_FILEFLAG);
|
||||
if (error)
|
||||
text_pmem_status.set("!err. creating pmem flagfile!");
|
||||
text_pmem_status.set("Error creating P.Mem File!");
|
||||
else
|
||||
text_pmem_status.set("pmem flag file created");
|
||||
text_pmem_status.set("P.Mem flag file created.");
|
||||
}
|
||||
} else {
|
||||
auto result = delete_file(PMEM_FILEFLAG);
|
||||
if (result.code() != FR_OK)
|
||||
text_pmem_status.set("!err. deleting pmem flagfile!");
|
||||
text_pmem_status.set("Error deleting P.Mem flag!");
|
||||
else
|
||||
text_pmem_status.set("pmem flag file deleted");
|
||||
text_pmem_status.set("P.Mem flag file deleted.");
|
||||
}
|
||||
};
|
||||
|
||||
button_save_mem_to_file.on_select = [&nav, this](Button&) {
|
||||
if (!pmem::save_persistent_settings_to_file())
|
||||
text_pmem_status.set("!problem saving settings!");
|
||||
text_pmem_status.set("Error saving settings!");
|
||||
else
|
||||
text_pmem_status.set("settings saved");
|
||||
text_pmem_status.set("Settings saved.");
|
||||
};
|
||||
|
||||
button_load_mem_from_file.on_select = [&nav, this](Button&) {
|
||||
if (!pmem::load_persistent_settings_from_file()) {
|
||||
text_pmem_status.set("!problem loading settings!");
|
||||
text_pmem_status.set("Error loading settings!");
|
||||
} else {
|
||||
text_pmem_status.set("settings loaded");
|
||||
text_pmem_status.set("Settings loaded.");
|
||||
// Refresh status bar with icon up or down
|
||||
send_system_refresh();
|
||||
}
|
||||
@@ -531,7 +554,7 @@ SetPersistentMemoryView::SetPersistentMemoryView(NavigationView& nav) {
|
||||
button_load_mem_defaults.on_select = [&nav, this](Button&) {
|
||||
nav.push<ModalMessageView>(
|
||||
"Warning!",
|
||||
"This will reset the p.mem\nand set the default settings",
|
||||
"This will reset the P.Mem\nto default settings.",
|
||||
YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice) {
|
||||
@@ -577,9 +600,12 @@ void SetAudioView::focus() {
|
||||
/* SetQRCodeView *****************************************/
|
||||
|
||||
SetQRCodeView::SetQRCodeView(NavigationView& nav) {
|
||||
add_children({&checkbox_bigger_qr,
|
||||
&button_save,
|
||||
&button_cancel});
|
||||
add_children({
|
||||
&labels,
|
||||
&checkbox_bigger_qr,
|
||||
&button_save,
|
||||
&button_cancel,
|
||||
});
|
||||
|
||||
checkbox_bigger_qr.set_value(pmem::show_bigger_qr_code());
|
||||
|
||||
@@ -635,7 +661,7 @@ SettingsMenuView::SettingsMenuView(NavigationView& nav) {
|
||||
{"Calibration", ui::Color::dark_cyan(), &bitmap_icon_options_touch, [&nav]() { nav.push<TouchCalibrationView>(); }},
|
||||
{"App Settings", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetAppSettingsView>(); }},
|
||||
{"Converter", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetConverterSettingsView>(); }},
|
||||
{"FreqCorrection", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetFrequencyCorrectionView>(); }},
|
||||
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetFrequencyCorrectionView>(); }},
|
||||
{"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push<SetQRCodeView>(); }},
|
||||
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<SetPersistentMemoryView>(); }},
|
||||
{"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetEncoderDialView>(); }},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -26,6 +27,7 @@
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_menu.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "bitmap.hpp"
|
||||
#include "ff.h"
|
||||
@@ -35,8 +37,6 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
#define MAX_FREQ_CORRECTION INT32_MAX
|
||||
|
||||
struct SetDateTimeModel {
|
||||
uint16_t year;
|
||||
uint8_t month;
|
||||
@@ -56,25 +56,28 @@ class SetDateTimeView : public View {
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{6 * 8, 7 * 16}, "YYYY-MM-DD HH:MM:SS", Color::grey()},
|
||||
{{10 * 8, 9 * 16}, "- - : :", Color::light_grey()}};
|
||||
{{1 * 8, 1 * 16}, "Adjust the RTC clock date &", Color::light_grey()},
|
||||
{{1 * 8, 2 * 16}, "time. If clock resets after", Color::light_grey()},
|
||||
{{1 * 8, 3 * 16}, "reboot, coin batt. is dead. ", Color::light_grey()},
|
||||
{{5 * 8, 8 * 16 - 2}, "YYYY-MM-DD HH:MM:SS", Color::grey()},
|
||||
{{9 * 8, 9 * 16}, "- - : :", Color::light_grey()}};
|
||||
|
||||
NumberField field_year{
|
||||
{6 * 8, 9 * 16},
|
||||
{5 * 8, 9 * 16},
|
||||
4,
|
||||
{2015, 2099},
|
||||
1,
|
||||
'0',
|
||||
};
|
||||
NumberField field_month{
|
||||
{11 * 8, 9 * 16},
|
||||
{10 * 8, 9 * 16},
|
||||
2,
|
||||
{1, 12},
|
||||
1,
|
||||
'0',
|
||||
};
|
||||
NumberField field_day{
|
||||
{14 * 8, 9 * 16},
|
||||
{13 * 8, 9 * 16},
|
||||
2,
|
||||
{1, 31},
|
||||
1,
|
||||
@@ -82,21 +85,21 @@ class SetDateTimeView : public View {
|
||||
};
|
||||
|
||||
NumberField field_hour{
|
||||
{17 * 8, 9 * 16},
|
||||
{16 * 8, 9 * 16},
|
||||
2,
|
||||
{0, 23},
|
||||
1,
|
||||
'0',
|
||||
};
|
||||
NumberField field_minute{
|
||||
{20 * 8, 9 * 16},
|
||||
{19 * 8, 9 * 16},
|
||||
2,
|
||||
{0, 59},
|
||||
1,
|
||||
'0',
|
||||
};
|
||||
NumberField field_second{
|
||||
{23 * 8, 9 * 16},
|
||||
{22 * 8, 9 * 16},
|
||||
2,
|
||||
{0, 59},
|
||||
1,
|
||||
@@ -131,22 +134,30 @@ class SetRadioView : public View {
|
||||
uint8_t freq_step_khz = 3;
|
||||
|
||||
Text label_source{
|
||||
{0, 1 * 16, 17 * 8, 16},
|
||||
{1 * 8, 1 * 16, 17 * 8, 16},
|
||||
"Reference Source:"};
|
||||
|
||||
Text value_source{
|
||||
{(240 - 11 * 8), 1 * 16, 11 * 8, 16},
|
||||
"---"};
|
||||
{18 * 8, 1 * 16, 11 * 8, 16},
|
||||
""};
|
||||
|
||||
Text value_source_frequency{
|
||||
{(240 - 11 * 8), 2 * 16, 11 * 8, 16},
|
||||
"---"};
|
||||
{18 * 8, 2 * 16, 11 * 8, 16},
|
||||
""};
|
||||
|
||||
Labels labels_correction{
|
||||
{{2 * 8, 3 * 16}, "Frequency correction:", Color::light_grey()},
|
||||
{{6 * 8, 4 * 16}, "PPM", Color::light_grey()},
|
||||
};
|
||||
|
||||
NumberField field_ppm{
|
||||
{2 * 8, 4 * 16},
|
||||
3,
|
||||
{-50, 50},
|
||||
1,
|
||||
'0',
|
||||
};
|
||||
|
||||
Checkbox check_clkout{
|
||||
{18, (6 * 16 - 4)},
|
||||
13,
|
||||
@@ -167,23 +178,20 @@ class SetRadioView : public View {
|
||||
"| "};
|
||||
|
||||
Labels labels_bias{
|
||||
{{24, 8 * 16}, "CAUTION: Ensure that all", Color::red()},
|
||||
{{28, 9 * 16}, "devices attached to the", Color::red()},
|
||||
{{8, 10 * 16}, "antenna connector can accept", Color::red()},
|
||||
{{68, 11 * 16}, "a DC voltage!", Color::red()}};
|
||||
|
||||
NumberField field_ppm{
|
||||
{2 * 8, 4 * 16},
|
||||
3,
|
||||
{-50, 50},
|
||||
1,
|
||||
'0',
|
||||
};
|
||||
{{4 * 8 + 4, 8 * 16}, "CAUTION: Ensure that all", Color::red()},
|
||||
{{5 * 8 + 0, 9 * 16}, "devices attached to the", Color::red()},
|
||||
{{6 * 8 + 0, 10 * 16}, "antenna connector can", Color::red()},
|
||||
{{6 * 8 + 4, 11 * 16}, "accept a DC voltage!", Color::red()}};
|
||||
|
||||
Checkbox check_bias{
|
||||
{18, 12 * 16},
|
||||
5,
|
||||
"Turn on bias voltage"};
|
||||
"Enable DC bias voltage"};
|
||||
|
||||
Checkbox disable_external_tcxo{
|
||||
{18, 14 * 16},
|
||||
5,
|
||||
"Disable external TCXO"};
|
||||
|
||||
Button button_save{
|
||||
{2 * 8, 16 * 16, 12 * 8, 32},
|
||||
@@ -307,19 +315,25 @@ class SetAppSettingsView : public View {
|
||||
SetAppSettingsView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "AppSettings"; };
|
||||
std::string title() const override { return "App Settings"; };
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{1 * 8, 1 * 16}, "App settings are saved to", Color::light_grey()},
|
||||
{{1 * 8, 2 * 16}, "the SD card in /SETTINGS.", Color::light_grey()},
|
||||
{{1 * 8, 3 * 16}, "Radio settings may also be", Color::light_grey()},
|
||||
{{1 * 8, 4 * 16}, "loaded or saved per app.", Color::light_grey()},
|
||||
};
|
||||
|
||||
Checkbox checkbox_load_app_settings{
|
||||
{3 * 8, 2 * 16},
|
||||
{3 * 8, 6 * 16},
|
||||
25,
|
||||
"Load app settings"};
|
||||
"Load radio settings"};
|
||||
|
||||
Checkbox checkbox_save_app_settings{
|
||||
{3 * 8, 4 * 16},
|
||||
{3 * 8, 8 * 16},
|
||||
25,
|
||||
"Save app settings"};
|
||||
"Save radio settings"};
|
||||
|
||||
Button button_save{
|
||||
{2 * 8, 16 * 16, 12 * 8, 32},
|
||||
@@ -340,32 +354,38 @@ class SetConverterSettingsView : public View {
|
||||
std::string title() const override { return "Converter"; };
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{1 * 8, 1 * 16}, "Options for working with", Color::light_grey()},
|
||||
{{1 * 8, 2 * 16}, "up/down converter hardware", Color::light_grey()},
|
||||
{{1 * 8, 3 * 16}, "like a Ham It Up.", Color::light_grey()},
|
||||
{{2 * 8, 9 * 16 - 2}, "Conversion frequency:", Color::light_grey()},
|
||||
{{18 * 8, 10 * 16}, "MHz", Color::light_grey()},
|
||||
};
|
||||
|
||||
Checkbox check_show_converter{
|
||||
{18, 4 * 16},
|
||||
{2 * 8, 5 * 16},
|
||||
19,
|
||||
"show/hide converter"};
|
||||
"Show converter icon"};
|
||||
|
||||
Checkbox check_converter{
|
||||
{18, 6 * 16},
|
||||
7,
|
||||
"enable/disable converter"};
|
||||
{2 * 8, 7 * 16},
|
||||
16,
|
||||
"Enable converter"};
|
||||
|
||||
OptionsField converter_mode{
|
||||
{18, 8 * 16 + 4},
|
||||
0,
|
||||
OptionsField opt_converter_mode{
|
||||
{5 * 8, 10 * 16},
|
||||
3,
|
||||
{
|
||||
{" + ", 0}, // up converter
|
||||
{" - ", 1} // down converter
|
||||
{" - ", 1}, // down converter
|
||||
}};
|
||||
|
||||
Button button_converter_freq{
|
||||
{18 + 4 * 8, 8 * 16, 16 * 8, 24},
|
||||
"",
|
||||
};
|
||||
FrequencyField field_converter_freq{
|
||||
{8 * 8, 10 * 16}};
|
||||
|
||||
Button button_return{
|
||||
{16 * 8, 16 * 16, 12 * 8, 32},
|
||||
"return",
|
||||
"Return",
|
||||
};
|
||||
};
|
||||
|
||||
@@ -375,33 +395,36 @@ class SetFrequencyCorrectionView : public View {
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "FreqCorrect"; };
|
||||
std::string title() const override { return "Freq Correct"; };
|
||||
|
||||
private:
|
||||
Text text_freqCorrection_about{
|
||||
{0, 2 * 16, 240, 16},
|
||||
"Set Frequency correction:"};
|
||||
Labels labels{
|
||||
{{1 * 8, 1 * 16}, "Frequency correction allows", Color::light_grey()},
|
||||
{{1 * 8, 2 * 16}, "RX and TX frequencies to be", Color::light_grey()},
|
||||
{{1 * 8, 3 * 16}, "adjusted for all apps.", Color::light_grey()},
|
||||
{{2 * 8, 6 * 16}, "RX Adjustment Frequency", Color::light_grey()},
|
||||
{{18 * 8, 7 * 16}, "MHz", Color::light_grey()},
|
||||
{{2 * 8, 9 * 16}, "TX Adjustment Frequency", Color::light_grey()},
|
||||
{{18 * 8, 10 * 16}, "MHz", Color::light_grey()},
|
||||
};
|
||||
|
||||
OptionsField frequency_rx_correction_mode{
|
||||
{18, 5 * 16 + 4},
|
||||
0,
|
||||
OptionsField opt_rx_correction_mode{
|
||||
{5 * 8, 7 * 16},
|
||||
3,
|
||||
{{" + ", 0},
|
||||
{" - ", 1}}};
|
||||
|
||||
OptionsField frequency_tx_correction_mode{
|
||||
{18, 9 * 16 + 4},
|
||||
0,
|
||||
FrequencyField field_rx_correction{
|
||||
{8 * 8, 7 * 16}};
|
||||
|
||||
OptionsField opt_tx_correction_mode{
|
||||
{5 * 8, 10 * 16},
|
||||
3,
|
||||
{{" + ", 0},
|
||||
{" - ", 1}}};
|
||||
|
||||
Button button_freq_rx_correction{
|
||||
{18 + 4 * 8, 5 * 16, 20 * 8, 24},
|
||||
"",
|
||||
};
|
||||
Button button_freq_tx_correction{
|
||||
{18 + 4 * 8, 9 * 16, 20 * 8, 24},
|
||||
"",
|
||||
};
|
||||
FrequencyField field_tx_correction{
|
||||
{8 * 8, 10 * 16}};
|
||||
|
||||
Button button_return{
|
||||
{16 * 8, 16 * 16, 12 * 8, 32},
|
||||
@@ -419,11 +442,14 @@ class SetAudioView : public View {
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{2 * 8, 3 * 16}, "Tone key mix: %", Color::light_grey()},
|
||||
{{1 * 8, 1 * 16}, "Controls the volume of the", Color::light_grey()},
|
||||
{{1 * 8, 2 * 16}, "tone when transmitting in", Color::light_grey()},
|
||||
{{1 * 8, 3 * 16}, "Soundboard or Mic apps.", Color::light_grey()},
|
||||
{{2 * 8, 5 * 16}, "Tone key mix: %", Color::light_grey()},
|
||||
};
|
||||
|
||||
NumberField field_tone_mix{
|
||||
{16 * 8, 3 * 16},
|
||||
{16 * 8, 5 * 16},
|
||||
2,
|
||||
{10, 99},
|
||||
1,
|
||||
@@ -448,8 +474,13 @@ class SetQRCodeView : public View {
|
||||
std::string title() const override { return "QR Code"; };
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{1 * 8, 1 * 16}, "Change the size of the QR", Color::light_grey()},
|
||||
{{1 * 8, 2 * 16}, "code shown in Radiosonde.", Color::light_grey()},
|
||||
};
|
||||
|
||||
Checkbox checkbox_bigger_qr{
|
||||
{3 * 8, 9 * 16},
|
||||
{3 * 8, 4 * 16},
|
||||
20,
|
||||
"Show large QR code"};
|
||||
|
||||
@@ -475,11 +506,13 @@ class SetEncoderDialView : public View {
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{2 * 8, 3 * 16}, "Dial sensitivity:", Color::light_grey()},
|
||||
{{1 * 8, 1 * 16}, "Adjusts how many steps to", Color::light_grey()},
|
||||
{{1 * 8, 2 * 16}, "change the encoder value.", Color::light_grey()},
|
||||
{{2 * 8, 4 * 16}, "Dial sensitivity:", Color::light_grey()},
|
||||
};
|
||||
|
||||
OptionsField field_encoder_dial_sensitivity{
|
||||
{20 * 8, 3 * 16},
|
||||
{20 * 8, 4 * 16},
|
||||
6,
|
||||
{{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
|
||||
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_NORMAL},
|
||||
@@ -504,34 +537,32 @@ class SetPersistentMemoryView : public View {
|
||||
std::string title() const override { return "P.Mem Mgmt"; };
|
||||
|
||||
private:
|
||||
Text text_pmem_about{
|
||||
{0, 1 * 16, 240, 16},
|
||||
"Persistent Memory from/to SD"};
|
||||
|
||||
Text text_pmem_informations{
|
||||
{0, 2 * 16, 240, 16},
|
||||
"use: when no/dead coin bat."};
|
||||
Labels labels{
|
||||
{{1 * 8, 1 * 16}, "Save persistent memory on SD", Color::light_grey()},
|
||||
{{1 * 8, 2 * 16}, "card. Needed when device has", Color::light_grey()},
|
||||
{{1 * 8, 3 * 16}, "dead/missing coin battery.", Color::light_grey()},
|
||||
};
|
||||
|
||||
Text text_pmem_status{
|
||||
{0, 3 * 16, 240, 16},
|
||||
{1 * 8, 4 * 16 + 8, 28 * 8, 16},
|
||||
""};
|
||||
|
||||
Checkbox check_use_sdcard_for_pmem{
|
||||
{18, 6 * 16},
|
||||
19,
|
||||
"use sdcard for p.mem"};
|
||||
{2 * 8, 6 * 16},
|
||||
21,
|
||||
"Use SD card for P.Mem"};
|
||||
|
||||
Button button_save_mem_to_file{
|
||||
{0, 8 * 16, 240, 32},
|
||||
"save p.mem to sdcard"};
|
||||
{1 * 8, 8 * 16, 28 * 8, 2 * 16},
|
||||
"Save P.Mem to SD card"};
|
||||
|
||||
Button button_load_mem_from_file{
|
||||
{0, 10 * 16 + 4, 240, 32},
|
||||
"load p.mem from sdcard"};
|
||||
{1 * 8, 10 * 16 + 2, 28 * 8, 2 * 16},
|
||||
"Load P.Mem from SD Card"};
|
||||
|
||||
Button button_load_mem_defaults{
|
||||
{0, 12 * 16 + 8, 240, 32},
|
||||
"! reset p.mem, load defaults !"};
|
||||
{1 * 8, 12 * 16 + 4, 28 * 8, 2 * 16},
|
||||
"Reset P.Mem to defaults"};
|
||||
|
||||
Button button_return{
|
||||
{16 * 8, 16 * 16, 12 * 8, 32},
|
||||
|
||||
@@ -50,7 +50,7 @@ void SigGenView::update_config() {
|
||||
}
|
||||
|
||||
void SigGenView::update_tone() {
|
||||
baseband::set_siggen_tone(symfield_tone.value_dec_u32());
|
||||
baseband::set_siggen_tone(symfield_tone.to_integer());
|
||||
}
|
||||
|
||||
void SigGenView::start_tx() {
|
||||
@@ -97,8 +97,8 @@ SigGenView::SigGenView(
|
||||
|
||||
field_stop.set_value(1);
|
||||
|
||||
symfield_tone.set_sym(1, 1); // Default: 1000 Hz
|
||||
symfield_tone.on_change = [this]() {
|
||||
symfield_tone.set_value(1000); // Default: 1000 Hz
|
||||
symfield_tone.on_change = [this](SymField&) {
|
||||
if (auto_update)
|
||||
update_tone();
|
||||
};
|
||||
|
||||
@@ -92,9 +92,8 @@ class SigGenView : public View {
|
||||
""};
|
||||
|
||||
SymField symfield_tone{
|
||||
{13 * 8, 7 * 8},
|
||||
5,
|
||||
SymField::SYMFIELD_DEC};
|
||||
{12 * 8, 7 * 8},
|
||||
5};
|
||||
|
||||
Button button_update{
|
||||
{5 * 8, 10 * 8, 8 * 8, 3 * 8},
|
||||
|
||||
@@ -66,7 +66,7 @@ SondeView::SondeView(NavigationView& nav)
|
||||
&button_see_qr,
|
||||
&button_see_map});
|
||||
|
||||
if (!settings_.loaded())
|
||||
if (!settings_.radio_loaded())
|
||||
field_frequency.set_value(initial_target_frequency);
|
||||
|
||||
field_frequency.set_step(500); // euquiq: was 10000, but we are using this for fine-tunning
|
||||
|
||||
@@ -189,7 +189,16 @@ void kill_afsk() {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_audiotx_config(const uint32_t divider, const float deviation_hz, const float audio_gain, uint8_t audio_shift_bits_s16, const uint32_t tone_key_delta, const bool am_enabled, const bool dsb_enabled, const bool usb_enabled, const bool lsb_enabled) {
|
||||
void set_audiotx_config(
|
||||
const uint32_t divider,
|
||||
const float deviation_hz,
|
||||
const float audio_gain,
|
||||
uint8_t audio_shift_bits_s16,
|
||||
const uint32_t tone_key_delta,
|
||||
const bool am_enabled,
|
||||
const bool dsb_enabled,
|
||||
const bool usb_enabled,
|
||||
const bool lsb_enabled) {
|
||||
const AudioTXConfigMessage message{
|
||||
divider,
|
||||
deviation_hz,
|
||||
|
||||
@@ -325,12 +325,18 @@ uint32_t ClockManager::measure_gp_clkin_frequency() {
|
||||
}
|
||||
|
||||
bool ClockManager::loss_of_signal() {
|
||||
return hackrf_r9
|
||||
? clock_generator.plla_loss_of_signal()
|
||||
: clock_generator.clkin_loss_of_signal();
|
||||
if (hackrf_r9) {
|
||||
const auto frequency = measure_gp_clkin_frequency();
|
||||
return (frequency < 9850000) || (frequency > 10150000);
|
||||
} else {
|
||||
return clock_generator.clkin_loss_of_signal();
|
||||
}
|
||||
}
|
||||
|
||||
ClockManager::ReferenceSource ClockManager::detect_reference_source() {
|
||||
if (portapack::persistent_memory::config_disable_external_tcxo())
|
||||
return ReferenceSource::Xtal;
|
||||
|
||||
if (loss_of_signal()) {
|
||||
// No external reference. Turn on PortaPack reference (if present).
|
||||
portapack_tcxo_enable();
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "config_mode.hpp"
|
||||
#include "core_control.hpp"
|
||||
#include "hackrf_gpio.hpp"
|
||||
#include "portapack_hal.hpp"
|
||||
|
||||
void config_mode_blink_until_dfu();
|
||||
|
||||
void config_mode_set() {
|
||||
portapack::persistent_memory::set_config_mode_storage(CONFIG_MODE_GUARD_VALUE);
|
||||
}
|
||||
|
||||
bool config_mode_should_enter() {
|
||||
return portapack::persistent_memory::config_mode_storage() == CONFIG_MODE_GUARD_VALUE;
|
||||
}
|
||||
|
||||
void config_mode_clear() {
|
||||
portapack::persistent_memory::set_config_mode_storage(CONFIG_MODE_NORMAL_VALUE);
|
||||
}
|
||||
|
||||
uint32_t blink_patterns[] = {
|
||||
0x00000000, // 0 Off
|
||||
0xFFFFFFFF, // 1 On
|
||||
0xF0F0F0F0, // 2 blink fast
|
||||
0x00FF00FF, // 3 blink slow
|
||||
0xFFF3FFF3 // 4 inverse blink slow
|
||||
};
|
||||
|
||||
void config_mode_run() {
|
||||
configure_pins_portapack();
|
||||
portapack::gpio_dfu.input();
|
||||
portapack::persistent_memory::cache::init();
|
||||
|
||||
if (hackrf_r9) {
|
||||
// When this runs on the hackrf r9 there likely was a crash during the last boot
|
||||
// caused by the external tcxo. So we disable it unless the user is intentially
|
||||
// running the config mode by pressing reset twice followed by pressing DFU.
|
||||
auto old_disable_external_tcxo = portapack::persistent_memory::config_disable_external_tcxo();
|
||||
portapack::persistent_memory::set_config_disable_external_tcxo(true);
|
||||
portapack::persistent_memory::cache::persist();
|
||||
|
||||
config_mode_blink_until_dfu();
|
||||
|
||||
portapack::persistent_memory::set_config_disable_external_tcxo(old_disable_external_tcxo);
|
||||
portapack::persistent_memory::cache::persist();
|
||||
} else {
|
||||
config_mode_blink_until_dfu();
|
||||
}
|
||||
|
||||
auto last_dfu_btn = portapack::gpio_dfu.read();
|
||||
|
||||
int32_t counter = 0;
|
||||
int8_t blink_pattern_value = portapack::persistent_memory::config_cpld() +
|
||||
(portapack::persistent_memory::config_disable_external_tcxo() ? 5 : 0);
|
||||
|
||||
while (true) {
|
||||
auto dfu_btn = portapack::gpio_dfu.read();
|
||||
auto dfu_clicked = last_dfu_btn == true && dfu_btn == false;
|
||||
last_dfu_btn = dfu_btn;
|
||||
|
||||
if (dfu_clicked) {
|
||||
int8_t value = portapack::persistent_memory::config_cpld() +
|
||||
(portapack::persistent_memory::config_disable_external_tcxo() ? 5 : 0);
|
||||
|
||||
blink_pattern_value = value = (value + 1) % 10;
|
||||
|
||||
portapack::persistent_memory::set_config_cpld(value % 5);
|
||||
portapack::persistent_memory::set_config_disable_external_tcxo((value / 5) == 1);
|
||||
|
||||
portapack::persistent_memory::cache::persist();
|
||||
}
|
||||
|
||||
auto tx_blink_pattern = blink_patterns[blink_pattern_value % 5];
|
||||
auto rx_blink_pattern = blink_patterns[blink_pattern_value / 5];
|
||||
|
||||
auto tx_value = ((tx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
|
||||
if (tx_value) {
|
||||
hackrf::one::led_tx.on();
|
||||
} else {
|
||||
hackrf::one::led_tx.off();
|
||||
}
|
||||
|
||||
auto rx_value = ((rx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
|
||||
if (rx_value) {
|
||||
hackrf::one::led_rx.on();
|
||||
} else {
|
||||
hackrf::one::led_rx.off();
|
||||
}
|
||||
|
||||
chThdSleepMilliseconds(100);
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
void config_mode_blink_until_dfu() {
|
||||
while (true) {
|
||||
hackrf::one::led_tx.on();
|
||||
hackrf::one::led_rx.on();
|
||||
hackrf::one::led_usb.on();
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
hackrf::one::led_tx.off();
|
||||
hackrf::one::led_rx.off();
|
||||
hackrf::one::led_usb.off();
|
||||
chThdSleepMilliseconds(115);
|
||||
|
||||
auto dfu_btn = portapack::gpio_dfu.read();
|
||||
if (dfu_btn)
|
||||
break;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
auto dfu_btn = portapack::gpio_dfu.read();
|
||||
if (!dfu_btn)
|
||||
break;
|
||||
}
|
||||
|
||||
chThdSleepMilliseconds(10);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __CONFIG_MODE_H__
|
||||
#define __CONFIG_MODE_H__
|
||||
|
||||
#include "ch.h"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
void config_mode_set();
|
||||
bool config_mode_should_enter();
|
||||
void config_mode_clear();
|
||||
|
||||
void config_mode_run();
|
||||
|
||||
#endif /* __CONFIG_MODE_H__ */
|
||||
@@ -115,6 +115,7 @@ Continuous (Fox-oring)
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "config_mode.hpp"
|
||||
|
||||
#include "message_queue.hpp"
|
||||
|
||||
@@ -161,9 +162,17 @@ static void event_loop() {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
if (config_mode_should_enter()) {
|
||||
config_mode_clear();
|
||||
config_mode_run();
|
||||
}
|
||||
|
||||
config_mode_set();
|
||||
|
||||
first_if.init(); /* To avoid initial short Ant_DC_Bias pulse ,we need quick set up GP01_RFF507X =1 */
|
||||
if (portapack::init()) {
|
||||
portapack::display.init();
|
||||
config_mode_clear();
|
||||
|
||||
// sdcStart(&SDCD1, nullptr); // Commented out as now happens in portapack.cpp
|
||||
|
||||
|
||||
@@ -408,6 +408,10 @@ bool init() {
|
||||
|
||||
portapack::io.init();
|
||||
|
||||
/* Cache some configuration data from persistent memory. */
|
||||
persistent_memory::cache::init();
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
clock_manager.init_clock_generator();
|
||||
|
||||
i2c0.stop();
|
||||
@@ -467,9 +471,6 @@ bool init() {
|
||||
controls_init();
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
/* Cache some configuration data from persistent memory. */
|
||||
persistent_memory::cache::init();
|
||||
|
||||
clock_manager.set_reference_ppb(persistent_memory::correction_ppb());
|
||||
clock_manager.enable_if_clocks();
|
||||
clock_manager.enable_codec_clocks();
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
/* This takes a pointer to the end of a buffer
|
||||
* and fills it backwards towards the front.
|
||||
* The return value 'q' is a pointer to the start.
|
||||
@@ -230,29 +232,31 @@ std::string to_string_time_ms(const uint32_t ms) {
|
||||
return final_str;
|
||||
}
|
||||
|
||||
static void to_string_hex_internal(char* p, const uint64_t n, const int32_t l) {
|
||||
const uint32_t d = n & 0xf;
|
||||
p[l] = (d > 9) ? (d + 55) : (d + 48);
|
||||
if (l > 0) {
|
||||
to_string_hex_internal(p, n >> 4, l - 1);
|
||||
}
|
||||
static char* to_string_hex_internal(char* ptr, uint64_t value, uint8_t length) {
|
||||
if (length == 0)
|
||||
return ptr;
|
||||
|
||||
*(--ptr) = uint_to_char(value & 0xF, 16);
|
||||
return to_string_hex_internal(ptr, value >> 4, length - 1);
|
||||
}
|
||||
|
||||
std::string to_string_hex(const uint64_t n, int32_t l) {
|
||||
char p[32];
|
||||
std::string to_string_hex(uint64_t value, int32_t length) {
|
||||
constexpr uint8_t buffer_length = 33;
|
||||
char buffer[buffer_length];
|
||||
|
||||
l = std::min<int32_t>(l, 31);
|
||||
to_string_hex_internal(p, n, l - 1);
|
||||
p[l] = 0;
|
||||
return p;
|
||||
char* ptr = &buffer[buffer_length - 1];
|
||||
*ptr = '\0';
|
||||
|
||||
length = std::min<uint8_t>(buffer_length - 1, length);
|
||||
return to_string_hex_internal(ptr, value, length);
|
||||
}
|
||||
|
||||
std::string to_string_hex_array(uint8_t* const array, const int32_t l) {
|
||||
std::string str_return = "";
|
||||
uint8_t bytes;
|
||||
std::string to_string_hex_array(uint8_t* array, int32_t length) {
|
||||
std::string str_return;
|
||||
str_return.reserve(length);
|
||||
|
||||
for (bytes = 0; bytes < l; bytes++)
|
||||
str_return += to_string_hex(array[bytes], 2);
|
||||
for (uint8_t i = 0; i < length; i++)
|
||||
str_return += to_string_hex(array[i], 2);
|
||||
|
||||
return str_return;
|
||||
}
|
||||
@@ -364,3 +368,26 @@ std::string trimr(std::string_view str) {
|
||||
std::string truncate(std::string_view str, size_t length) {
|
||||
return std::string{str.length() <= length ? str : str.substr(0, length)};
|
||||
}
|
||||
|
||||
uint8_t char_to_uint(char c, uint8_t radix) {
|
||||
uint8_t v = 0;
|
||||
|
||||
if (c >= '0' && c <= '9')
|
||||
v = c - '0';
|
||||
else if (c >= 'A' && c <= 'F')
|
||||
v = c - 'A' + 10; // A is dec: 10
|
||||
else if (c >= 'a' && c <= 'f')
|
||||
v = c - 'a' + 10; // A is dec: 10
|
||||
|
||||
return v < radix ? v : 0;
|
||||
}
|
||||
|
||||
char uint_to_char(uint8_t val, uint8_t radix) {
|
||||
if (val >= radix)
|
||||
return 0;
|
||||
|
||||
if (val < 10)
|
||||
return '0' + val;
|
||||
else
|
||||
return 'A' + val - 10; // A is dec: 10
|
||||
}
|
||||
|
||||
@@ -50,14 +50,19 @@ char* to_string_dec_uint(uint64_t n, StringFormatBuffer& buffer, size_t& length)
|
||||
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, 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);
|
||||
std::string to_string_hex_array(uint8_t* const array, const int32_t l = 0);
|
||||
std::string to_string_hex(uint64_t n, int32_t length);
|
||||
std::string to_string_hex_array(uint8_t* array, int32_t length);
|
||||
|
||||
/* Helper to select length based on type size. */
|
||||
template <typename T>
|
||||
std::string to_string_hex(T n) {
|
||||
return to_string_hex(n, sizeof(T) * 2); // Two digits/byte.
|
||||
}
|
||||
|
||||
std::string to_string_freq(const uint64_t f);
|
||||
std::string to_string_short_freq(const uint64_t f);
|
||||
@@ -78,4 +83,12 @@ std::string trim(std::string_view str); // Remove whitespace at ends.
|
||||
std::string trimr(std::string_view str); // Remove trailing spaces
|
||||
std::string truncate(std::string_view, size_t length);
|
||||
|
||||
/* Gets the int value for a character given the radix.
|
||||
* e.g. '5' => 5, 'D' => 13. Out of bounds => 0. */
|
||||
uint8_t char_to_uint(char c, uint8_t radix = 10);
|
||||
|
||||
/* Gets the int value for a character given the radix.
|
||||
* e.g. 5 => '5', 13 => 'D'. Out of bounds => '\0'. */
|
||||
char uint_to_char(uint8_t val, uint8_t radix = 10);
|
||||
|
||||
#endif /*__STRING_FORMAT_H__*/
|
||||
|
||||
@@ -449,7 +449,7 @@ DeclareTargets(PPOC pocsag)
|
||||
set(MODE_CPPSRC
|
||||
proc_pocsag2.cpp
|
||||
)
|
||||
DeclareTargets(PPOC pocsag2)
|
||||
DeclareTargets(PPO2 pocsag2)
|
||||
|
||||
### RDS
|
||||
|
||||
|
||||
@@ -29,20 +29,19 @@ bool FMSquelch::execute(const buffer_f32_t& audio) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: No hard-coded array size.
|
||||
std::array<float, N> squelch_energy_buffer;
|
||||
const buffer_f32_t squelch_energy{
|
||||
squelch_energy_buffer.data(),
|
||||
squelch_energy_buffer.size()};
|
||||
// TODO: alloca temp buffer, assert audio.count
|
||||
std::array<float, 32> squelch_energy_buffer;
|
||||
const buffer_f32_t squelch_energy{squelch_energy_buffer.data(), audio.count};
|
||||
non_audio_hpf.execute(audio, squelch_energy);
|
||||
|
||||
// "Non-audio" implies "noise" here. Find the loudest noise sample.
|
||||
float non_audio_max_squared = 0;
|
||||
for (const auto sample : squelch_energy_buffer) {
|
||||
const float sample_squared = sample * sample;
|
||||
if (sample_squared > non_audio_max_squared) {
|
||||
for (size_t i = 0; i < squelch_energy.count; ++i) {
|
||||
auto sample = squelch_energy.p[i];
|
||||
float sample_squared = sample * sample;
|
||||
|
||||
if (sample_squared > non_audio_max_squared)
|
||||
non_audio_max_squared = sample_squared;
|
||||
}
|
||||
}
|
||||
|
||||
// Is the noise less than the threshold?
|
||||
|
||||
@@ -39,7 +39,6 @@ class FMSquelch {
|
||||
bool enabled() const;
|
||||
|
||||
private:
|
||||
static constexpr size_t N = 32;
|
||||
float threshold_squared{0.0f};
|
||||
|
||||
IIRBiquadFilter non_audio_hpf{non_audio_hpf_config};
|
||||
|
||||
@@ -56,6 +56,12 @@ void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
|
||||
processDemodulatedSamples(audio.p, 16);
|
||||
extractFrames();
|
||||
|
||||
samples_processed += buffer.count;
|
||||
if (samples_processed >= stat_update_threshold) {
|
||||
send_stats();
|
||||
samples_processed = 0;
|
||||
}
|
||||
|
||||
// Clear the output before sending to audio chip.
|
||||
// Only clear the audio buffer when there hasn't been any audio for a while.
|
||||
if (squelch_.enabled() && squelch_history == 0) {
|
||||
@@ -137,6 +143,11 @@ void POCSAGProcessor::configure() {
|
||||
configured = true;
|
||||
}
|
||||
|
||||
void POCSAGProcessor::send_stats() const {
|
||||
POCSAGStatsMessage message(m_fifo.codeword, m_numCode, m_gotSync, getRate());
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Frame extractraction methods
|
||||
// -----------------------------
|
||||
@@ -511,7 +522,7 @@ int POCSAGProcessor::getNoOfBits() {
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
uint32_t POCSAGProcessor::getRate() {
|
||||
uint32_t POCSAGProcessor::getRate() const {
|
||||
return ((m_samplesPerSec << 10) + 512) / m_lastStableSymbolLen_1024;
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,9 @@ class POCSAGProcessor : public BasebandProcessor {
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 3072000;
|
||||
static constexpr uint8_t stat_update_interval = 10;
|
||||
static constexpr uint32_t stat_update_threshold =
|
||||
baseband_fs / stat_update_interval;
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
@@ -158,7 +161,10 @@ class POCSAGProcessor : public BasebandProcessor {
|
||||
bool configured = false;
|
||||
pocsag::POCSAGPacket packet{};
|
||||
|
||||
uint32_t samples_processed = 0;
|
||||
|
||||
void configure();
|
||||
void send_stats() const;
|
||||
|
||||
// ----------------------------------------
|
||||
// Frame extractraction methods and members
|
||||
@@ -181,7 +187,7 @@ class POCSAGProcessor : public BasebandProcessor {
|
||||
short getBit();
|
||||
|
||||
int getNoOfBits();
|
||||
uint32_t getRate();
|
||||
uint32_t getRate() const;
|
||||
|
||||
uint32_t m_averageSymbolLen_1024{0};
|
||||
uint32_t m_lastStableSymbolLen_1024{0};
|
||||
|
||||
+326
-428
@@ -3,7 +3,7 @@
|
||||
* Copyright (C) 2012-2014 Elias Oenal (multimon-ng@eliasoenal.com)
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2016 Kyle Reed
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
#include "proc_pocsag2.hpp"
|
||||
|
||||
#include "dsp_iir_config.hpp"
|
||||
#include "event_m4.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -33,59 +32,320 @@
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
/* Count of bits that differ between the two values. */
|
||||
uint8_t diff_bit_count(uint32_t left, uint32_t right) {
|
||||
uint32_t diff = left ^ right;
|
||||
uint8_t count = 0;
|
||||
for (size_t i = 0; i < sizeof(diff) * 8; ++i) {
|
||||
if (((diff >> i) & 0x1) == 1)
|
||||
++count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/* AudioNormalizer ***************************************/
|
||||
|
||||
void AudioNormalizer::execute_in_place(const buffer_f32_t& audio) {
|
||||
// Decay min/max every second (@24kHz).
|
||||
if (counter_ >= 24'000) {
|
||||
// 90% decay factor seems to work well.
|
||||
// This keeps large transients from wrecking the filter.
|
||||
max_ *= 0.9f;
|
||||
min_ *= 0.9f;
|
||||
counter_ = 0;
|
||||
calculate_thresholds();
|
||||
}
|
||||
|
||||
counter_ += audio.count;
|
||||
|
||||
for (size_t i = 0; i < audio.count; ++i) {
|
||||
auto& val = audio.p[i];
|
||||
|
||||
if (val > max_) {
|
||||
max_ = val;
|
||||
calculate_thresholds();
|
||||
}
|
||||
if (val < min_) {
|
||||
min_ = val;
|
||||
calculate_thresholds();
|
||||
}
|
||||
|
||||
if (val >= t_hi_)
|
||||
val = 1.0f;
|
||||
else if (val <= t_lo_)
|
||||
val = -1.0f;
|
||||
else
|
||||
val = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioNormalizer::calculate_thresholds() {
|
||||
auto center = (max_ + min_) / 2.0f;
|
||||
auto range = (max_ - min_) / 2.0f;
|
||||
|
||||
// 10% off center force either +/-1.0f.
|
||||
// Higher == larger dead zone.
|
||||
// Lower == more false positives.
|
||||
auto threshold = range * 0.1;
|
||||
t_hi_ = center + threshold;
|
||||
t_lo_ = center - threshold;
|
||||
}
|
||||
|
||||
/* BitQueue **********************************************/
|
||||
|
||||
void BitQueue::push(bool bit) {
|
||||
data_ = (data_ << 1) | (bit ? 1 : 0);
|
||||
if (count_ < max_size_) ++count_;
|
||||
}
|
||||
|
||||
bool BitQueue::pop() {
|
||||
if (count_ == 0) return false;
|
||||
|
||||
--count_;
|
||||
return (data_ & (1 << count_)) != 0;
|
||||
}
|
||||
|
||||
void BitQueue::reset() {
|
||||
data_ = 0;
|
||||
count_ = 0;
|
||||
}
|
||||
|
||||
uint8_t BitQueue::size() const {
|
||||
return count_;
|
||||
}
|
||||
|
||||
uint32_t BitQueue::data() const {
|
||||
return data_;
|
||||
}
|
||||
|
||||
/* BitExtractor ******************************************/
|
||||
|
||||
void BitExtractor::extract_bits(const buffer_f32_t& audio) {
|
||||
// Assumes input has been normalized +/- 1.0f.
|
||||
// Positive == 0, Negative == 1.
|
||||
for (size_t i = 0; i < audio.count; ++i) {
|
||||
auto sample = audio.p[i];
|
||||
|
||||
if (current_rate_) {
|
||||
if (current_rate_->handle_sample(sample)) {
|
||||
auto value = (current_rate_->bits.data() & 1) == 1;
|
||||
bits_.push(value);
|
||||
}
|
||||
} else {
|
||||
// Feed sample to all known rates for clock detection.
|
||||
for (auto& rate : known_rates_) {
|
||||
if (rate.handle_sample(sample) &&
|
||||
diff_bit_count(rate.bits.data(), clock_magic_number) <= 3) {
|
||||
// Clock detected, continue with this rate.
|
||||
rate.is_stable = true;
|
||||
current_rate_ = &rate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BitExtractor::configure(uint32_t sample_rate) {
|
||||
sample_rate_ = sample_rate;
|
||||
|
||||
// Build the baud rate info table based on the sample rate.
|
||||
// Sampling at 2x the baud rate to synchronize to bit transitions
|
||||
// without needing to know exact transition boundaries.
|
||||
for (auto& rate : known_rates_)
|
||||
rate.sample_interval = sample_rate / (2.0 * rate.baud_rate);
|
||||
}
|
||||
|
||||
void BitExtractor::reset() {
|
||||
current_rate_ = nullptr;
|
||||
|
||||
for (auto& rate : known_rates_)
|
||||
rate.reset();
|
||||
}
|
||||
|
||||
uint16_t BitExtractor::baud_rate() const {
|
||||
return current_rate_ ? current_rate_->baud_rate : 0;
|
||||
}
|
||||
|
||||
bool BitExtractor::RateInfo::handle_sample(float sample) {
|
||||
samples_until_next -= 1;
|
||||
|
||||
// Time to process a sample?
|
||||
if (samples_until_next > 0)
|
||||
return false;
|
||||
|
||||
bool value = signbit(sample); // NB: negative == '1'
|
||||
bool bit_pushed = false;
|
||||
|
||||
switch (state) {
|
||||
case State::WaitForSample:
|
||||
// Just need to wait for the first sample of the bit.
|
||||
state = State::ReadyToSend;
|
||||
break;
|
||||
|
||||
case State::ReadyToSend:
|
||||
if (!is_stable && prev_value != value) {
|
||||
// Still looking for the clock signal but found a transition.
|
||||
// Nudge the next sample a bit to try avoiding pulse edges.
|
||||
samples_until_next += (sample_interval / 8.0);
|
||||
} else {
|
||||
// Either the clock has been found or both samples were
|
||||
// (probably) in the same pulse. Send the bit.
|
||||
// TODO: Wider/more samples for noise reduction?
|
||||
state = State::WaitForSample;
|
||||
bit_pushed = true;
|
||||
bits.push(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// How long until the next sample?
|
||||
samples_until_next += sample_interval;
|
||||
prev_value = value;
|
||||
|
||||
return bit_pushed;
|
||||
}
|
||||
|
||||
void BitExtractor::RateInfo::reset() {
|
||||
state = State::WaitForSample;
|
||||
samples_until_next = 0.0;
|
||||
prev_value = false;
|
||||
is_stable = false;
|
||||
bits.reset();
|
||||
}
|
||||
|
||||
/* CodewordExtractor *************************************/
|
||||
|
||||
void CodewordExtractor::process_bits() {
|
||||
// Process all of the bits in the bits queue.
|
||||
while (bits_.size() > 0) {
|
||||
take_one_bit();
|
||||
|
||||
// Wait until data_ is full.
|
||||
if (bit_count_ < data_bit_count)
|
||||
continue;
|
||||
|
||||
// Wait for the sync frame.
|
||||
if (!has_sync_) {
|
||||
if (diff_bit_count(data_, sync_codeword) <= 2)
|
||||
handle_sync(/*inverted=*/false);
|
||||
else if (diff_bit_count(data_, ~sync_codeword) <= 2)
|
||||
handle_sync(/*inverted=*/true);
|
||||
continue;
|
||||
}
|
||||
|
||||
save_current_codeword();
|
||||
|
||||
if (word_count_ == pocsag::batch_size)
|
||||
handle_batch_complete();
|
||||
}
|
||||
}
|
||||
|
||||
void CodewordExtractor::flush() {
|
||||
// Don't bother flushing if there's no pending data.
|
||||
if (word_count_ == 0) return;
|
||||
|
||||
pad_idle();
|
||||
handle_batch_complete();
|
||||
}
|
||||
|
||||
void CodewordExtractor::reset() {
|
||||
clear_data_bits();
|
||||
has_sync_ = false;
|
||||
inverted_ = false;
|
||||
word_count_ = 0;
|
||||
}
|
||||
|
||||
void CodewordExtractor::clear_data_bits() {
|
||||
data_ = 0;
|
||||
bit_count_ = 0;
|
||||
}
|
||||
|
||||
void CodewordExtractor::take_one_bit() {
|
||||
data_ = (data_ << 1) | bits_.pop();
|
||||
if (bit_count_ < data_bit_count)
|
||||
++bit_count_;
|
||||
}
|
||||
|
||||
void CodewordExtractor::handle_sync(bool inverted) {
|
||||
clear_data_bits();
|
||||
has_sync_ = true;
|
||||
inverted_ = inverted;
|
||||
word_count_ = 0;
|
||||
}
|
||||
|
||||
void CodewordExtractor::save_current_codeword() {
|
||||
batch_[word_count_++] = inverted_ ? ~data_ : data_;
|
||||
clear_data_bits();
|
||||
}
|
||||
|
||||
void CodewordExtractor::handle_batch_complete() {
|
||||
on_batch_(*this);
|
||||
has_sync_ = false;
|
||||
word_count_ = 0;
|
||||
}
|
||||
|
||||
void CodewordExtractor::pad_idle() {
|
||||
while (word_count_ < pocsag::batch_size)
|
||||
batch_[word_count_++] = idle_codeword;
|
||||
}
|
||||
|
||||
/* POCSAGProcessor ***************************************/
|
||||
|
||||
void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured) return;
|
||||
|
||||
// buffer has 2048 samples
|
||||
// decim0 out: 2048/8 = 256 samples
|
||||
// decim1 out: 256/8 = 32 samples
|
||||
// channel out: 32/2 = 16 samples
|
||||
|
||||
// Get 24kHz audio
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||
auto audio = demod.execute(channel_out, audio_buffer);
|
||||
|
||||
// If squelching, check for audio before smoothing because smoothing
|
||||
// causes the squelch noise detection to fail. Likely because squelch
|
||||
// looks for HF noise and smoothing is basically a lowpass filter.
|
||||
// NB: Squelch in this processor is only for the the audio output.
|
||||
// Squelching will likely drop data "noise" and break processing.
|
||||
if (squelch_.enabled()) {
|
||||
bool has_audio = squelch_.execute(audio);
|
||||
squelch_history = (squelch_history << 1) | (has_audio ? 1 : 0);
|
||||
}
|
||||
// Check if there's any signal in the audio buffer.
|
||||
bool has_audio = squelch.execute(audio);
|
||||
squelch_history = (squelch_history << 1) | (has_audio ? 1 : 0);
|
||||
|
||||
smooth.Process(audio.p, audio.count);
|
||||
processDemodulatedSamples(audio.p, 16);
|
||||
extractFrames();
|
||||
|
||||
// Clear the output before sending to audio chip.
|
||||
if (squelch_.enabled() && squelch_history == 0) {
|
||||
for (size_t i = 0; i < audio.count; ++i) {
|
||||
audio.p[i] = 0.0;
|
||||
// Has there been any signal recently?
|
||||
if (squelch_history == 0) {
|
||||
// No recent signal, flush and prepare for next message.
|
||||
if (word_extractor.current() > 0) {
|
||||
flush();
|
||||
reset();
|
||||
send_stats();
|
||||
}
|
||||
|
||||
// Clear the audio stream before sending.
|
||||
for (size_t i = 0; i < audio.count; ++i)
|
||||
audio.p[i] = 0.0;
|
||||
|
||||
audio_output.write(audio);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out high-frequency noise then normalize.
|
||||
lpf.execute_in_place(audio);
|
||||
normalizer.execute_in_place(audio);
|
||||
audio_output.write(audio);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
int POCSAGProcessor::OnDataWord(uint32_t word, int pos) {
|
||||
packet.set(pos, word);
|
||||
return 0;
|
||||
}
|
||||
// Decode the messages from the audio.
|
||||
bit_extractor.extract_bits(audio);
|
||||
word_extractor.process_bits();
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
int POCSAGProcessor::OnDataFrame(int len, int baud) {
|
||||
if (len > 0) {
|
||||
packet.set_bitrate(baud);
|
||||
packet.set_flag(pocsag::PacketFlag::NORMAL);
|
||||
packet.set_timestamp(Timestamp::now());
|
||||
const POCSAGPacketMessage message(packet);
|
||||
shared_memory.application_queue.push(message);
|
||||
// Update the status.
|
||||
samples_processed += buffer.count;
|
||||
if (samples_processed >= stat_update_threshold) {
|
||||
send_stats();
|
||||
samples_processed -= stat_update_threshold;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void POCSAGProcessor::on_message(const Message* const message) {
|
||||
@@ -96,7 +356,7 @@ void POCSAGProcessor::on_message(const Message* const message) {
|
||||
|
||||
case Message::ID::NBFMConfigure: {
|
||||
auto config = reinterpret_cast<const NBFMConfigureMessage*>(message);
|
||||
squelch_.set_threshold(config->squelch_level / 100.0);
|
||||
squelch.set_threshold(config->squelch_level / 99.0);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -106,417 +366,55 @@ void POCSAGProcessor::on_message(const Message* const message) {
|
||||
}
|
||||
|
||||
void POCSAGProcessor::configure() {
|
||||
constexpr size_t decim_0_input_fs = baseband_fs;
|
||||
constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor;
|
||||
|
||||
constexpr size_t decim_1_input_fs = decim_0_output_fs;
|
||||
constexpr size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
|
||||
|
||||
constexpr size_t channel_filter_input_fs = decim_1_output_fs;
|
||||
const size_t channel_filter_output_fs = channel_filter_input_fs / 2;
|
||||
|
||||
const size_t demod_input_fs = channel_filter_output_fs;
|
||||
constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor;
|
||||
constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor;
|
||||
constexpr size_t channel_filter_output_fs = decim_1_output_fs / 2;
|
||||
constexpr size_t demod_input_fs = channel_filter_output_fs;
|
||||
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||
demod.configure(demod_input_fs, 4'500); // FSK +/- 4k5Hz.
|
||||
|
||||
// Smoothing should be roughly sample rate over max baud
|
||||
// 24k / 3.2k = 7.5
|
||||
smooth.SetSize(8);
|
||||
|
||||
// Don't have audio process the stream.
|
||||
// Don't process the audio stream.
|
||||
audio_output.configure(false);
|
||||
|
||||
// Set up the frame extraction, limits of baud
|
||||
setFrameExtractParams(demod_input_fs, 4000, 300, 32);
|
||||
bit_extractor.configure(demod_input_fs);
|
||||
|
||||
// Mark the class as ready to accept data
|
||||
// Set ready to process data.
|
||||
configured = true;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Frame extractraction methods
|
||||
// -----------------------------
|
||||
#define BAUD_STABLE (104)
|
||||
#define MAX_CONSEC_SAME (32)
|
||||
#define MAX_WITHOUT_SINGLE (64)
|
||||
#define MAX_BAD_TRANS (10)
|
||||
|
||||
#define M_SYNC (0x7cd215d8)
|
||||
#define M_NOTSYNC (0x832dea27)
|
||||
|
||||
#define M_IDLE (0x7a89c197)
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
inline int bitsDiff(unsigned long left, unsigned long right) {
|
||||
unsigned long xord = left ^ right;
|
||||
int count = 0;
|
||||
for (int i = 0; i < 32; i++) {
|
||||
if ((xord & 0x01) != 0) ++count;
|
||||
xord = xord >> 1;
|
||||
}
|
||||
return (count);
|
||||
void POCSAGProcessor::flush() {
|
||||
word_extractor.flush();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
void POCSAGProcessor::initFrameExtraction() {
|
||||
m_averageSymbolLen_1024 = m_maxSymSamples_1024;
|
||||
m_lastStableSymbolLen_1024 = m_minSymSamples_1024;
|
||||
|
||||
m_badTransitions = 0;
|
||||
m_bitsStart = 0;
|
||||
m_bitsEnd = 0;
|
||||
m_inverted = false;
|
||||
|
||||
resetVals();
|
||||
void POCSAGProcessor::reset() {
|
||||
bits.reset();
|
||||
bit_extractor.reset();
|
||||
word_extractor.reset();
|
||||
samples_processed = 0;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
void POCSAGProcessor::resetVals() {
|
||||
// Reset the parameters
|
||||
// --------------------
|
||||
m_goodTransitions = 0;
|
||||
m_badTransitions = 0;
|
||||
m_averageSymbolLen_1024 = m_maxSymSamples_1024;
|
||||
m_shortestGoodTrans_1024 = m_maxSymSamples_1024;
|
||||
m_valMid = 0;
|
||||
|
||||
// And reset the counts
|
||||
// --------------------
|
||||
m_lastTransPos_1024 = 0;
|
||||
m_lastBitPos_1024 = 0;
|
||||
m_lastSample = 0;
|
||||
m_sampleNo = 0;
|
||||
m_nextBitPos_1024 = m_maxSymSamples_1024;
|
||||
m_nextBitPosInt = (long)m_nextBitPos_1024;
|
||||
|
||||
// Extraction
|
||||
m_fifo.numBits = 0;
|
||||
m_gotSync = false;
|
||||
m_numCode = 0;
|
||||
void POCSAGProcessor::send_stats() const {
|
||||
POCSAGStatsMessage message(
|
||||
word_extractor.current(), word_extractor.count(),
|
||||
word_extractor.has_sync(), bit_extractor.baud_rate());
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
void POCSAGProcessor::setFrameExtractParams(long a_samplesPerSec, long a_maxBaud, long a_minBaud, long maxRunOfSameValue) {
|
||||
m_samplesPerSec = a_samplesPerSec;
|
||||
m_minSymSamples_1024 = (uint32_t)(1024.0f * (float)a_samplesPerSec / (float)a_maxBaud);
|
||||
m_maxSymSamples_1024 = (uint32_t)(1024.0f * (float)a_samplesPerSec / (float)a_minBaud);
|
||||
m_maxRunOfSameValue = maxRunOfSameValue;
|
||||
void POCSAGProcessor::send_packet() {
|
||||
packet.set_flag(pocsag::PacketFlag::NORMAL);
|
||||
packet.set_timestamp(Timestamp::now());
|
||||
packet.set_bitrate(bit_extractor.baud_rate());
|
||||
packet.set(word_extractor.batch());
|
||||
|
||||
m_shortestGoodTrans_1024 = m_maxSymSamples_1024;
|
||||
m_averageSymbolLen_1024 = m_maxSymSamples_1024;
|
||||
m_lastStableSymbolLen_1024 = m_minSymSamples_1024;
|
||||
|
||||
m_nextBitPos_1024 = m_averageSymbolLen_1024 / 2;
|
||||
m_nextBitPosInt = m_nextBitPos_1024 >> 10;
|
||||
|
||||
initFrameExtraction();
|
||||
POCSAGPacketMessage message(packet);
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
int POCSAGProcessor::processDemodulatedSamples(float* sampleBuff, int noOfSamples) {
|
||||
bool transition = false;
|
||||
uint32_t samplePos_1024 = 0;
|
||||
uint32_t len_1024 = 0;
|
||||
/* main **************************************************/
|
||||
|
||||
// Loop through the block of data
|
||||
// ------------------------------
|
||||
for (int pos = 0; pos < noOfSamples; ++pos) {
|
||||
m_sample = sampleBuff[pos];
|
||||
m_valMid += (m_sample - m_valMid) / 1024.0f;
|
||||
|
||||
++m_sampleNo;
|
||||
|
||||
// Detect Transition
|
||||
// -----------------
|
||||
transition = !((m_lastSample < m_valMid) ^ (m_sample >= m_valMid)); // use XOR for speed
|
||||
|
||||
// If this is a transition
|
||||
// -----------------------
|
||||
if (transition) {
|
||||
// Calculate samples since last trans
|
||||
// ----------------------------------
|
||||
int32_t fractional_1024 = (int32_t)(((m_sample - m_valMid) * 1024) / (m_sample - m_lastSample));
|
||||
if (fractional_1024 < 0) {
|
||||
fractional_1024 = -fractional_1024;
|
||||
}
|
||||
|
||||
samplePos_1024 = (m_sampleNo << 10) - fractional_1024;
|
||||
len_1024 = samplePos_1024 - m_lastTransPos_1024;
|
||||
m_lastTransPos_1024 = samplePos_1024;
|
||||
|
||||
// If symbol is large enough to be valid
|
||||
// -------------------------------------
|
||||
if (len_1024 > m_minSymSamples_1024) {
|
||||
// Check for shortest good transition
|
||||
// ----------------------------------
|
||||
if ((len_1024 < m_shortestGoodTrans_1024) &&
|
||||
(m_goodTransitions < BAUD_STABLE)) // detect change of symbol size
|
||||
{
|
||||
int32_t fractionOfShortest_1024 = (len_1024 << 10) / m_shortestGoodTrans_1024;
|
||||
|
||||
// If currently at half the baud rate
|
||||
// ----------------------------------
|
||||
if ((fractionOfShortest_1024 > 410) && (fractionOfShortest_1024 < 614)) // 0.4 and 0.6
|
||||
{
|
||||
m_averageSymbolLen_1024 /= 2;
|
||||
m_shortestGoodTrans_1024 = len_1024;
|
||||
}
|
||||
// If currently at the wrong baud rate
|
||||
// -----------------------------------
|
||||
else if (fractionOfShortest_1024 < 768) // 0.75
|
||||
{
|
||||
m_averageSymbolLen_1024 = len_1024;
|
||||
m_shortestGoodTrans_1024 = len_1024;
|
||||
m_goodTransitions = 0;
|
||||
m_lastSingleBitPos_1024 = samplePos_1024 - len_1024;
|
||||
}
|
||||
}
|
||||
|
||||
// Calc the number of bits since events
|
||||
// ------------------------------------
|
||||
int32_t halfSymbol_1024 = m_averageSymbolLen_1024 / 2;
|
||||
int bitsSinceLastTrans = max((uint32_t)1, (len_1024 + halfSymbol_1024) / m_averageSymbolLen_1024);
|
||||
int bitsSinceLastSingle = (((m_sampleNo << 10) - m_lastSingleBitPos_1024) + halfSymbol_1024) / m_averageSymbolLen_1024;
|
||||
|
||||
// Check for single bit
|
||||
// --------------------
|
||||
if (bitsSinceLastTrans == 1) {
|
||||
m_lastSingleBitPos_1024 = samplePos_1024;
|
||||
}
|
||||
|
||||
// If too long since last transition
|
||||
// ---------------------------------
|
||||
if (bitsSinceLastTrans > MAX_CONSEC_SAME) {
|
||||
resetVals();
|
||||
}
|
||||
// If too long sice last single bit
|
||||
// --------------------------------
|
||||
else if (bitsSinceLastSingle > MAX_WITHOUT_SINGLE) {
|
||||
resetVals();
|
||||
} else {
|
||||
// If this is a good transition
|
||||
// ----------------------------
|
||||
int32_t offsetFromExtectedTransition_1024 = len_1024 - (bitsSinceLastTrans * m_averageSymbolLen_1024);
|
||||
if (offsetFromExtectedTransition_1024 < 0) {
|
||||
offsetFromExtectedTransition_1024 = -offsetFromExtectedTransition_1024;
|
||||
}
|
||||
if (offsetFromExtectedTransition_1024 < ((int32_t)m_averageSymbolLen_1024 / 4)) // Has to be within 1/4 of symbol to be good
|
||||
{
|
||||
++m_goodTransitions;
|
||||
uint32_t bitsCount = min((uint32_t)BAUD_STABLE, m_goodTransitions);
|
||||
|
||||
uint32_t propFromPrevious = m_averageSymbolLen_1024 * bitsCount;
|
||||
uint32_t propFromCurrent = (len_1024 / bitsSinceLastTrans);
|
||||
m_averageSymbolLen_1024 = (propFromPrevious + propFromCurrent) / (bitsCount + 1);
|
||||
m_badTransitions = 0;
|
||||
// if ( len < m_shortestGoodTrans ){m_shortestGoodTrans = len;}
|
||||
// Store the old symbol size
|
||||
if (m_goodTransitions >= BAUD_STABLE) {
|
||||
m_lastStableSymbolLen_1024 = m_averageSymbolLen_1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the point of the last bit if not yet stable
|
||||
// -----------------------------------------------
|
||||
if ((m_goodTransitions < BAUD_STABLE) || (m_badTransitions > 0)) {
|
||||
m_lastBitPos_1024 = samplePos_1024 - (m_averageSymbolLen_1024 / 2);
|
||||
}
|
||||
|
||||
// Calculate the exact positiom of the next bit
|
||||
// --------------------------------------------
|
||||
int32_t thisPlusHalfsymbol_1024 = samplePos_1024 + (m_averageSymbolLen_1024 / 2);
|
||||
int32_t lastPlusSymbol = m_lastBitPos_1024 + m_averageSymbolLen_1024;
|
||||
m_nextBitPos_1024 = lastPlusSymbol + ((thisPlusHalfsymbol_1024 - lastPlusSymbol) / 16);
|
||||
|
||||
// Check for bad pos error
|
||||
// -----------------------
|
||||
if (m_nextBitPos_1024 < samplePos_1024) m_nextBitPos_1024 += m_averageSymbolLen_1024;
|
||||
|
||||
// Calculate integer sample after next bit
|
||||
// ---------------------------------------
|
||||
m_nextBitPosInt = (m_nextBitPos_1024 >> 10) + 1;
|
||||
|
||||
} // symbol is large enough to be valid
|
||||
else {
|
||||
// Bad transition, so reset the counts
|
||||
// -----------------------------------
|
||||
++m_badTransitions;
|
||||
if (m_badTransitions > MAX_BAD_TRANS) {
|
||||
resetVals();
|
||||
}
|
||||
}
|
||||
} // end of if transition
|
||||
|
||||
// Reached the point of the next bit
|
||||
// ---------------------------------
|
||||
if (m_sampleNo >= m_nextBitPosInt) {
|
||||
// Everything is good so extract a bit
|
||||
// -----------------------------------
|
||||
if (m_goodTransitions > 20) {
|
||||
// Store value at the center of bit
|
||||
// --------------------------------
|
||||
storeBit();
|
||||
}
|
||||
// Check for long 1 or zero
|
||||
// ------------------------
|
||||
uint32_t bitsSinceLastTrans = ((m_sampleNo << 10) - m_lastTransPos_1024) / m_averageSymbolLen_1024;
|
||||
if (bitsSinceLastTrans > m_maxRunOfSameValue) {
|
||||
resetVals();
|
||||
}
|
||||
|
||||
// Store the point of the last bit
|
||||
// -------------------------------
|
||||
m_lastBitPos_1024 = m_nextBitPos_1024;
|
||||
|
||||
// Calculate the exact point of the next bit
|
||||
// -----------------------------------------
|
||||
m_nextBitPos_1024 += m_averageSymbolLen_1024;
|
||||
|
||||
// Look for the bit after the next bit pos
|
||||
// ---------------------------------------
|
||||
m_nextBitPosInt = (m_nextBitPos_1024 >> 10) + 1;
|
||||
|
||||
} // Reached the point of the next bit
|
||||
|
||||
m_lastSample = m_sample;
|
||||
|
||||
} // Loop through the block of data
|
||||
|
||||
return getNoOfBits();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
void POCSAGProcessor::storeBit() {
|
||||
if (++m_bitsStart >= BIT_BUF_SIZE) {
|
||||
m_bitsStart = 0;
|
||||
}
|
||||
|
||||
// Calculate the bit value
|
||||
float sample = (m_sample + m_lastSample) / 2;
|
||||
// int32_t sample_1024 = m_sample_1024;
|
||||
bool bit = sample > m_valMid;
|
||||
|
||||
// If buffer not full
|
||||
if (m_bitsStart != m_bitsEnd) {
|
||||
// Decide on output val
|
||||
if (bit) {
|
||||
m_bits[m_bitsStart] = 0;
|
||||
} else {
|
||||
m_bits[m_bitsStart] = 1;
|
||||
}
|
||||
}
|
||||
// Throw away bits if the buffer is full
|
||||
else {
|
||||
if (--m_bitsStart <= -1) {
|
||||
m_bitsStart = BIT_BUF_SIZE - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
int POCSAGProcessor::extractFrames() {
|
||||
int msgCnt = 0;
|
||||
// While there is unread data in the bits buffer
|
||||
//----------------------------------------------
|
||||
while (getNoOfBits() > 0) {
|
||||
m_fifo.codeword = (m_fifo.codeword << 1) + getBit();
|
||||
m_fifo.numBits++;
|
||||
|
||||
// If number of bits in fifo equals 32
|
||||
//------------------------------------
|
||||
if (m_fifo.numBits >= 32) {
|
||||
// Not got sync
|
||||
// ------------
|
||||
if (!m_gotSync) {
|
||||
if (bitsDiff(m_fifo.codeword, M_SYNC) <= 2) {
|
||||
m_inverted = false;
|
||||
m_gotSync = true;
|
||||
m_numCode = -1;
|
||||
m_fifo.numBits = 0;
|
||||
} else if (bitsDiff(m_fifo.codeword, M_NOTSYNC) <= 2) {
|
||||
m_inverted = true;
|
||||
m_gotSync = true;
|
||||
m_numCode = -1;
|
||||
m_fifo.numBits = 0;
|
||||
} else {
|
||||
// Cause it to load one more bit
|
||||
m_fifo.numBits = 31;
|
||||
}
|
||||
} // Not got sync
|
||||
else {
|
||||
// Increment the word count
|
||||
// ------------------------
|
||||
++m_numCode; // It got set to -1 when a sync was found, now count the 16 words
|
||||
uint32_t val = m_inverted ? ~m_fifo.codeword : m_fifo.codeword;
|
||||
OnDataWord(val, m_numCode);
|
||||
|
||||
// If at the end of a 16 word block
|
||||
// --------------------------------
|
||||
if (m_numCode >= 15) {
|
||||
msgCnt += OnDataFrame(m_numCode + 1, (m_samplesPerSec << 10) / m_lastStableSymbolLen_1024);
|
||||
m_gotSync = false;
|
||||
m_numCode = -1;
|
||||
}
|
||||
m_fifo.numBits = 0;
|
||||
}
|
||||
} // If number of bits in fifo equals 32
|
||||
} // While there is unread data in the bits buffer
|
||||
return msgCnt;
|
||||
} // extractFrames
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
short POCSAGProcessor::getBit() {
|
||||
if (m_bitsEnd != m_bitsStart) {
|
||||
if (++m_bitsEnd >= BIT_BUF_SIZE) {
|
||||
m_bitsEnd = 0;
|
||||
}
|
||||
return m_bits[m_bitsEnd];
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
int POCSAGProcessor::getNoOfBits() {
|
||||
int bits = m_bitsEnd - m_bitsStart;
|
||||
if (bits < 0) {
|
||||
bits += BIT_BUF_SIZE;
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
uint32_t POCSAGProcessor::getRate() {
|
||||
return ((m_samplesPerSec << 10) + 512) / m_lastStableSymbolLen_1024;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<POCSAGProcessor>()};
|
||||
event_dispatcher.run();
|
||||
|
||||
+209
-162
@@ -26,199 +26,246 @@
|
||||
#ifndef __PROC_POCSAG2_H__
|
||||
#define __PROC_POCSAG2_H__
|
||||
|
||||
/* https://www.aaroncake.net/schoolpage/pocsag.htm */
|
||||
|
||||
#include "audio_output.hpp"
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
|
||||
#include "dsp_decimate.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
|
||||
#include "pocsag_packet.hpp"
|
||||
|
||||
#include "pocsag.hpp"
|
||||
#include "dsp_iir_config.hpp"
|
||||
#include "message.hpp"
|
||||
#include "audio_output.hpp"
|
||||
#include "pocsag.hpp"
|
||||
#include "pocsag_packet.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <bitset>
|
||||
using namespace std;
|
||||
|
||||
// Class used to smooth demodulated waveform prior to decoding
|
||||
// -----------------------------------------------------------
|
||||
template <class ValType, class CalcType>
|
||||
class SmoothVals {
|
||||
protected:
|
||||
ValType* m_lastVals; // Previous N values
|
||||
int m_size; // The size N
|
||||
CalcType m_sumVal; // Running sum of lastVals
|
||||
int m_pos; // Current position in last vals ring buffer
|
||||
int m_count; //
|
||||
#include <functional>
|
||||
|
||||
/* Normalizes audio stream to +/-1.0f */
|
||||
class AudioNormalizer {
|
||||
public:
|
||||
SmoothVals()
|
||||
: m_lastVals(NULL), m_size(1), m_sumVal(0), m_pos(0), m_count(0) {
|
||||
m_lastVals = new ValType[m_size];
|
||||
}
|
||||
void execute_in_place(const buffer_f32_t& audio);
|
||||
|
||||
// --------------------------------------------------
|
||||
// --------------------------------------------------
|
||||
virtual ~SmoothVals() {
|
||||
delete[] m_lastVals;
|
||||
}
|
||||
private:
|
||||
void calculate_thresholds();
|
||||
|
||||
SmoothVals(const SmoothVals<float, float>&) {
|
||||
}
|
||||
|
||||
SmoothVals& operator=(const SmoothVals<float, float>&) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Set size of smoothing
|
||||
// --------------------------------------------------
|
||||
void SetSize(int size) {
|
||||
m_size = std::max(size, 1);
|
||||
m_pos = 0;
|
||||
delete[] m_lastVals;
|
||||
m_lastVals = new ValType[m_size];
|
||||
m_sumVal = 0;
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Get size of smoothing
|
||||
// --------------------------------------------------
|
||||
int Size() { return m_size; }
|
||||
|
||||
// --------------------------------------------------
|
||||
// In place processing
|
||||
// --------------------------------------------------
|
||||
void Process(ValType* valBuff, int numVals) {
|
||||
ValType tmpVal;
|
||||
|
||||
if (m_count > (1024 * 10)) {
|
||||
// Recalculate the sum value occasionaly, stops accumulated errors when using float
|
||||
m_count = 0;
|
||||
m_sumVal = 0;
|
||||
for (int i = 0; i < m_size; ++i) {
|
||||
m_sumVal += (CalcType)m_lastVals[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Use a rolling smoothed value while processing the buffer
|
||||
for (int buffPos = 0; buffPos < numVals; ++buffPos) {
|
||||
m_pos++;
|
||||
if (m_pos >= m_size) {
|
||||
m_pos = 0;
|
||||
}
|
||||
|
||||
m_sumVal -= (CalcType)m_lastVals[m_pos]; // Subtract the oldest value
|
||||
m_lastVals[m_pos] = valBuff[buffPos]; // Store the new value
|
||||
m_sumVal += (CalcType)m_lastVals[m_pos]; // Add on the new value
|
||||
|
||||
tmpVal = (ValType)(m_sumVal / m_size); // Scale by number of values smoothed
|
||||
valBuff[buffPos] = tmpVal;
|
||||
}
|
||||
|
||||
m_count += numVals;
|
||||
}
|
||||
uint32_t counter_ = 0;
|
||||
float min_ = 99.0f;
|
||||
float max_ = -99.0f;
|
||||
float t_hi_ = 1.0;
|
||||
float t_lo_ = 1.0;
|
||||
};
|
||||
|
||||
// --------------------------------------------------
|
||||
// Class to process base band data to pocsag frames
|
||||
// --------------------------------------------------
|
||||
/* FIFO wrapper over a uint32_t's bits. */
|
||||
class BitQueue {
|
||||
public:
|
||||
void push(bool bit);
|
||||
bool pop();
|
||||
void reset();
|
||||
uint8_t size() const;
|
||||
uint32_t data() const;
|
||||
|
||||
private:
|
||||
uint32_t data_ = 0;
|
||||
uint8_t count_ = 0;
|
||||
|
||||
static constexpr uint8_t max_size_ = sizeof(data_) * 8;
|
||||
};
|
||||
|
||||
/* Extracts bits and bitrate from audio stream. */
|
||||
class BitExtractor {
|
||||
public:
|
||||
BitExtractor(BitQueue& bits)
|
||||
: bits_{bits} {}
|
||||
|
||||
void extract_bits(const buffer_f32_t& audio);
|
||||
void configure(uint32_t sample_rate);
|
||||
void reset();
|
||||
uint16_t baud_rate() const;
|
||||
|
||||
private:
|
||||
/* Clock signal detection magic number. */
|
||||
static constexpr uint32_t clock_magic_number = 0xAAAAAAAA;
|
||||
|
||||
struct RateInfo {
|
||||
enum class State : uint8_t {
|
||||
WaitForSample,
|
||||
ReadyToSend
|
||||
};
|
||||
|
||||
const int16_t baud_rate = 0;
|
||||
float sample_interval = 0.0;
|
||||
|
||||
State state = State::WaitForSample;
|
||||
float samples_until_next = 0.0;
|
||||
bool prev_value = false;
|
||||
bool is_stable = false;
|
||||
BitQueue bits{};
|
||||
|
||||
/* Updates a rate info with the given sample.
|
||||
* Returns true if the rate info has a new bit in its queue. */
|
||||
bool handle_sample(float sample);
|
||||
void reset();
|
||||
};
|
||||
|
||||
std::array<RateInfo, 3> known_rates_{
|
||||
RateInfo{512},
|
||||
RateInfo{1200},
|
||||
RateInfo{2400}};
|
||||
|
||||
BitQueue& bits_;
|
||||
|
||||
uint32_t sample_rate_ = 0;
|
||||
RateInfo* current_rate_ = nullptr;
|
||||
};
|
||||
|
||||
/* Extracts codeword batches from the BitQueue. */
|
||||
class CodewordExtractor {
|
||||
public:
|
||||
using batch_t = pocsag::batch_t;
|
||||
using batch_handler_t = std::function<void(CodewordExtractor&)>;
|
||||
|
||||
CodewordExtractor(BitQueue& bits, batch_handler_t on_batch)
|
||||
: bits_{bits}, on_batch_{on_batch} {}
|
||||
|
||||
/* Process the BitQueue to extract codeword batches. */
|
||||
void process_bits();
|
||||
|
||||
/* Pad then send any pending frames. */
|
||||
void flush();
|
||||
|
||||
/* Completely reset to prepare for a new message. */
|
||||
void reset();
|
||||
|
||||
/* Gets the underlying batch array. */
|
||||
const batch_t& batch() const { return batch_; }
|
||||
|
||||
/* Gets in-progress codeword. */
|
||||
uint32_t current() const { return data_; }
|
||||
|
||||
/* Gets the count of completed codewords. */
|
||||
uint8_t count() const { return word_count_; }
|
||||
|
||||
/* Returns true if the batch has as sync frame. */
|
||||
bool has_sync() const { return has_sync_; }
|
||||
|
||||
private:
|
||||
/* Sync frame codeword. */
|
||||
static constexpr uint32_t sync_codeword = 0x7cd215d8;
|
||||
|
||||
/* Idle codeword used to pad a 16 codeword "batch". */
|
||||
static constexpr uint32_t idle_codeword = 0x7a89c197;
|
||||
|
||||
/* Number of bits in 'data_' member. */
|
||||
static constexpr uint8_t data_bit_count = sizeof(uint32_t) * 8;
|
||||
|
||||
/* Clears data_ and bit_count_ to prepare for next codeword. */
|
||||
void clear_data_bits();
|
||||
|
||||
/* Pop a bit off the queue and add it to data_. */
|
||||
void take_one_bit();
|
||||
|
||||
/* Handles receiving the sync frame codeword, start of batch. */
|
||||
void handle_sync(bool inverted);
|
||||
|
||||
/* Saves the current codeword in data_ to the batch. */
|
||||
void save_current_codeword();
|
||||
|
||||
/* Sends the batch to the handler, resets for next batch. */
|
||||
void handle_batch_complete();
|
||||
|
||||
/* Fill the rest of the batch with 'idle' codewords. */
|
||||
void pad_idle();
|
||||
|
||||
BitQueue& bits_;
|
||||
batch_handler_t on_batch_{};
|
||||
|
||||
/* When true, sync frame has been received. */
|
||||
bool has_sync_ = false;
|
||||
|
||||
/* When true, bit vales are flipped in the codewords. */
|
||||
bool inverted_ = false;
|
||||
|
||||
uint32_t data_ = 0;
|
||||
uint8_t bit_count_ = 0;
|
||||
uint8_t word_count_ = 0;
|
||||
batch_t batch_{};
|
||||
};
|
||||
|
||||
/* Processes POCSAG signal into codeword batches. */
|
||||
class POCSAGProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
int OnDataFrame(int len, int baud);
|
||||
int OnDataWord(uint32_t word, int pos);
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 3072000;
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
std::array<float, 32> audio{};
|
||||
const buffer_f32_t audio_buffer{
|
||||
audio.data(),
|
||||
audio.size()};
|
||||
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
dsp::demodulate::FM demod{};
|
||||
SmoothVals<float, float> smooth = {};
|
||||
|
||||
AudioOutput audio_output{};
|
||||
|
||||
bool configured = false;
|
||||
pocsag::POCSAGPacket packet{};
|
||||
static constexpr uint8_t stat_update_interval = 10;
|
||||
static constexpr uint32_t stat_update_threshold =
|
||||
baseband_fs / stat_update_interval;
|
||||
|
||||
void configure();
|
||||
void flush();
|
||||
void reset();
|
||||
void send_stats() const;
|
||||
void send_packet();
|
||||
|
||||
// ----------------------------------------
|
||||
// Frame extractraction methods and members
|
||||
// ----------------------------------------
|
||||
void initFrameExtraction();
|
||||
struct FIFOStruct {
|
||||
unsigned long codeword;
|
||||
int numBits;
|
||||
};
|
||||
/* Set once app is ready to receive messages. */
|
||||
bool configured = false;
|
||||
|
||||
#define BIT_BUF_SIZE (64)
|
||||
/* Buffer for decimated IQ data. */
|
||||
std::array<complex16_t, 256> dst{};
|
||||
const buffer_c16_t dst_buffer{dst.data(), dst.size()};
|
||||
|
||||
void resetVals();
|
||||
void setFrameExtractParams(long a_samplesPerSec, long a_maxBaud = 8000, long a_minBaud = 200, long maxRunOfSameValue = 32);
|
||||
/* Buffer for demodulated audio. */
|
||||
std::array<float, 16> audio{};
|
||||
const buffer_f32_t audio_buffer{audio.data(), audio.size()};
|
||||
|
||||
int processDemodulatedSamples(float* sampleBuff, int noOfSamples);
|
||||
int extractFrames();
|
||||
/* Decimate to 48kHz. */
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
|
||||
void storeBit();
|
||||
short getBit();
|
||||
/* Filter to 24kHz and demodulate. */
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
dsp::demodulate::FM demod{};
|
||||
|
||||
int getNoOfBits();
|
||||
uint32_t getRate();
|
||||
|
||||
uint32_t m_averageSymbolLen_1024{0};
|
||||
uint32_t m_lastStableSymbolLen_1024{0};
|
||||
|
||||
uint32_t m_samplesPerSec{0};
|
||||
uint32_t m_goodTransitions{0};
|
||||
uint32_t m_badTransitions{0};
|
||||
|
||||
uint32_t m_sampleNo{0};
|
||||
float m_sample{0};
|
||||
float m_valMid{0.0f};
|
||||
float m_lastSample{0.0f};
|
||||
|
||||
uint32_t m_lastTransPos_1024{0};
|
||||
uint32_t m_lastSingleBitPos_1024{0};
|
||||
|
||||
uint32_t m_nextBitPosInt{0}; // Integer rounded up version to save on ops
|
||||
uint32_t m_nextBitPos_1024{0};
|
||||
uint32_t m_lastBitPos_1024{0};
|
||||
|
||||
uint32_t m_shortestGoodTrans_1024{0};
|
||||
uint32_t m_minSymSamples_1024{0};
|
||||
uint32_t m_maxSymSamples_1024{0};
|
||||
uint32_t m_maxRunOfSameValue{0};
|
||||
|
||||
bitset<(size_t)BIT_BUF_SIZE> m_bits{0};
|
||||
long m_bitsStart{0};
|
||||
long m_bitsEnd{0};
|
||||
|
||||
FIFOStruct m_fifo{0, 0};
|
||||
bool m_gotSync{false};
|
||||
int m_numCode{0};
|
||||
bool m_inverted{false};
|
||||
|
||||
FMSquelch squelch_{};
|
||||
/* Squelch to ignore noise. */
|
||||
FMSquelch squelch{};
|
||||
uint64_t squelch_history = 0;
|
||||
|
||||
/* LPF to reduce noise. POCSAG supports 2400 baud, but that falls
|
||||
* nicely into the transition band of this 1800Hz filter.
|
||||
* scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False) */
|
||||
IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f},
|
||||
{1.00000000f, -1.34896775f, 0.51398189f}}};
|
||||
|
||||
/* Attempts to de-noise and normalize signal. */
|
||||
AudioNormalizer normalizer{};
|
||||
|
||||
/* Handles writing audio stream to hardware. */
|
||||
AudioOutput audio_output{};
|
||||
|
||||
/* Holds the data sent to the app. */
|
||||
pocsag::POCSAGPacket packet{};
|
||||
|
||||
/* Used to keep track of how many samples were processed
|
||||
* between status update messages. */
|
||||
uint32_t samples_processed = 0;
|
||||
|
||||
BitQueue bits{};
|
||||
|
||||
/* Processes audio into bits. */
|
||||
BitExtractor bit_extractor{bits};
|
||||
|
||||
/* Processes bits into codewords. */
|
||||
CodewordExtractor word_extractor{
|
||||
bits, [this](CodewordExtractor&) {
|
||||
send_packet();
|
||||
}};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
@@ -77,12 +77,6 @@ constexpr iir_biquad_config_t audio_24k_deemph_300_6_config{
|
||||
{0.03780475f, 0.03780475f, 0.00000000f},
|
||||
{1.00000000f, -0.92439049f, 0.00000000f}};
|
||||
|
||||
// scipy.signal.butter(1, 2400 / 12000.0, 'lowpass', analog=False)
|
||||
// NOTE: Technically, order-1 filter, b[2] = a[2] = 0.
|
||||
constexpr iir_biquad_config_t audio_24k_lpf_2400hz_config{
|
||||
{0.03780475f, 0.03780475f, 0.00000000f},
|
||||
{1.00000000f, -0.92439049f, 0.00000000f}};
|
||||
|
||||
// scipy.signal.butter(1, 300 / 8000.0, 'lowpass', analog=False)
|
||||
// NOTE: Technically, order-1 filter, b[2] = a[2] = 0.
|
||||
constexpr iir_biquad_config_t audio_16k_deemph_300_6_config{
|
||||
|
||||
@@ -405,13 +405,12 @@ void ILI9341::drawBMP(const ui::Point p, const uint8_t* bitmap, const bool trans
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Draw BMP from SD card.
|
||||
Currently supported formats:
|
||||
16bpp ARGB, RGB565
|
||||
24bpp RGB
|
||||
32bpp ARGB
|
||||
*/
|
||||
/* Draw BMP from SD card.
|
||||
* Currently supported formats:
|
||||
* 16bpp ARGB, RGB565
|
||||
* 24bpp RGB
|
||||
* 32bpp ARGB
|
||||
*/
|
||||
bool ILI9341::drawBMP2(const ui::Point p, const std::filesystem::path& file) {
|
||||
File bmpimage;
|
||||
size_t file_pos = 0;
|
||||
|
||||
@@ -90,14 +90,52 @@ class ILI9341 {
|
||||
const ui::Color foreground,
|
||||
const ui::Color background);
|
||||
|
||||
/*** Scrolling ***
|
||||
* Scrolling support is implemented in the ILI9341 driver. Basically a region
|
||||
* of the screen is set up to act as a circular buffer. The VSA (vertical scroll
|
||||
* address) is the line that defines the "start" of the circular buffer. In our
|
||||
* case, the driver is set up for "bottom-up" scrolling. In this mode, drawing
|
||||
* starts at the bottom of the scroll region and draws the buffer upward.
|
||||
* However, the whole display's address space is inverted (the screen is actually
|
||||
* upside down in the PortaPack) so this bottom-up drawing appears to be top-down.
|
||||
*
|
||||
* What this means is that the line pointed to by VSA will be drawn at the top
|
||||
* of the scroll region and the line at VSA - 1 (wrapped) will be the bottom.
|
||||
* Consider the following screen buffers and VSA pointers.
|
||||
*
|
||||
* Buffer: Display: Buffer: Display:
|
||||
* VSA > A A A C NB: VSA points to the "Top"
|
||||
* B B B D of the rendered output.
|
||||
* C C VSA > C A
|
||||
* D D D B
|
||||
*/
|
||||
|
||||
/* Sets the top and bottom lines of the scrolling region. */
|
||||
void scroll_set_area(const ui::Coord top_y, const ui::Coord bottom_y);
|
||||
ui::Coord scroll_set_position(const ui::Coord position);
|
||||
ui::Coord scroll(const int32_t delta);
|
||||
ui::Coord scroll_area_y(const ui::Coord y) const;
|
||||
void scroll_disable();
|
||||
|
||||
constexpr ui::Dim width() const { return 240; }
|
||||
constexpr ui::Dim height() const { return 320; }
|
||||
/* Sets VSA. This an offset from top_y not a screen coordinate. */
|
||||
ui::Coord scroll_set_position(const ui::Coord position);
|
||||
|
||||
/* Relative adjustment to VSA. Positive value will cause output
|
||||
* to scoll "down", negative values will cause the output to scroll "up". */
|
||||
ui::Coord scroll(const int32_t delta);
|
||||
|
||||
/* Gets a screen coordinate from a scroll region offset. Values are wrapped
|
||||
* so the offset is wrapped within the bounds of the scroll region. The
|
||||
* specified offset is applied to VSA and then offset by the scroll region
|
||||
* top to produce a screen coordinate.
|
||||
* Consider the following screen buffers and VSA pointer. Note the offsets.
|
||||
*
|
||||
* VSA > A = +0 A = +2
|
||||
* B = +1 B = +3 NB: As before, VSA is always the "top"
|
||||
* C = +2 VSA > C = +0 and line at VSA - 1 will be the
|
||||
* D = +3 D = +1 "bottom" or the max 'y' offset.
|
||||
*/
|
||||
ui::Coord scroll_area_y(const ui::Coord y) const;
|
||||
|
||||
constexpr ui::Dim width() const { return ui::screen_width; }
|
||||
constexpr ui::Dim height() const { return ui::screen_height; }
|
||||
constexpr ui::Rect screen_rect() const { return {0, 0, width(), height()}; }
|
||||
|
||||
void draw_pixels(const ui::Rect r, const ui::Color* const colors, const size_t count);
|
||||
|
||||
@@ -344,12 +344,21 @@ class POCSAGPacketMessage : public Message {
|
||||
class POCSAGStatsMessage : public Message {
|
||||
public:
|
||||
constexpr POCSAGStatsMessage(
|
||||
uint32_t current_bits,
|
||||
uint8_t current_frames,
|
||||
bool has_sync,
|
||||
uint16_t baud_rate)
|
||||
: Message{ID::POCSAGStats},
|
||||
baud_rate_{baud_rate} {
|
||||
current_bits{current_bits},
|
||||
current_frames{current_frames},
|
||||
has_sync{has_sync},
|
||||
baud_rate{baud_rate} {
|
||||
}
|
||||
|
||||
uint16_t baud_rate_;
|
||||
uint32_t current_bits = 0;
|
||||
uint8_t current_frames = 0;
|
||||
bool has_sync = false;
|
||||
uint16_t baud_rate = 0;
|
||||
};
|
||||
|
||||
class ACARSPacketMessage : public Message {
|
||||
|
||||
@@ -57,10 +57,10 @@
|
||||
* 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 < 25'000) return OversampleRate::x64; // 12k5, 16k.
|
||||
if (sample_rate < 80'000) return OversampleRate::x32; // 25k, 32k, 50k, 75k.
|
||||
if (sample_rate < 250'000) return OversampleRate::x16; // 100k, 150k.
|
||||
if (sample_rate < 1'250'000) return OversampleRate::x8; // 250k, 500k, 600k, 650k, 750k, 1Mhz.
|
||||
if (sample_rate < 1'250'000) return OversampleRate::x8; // 250k, 500k, 600k, 750k, 1Mhz.
|
||||
|
||||
return OversampleRate::x4; // Top range (1.25Mhz ... 5.5Mhz).
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@ bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state) {
|
||||
state.ascii_idx -= 7;
|
||||
char ascii_char = (state.ascii_data >> state.ascii_idx) & 0x7F;
|
||||
|
||||
// Bottom's up (reverse the bits).
|
||||
// Reverse the bits. (TODO: __RBIT?)
|
||||
ascii_char = (ascii_char & 0xF0) >> 4 | (ascii_char & 0x0F) << 4; // 01234567 -> 45670123
|
||||
ascii_char = (ascii_char & 0xCC) >> 2 | (ascii_char & 0x33) << 2; // 45670123 -> 67452301
|
||||
ascii_char = (ascii_char & 0xAA) >> 2 | (ascii_char & 0x55); // 67452301 -> 76543210
|
||||
|
||||
@@ -45,6 +45,10 @@ enum PacketFlag : uint32_t {
|
||||
TOO_LONG
|
||||
};
|
||||
|
||||
/* Number of codewords in a batch. */
|
||||
constexpr uint8_t batch_size = 16;
|
||||
using batch_t = std::array<uint32_t, batch_size>;
|
||||
|
||||
class POCSAGPacket {
|
||||
public:
|
||||
void set_timestamp(const Timestamp& value) {
|
||||
@@ -55,16 +59,20 @@ class POCSAGPacket {
|
||||
return timestamp_;
|
||||
}
|
||||
|
||||
void set(const size_t index, const uint32_t data) {
|
||||
if (index < 16)
|
||||
void set(size_t index, uint32_t data) {
|
||||
if (index < batch_size)
|
||||
codewords[index] = data;
|
||||
}
|
||||
|
||||
uint32_t operator[](const size_t index) const {
|
||||
return (index < 16) ? codewords[index] : 0;
|
||||
void set(const batch_t& batch) {
|
||||
codewords = batch;
|
||||
}
|
||||
|
||||
void set_bitrate(const uint16_t bitrate) {
|
||||
uint32_t operator[](size_t index) const {
|
||||
return (index < batch_size) ? codewords[index] : 0;
|
||||
}
|
||||
|
||||
void set_bitrate(uint16_t bitrate) {
|
||||
bitrate_ = bitrate;
|
||||
}
|
||||
|
||||
@@ -72,7 +80,7 @@ class POCSAGPacket {
|
||||
return bitrate_;
|
||||
}
|
||||
|
||||
void set_flag(const PacketFlag flag) {
|
||||
void set_flag(PacketFlag flag) {
|
||||
flag_ = flag;
|
||||
}
|
||||
|
||||
@@ -89,7 +97,7 @@ class POCSAGPacket {
|
||||
private:
|
||||
uint16_t bitrate_{0};
|
||||
PacketFlag flag_{NORMAL};
|
||||
std::array<uint32_t, 16> codewords{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
batch_t codewords{};
|
||||
Timestamp timestamp_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ static_assert(sizeof(ui_config2_t) == sizeof(uint32_t));
|
||||
struct misc_config_t {
|
||||
bool mute_audio : 1;
|
||||
bool disable_speaker : 1;
|
||||
bool UNUSED_2 : 1;
|
||||
bool config_disable_external_tcxo : 1;
|
||||
bool UNUSED_3 : 1;
|
||||
bool UNUSED_4 : 1;
|
||||
bool UNUSED_5 : 1;
|
||||
@@ -226,6 +226,9 @@ struct data_t {
|
||||
// Additional UI settings.
|
||||
ui_config2_t ui_config2;
|
||||
|
||||
// recovery mode magic value storage
|
||||
uint32_t config_mode_storage;
|
||||
|
||||
constexpr data_t()
|
||||
: structure_version(data_structure_version_enum::VERSION_CURRENT),
|
||||
target_frequency(target_frequency_reset_value),
|
||||
@@ -273,7 +276,8 @@ struct data_t {
|
||||
UNUSED_8(0),
|
||||
headphone_volume_cb(-600),
|
||||
misc_config(),
|
||||
ui_config2() {
|
||||
ui_config2(),
|
||||
config_mode_storage(CONFIG_MODE_NORMAL_VALUE) {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -363,6 +367,7 @@ void defaults() {
|
||||
|
||||
set_config_backlight_timer(backlight_config_t{});
|
||||
set_config_splash(true);
|
||||
set_config_disable_external_tcxo(false);
|
||||
set_encoder_dial_sensitivity(DIAL_SENSITIVITY_NORMAL);
|
||||
set_config_speaker_disable(true); // Disable AK4951 speaker by default (in case of OpenSourceSDRLab H2)
|
||||
|
||||
@@ -380,6 +385,10 @@ void defaults() {
|
||||
|
||||
void init() {
|
||||
const auto switches_state = get_switches_state();
|
||||
|
||||
// ignore for valid check
|
||||
auto config_mode_backup = config_mode_storage();
|
||||
set_config_mode_storage(CONFIG_MODE_NORMAL_VALUE);
|
||||
if (!(switches_state[(size_t)ui::KeyEvent::Left] && switches_state[(size_t)ui::KeyEvent::Right]) && backup_ram->is_valid()) {
|
||||
// Copy valid persistent data into cache.
|
||||
cached_backup_ram = *backup_ram;
|
||||
@@ -395,6 +404,7 @@ void init() {
|
||||
// Copy defaults into cache.
|
||||
defaults();
|
||||
}
|
||||
set_config_mode_storage(config_mode_backup);
|
||||
}
|
||||
|
||||
void persist() {
|
||||
@@ -550,6 +560,10 @@ bool config_speaker_disable() {
|
||||
return data->misc_config.disable_speaker;
|
||||
}
|
||||
|
||||
bool config_disable_external_tcxo() {
|
||||
return data->misc_config.config_disable_external_tcxo;
|
||||
}
|
||||
|
||||
bool stealth_mode() {
|
||||
return data->ui_config.stealth_mode;
|
||||
}
|
||||
@@ -611,6 +625,10 @@ void set_config_speaker_disable(bool v) {
|
||||
data->misc_config.disable_speaker = v;
|
||||
}
|
||||
|
||||
void set_config_disable_external_tcxo(bool v) {
|
||||
data->misc_config.config_disable_external_tcxo = v;
|
||||
}
|
||||
|
||||
void set_stealth_mode(bool v) {
|
||||
data->ui_config.stealth_mode = v;
|
||||
}
|
||||
@@ -840,6 +858,16 @@ void set_encoder_dial_sensitivity(uint8_t v) {
|
||||
data->encoder_dial_sensitivity = v;
|
||||
}
|
||||
|
||||
// Recovery mode magic value storage
|
||||
static data_t* data_direct_access = reinterpret_cast<data_t*>(memory::map::backup_ram.base());
|
||||
|
||||
uint32_t config_mode_storage() {
|
||||
return data_direct_access->config_mode_storage;
|
||||
}
|
||||
void set_config_mode_storage(uint32_t v) {
|
||||
data_direct_access->config_mode_storage = v;
|
||||
}
|
||||
|
||||
// PMem to sdcard settings
|
||||
|
||||
bool should_use_sdcard_for_pmem() {
|
||||
@@ -965,6 +993,8 @@ bool debug_dump() {
|
||||
// misc_config bits
|
||||
pmem_dump_file.write_line("misc_config config_audio_mute: " + to_string_dec_int(config_audio_mute()));
|
||||
pmem_dump_file.write_line("misc_config config_speaker_disable: " + to_string_dec_int(config_speaker_disable()));
|
||||
pmem_dump_file.write_line("ui_config config_disable_external_tcxo: " + to_string_dec_uint(config_disable_external_tcxo()));
|
||||
|
||||
// receiver_model
|
||||
pmem_dump_file.write_line("\n[Receiver Model]");
|
||||
pmem_dump_file.write_line("target_frequency: " + to_string_dec_uint(receiver_model.target_frequency()));
|
||||
|
||||
@@ -173,6 +173,7 @@ void set_stealth_mode(const bool v);
|
||||
uint8_t config_cpld();
|
||||
void set_config_cpld(uint8_t i);
|
||||
|
||||
bool config_disable_external_tcxo();
|
||||
bool config_splash();
|
||||
bool config_converter();
|
||||
bool config_updown_converter();
|
||||
@@ -193,6 +194,7 @@ void set_gui_return_icon(bool v);
|
||||
void set_load_app_settings(bool v);
|
||||
void set_save_app_settings(bool v);
|
||||
void set_show_bigger_qr_code(bool v);
|
||||
void set_config_disable_external_tcxo(bool v);
|
||||
void set_config_splash(bool v);
|
||||
bool config_converter();
|
||||
bool config_updown_converter();
|
||||
@@ -218,6 +220,11 @@ void set_disable_touchscreen(bool v);
|
||||
uint8_t config_encoder_dial_sensitivity();
|
||||
void set_encoder_dial_sensitivity(uint8_t v);
|
||||
|
||||
#define CONFIG_MODE_GUARD_VALUE 2001
|
||||
#define CONFIG_MODE_NORMAL_VALUE 1999
|
||||
uint32_t config_mode_storage();
|
||||
void set_config_mode_storage(uint32_t v);
|
||||
|
||||
uint32_t pocsag_last_address();
|
||||
void set_pocsag_last_address(uint32_t address);
|
||||
|
||||
|
||||
@@ -37,6 +37,29 @@ struct Style {
|
||||
Style invert() const;
|
||||
};
|
||||
|
||||
/* Sometimes mutation is just the more readable thing... */
|
||||
struct MutableStyle {
|
||||
const Font* font;
|
||||
Color background;
|
||||
Color foreground;
|
||||
|
||||
MutableStyle(const Style& s)
|
||||
: font{&s.font},
|
||||
background{s.background},
|
||||
foreground{s.foreground} {}
|
||||
|
||||
void invert() {
|
||||
std::swap(background, foreground);
|
||||
}
|
||||
|
||||
operator Style() const {
|
||||
return {
|
||||
.font = *font,
|
||||
.background = background,
|
||||
.foreground = foreground};
|
||||
}
|
||||
};
|
||||
|
||||
class Widget;
|
||||
|
||||
class Painter {
|
||||
|
||||
+254
-171
@@ -494,7 +494,9 @@ void BigFrequency::paint(Painter& painter) {
|
||||
const auto rect = screen_rect();
|
||||
|
||||
// Erase
|
||||
painter.fill_rectangle({{0, rect.location().y()}, {240, 52}}, ui::Color::black());
|
||||
painter.fill_rectangle(
|
||||
{{0, rect.location().y()}, {screen_width, 52}},
|
||||
ui::Color::black());
|
||||
|
||||
// Prepare digits
|
||||
if (!_frequency) {
|
||||
@@ -612,10 +614,10 @@ void Console::write(std::string message) {
|
||||
if (!hidden() && visible()) {
|
||||
const Style& s = style();
|
||||
const Font& font = s.font;
|
||||
const auto rect = screen_rect();
|
||||
auto rect = screen_rect();
|
||||
ui::Color pen_color = s.foreground;
|
||||
|
||||
for (const auto c : message) {
|
||||
for (auto c : message) {
|
||||
if (escape) {
|
||||
if (c < std::size(term_colors))
|
||||
pen_color = term_colors[(uint8_t)c];
|
||||
@@ -628,12 +630,13 @@ void Console::write(std::string message) {
|
||||
} else if (c == '\x1B') {
|
||||
escape = true;
|
||||
} else {
|
||||
const auto glyph = font.glyph(c);
|
||||
const auto advance = glyph.advance();
|
||||
if ((pos.x() + advance.x()) > rect.width()) {
|
||||
auto glyph = font.glyph(c);
|
||||
auto advance = glyph.advance();
|
||||
// Would drawing next character be off the end? Newline.
|
||||
if ((pos.x() + advance.x()) > rect.width())
|
||||
crlf();
|
||||
}
|
||||
const Point pos_glyph{
|
||||
|
||||
Point pos_glyph{
|
||||
rect.left() + pos.x(),
|
||||
display.scroll_area_y(pos.y())};
|
||||
display.draw_glyph(pos_glyph, glyph, pen_color, s.background);
|
||||
@@ -649,7 +652,6 @@ void Console::write(std::string message) {
|
||||
|
||||
void Console::writeln(std::string message) {
|
||||
write(message + "\n");
|
||||
// crlf();
|
||||
}
|
||||
|
||||
void Console::paint(Painter&) {
|
||||
@@ -659,15 +661,23 @@ void Console::paint(Painter&) {
|
||||
void Console::on_show() {
|
||||
enable_scrolling(true);
|
||||
clear();
|
||||
// visible = true;
|
||||
}
|
||||
|
||||
bool Console::scrolling_enabled = false;
|
||||
|
||||
void Console::enable_scrolling(bool enable) {
|
||||
if (enable) {
|
||||
const auto screen_r = screen_rect();
|
||||
display.scroll_set_area(screen_r.top(), screen_r.bottom());
|
||||
auto sr = screen_rect();
|
||||
auto line_height = style().font.line_height();
|
||||
|
||||
// Count full lines that can fit in console's rectangle.
|
||||
auto max_lines = sr.height() / line_height; // NB: int division to floor.
|
||||
|
||||
// The scroll area must be a multiple of the line_height
|
||||
// or some lines will end up vertically truncated.
|
||||
scroll_height = max_lines * line_height;
|
||||
|
||||
display.scroll_set_area(sr.top(), sr.top() + scroll_height);
|
||||
display.scroll_set_position(0);
|
||||
scrolling_enabled = true;
|
||||
} else {
|
||||
@@ -678,28 +688,37 @@ void Console::enable_scrolling(bool enable) {
|
||||
|
||||
void Console::on_hide() {
|
||||
/* TODO: Clear region to eliminate brief flash of content at un-shifted
|
||||
* position?
|
||||
*/
|
||||
* position? */
|
||||
enable_scrolling(false);
|
||||
// visible = false;
|
||||
}
|
||||
|
||||
void Console::crlf() {
|
||||
if (hidden() || !visible()) return;
|
||||
|
||||
const Style& s = style();
|
||||
const auto sr = screen_rect();
|
||||
const auto line_height = s.font.line_height();
|
||||
pos = {0, pos.y() + line_height};
|
||||
const int32_t y_excess = pos.y() + line_height - sr.height();
|
||||
if (y_excess > 0) {
|
||||
if (!scrolling_enabled) {
|
||||
enable_scrolling(true);
|
||||
}
|
||||
display.scroll(-y_excess);
|
||||
pos = {pos.x(), pos.y() - y_excess};
|
||||
const auto& s = style();
|
||||
auto sr = screen_rect();
|
||||
auto line_height = s.font.line_height();
|
||||
|
||||
const Rect dirty{sr.left(), display.scroll_area_y(pos.y()), sr.width(), line_height};
|
||||
// Advance to the next line (\n) position and "carriage return" x to 0.
|
||||
pos = {0, pos.y() + line_height};
|
||||
|
||||
if (pos.y() >= scroll_height) {
|
||||
// Line is past off the "bottom", need to scroll.
|
||||
if (!scrolling_enabled)
|
||||
enable_scrolling(true);
|
||||
|
||||
// See the notes in lcd_ili9341.hpp about how scrolling works.
|
||||
// The gist is that VSA will be moved to scroll the "top" off the
|
||||
// screen. The drawing code uses 'scroll_area_y' to get the actual
|
||||
// screen coordinate based on VSA. The "bottom" line is *always*
|
||||
// at 'VSA + ((max_lines - 1) * line_height)' and so is constant.
|
||||
pos = {0, scroll_height - line_height};
|
||||
|
||||
// Scroll off the "top" line.
|
||||
display.scroll(-line_height);
|
||||
|
||||
// Clear the new line at the "bottom".
|
||||
Rect dirty{sr.left(), display.scroll_area_y(pos.y()), sr.width(), line_height};
|
||||
display.fill_rectangle(dirty, s.background);
|
||||
}
|
||||
}
|
||||
@@ -1813,7 +1832,7 @@ void NumberField::set_value(int32_t new_value, bool trigger_change) {
|
||||
new_value = range.second + new_value + 1;
|
||||
}
|
||||
|
||||
new_value = clip_value(new_value);
|
||||
new_value = clip(new_value, range.first, range.second);
|
||||
|
||||
if (new_value != value()) {
|
||||
value_ = new_value;
|
||||
@@ -1868,169 +1887,182 @@ bool NumberField::on_touch(const TouchEvent event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t NumberField::clip_value(int32_t value) {
|
||||
if (value > range.second) {
|
||||
value = range.second;
|
||||
}
|
||||
if (value < range.first) {
|
||||
value = range.first;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/* SymField **************************************************************/
|
||||
|
||||
SymField::SymField(
|
||||
Point parent_pos,
|
||||
size_t length,
|
||||
symfield_type type)
|
||||
: Widget{{parent_pos, {static_cast<ui::Dim>(8 * length), 16}}},
|
||||
length_{length},
|
||||
type_{type} {
|
||||
uint32_t c;
|
||||
Type type,
|
||||
bool explicit_edits)
|
||||
: Widget{{parent_pos, {char_width * (int)length, 16}}},
|
||||
type_{type},
|
||||
explicit_edits_{explicit_edits} {
|
||||
if (length == 0)
|
||||
length = 1;
|
||||
|
||||
// Auto-init
|
||||
if (type == SYMFIELD_OCT) {
|
||||
for (c = 0; c < length; c++)
|
||||
set_symbol_list(c, "01234567");
|
||||
} else if (type == SYMFIELD_DEC) {
|
||||
for (c = 0; c < length; c++)
|
||||
set_symbol_list(c, "0123456789");
|
||||
} else if (type == SYMFIELD_HEX) {
|
||||
for (c = 0; c < length; c++)
|
||||
set_symbol_list(c, "0123456789ABCDEF");
|
||||
} else if (type == SYMFIELD_ALPHANUM) {
|
||||
for (c = 0; c < length; c++)
|
||||
set_symbol_list(c, " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
|
||||
selected_ = length - 1;
|
||||
value_.resize(length);
|
||||
|
||||
switch (type) {
|
||||
case Type::Oct:
|
||||
set_symbol_list("01234567");
|
||||
break;
|
||||
|
||||
case Type::Dec:
|
||||
set_symbol_list("0123456789");
|
||||
break;
|
||||
|
||||
case Type::Hex:
|
||||
set_symbol_list("0123456789ABCDEF");
|
||||
break;
|
||||
|
||||
case Type::Alpha:
|
||||
set_symbol_list(" 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
|
||||
break;
|
||||
|
||||
default:
|
||||
set_symbol_list("01");
|
||||
break;
|
||||
}
|
||||
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
uint16_t SymField::concatenate_4_octal_u16() {
|
||||
// input array 4 octal digits , return 12 bits, same order, trippled A4-A2-A1|B4-B2-B1|C4-C2-C1|D4-D2-D1
|
||||
uint32_t mul = 1;
|
||||
uint16_t v = 0;
|
||||
|
||||
if (type_ == SYMFIELD_OCT) {
|
||||
for (uint32_t c = 0; c < length_; c++) {
|
||||
v += values_[(length_ - 1 - c)] * mul;
|
||||
mul *= 8; // shift 3 bits to the right every new octal squawk digit
|
||||
}
|
||||
return v;
|
||||
} else
|
||||
return 0;
|
||||
SymField::SymField(
|
||||
Point parent_pos,
|
||||
size_t length,
|
||||
std::string symbol_list,
|
||||
bool explicit_edits)
|
||||
: SymField{parent_pos, length, Type::Custom, explicit_edits} {
|
||||
set_symbol_list(std::move(symbol_list));
|
||||
}
|
||||
|
||||
uint32_t SymField::value_dec_u32() {
|
||||
uint32_t mul = 1, v = 0;
|
||||
|
||||
if (type_ == SYMFIELD_DEC) {
|
||||
for (uint32_t c = 0; c < length_; c++) {
|
||||
v += values_[(length_ - 1 - c)] * mul;
|
||||
mul *= 10;
|
||||
}
|
||||
return v;
|
||||
} else
|
||||
char SymField::get_symbol(size_t index) const {
|
||||
if (index >= value_.length())
|
||||
return 0;
|
||||
|
||||
return value_[index];
|
||||
}
|
||||
|
||||
uint64_t SymField::value_hex_u64() {
|
||||
void SymField::set_symbol(size_t index, char symbol) {
|
||||
if (index >= value_.length())
|
||||
return;
|
||||
|
||||
set_symbol_internal(index, ensure_valid(symbol));
|
||||
}
|
||||
|
||||
size_t SymField::get_offset(size_t index) const {
|
||||
if (index >= value_.length())
|
||||
return 0;
|
||||
|
||||
// NB: Linear search - symbol lists are small.
|
||||
return symbols_.find(value_[index]);
|
||||
}
|
||||
|
||||
void SymField::set_offset(size_t index, size_t offset) {
|
||||
if (index >= value_.length() || offset >= symbols_.length())
|
||||
return;
|
||||
|
||||
set_symbol_internal(index, symbols_[offset]);
|
||||
}
|
||||
|
||||
void SymField::set_symbol_list(std::string symbol_list) {
|
||||
if (symbol_list.length() == 0)
|
||||
return;
|
||||
|
||||
symbols_ = std::move(symbol_list);
|
||||
ensure_all_symbols();
|
||||
}
|
||||
|
||||
void SymField::set_value(uint64_t value) {
|
||||
auto v = value;
|
||||
uint8_t radix = get_radix();
|
||||
|
||||
for (int i = value_.length() - 1; i >= 0; --i) {
|
||||
uint8_t temp = v % radix;
|
||||
value_[i] = uint_to_char(temp, radix);
|
||||
v /= radix;
|
||||
}
|
||||
}
|
||||
|
||||
void SymField::set_value(std::string_view value) {
|
||||
// Is new value too long?
|
||||
// TODO: Truncate instead? Which end?
|
||||
if (value.length() > value_.length())
|
||||
return;
|
||||
|
||||
// Right-align string in field.
|
||||
auto left_padding = value_.length() - value.length();
|
||||
value_ = std::string(static_cast<size_t>(left_padding), '\0') + std::string{value};
|
||||
ensure_all_symbols();
|
||||
}
|
||||
|
||||
uint64_t SymField::to_integer() const {
|
||||
uint64_t v = 0;
|
||||
uint64_t mul = 1;
|
||||
uint8_t radix = get_radix();
|
||||
|
||||
if (type_ != SYMFIELD_DEF) {
|
||||
for (uint32_t c = 0; c < length_; c++)
|
||||
v += (uint64_t)(values_[c]) << (4 * (length_ - 1 - c));
|
||||
return v;
|
||||
} else
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string SymField::value_string() {
|
||||
std::string return_string{""};
|
||||
|
||||
if (type_ == SYMFIELD_ALPHANUM) {
|
||||
for (uint32_t c = 0; c < length_; c++) {
|
||||
return_string += symbol_list_[0][values_[c]];
|
||||
}
|
||||
for (int i = value_.length() - 1; i >= 0; --i) {
|
||||
auto temp = char_to_uint(value_[i], radix);
|
||||
v += temp * mul;
|
||||
mul *= radix;
|
||||
}
|
||||
|
||||
return return_string;
|
||||
return v;
|
||||
}
|
||||
|
||||
uint32_t SymField::get_sym(const uint32_t index) {
|
||||
if (index >= length_) return 0;
|
||||
|
||||
return values_[index];
|
||||
}
|
||||
|
||||
void SymField::set_sym(const uint32_t index, const uint32_t new_value) {
|
||||
if (index >= length_) return;
|
||||
|
||||
uint32_t clipped_value = clip_value(index, new_value);
|
||||
|
||||
if (clipped_value != values_[index]) {
|
||||
values_[index] = clipped_value;
|
||||
if (on_change) {
|
||||
on_change();
|
||||
}
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void SymField::set_length(const uint32_t new_length) {
|
||||
if ((new_length <= 32) && (new_length != length_)) {
|
||||
prev_length_ = length_;
|
||||
length_ = new_length;
|
||||
|
||||
// Clip eventual garbage from previous shorter word
|
||||
for (size_t n = 0; n < length_; n++)
|
||||
set_sym(n, values_[n]);
|
||||
|
||||
erase_prev_ = true;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void SymField::set_symbol_list(const uint32_t index, const std::string symbol_list) {
|
||||
if (index >= length_) return;
|
||||
|
||||
symbol_list_[index] = symbol_list;
|
||||
|
||||
// Re-clip symbol's value
|
||||
set_sym(index, values_[index]);
|
||||
const std::string& SymField::to_string() const {
|
||||
return value_;
|
||||
}
|
||||
|
||||
void SymField::paint(Painter& painter) {
|
||||
Point pt_draw = screen_pos();
|
||||
Point p = screen_pos();
|
||||
|
||||
if (erase_prev_) {
|
||||
painter.fill_rectangle({pt_draw, {(int)prev_length_ * 8, 16}}, Color::black());
|
||||
erase_prev_ = false;
|
||||
}
|
||||
for (size_t n = 0; n < value_.length(); n++) {
|
||||
auto c = value_[n];
|
||||
MutableStyle paint_style{style()};
|
||||
|
||||
for (size_t n = 0; n < length_; n++) {
|
||||
const auto text = symbol_list_[n].substr(values_[n], 1);
|
||||
// Only highlight while focused.
|
||||
if (has_focus()) {
|
||||
if (explicit_edits_) {
|
||||
// Invert the whole field on focus if explicit edits is enabled.
|
||||
paint_style.invert();
|
||||
} else if (n == selected_) {
|
||||
// Otherwise only highlight the selected symbol.
|
||||
paint_style.invert();
|
||||
}
|
||||
|
||||
const auto paint_style = (has_focus() && (n == selected_)) ? style().invert() : style();
|
||||
if (editing_ && n == selected_) {
|
||||
// Use 'bg_blue' style to indicate in editing mode.
|
||||
paint_style.foreground = Color::white();
|
||||
paint_style.background = Color::blue();
|
||||
}
|
||||
}
|
||||
|
||||
painter.draw_string(
|
||||
pt_draw,
|
||||
paint_style,
|
||||
text);
|
||||
|
||||
pt_draw += {8, 0};
|
||||
painter.draw_char(p, paint_style, c);
|
||||
p += {8, 0};
|
||||
}
|
||||
}
|
||||
|
||||
bool SymField::on_key(const KeyEvent key) {
|
||||
bool SymField::on_key(KeyEvent key) {
|
||||
// If explicit edits are enabled, only Select is handled when not in edit mode.
|
||||
if (explicit_edits_ && !editing_) {
|
||||
switch (key) {
|
||||
case KeyEvent::Select:
|
||||
editing_ = true;
|
||||
set_dirty();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case KeyEvent::Select:
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
editing_ = !editing_;
|
||||
set_dirty();
|
||||
return true;
|
||||
|
||||
case KeyEvent::Left:
|
||||
if (selected_ > 0) {
|
||||
@@ -2041,13 +2073,27 @@ bool SymField::on_key(const KeyEvent key) {
|
||||
break;
|
||||
|
||||
case KeyEvent::Right:
|
||||
if (selected_ < (length_ - 1)) {
|
||||
if (selected_ < (value_.length() - 1)) {
|
||||
selected_++;
|
||||
set_dirty();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyEvent::Up:
|
||||
if (editing_) {
|
||||
on_encoder(1);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyEvent::Down:
|
||||
if (editing_) {
|
||||
on_encoder(-1);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -2055,29 +2101,66 @@ bool SymField::on_key(const KeyEvent key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SymField::on_encoder(const EncoderEvent delta) {
|
||||
int32_t new_value = (int)values_[selected_] + delta;
|
||||
bool SymField::on_encoder(EncoderEvent delta) {
|
||||
if (explicit_edits_ && !editing_)
|
||||
return false;
|
||||
|
||||
if (new_value >= 0)
|
||||
set_sym(selected_, values_[selected_] + delta);
|
||||
// TODO: Wrapping or carrying might be nice.
|
||||
int offset = get_offset(selected_) + delta;
|
||||
|
||||
offset = clip<int>(offset, 0, symbols_.length() - 1);
|
||||
set_offset(selected_, offset);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SymField::on_touch(const TouchEvent event) {
|
||||
if (event.type == TouchEvent::Type::Start) {
|
||||
bool SymField::on_touch(TouchEvent event) {
|
||||
if (event.type == TouchEvent::Type::Start)
|
||||
focus();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t SymField::clip_value(const uint32_t index, const uint32_t value) {
|
||||
size_t symbol_count = symbol_list_[index].length() - 1;
|
||||
char SymField::ensure_valid(char symbol) const {
|
||||
// NB: Linear search - symbol lists are small.
|
||||
auto pos = symbols_.find(symbol);
|
||||
return pos != std::string::npos ? symbol : symbols_[0];
|
||||
}
|
||||
|
||||
if (value > symbol_count)
|
||||
return symbol_count;
|
||||
else
|
||||
return value;
|
||||
void SymField::ensure_all_symbols() {
|
||||
auto temp = value_;
|
||||
|
||||
for (auto& c : value_)
|
||||
c = ensure_valid(c);
|
||||
|
||||
if (temp != value_) {
|
||||
if (on_change)
|
||||
on_change(*this);
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void SymField::set_symbol_internal(size_t index, char symbol) {
|
||||
if (value_[index] == symbol)
|
||||
return;
|
||||
|
||||
value_[index] = symbol;
|
||||
if (on_change)
|
||||
on_change(*this);
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
uint8_t SymField::get_radix() const {
|
||||
switch (type_) {
|
||||
case Type::Oct:
|
||||
return 8;
|
||||
case Type::Dec:
|
||||
return 10;
|
||||
case Type::Hex:
|
||||
return 16;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Waveform **************************************************************/
|
||||
|
||||
@@ -332,8 +332,8 @@ class Console : public Widget {
|
||||
void on_hide() override;
|
||||
|
||||
private:
|
||||
// bool visible = false;
|
||||
Point pos{0, 0};
|
||||
Dim scroll_height = 0;
|
||||
std::string buffer{};
|
||||
static bool scrolling_enabled;
|
||||
|
||||
@@ -751,52 +751,93 @@ class NumberField : public Widget {
|
||||
const char fill_char;
|
||||
int32_t value_{0};
|
||||
bool can_loop{};
|
||||
|
||||
int32_t clip_value(int32_t value);
|
||||
};
|
||||
|
||||
/* A widget that allows for character-by-character editing of its value. */
|
||||
class SymField : public Widget {
|
||||
public:
|
||||
std::function<void(SymField&)> on_select{};
|
||||
std::function<void()> on_change{};
|
||||
std::function<void(SymField&)> on_change{};
|
||||
|
||||
enum symfield_type {
|
||||
SYMFIELD_OCT,
|
||||
SYMFIELD_DEC,
|
||||
SYMFIELD_HEX,
|
||||
SYMFIELD_ALPHANUM,
|
||||
SYMFIELD_DEF // User DEFined
|
||||
enum class Type {
|
||||
Custom,
|
||||
Oct,
|
||||
Dec,
|
||||
Hex,
|
||||
Alpha,
|
||||
};
|
||||
|
||||
SymField(Point parent_pos, size_t length, symfield_type type);
|
||||
/* When "explicit_edits" is true, the field must be selected before it can
|
||||
* be modified. This means that "slots" are not individually highlighted.
|
||||
* This makes navigation on Views with SymFields easier because the
|
||||
* whole control can be skipped over instead of one "slot" at a time. */
|
||||
|
||||
SymField(
|
||||
Point parent_pos,
|
||||
size_t length,
|
||||
Type type = Type::Dec,
|
||||
bool explicit_edits = false);
|
||||
|
||||
SymField(
|
||||
Point parent_pos,
|
||||
size_t length,
|
||||
std::string symbol_list,
|
||||
bool explicit_edits = false);
|
||||
|
||||
SymField(const SymField&) = delete;
|
||||
SymField(SymField&&) = delete;
|
||||
|
||||
uint32_t get_sym(const uint32_t index);
|
||||
void set_sym(const uint32_t index, const uint32_t new_value);
|
||||
void set_length(const uint32_t new_length);
|
||||
void set_symbol_list(const uint32_t index, const std::string symbol_list);
|
||||
uint16_t concatenate_4_octal_u16();
|
||||
uint32_t value_dec_u32();
|
||||
uint64_t value_hex_u64();
|
||||
std::string value_string();
|
||||
/* Gets the symbol (character) at the specified index. */
|
||||
char get_symbol(size_t index) const;
|
||||
|
||||
/* Sets the symbol (character) at the specified index. */
|
||||
void set_symbol(size_t index, char symbol);
|
||||
|
||||
/* Gets the symbol's offset in the symbol list at the specified index. */
|
||||
size_t get_offset(size_t index) const;
|
||||
|
||||
/* Sets the symbol's offset in the symbol list at the specified index. */
|
||||
void set_offset(size_t index, size_t offset);
|
||||
|
||||
/* Sets the list of allowable symbols for the field. */
|
||||
void set_symbol_list(std::string symbol_list);
|
||||
|
||||
/* Sets the integer value of the field. Only works for OCT/DEC/HEX. */
|
||||
void set_value(uint64_t value);
|
||||
|
||||
/* Sets the string value of the field. Characters that are not in
|
||||
* the symbol list will be set to the first symbol in the symbol list. */
|
||||
void set_value(std::string_view value);
|
||||
|
||||
/* Gets the integer value of the field for OCT/DEC/HEX. */
|
||||
uint64_t to_integer() const;
|
||||
|
||||
/* Gets the string value of the field. */
|
||||
const std::string& to_string() const;
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
bool on_key(const KeyEvent key) override;
|
||||
bool on_encoder(const EncoderEvent delta) override;
|
||||
bool on_touch(const TouchEvent event) override;
|
||||
bool on_key(KeyEvent key) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
bool on_touch(TouchEvent event) override;
|
||||
|
||||
private:
|
||||
std::string symbol_list_[32] = {"01"}; // Failsafe init
|
||||
uint32_t values_[32] = {0};
|
||||
uint32_t selected_ = 0;
|
||||
size_t length_, prev_length_ = 0;
|
||||
bool erase_prev_ = false;
|
||||
symfield_type type_{};
|
||||
/* Ensure the specified symbol is in the symbol list. */
|
||||
char ensure_valid(char symbol) const;
|
||||
|
||||
int32_t clip_value(const uint32_t index, const uint32_t value);
|
||||
/* Ensures all the symbols are valid. */
|
||||
void ensure_all_symbols();
|
||||
|
||||
/* Sets without validation and fires on_change if necessary. */
|
||||
void set_symbol_internal(size_t index, char symbol);
|
||||
|
||||
/* Gets the radix for the current Type. */
|
||||
uint8_t get_radix() const;
|
||||
|
||||
std::string symbols_{};
|
||||
std::string value_{};
|
||||
Type type_ = Type::Custom;
|
||||
size_t selected_ = 0;
|
||||
bool editing_ = false;
|
||||
bool explicit_edits_ = true;
|
||||
};
|
||||
|
||||
class Waveform : public Widget {
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
#ifndef __UTILITY_H__
|
||||
#define __UTILITY_H__
|
||||
|
||||
#include <type_traits>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <complex>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <algorithm>
|
||||
#include <complex>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
|
||||
#define LOCATE_IN_RAM __attribute__((section(".ramtext")))
|
||||
|
||||
@@ -141,6 +142,16 @@ constexpr std::enable_if_t<is_flags_type_v<TEnum>, bool> flags_enabled(TEnum val
|
||||
return (i_value & i_flags) == i_flags;
|
||||
}
|
||||
|
||||
// TODO: Constrain to integrals?
|
||||
/* Converts an integer into a byte array. */
|
||||
template <typename T, size_t N = sizeof(T)>
|
||||
constexpr std::array<uint8_t, N> to_byte_array(T value) {
|
||||
std::array<uint8_t, N> bytes{};
|
||||
for (size_t i = 0; i < N; ++i)
|
||||
bytes[i] = (value >> ((N - i - 1) * 8)) & 0xFF;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/* Returns value constrained to min and max. */
|
||||
template <class T>
|
||||
constexpr const T& clip(const T& value, const T& minimum, const T& maximum) {
|
||||
|
||||
@@ -83,6 +83,54 @@ TEST_CASE("to_string_rounded_freq returns correct value.") {
|
||||
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 9), "1234.567891");
|
||||
}
|
||||
|
||||
TEST_CASE("to_string_hex returns correct value.") {
|
||||
CHECK_EQ(to_string_hex(0x0, 1), "0");
|
||||
CHECK_EQ(to_string_hex(0x0, 4), "0000");
|
||||
CHECK_EQ(to_string_hex(0x1, 1), "1");
|
||||
CHECK_EQ(to_string_hex(0xA, 1), "A");
|
||||
CHECK_EQ(to_string_hex(0xA0, 2), "A0");
|
||||
CHECK_EQ(to_string_hex(0xA1, 2), "A1");
|
||||
CHECK_EQ(to_string_hex(0xA1FE, 2), "FE"); // NB: Truncates front.
|
||||
CHECK_EQ(to_string_hex(0x12345678, 8), "12345678");
|
||||
CHECK_EQ(to_string_hex(0x9ABCDEF0, 8), "9ABCDEF0");
|
||||
}
|
||||
|
||||
TEST_CASE("to_string_hex returns correct value based on type.") {
|
||||
CHECK_EQ(to_string_hex<uint8_t>(0xA), "0A");
|
||||
CHECK_EQ(to_string_hex<uint8_t>(0xFE), "FE");
|
||||
CHECK_EQ(to_string_hex<uint16_t>(0x00FE), "00FE");
|
||||
CHECK_EQ(to_string_hex<uint16_t>(0xCAFE), "CAFE");
|
||||
CHECK_EQ(to_string_hex<uint32_t>(0xCAFE), "0000CAFE");
|
||||
CHECK_EQ(to_string_hex<uint32_t>(0xCAFEBEEF), "CAFEBEEF");
|
||||
CHECK_EQ(to_string_hex<uint64_t>(0xCAFEBEEF), "00000000CAFEBEEF");
|
||||
CHECK_EQ(to_string_hex<uint64_t>(0xC0FFEE00CAFEBABE), "C0FFEE00CAFEBABE");
|
||||
}
|
||||
|
||||
TEST_CASE("char_to_uint returns correct value.") {
|
||||
CHECK_EQ(char_to_uint('0', 10), 0);
|
||||
CHECK_EQ(char_to_uint('5', 10), 5);
|
||||
CHECK_EQ(char_to_uint('8', 8), 0); // Bad value given radix.
|
||||
CHECK_EQ(char_to_uint('8', 10), 8);
|
||||
CHECK_EQ(char_to_uint('8', 16), 8);
|
||||
CHECK_EQ(char_to_uint('A', 10), 0); // Bad value given radix.
|
||||
CHECK_EQ(char_to_uint('A', 16), 0xA);
|
||||
CHECK_EQ(char_to_uint('a', 16), 0xA); // Lowercase.
|
||||
CHECK_EQ(char_to_uint('f', 16), 0xF); // Lowercase.
|
||||
CHECK_EQ(char_to_uint('G', 16), 0); // Bad value given radix.
|
||||
}
|
||||
|
||||
TEST_CASE("uint_to_char returns correct value.") {
|
||||
CHECK_EQ(uint_to_char(0, 10), '0');
|
||||
CHECK_EQ(uint_to_char(5, 10), '5');
|
||||
CHECK_EQ(uint_to_char(8, 8), '\0'); // Bad value given radix.
|
||||
CHECK_EQ(uint_to_char(8, 10), '8');
|
||||
CHECK_EQ(uint_to_char(8, 16), '8');
|
||||
CHECK_EQ(uint_to_char(10, 10), '\0'); // Bad value given radix.
|
||||
CHECK_EQ(uint_to_char(10, 16), 'A');
|
||||
CHECK_EQ(uint_to_char(15, 16), 'F');
|
||||
CHECK_EQ(uint_to_char(16, 16), '\0'); // Bad value given radix.
|
||||
}
|
||||
|
||||
TEST_CASE("trim removes whitespace.") {
|
||||
CHECK(trim(" foo\n") == "foo");
|
||||
}
|
||||
|
||||
@@ -71,4 +71,22 @@ TEST_CASE("ms_duration should return duration.") {
|
||||
|
||||
TEST_CASE("ms_duration not fault when passed zero.") {
|
||||
CHECK_EQ(ms_duration(0, 0, 0), 0);
|
||||
}
|
||||
|
||||
TEST_CASE("to_byte_array returns correct size and values.") {
|
||||
auto arr1 = to_byte_array<uint8_t>(0xAB);
|
||||
REQUIRE_EQ(std::size(arr1), 1);
|
||||
CHECK_EQ(arr1[0], 0xAB);
|
||||
|
||||
auto arr2 = to_byte_array<uint16_t>(0xABCD);
|
||||
REQUIRE_EQ(std::size(arr2), 2);
|
||||
CHECK_EQ(arr2[0], 0xAB);
|
||||
CHECK_EQ(arr2[1], 0xCD);
|
||||
|
||||
auto arr4 = to_byte_array<uint32_t>(0xABCD1234);
|
||||
REQUIRE_EQ(std::size(arr4), 4);
|
||||
CHECK_EQ(arr4[0], 0xAB);
|
||||
CHECK_EQ(arr4[1], 0xCD);
|
||||
CHECK_EQ(arr4[2], 0x12);
|
||||
CHECK_EQ(arr4[3], 0x34);
|
||||
}
|
||||
@@ -50,8 +50,46 @@ TEST_CASE("ifft successfully calculates dc on zero frequency") {
|
||||
for (uint32_t i = 0; i < fft_width; i++)
|
||||
CHECK(v[i].imag() == 0);
|
||||
|
||||
free(v);
|
||||
free(tmp);
|
||||
delete[] v;
|
||||
delete[] tmp;
|
||||
}
|
||||
|
||||
TEST_CASE("ifft successfully calculates sine of quarter the sample rate") {
|
||||
uint32_t fft_width = 8;
|
||||
complex16_t* v = new complex16_t[fft_width];
|
||||
complex16_t* tmp = new complex16_t[fft_width];
|
||||
|
||||
v[0] = {0, 0};
|
||||
v[1] = {0, 0};
|
||||
v[2] = {1024, 0}; // sample rate /4 bin
|
||||
v[3] = {0, 0};
|
||||
v[4] = {0, 0};
|
||||
v[5] = {0, 0};
|
||||
v[6] = {0, 0};
|
||||
v[7] = {0, 0};
|
||||
|
||||
ifft<complex16_t>(v, fft_width, tmp);
|
||||
|
||||
CHECK(v[0].real() == 1024);
|
||||
CHECK(v[1].real() == 0);
|
||||
CHECK(v[2].real() == -1024);
|
||||
CHECK(v[3].real() == 0);
|
||||
CHECK(v[4].real() == 1024);
|
||||
CHECK(v[5].real() == 0);
|
||||
CHECK(v[6].real() == -1024);
|
||||
CHECK(v[7].real() == 0);
|
||||
|
||||
CHECK(v[0].imag() == 0);
|
||||
CHECK(v[1].imag() == 1024);
|
||||
CHECK(v[2].imag() == 0);
|
||||
CHECK(v[3].imag() == -1024);
|
||||
CHECK(v[4].imag() == 0);
|
||||
CHECK(v[5].imag() == 1024);
|
||||
CHECK(v[6].imag() == 0);
|
||||
CHECK(v[7].imag() == -1024);
|
||||
|
||||
delete[] v;
|
||||
delete[] tmp;
|
||||
}
|
||||
|
||||
TEST_CASE("ifft successfully calculates pure sine of half the sample rate") {
|
||||
@@ -82,6 +120,6 @@ TEST_CASE("ifft successfully calculates pure sine of half the sample rate") {
|
||||
for (uint32_t i = 0; i < fft_width; i++)
|
||||
CHECK(v[i].imag() == 0);
|
||||
|
||||
free(v);
|
||||
free(tmp);
|
||||
delete[] v;
|
||||
delete[] tmp;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user