SymField rewrite (#1444)

* First WIP symfield

* Cleanup widget code

* Rebase and format

* Fix 'to_integer' bug, fix siggen UI.

* to_string_hex fix, unit tests for string code

* Pass instance in callback

* Fix on_change callbacks

* Fix keyfob (not active)

* to_byte_array, coaster tx cleanup

* Add to_byte_array tests

* Changes in ui_numbers

* Fix ui_encoders

* Format

* Fix modemsetup view's symfields

* Remove header

* Format
This commit is contained in:
Kyle Reed 2023-09-12 12:38:19 -07:00 committed by GitHub
parent 70e0f2913f
commit af424aa5f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 607 additions and 371 deletions

View file

@ -71,6 +71,7 @@ POCSAGSettingsView::POCSAGSettingsView(
check_hide_bad.set_value(settings_.hide_bad_data); check_hide_bad.set_value(settings_.hide_bad_data);
check_hide_addr_only.set_value(settings_.hide_addr_only); check_hide_addr_only.set_value(settings_.hide_addr_only);
check_ignore.set_value(settings_.enable_ignore); check_ignore.set_value(settings_.enable_ignore);
field_ignore.set_value(settings_.address_to_ignore); field_ignore.set_value(settings_.address_to_ignore);
button_save.on_select = [this, &nav](Button&) { button_save.on_select = [this, &nav](Button&) {
@ -81,7 +82,7 @@ POCSAGSettingsView::POCSAGSettingsView(
settings_.hide_bad_data = check_hide_bad.value(); settings_.hide_bad_data = check_hide_bad.value();
settings_.hide_addr_only = check_hide_addr_only.value(); settings_.hide_addr_only = check_hide_addr_only.value();
settings_.enable_ignore = check_ignore.value(); settings_.enable_ignore = check_ignore.value();
settings_.address_to_ignore = field_ignore.value(); settings_.address_to_ignore = field_ignore.to_integer();
nav.pop(); nav.pop();
}; };
@ -155,7 +156,7 @@ POCSAGAppView::~POCSAGAppView() {
// Save pmem settings. // Save pmem settings.
pmem::set_pocsag_ignore_address(settings_.address_to_ignore); 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() { void POCSAGAppView::refresh_ui() {

View file

@ -171,12 +171,11 @@ class POCSAGSettingsView : public View {
22, 22,
"Enable Ignored Address"}; "Enable Ignored Address"};
NumberField field_ignore{ SymField field_ignore{
{7 * 8, 13 * 16 + 8}, {7 * 8, 13 * 16 + 8},
7, 7,
{0, 9999999}, SymField::Type::Dec,
1, true /*explicit_edit*/};
'0'};
Button button_save{ Button button_save{
{11 * 8, 16 * 16, 10 * 8, 2 * 16}, {11 * 8, 16 * 16, 10 * 8, 2 * 16},
@ -223,7 +222,7 @@ class POCSAGAppView : public View {
void on_packet(const POCSAGPacketMessage* message); void on_packet(const POCSAGPacketMessage* message);
void on_stats(const POCSAGStatsMessage* stats); void on_stats(const POCSAGStatsMessage* stats);
uint32_t last_address = 0xFFFFFFFF; uint32_t last_address = 0;
pocsag::EccContainer ecc{}; pocsag::EccContainer ecc{};
pocsag::POCSAGState pocsag_state{&ecc}; pocsag::POCSAGState pocsag_state{&ecc};
POCSAGLogger logger{}; POCSAGLogger logger{};

View file

@ -186,13 +186,12 @@ ADSBSquawkView::ADSBSquawkView(
&field_squawk}); &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; if (!enabled) return;
ADSBFrame temp_frame; 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); frame_list.emplace_back(temp_frame);
} }
@ -277,7 +276,7 @@ ADSBTxView::~ADSBTxView() {
} }
void ADSBTxView::generate_frames() { 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(); frames.clear();

View file

@ -135,7 +135,7 @@ class ADSBSquawkView : public OptionTabView {
SymField field_squawk{ SymField field_squawk{
{10 * 8, 2 * 16}, {10 * 8, 2 * 16},
4, 4,
SymField::SYMFIELD_OCT}; SymField::Type::Oct};
}; };
class ADSBTXThread { class ADSBTXThread {
@ -235,7 +235,7 @@ class ADSBTxView : public View {
SymField sym_icao{ SymField sym_icao{
{10 * 8, 4 * 8}, {10 * 8, 4 * 8},
6, 6,
SymField::SYMFIELD_HEX}; SymField::Type::Hex};
Text text_frame{ Text text_frame{
{1 * 8, 29 * 8, 14 * 8, 16}, {1 * 8, 29 * 8, 14 * 8, 16},

View file

@ -48,9 +48,10 @@ APRSTXView::~APRSTXView() {
} }
void APRSTXView::start_tx() { void APRSTXView::start_tx() {
// TODO: Clean up this API to take string_views to avoid allocations.
make_aprs_frame( make_aprs_frame(
sym_source.value_string().c_str(), num_ssid_source.value(), sym_source.to_string().c_str(), num_ssid_source.value(),
sym_dest.value_string().c_str(), num_ssid_dest.value(), sym_dest.to_string().c_str(), num_ssid_dest.value(),
payload); payload);
// uint8_t * bb_data_ptr = shared_memory.bb_data.data; // uint8_t * bb_data_ptr = shared_memory.bb_data.data;

View file

@ -68,7 +68,8 @@ class APRSTXView : public View {
SymField sym_source{ SymField sym_source{
{7 * 8, 1 * 16}, {7 * 8, 1 * 16},
6, 6,
SymField::SYMFIELD_ALPHANUM}; SymField::Type::Alpha};
NumberField num_ssid_source{ NumberField num_ssid_source{
{19 * 8, 1 * 16}, {19 * 8, 1 * 16},
2, 2,
@ -79,7 +80,7 @@ class APRSTXView : public View {
SymField sym_dest{ SymField sym_dest{
{7 * 8, 2 * 16}, {7 * 8, 2 * 16},
6, 6,
SymField::SYMFIELD_ALPHANUM}; SymField::Type::Alpha};
NumberField num_ssid_dest{ NumberField num_ssid_dest{
{19 * 8, 2 * 16}, {19 * 8, 2 * 16},

View file

@ -42,12 +42,12 @@ CoasterPagerView::~CoasterPagerView() {
} }
void CoasterPagerView::generate_frame() { void CoasterPagerView::generate_frame() {
uint8_t frame[19]; constexpr uint8_t frame_bytes = 19;
uint32_t c; uint8_t frame[frame_bytes];
// Preamble (8 bytes) // Preamble (8 bytes)
for (c = 0; c < 8; c++) for (uint8_t c = 0; c < 8; c++)
frame[c] = 0x55; // Isn't this 0xAA ? frame[c] = 0x55;
// Sync word // Sync word
frame[8] = 0x2D; frame[8] = 0x2D;
@ -57,11 +57,11 @@ void CoasterPagerView::generate_frame() {
frame[10] = 8; frame[10] = 8;
// Data // Data
for (c = 0; c < 8; c++) auto data_bytes = to_byte_array(sym_data.to_integer());
frame[c + 11] = (sym_data.get_sym(c * 2) << 4) | sym_data.get_sym(c * 2 + 1); memcpy(&frame[11], data_bytes.data(), data_bytes.size());
// Copy for baseband // Copy for baseband
memcpy(shared_memory.bb_data.data, frame, 19); memcpy(shared_memory.bb_data.data, frame, frame_bytes);
} }
void CoasterPagerView::start_tx() { void CoasterPagerView::start_tx() {
@ -72,12 +72,7 @@ void CoasterPagerView::start_tx() {
baseband::set_fsk_data(19 * 8, 2280000 / 1000, 5000, 32); baseband::set_fsk_data(19 * 8, 2280000 / 1000, 5000, 32);
} }
void CoasterPagerView::on_tx_progress(const uint32_t progress, const bool done) { void CoasterPagerView::on_tx_progress(const uint32_t /*progress*/, const bool done) {
(void)progress;
uint16_t address = 0;
uint32_t c;
if (done) { if (done) {
if (tx_mode == SINGLE) { if (tx_mode == SINGLE) {
transmitter_model.disable(); transmitter_model.disable();
@ -85,18 +80,12 @@ void CoasterPagerView::on_tx_progress(const uint32_t progress, const bool done)
tx_view.set_transmitting(false); tx_view.set_transmitting(false);
} else if (tx_mode == SCAN) { } else if (tx_mode == SCAN) {
// Increment address // Increment address
uint64_t data = sym_data.to_integer();
for (c = 0; c < 4; c++) { uint16_t address = data & 0xFFFF;
address <<= 4;
address |= sym_data.get_sym(12 + c);
}
address++; address++;
data = (data & 0xFFFFFFFFFFFF0000) + address;
for (c = 0; c < 4; c++) { sym_data.set_value(data);
sym_data.set_sym(15 - c, address & 0x0F);
address >>= 4;
}
start_tx(); start_tx();
} }
@ -104,9 +93,6 @@ void CoasterPagerView::on_tx_progress(const uint32_t progress, const bool done)
} }
CoasterPagerView::CoasterPagerView(NavigationView& nav) { 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); baseband::run_image(portapack::spi_flash::image_tag_fsktx);
add_children({&labels, add_children({&labels,
@ -115,9 +101,7 @@ CoasterPagerView::CoasterPagerView(NavigationView& nav) {
&text_message, &text_message,
&tx_view}); &tx_view});
// Bytes to nibbles sym_data.set_value(0x44013B30303034BC);
for (c = 0; c < 16; c++)
sym_data.set_sym(c, (data_init[c >> 1] >> ((c & 1) ? 0 : 4)) & 0x0F);
checkbox_scan.set_value(false); checkbox_scan.set_value(false);

View file

@ -68,17 +68,19 @@ class CoasterPagerView : public View {
SymField sym_data{ SymField sym_data{
{7 * 8, 8 * 8}, {7 * 8, 8 * 8},
16, // 14 ? 12 ? 16,
SymField::SYMFIELD_HEX}; SymField::Type::Hex};
Checkbox checkbox_scan{ Checkbox checkbox_scan{
{10 * 8, 14 * 8}, {10 * 8, 14 * 8},
4, 4,
"Scan"}; "Scan"};
/*ProgressBar progressbar { /*
{ 5 * 8, 12 * 16, 20 * 8, 16 }, ProgressBar progressbar {
};*/ { 5 * 8, 12 * 16, 20 * 8, 16 }};
*/
Text text_message{ Text text_message{
{5 * 8, 13 * 16, 20 * 8, 16}, {5 * 8, 13 * 16, 20 * 8, 16},
""}; ""};

View file

@ -49,7 +49,6 @@ EncodersConfigView::EncodersConfigView(
&field_clk_step, &field_clk_step,
&field_frameduration, &field_frameduration,
&field_frameduration_step, &field_frameduration_step,
&symfield_word,
&text_format, &text_format,
&waveform}); &waveform});
@ -64,10 +63,6 @@ EncodersConfigView::EncodersConfigView(
options_enctype.set_options(enc_options); options_enctype.set_options(enc_options);
options_enctype.set_selected_index(0); options_enctype.set_selected_index(0);
symfield_word.on_change = [this]() {
generate_frame();
};
// Selecting input clock changes symbol and word duration // Selecting input clock changes symbol and word duration
field_clk.on_change = [this](int32_t value) { field_clk.on_change = [this](int32_t value) {
// value is in kHz, new_value is in us // value is in kHz, new_value is in us
@ -98,33 +93,46 @@ void EncodersConfigView::focus() {
} }
void EncodersConfigView::on_type_change(size_t index) { void EncodersConfigView::on_type_change(size_t index) {
std::string format_string = ""; // Remove existing SymField controls.
size_t word_length; for (auto& symfield : symfields_word)
char symbol_type; remove_child(symfield.get());
symfields_word.clear();
encoder_def = &encoder_defs[index]; encoder_def = &encoder_defs[index];
field_clk.set_value(encoder_def->default_speed / 1000); field_clk.set_value(encoder_def->default_speed / 1000);
field_repeat_min.set_value(encoder_def->repeat_min); field_repeat_min.set_value(encoder_def->repeat_min);
// SymField setup // Add new SymFields.
word_length = encoder_def->word_length; Point pos{2 * 8, 9 * 8};
symfield_word.set_length(word_length); std::string format_string;
size_t n = 0, i = 0; uint8_t word_length = encoder_def->word_length;
while (n < word_length) { auto on_change_handler = [this](SymField&) {
symbol_type = encoder_def->word_format[i++]; generate_frame();
if (symbol_type == 'A') { };
symfield_word.set_symbol_list(n++, encoder_def->address_symbols);
format_string += 'A'; for (uint8_t i = 0; i < word_length; i++) {
} else if (symbol_type == 'D') { auto symbol_type = encoder_def->word_format[i];
symfield_word.set_symbol_list(n++, encoder_def->data_symbols); symfields_word.push_back(std::make_unique<SymField>(pos, 1));
format_string += 'D'; 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 // Ugly :( Pad to erase
format_string.append(24 - format_string.size(), ' '); format_string.append(24 - format_string.size(), ' ');
text_format.set(format_string); text_format.set(format_string);
generate_frame(); generate_frame();
@ -146,17 +154,18 @@ void EncodersConfigView::draw_waveform() {
} }
void EncodersConfigView::generate_frame() { void EncodersConfigView::generate_frame() {
size_t i = 0;
frame_fragments.clear(); frame_fragments.clear();
size_t i = 0;
for (auto c : encoder_def->word_format) { for (auto c : encoder_def->word_format) {
if (c == 'S') if (c == 'S')
frame_fragments += encoder_def->sync; frame_fragments += encoder_def->sync;
else if (!c) else if (c == '\0')
break; break;
else else {
frame_fragments += encoder_def->bit_format[symfield_word.get_sym(i++)]; auto offset = symfields_word[i++]->get_offset(0);
frame_fragments += encoder_def->bit_format[offset];
}
} }
draw_waveform(); draw_waveform();

View file

@ -29,6 +29,9 @@
#include "app_settings.hpp" #include "app_settings.hpp"
#include "radio_state.hpp" #include "radio_state.hpp"
#include <memory>
#include <vector>
using namespace encoders; using namespace encoders;
namespace ui { namespace ui {
@ -113,10 +116,7 @@ class EncodersConfigView : public View {
{"100", 100}, {"100", 100},
{"1000", 1000}}}; {"1000", 1000}}};
SymField symfield_word{ std::vector<std::unique_ptr<SymField>> symfields_word{};
{2 * 8, 9 * 8},
20,
SymField::SYMFIELD_DEF};
Text text_format{ Text text_format{
{2 * 8, 11 * 8, 24 * 8, 16}, {2 * 8, 11 * 8, 24 * 8, 16},

View file

@ -97,12 +97,12 @@ size_t KeyfobView::generate_frame() {
uint64_t payload; uint64_t payload;
// Symfield word to frame // 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++) { for (size_t i = 0; i < 5; i++) {
frame[4 - i] = payload & 0xFF; frame[4 - i] = payload & 0xFF;
payload >>= 8; payload >>= 8;
} }
payload = field_payload_b.value_hex_u64(); payload = field_payload_b.to_integer();
for (size_t i = 0; i < 5; i++) { for (size_t i = 0; i < 5; i++) {
frame[9 - i] = payload & 0xFF; frame[9 - i] = payload & 0xFF;
payload >>= 8; payload >>= 8;
@ -136,9 +136,6 @@ void KeyfobView::focus() {
} }
KeyfobView::~KeyfobView() { KeyfobView::~KeyfobView() {
// save app settings
settings.save("tx_keyfob", &app_settings);
transmitter_model.disable(); transmitter_model.disable();
baseband::shutdown(); baseband::shutdown();
} }
@ -167,12 +164,12 @@ void KeyfobView::on_make_change(size_t index) {
// DEBUG // DEBUG
void KeyfobView::update_symfields() { void KeyfobView::update_symfields() {
for (size_t i = 0; i < 5; i++) { for (size_t i = 0; i < 5; i++) {
field_payload_a.set_sym(i << 1, frame[i] >> 4); field_payload_a.set_offset(i << 1, frame[i] >> 4);
field_payload_a.set_sym((i << 1) + 1, frame[i] & 0x0F); field_payload_a.set_offset((i << 1) + 1, frame[i] & 0x0F);
} }
for (size_t i = 0; i < 5; i++) { for (size_t i = 0; i < 5; i++) {
field_payload_b.set_sym(i << 1, frame[5 + i] >> 4); field_payload_b.set_offset(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) + 1, frame[5 + i] & 0x0F);
} }
} }
@ -212,13 +209,6 @@ KeyfobView::KeyfobView(
&progressbar, &progressbar,
&tx_view}); &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; frame[0] = 0x55;
update_symfields(); update_symfields();

View file

@ -48,7 +48,7 @@ class KeyfobView : public View {
OOK_SAMPLERATE /* sampling rate */ OOK_SAMPLERATE /* sampling rate */
}; };
app_settings::SettingsManager settings_{ app_settings::SettingsManager settings_{
"tx_keyfob", , app_settings::Mode::TX}; "tx_keyfob", app_settings::Mode::TX};
// 1013210ns / bit // 1013210ns / bit
static constexpr uint32_t subaru_samples_per_bit = (OOK_SAMPLERATE * 0.00101321); static constexpr uint32_t subaru_samples_per_bit = (OOK_SAMPLERATE * 0.00101321);
@ -98,11 +98,11 @@ class KeyfobView : public View {
SymField field_payload_a{ SymField field_payload_a{
{2 * 8, 5 * 16}, {2 * 8, 5 * 16},
10, 10,
SymField::SYMFIELD_HEX}; SymField::Type::Hex};
SymField field_payload_b{ SymField field_payload_b{
{13 * 8, 5 * 16}, {13 * 8, 5 * 16},
10, 10,
SymField::SYMFIELD_HEX}; SymField::Type::Hex};
Text text_status{ Text text_status{
{2 * 8, 13 * 16, 128, 16}, {2 * 8, 13 * 16, 128, 16},

View file

@ -23,7 +23,6 @@
#include "ui_modemsetup.hpp" #include "ui_modemsetup.hpp"
#include "portapack.hpp" #include "portapack.hpp"
#include "portapack_persistent_memory.hpp" #include "portapack_persistent_memory.hpp"
using namespace portapack; using namespace portapack;
@ -41,15 +40,20 @@ ModemSetupView::ModemSetupView(
using options_t = std::vector<option_t>; using options_t = std::vector<option_t>;
options_t modem_options; options_t modem_options;
add_children({&labels, add_children({
&field_baudrate, &labels,
&field_mark, &field_baudrate,
&field_space, &field_mark,
&field_repeat, &field_space,
&options_modem, &field_repeat,
&button_set_modem, &options_modem,
&sym_format, &button_set_modem,
&button_save}); &sym_format_data,
&sym_format_parity,
&sym_format_stop,
&sym_format_msb,
&button_save,
});
// Only list AFSK modems for now // Only list AFSK modems for now
for (size_t i = 0; i < MODEM_DEF_COUNT; i++) { 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_options(modem_options);
options_modem.set_selected_index(0); options_modem.set_selected_index(0);
sym_format.set_symbol_list(0, "6789"); // Data bits sym_format_data.set_offset(0, persistent_memory::serial_format().data_bits - 6);
sym_format.set_symbol_list(1, "NEo"); // Parity sym_format_parity.set_offset(0, persistent_memory::serial_format().parity);
sym_format.set_symbol_list(2, "012"); // Stop bits sym_format_stop.set_offset(0, persistent_memory::serial_format().stop_bits);
sym_format.set_symbol_list(3, "ML"); // MSB/LSB first sym_format_msb.set_offset(0, persistent_memory::serial_format().bit_order);
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);
field_mark.set_value(persistent_memory::afsk_mark_freq()); field_mark.set_value(persistent_memory::afsk_mark_freq());
field_space.set_value(persistent_memory::afsk_space_freq()); field_space.set_value(persistent_memory::afsk_space_freq());
@ -84,18 +83,17 @@ ModemSetupView::ModemSetupView(
}; };
button_save.on_select = [this, &nav](Button&) { button_save.on_select = [this, &nav](Button&) {
serial_format_t serial_format;
persistent_memory::set_afsk_mark(field_mark.value()); persistent_memory::set_afsk_mark(field_mark.value());
persistent_memory::set_afsk_space(field_space.value()); persistent_memory::set_afsk_space(field_space.value());
persistent_memory::set_modem_baudrate(field_baudrate.value()); persistent_memory::set_modem_baudrate(field_baudrate.value());
persistent_memory::set_modem_repeat(field_repeat.value()); persistent_memory::set_modem_repeat(field_repeat.value());
serial_format.data_bits = sym_format.get_sym(0) + 6; serial_format_t serial_format{};
serial_format.parity = (parity_enum)sym_format.get_sym(1); serial_format.data_bits = sym_format_data.get_offset(0) + 6;
serial_format.stop_bits = sym_format.get_sym(2); serial_format.parity = (parity_enum)sym_format_parity.get_offset(0);
serial_format.bit_order = (order_enum)sym_format.get_sym(3); 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); persistent_memory::set_serial_format(serial_format);

View file

@ -79,10 +79,25 @@ class ModemSetupView : public View {
7, 7,
{}}; {}};
SymField sym_format{ SymField sym_format_data{
{16 * 8, 22 * 8}, {16 * 8, 22 * 8},
4, 1,
SymField::SYMFIELD_DEF}; "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{ Button button_set_modem{
{23 * 8, 6 * 8 - 4, 6 * 8, 24}, {23 * 8, 6 * 8 - 4, 6 * 8, 24},

View file

@ -21,6 +21,7 @@
*/ */
#include "ui_numbers.hpp" #include "ui_numbers.hpp"
#include "ui_styles.hpp"
#include "string_format.hpp" #include "string_format.hpp"
#include "portapack.hpp" #include "portapack.hpp"
@ -75,7 +76,7 @@ void NumbersStationView::prepare_audio() {
return; return;
} }
code = symfield_code.get_sym(code_index); code = symfield_code.get_offset(code_index);
if (code >= 10) { if (code >= 10) {
memset(audio_buffer, 0, 1024); memset(audio_buffer, 0, 1024);
@ -144,7 +145,7 @@ void NumbersStationView::on_tick_second() {
armed_blink = not armed_blink; armed_blink = not armed_blink;
if (armed_blink) if (armed_blink)
check_armed.set_style(&style_red); check_armed.set_style(&Styles::red);
else else
check_armed.set_style(&style()); check_armed.set_style(&style());
@ -152,14 +153,12 @@ void NumbersStationView::on_tick_second() {
} }
void NumbersStationView::on_voice_changed(size_t index) { void NumbersStationView::on_voice_changed(size_t index) {
std::string code_list = ""; std::string code_list;
for (const auto& wavs : voices[index].available_wavs) for (const auto& wavs : voices[index].available_wavs)
code_list += wavs.code; code_list += wavs.code;
for (uint32_t c = 0; c < 25; c++) symfield_code.set_symbol_list(code_list);
symfield_code.set_symbol_list(c, code_list);
current_voice = &voices[index]; current_voice = &voices[index];
} }
@ -265,17 +264,17 @@ NumbersStationView::NumbersStationView(
}; };
// DEBUG // DEBUG
symfield_code.set_sym(0, 10); symfield_code.set_offset(0, 10);
symfield_code.set_sym(1, 3); symfield_code.set_offset(1, 3);
symfield_code.set_sym(2, 4); symfield_code.set_offset(2, 4);
symfield_code.set_sym(3, 11); symfield_code.set_offset(3, 11);
symfield_code.set_sym(4, 6); symfield_code.set_offset(4, 6);
symfield_code.set_sym(5, 1); symfield_code.set_offset(5, 1);
symfield_code.set_sym(6, 9); symfield_code.set_offset(6, 9);
symfield_code.set_sym(7, 7); symfield_code.set_offset(7, 7);
symfield_code.set_sym(8, 8); symfield_code.set_offset(8, 8);
symfield_code.set_sym(9, 0); symfield_code.set_offset(9, 0);
symfield_code.set_sym(10, 12); // End symfield_code.set_offset(10, 12); // End
/* /*
rtc::RTC datetime; rtc::RTC datetime;

View file

@ -142,16 +142,19 @@ class NumbersStationView : public View {
SymField symfield_code{ SymField symfield_code{
{1 * 8, 10 * 8}, {1 * 8, 10 * 8},
25, 25,
SymField::SYMFIELD_DEF}; SymField::Type::Custom};
Checkbox check_armed{ Checkbox check_armed{
{2 * 8, 13 * 16}, {2 * 8, 13 * 16},
5, 5,
"Armed"}; "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{ Button button_exit{
{21 * 8, 16 * 16, 64, 32}, {21 * 8, 16 * 16, 64, 32},
"Exit"}; "Exit"};

View file

@ -56,7 +56,7 @@ bool POCSAGTXView::start_tx() {
pocsag::BitRate bitrate; pocsag::BitRate bitrate;
std::vector<uint32_t> codewords; std::vector<uint32_t> codewords;
address = field_address.value_dec_u32(); address = field_address.to_integer();
if (address > 0x1FFFFFU) { if (address > 0x1FFFFFU) {
nav_.display_modal("Bad address", "Address must be less\nthan 2097152."); nav_.display_modal("Bad address", "Address must be less\nthan 2097152.");
return false; return false;
@ -137,12 +137,7 @@ POCSAGTXView::POCSAGTXView(
options_bitrate.set_selected_index(1); // 1200bps options_bitrate.set_selected_index(1); // 1200bps
options_type.set_selected_index(0); // Address only options_type.set_selected_index(0); // Address only
// TODO: set_value for whole symfield field_address.set_value(persistent_memory::pocsag_last_address());
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;
}
options_type.on_change = [this](size_t, int32_t i) { options_type.on_change = [this](size_t, int32_t i) {
if (i == 2) if (i == 2)

View file

@ -94,8 +94,7 @@ class POCSAGTXView : public View {
SymField field_address{ SymField field_address{
{11 * 8, 6 * 8}, {11 * 8, 6 * 8},
7, 7};
SymField::SYMFIELD_DEC};
OptionsField options_type{ OptionsField options_type{
{11 * 8, 8 * 8}, {11 * 8, 8 * 8},

View file

@ -164,7 +164,7 @@ RDSView::~RDSView() {
} }
void RDSView::start_tx() { 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.PTY = options_pty.selected_index_value();
rds_flags.DI = view_PSN.mono_stereo ? 1 : 0; rds_flags.DI = view_PSN.mono_stereo ? 1 : 0;
rds_flags.TP = check_TP.value(); rds_flags.TP = check_TP.value();
@ -212,12 +212,9 @@ RDSView::RDSView(
check_TP.set_value(true); check_TP.set_value(true);
sym_pi_code.set_sym(0, 0xF); sym_pi_code.set_value(0xF3E0);
sym_pi_code.set_sym(1, 0x3); sym_pi_code.on_change = [this](SymField&) {
sym_pi_code.set_sym(2, 0xE); rds_flags.PI_code = sym_pi_code.to_integer();
sym_pi_code.set_sym(3, 0x0);
sym_pi_code.on_change = [this]() {
rds_flags.PI_code = sym_pi_code.value_hex_u64();
}; };
options_pty.set_selected_index(0); // None options_pty.set_selected_index(0); // None

View file

@ -287,7 +287,7 @@ class RDSView : public View {
SymField sym_pi_code{ SymField sym_pi_code{
{13 * 8, 28 + 16}, {13 * 8, 28 + 16},
4, 4,
SymField::SYMFIELD_HEX}; SymField::Type::Hex};
/*OptionsField options_coverage { /*OptionsField options_coverage {
{ 17 * 8, 32 + 8 }, { 17 * 8, 32 + 8 },

View file

@ -50,7 +50,7 @@ void SigGenView::update_config() {
} }
void SigGenView::update_tone() { 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() { void SigGenView::start_tx() {
@ -97,8 +97,8 @@ SigGenView::SigGenView(
field_stop.set_value(1); field_stop.set_value(1);
symfield_tone.set_sym(1, 1); // Default: 1000 Hz symfield_tone.set_value(1000); // Default: 1000 Hz
symfield_tone.on_change = [this]() { symfield_tone.on_change = [this](SymField&) {
if (auto_update) if (auto_update)
update_tone(); update_tone();
}; };

View file

@ -92,9 +92,8 @@ class SigGenView : public View {
""}; ""};
SymField symfield_tone{ SymField symfield_tone{
{13 * 8, 7 * 8}, {12 * 8, 7 * 8},
5, 5};
SymField::SYMFIELD_DEC};
Button button_update{ Button button_update{
{5 * 8, 10 * 8, 8 * 8, 3 * 8}, {5 * 8, 10 * 8, 8 * 8, 3 * 8},

View file

@ -21,6 +21,8 @@
#include "string_format.hpp" #include "string_format.hpp"
using namespace std::literals;
/* This takes a pointer to the end of a buffer /* This takes a pointer to the end of a buffer
* and fills it backwards towards the front. * and fills it backwards towards the front.
* The return value 'q' is a pointer to the start. * 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; return final_str;
} }
static void to_string_hex_internal(char* p, const uint64_t n, const int32_t l) { static char* to_string_hex_internal(char* ptr, uint64_t value, uint8_t length) {
const uint32_t d = n & 0xf; if (length == 0)
p[l] = (d > 9) ? (d + 55) : (d + 48); return ptr;
if (l > 0) {
to_string_hex_internal(p, n >> 4, l - 1); *(--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) { std::string to_string_hex(uint64_t value, int32_t length) {
char p[32]; constexpr uint8_t buffer_length = 33;
char buffer[buffer_length];
l = std::min<int32_t>(l, 31); char* ptr = &buffer[buffer_length - 1];
to_string_hex_internal(p, n, l - 1); *ptr = '\0';
p[l] = 0;
return p; 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 to_string_hex_array(uint8_t* array, int32_t length) {
std::string str_return = ""; std::string str_return;
uint8_t bytes; str_return.reserve(length);
for (bytes = 0; bytes < l; bytes++) for (uint8_t i = 0; i < length; i++)
str_return += to_string_hex(array[bytes], 2); str_return += to_string_hex(array[i], 2);
return str_return; return str_return;
} }
@ -364,3 +368,26 @@ std::string trimr(std::string_view str) {
std::string truncate(std::string_view str, size_t length) { std::string truncate(std::string_view str, size_t length) {
return std::string{str.length() <= length ? str : str.substr(0, 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
}

View file

@ -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_int(int64_t n);
std::string to_string_dec_uint(uint64_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_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_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_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_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(uint64_t n, int32_t length);
std::string to_string_hex_array(uint8_t* const array, const int32_t l = 0); 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_freq(const uint64_t f);
std::string to_string_short_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 trimr(std::string_view str); // Remove trailing spaces
std::string truncate(std::string_view, size_t length); 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__*/ #endif /*__STRING_FORMAT_H__*/

View file

@ -37,6 +37,29 @@ struct Style {
Style invert() const; 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 Widget;
class Painter { class Painter {

View file

@ -1813,7 +1813,7 @@ void NumberField::set_value(int32_t new_value, bool trigger_change) {
new_value = range.second + new_value + 1; 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()) { if (new_value != value()) {
value_ = new_value; value_ = new_value;
@ -1868,169 +1868,182 @@ bool NumberField::on_touch(const TouchEvent event) {
return true; 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::SymField( SymField::SymField(
Point parent_pos, Point parent_pos,
size_t length, size_t length,
symfield_type type) Type type,
: Widget{{parent_pos, {static_cast<ui::Dim>(8 * length), 16}}}, bool explicit_edits)
length_{length}, : Widget{{parent_pos, {char_width * (int)length, 16}}},
type_{type} { type_{type},
uint32_t c; explicit_edits_{explicit_edits} {
if (length == 0)
length = 1;
// Auto-init selected_ = length - 1;
if (type == SYMFIELD_OCT) { value_.resize(length);
for (c = 0; c < length; c++)
set_symbol_list(c, "01234567"); switch (type) {
} else if (type == SYMFIELD_DEC) { case Type::Oct:
for (c = 0; c < length; c++) set_symbol_list("01234567");
set_symbol_list(c, "0123456789"); break;
} else if (type == SYMFIELD_HEX) {
for (c = 0; c < length; c++) case Type::Dec:
set_symbol_list(c, "0123456789ABCDEF"); set_symbol_list("0123456789");
} else if (type == SYMFIELD_ALPHANUM) { break;
for (c = 0; c < length; c++)
set_symbol_list(c, " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"); 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); set_focusable(true);
} }
uint16_t SymField::concatenate_4_octal_u16() { SymField::SymField(
// input array 4 octal digits , return 12 bits, same order, trippled A4-A2-A1|B4-B2-B1|C4-C2-C1|D4-D2-D1 Point parent_pos,
uint32_t mul = 1; size_t length,
uint16_t v = 0; std::string symbol_list,
bool explicit_edits)
if (type_ == SYMFIELD_OCT) { : SymField{parent_pos, length, Type::Custom, explicit_edits} {
for (uint32_t c = 0; c < length_; c++) { set_symbol_list(std::move(symbol_list));
v += values_[(length_ - 1 - c)] * mul;
mul *= 8; // shift 3 bits to the right every new octal squawk digit
}
return v;
} else
return 0;
} }
uint32_t SymField::value_dec_u32() { char SymField::get_symbol(size_t index) const {
uint32_t mul = 1, v = 0; if (index >= value_.length())
if (type_ == SYMFIELD_DEC) {
for (uint32_t c = 0; c < length_; c++) {
v += values_[(length_ - 1 - c)] * mul;
mul *= 10;
}
return v;
} else
return 0; 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 v = 0;
uint64_t mul = 1;
uint8_t radix = get_radix();
if (type_ != SYMFIELD_DEF) { for (int i = value_.length() - 1; i >= 0; --i) {
for (uint32_t c = 0; c < length_; c++) auto temp = char_to_uint(value_[i], radix);
v += (uint64_t)(values_[c]) << (4 * (length_ - 1 - c)); v += temp * mul;
return v; mul *= radix;
} 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]];
}
} }
return return_string; return v;
} }
uint32_t SymField::get_sym(const uint32_t index) { const std::string& SymField::to_string() const {
if (index >= length_) return 0; return value_;
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]);
} }
void SymField::paint(Painter& painter) { void SymField::paint(Painter& painter) {
Point pt_draw = screen_pos(); Point p = screen_pos();
if (erase_prev_) { for (size_t n = 0; n < value_.length(); n++) {
painter.fill_rectangle({pt_draw, {(int)prev_length_ * 8, 16}}, Color::black()); auto c = value_[n];
erase_prev_ = false; MutableStyle paint_style{style()};
}
for (size_t n = 0; n < length_; n++) { // Only highlight while focused.
const auto text = symbol_list_[n].substr(values_[n], 1); 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( painter.draw_char(p, paint_style, c);
pt_draw, p += {8, 0};
paint_style,
text);
pt_draw += {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) { switch (key) {
case KeyEvent::Select: case KeyEvent::Select:
if (on_select) { editing_ = !editing_;
on_select(*this); set_dirty();
return true; return true;
}
break;
case KeyEvent::Left: case KeyEvent::Left:
if (selected_ > 0) { if (selected_ > 0) {
@ -2041,13 +2054,27 @@ bool SymField::on_key(const KeyEvent key) {
break; break;
case KeyEvent::Right: case KeyEvent::Right:
if (selected_ < (length_ - 1)) { if (selected_ < (value_.length() - 1)) {
selected_++; selected_++;
set_dirty(); set_dirty();
return true; return true;
} }
break; break;
case KeyEvent::Up:
if (editing_) {
on_encoder(1);
return true;
}
break;
case KeyEvent::Down:
if (editing_) {
on_encoder(-1);
return true;
}
break;
default: default:
break; break;
} }
@ -2055,29 +2082,66 @@ bool SymField::on_key(const KeyEvent key) {
return false; return false;
} }
bool SymField::on_encoder(const EncoderEvent delta) { bool SymField::on_encoder(EncoderEvent delta) {
int32_t new_value = (int)values_[selected_] + delta; if (explicit_edits_ && !editing_)
return false;
if (new_value >= 0) // TODO: Wrapping or carrying might be nice.
set_sym(selected_, values_[selected_] + delta); int offset = get_offset(selected_) + delta;
offset = clip<int>(offset, 0, symbols_.length() - 1);
set_offset(selected_, offset);
return true; return true;
} }
bool SymField::on_touch(const TouchEvent event) { bool SymField::on_touch(TouchEvent event) {
if (event.type == TouchEvent::Type::Start) { if (event.type == TouchEvent::Type::Start)
focus(); focus();
}
return true; return true;
} }
int32_t SymField::clip_value(const uint32_t index, const uint32_t value) { char SymField::ensure_valid(char symbol) const {
size_t symbol_count = symbol_list_[index].length() - 1; // NB: Linear search - symbol lists are small.
auto pos = symbols_.find(symbol);
return pos != std::string::npos ? symbol : symbols_[0];
}
if (value > symbol_count) void SymField::ensure_all_symbols() {
return symbol_count; auto temp = value_;
else
return 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 **************************************************************/ /* Waveform **************************************************************/

View file

@ -751,52 +751,93 @@ class NumberField : public Widget {
const char fill_char; const char fill_char;
int32_t value_{0}; int32_t value_{0};
bool can_loop{}; 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 { class SymField : public Widget {
public: public:
std::function<void(SymField&)> on_select{}; std::function<void(SymField&)> on_change{};
std::function<void()> on_change{};
enum symfield_type { enum class Type {
SYMFIELD_OCT, Custom,
SYMFIELD_DEC, Oct,
SYMFIELD_HEX, Dec,
SYMFIELD_ALPHANUM, Hex,
SYMFIELD_DEF // User DEFined 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(const SymField&) = delete;
SymField(SymField&&) = delete; SymField(SymField&&) = delete;
uint32_t get_sym(const uint32_t index); /* Gets the symbol (character) at the specified index. */
void set_sym(const uint32_t index, const uint32_t new_value); char get_symbol(size_t index) const;
void set_length(const uint32_t new_length);
void set_symbol_list(const uint32_t index, const std::string symbol_list); /* Sets the symbol (character) at the specified index. */
uint16_t concatenate_4_octal_u16(); void set_symbol(size_t index, char symbol);
uint32_t value_dec_u32();
uint64_t value_hex_u64(); /* Gets the symbol's offset in the symbol list at the specified index. */
std::string value_string(); 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; void paint(Painter& painter) override;
bool on_key(KeyEvent key) override;
bool on_key(const KeyEvent key) override; bool on_encoder(EncoderEvent delta) override;
bool on_encoder(const EncoderEvent delta) override; bool on_touch(TouchEvent event) override;
bool on_touch(const TouchEvent event) override;
private: private:
std::string symbol_list_[32] = {"01"}; // Failsafe init /* Ensure the specified symbol is in the symbol list. */
uint32_t values_[32] = {0}; char ensure_valid(char symbol) const;
uint32_t selected_ = 0;
size_t length_, prev_length_ = 0;
bool erase_prev_ = false;
symfield_type type_{};
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 { class Waveform : public Widget {

View file

@ -22,12 +22,13 @@
#ifndef __UTILITY_H__ #ifndef __UTILITY_H__
#define __UTILITY_H__ #define __UTILITY_H__
#include <type_traits> #include <algorithm>
#include <array>
#include <complex>
#include <cstdint> #include <cstdint>
#include <cstddef> #include <cstddef>
#include <algorithm>
#include <complex>
#include <memory> #include <memory>
#include <type_traits>
#define LOCATE_IN_RAM __attribute__((section(".ramtext"))) #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; 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. */ /* Returns value constrained to min and max. */
template <class T> template <class T>
constexpr const T& clip(const T& value, const T& minimum, const T& maximum) { constexpr const T& clip(const T& value, const T& minimum, const T& maximum) {

View file

@ -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"); 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.") { TEST_CASE("trim removes whitespace.") {
CHECK(trim(" foo\n") == "foo"); CHECK(trim(" foo\n") == "foo");
} }

View file

@ -71,4 +71,22 @@ TEST_CASE("ms_duration should return duration.") {
TEST_CASE("ms_duration not fault when passed zero.") { TEST_CASE("ms_duration not fault when passed zero.") {
CHECK_EQ(ms_duration(0, 0, 0), 0); 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);
} }