diff --git a/firmware/application/apps/pocsag_app.cpp b/firmware/application/apps/pocsag_app.cpp index 20af8ab55..7fd814c0d 100644 --- a/firmware/application/apps/pocsag_app.cpp +++ b/firmware/application/apps/pocsag_app.cpp @@ -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(); }; @@ -155,7 +156,7 @@ POCSAGAppView::~POCSAGAppView() { // 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() { diff --git a/firmware/application/apps/pocsag_app.hpp b/firmware/application/apps/pocsag_app.hpp index ef8c2f616..3e93c9ebd 100644 --- a/firmware/application/apps/pocsag_app.hpp +++ b/firmware/application/apps/pocsag_app.hpp @@ -171,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}, @@ -223,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{}; diff --git a/firmware/application/apps/ui_adsb_tx.cpp b/firmware/application/apps/ui_adsb_tx.cpp index 407581455..913bbd6b2 100644 --- a/firmware/application/apps/ui_adsb_tx.cpp +++ b/firmware/application/apps/ui_adsb_tx.cpp @@ -186,13 +186,12 @@ ADSBSquawkView::ADSBSquawkView( &field_squawk}); } -void ADSBSquawkView::collect_frames(const uint32_t ICAO_address, std::vector& frame_list) { +void ADSBSquawkView::collect_frames(const uint32_t /*ICAO_address*/, std::vector& 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(); diff --git a/firmware/application/apps/ui_adsb_tx.hpp b/firmware/application/apps/ui_adsb_tx.hpp index cd80aaa05..507d9da13 100644 --- a/firmware/application/apps/ui_adsb_tx.hpp +++ b/firmware/application/apps/ui_adsb_tx.hpp @@ -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}, diff --git a/firmware/application/apps/ui_aprs_tx.cpp b/firmware/application/apps/ui_aprs_tx.cpp index 339ce22a0..e5c6c79ed 100644 --- a/firmware/application/apps/ui_aprs_tx.cpp +++ b/firmware/application/apps/ui_aprs_tx.cpp @@ -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; diff --git a/firmware/application/apps/ui_aprs_tx.hpp b/firmware/application/apps/ui_aprs_tx.hpp index 5336300cc..e94a75366 100644 --- a/firmware/application/apps/ui_aprs_tx.hpp +++ b/firmware/application/apps/ui_aprs_tx.hpp @@ -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}, diff --git a/firmware/application/apps/ui_coasterp.cpp b/firmware/application/apps/ui_coasterp.cpp index 25a51c839..9849b3d9b 100644 --- a/firmware/application/apps/ui_coasterp.cpp +++ b/firmware/application/apps/ui_coasterp.cpp @@ -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); diff --git a/firmware/application/apps/ui_coasterp.hpp b/firmware/application/apps/ui_coasterp.hpp index e227d0d56..e6cfcf18b 100644 --- a/firmware/application/apps/ui_coasterp.hpp +++ b/firmware/application/apps/ui_coasterp.hpp @@ -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}, ""}; diff --git a/firmware/application/apps/ui_encoders.cpp b/firmware/application/apps/ui_encoders.cpp index 2857aa1db..263b5af3b 100644 --- a/firmware/application/apps/ui_encoders.cpp +++ b/firmware/application/apps/ui_encoders.cpp @@ -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(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(); diff --git a/firmware/application/apps/ui_encoders.hpp b/firmware/application/apps/ui_encoders.hpp index f9c871628..b392e80f9 100644 --- a/firmware/application/apps/ui_encoders.hpp +++ b/firmware/application/apps/ui_encoders.hpp @@ -29,6 +29,9 @@ #include "app_settings.hpp" #include "radio_state.hpp" +#include +#include + 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> symfields_word{}; Text text_format{ {2 * 8, 11 * 8, 24 * 8, 16}, diff --git a/firmware/application/apps/ui_keyfob.cpp b/firmware/application/apps/ui_keyfob.cpp index 98e1bc70c..da4da14bc 100644 --- a/firmware/application/apps/ui_keyfob.cpp +++ b/firmware/application/apps/ui_keyfob.cpp @@ -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(); diff --git a/firmware/application/apps/ui_keyfob.hpp b/firmware/application/apps/ui_keyfob.hpp index d07940e33..f02e7e8b5 100644 --- a/firmware/application/apps/ui_keyfob.hpp +++ b/firmware/application/apps/ui_keyfob.hpp @@ -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}, diff --git a/firmware/application/apps/ui_modemsetup.cpp b/firmware/application/apps/ui_modemsetup.cpp index 7eb80cc45..e81f87ca2 100644 --- a/firmware/application/apps/ui_modemsetup.cpp +++ b/firmware/application/apps/ui_modemsetup.cpp @@ -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; 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); diff --git a/firmware/application/apps/ui_modemsetup.hpp b/firmware/application/apps/ui_modemsetup.hpp index efa25c8a9..91d73c8c6 100644 --- a/firmware/application/apps/ui_modemsetup.hpp +++ b/firmware/application/apps/ui_modemsetup.hpp @@ -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}, diff --git a/firmware/application/apps/ui_numbers.cpp b/firmware/application/apps/ui_numbers.cpp index 0cc622232..63e20e3d6 100644 --- a/firmware/application/apps/ui_numbers.cpp +++ b/firmware/application/apps/ui_numbers.cpp @@ -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; diff --git a/firmware/application/apps/ui_numbers.hpp b/firmware/application/apps/ui_numbers.hpp index 08253beb7..40afedf0e 100644 --- a/firmware/application/apps/ui_numbers.hpp +++ b/firmware/application/apps/ui_numbers.hpp @@ -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"}; diff --git a/firmware/application/apps/ui_pocsag_tx.cpp b/firmware/application/apps/ui_pocsag_tx.cpp index e83cc89c2..a8f796ec4 100644 --- a/firmware/application/apps/ui_pocsag_tx.cpp +++ b/firmware/application/apps/ui_pocsag_tx.cpp @@ -56,7 +56,7 @@ bool POCSAGTXView::start_tx() { pocsag::BitRate bitrate; std::vector 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) diff --git a/firmware/application/apps/ui_pocsag_tx.hpp b/firmware/application/apps/ui_pocsag_tx.hpp index a5f198616..649e233f3 100644 --- a/firmware/application/apps/ui_pocsag_tx.hpp +++ b/firmware/application/apps/ui_pocsag_tx.hpp @@ -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}, diff --git a/firmware/application/apps/ui_rds.cpp b/firmware/application/apps/ui_rds.cpp index ee63099ab..a17e7a59a 100644 --- a/firmware/application/apps/ui_rds.cpp +++ b/firmware/application/apps/ui_rds.cpp @@ -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 diff --git a/firmware/application/apps/ui_rds.hpp b/firmware/application/apps/ui_rds.hpp index 8b47a7257..87f529269 100644 --- a/firmware/application/apps/ui_rds.hpp +++ b/firmware/application/apps/ui_rds.hpp @@ -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 }, diff --git a/firmware/application/apps/ui_siggen.cpp b/firmware/application/apps/ui_siggen.cpp index 010c5fa7f..859ad9a3d 100644 --- a/firmware/application/apps/ui_siggen.cpp +++ b/firmware/application/apps/ui_siggen.cpp @@ -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(); }; diff --git a/firmware/application/apps/ui_siggen.hpp b/firmware/application/apps/ui_siggen.hpp index 85881a8fa..f9e2f44f2 100644 --- a/firmware/application/apps/ui_siggen.hpp +++ b/firmware/application/apps/ui_siggen.hpp @@ -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}, diff --git a/firmware/application/string_format.cpp b/firmware/application/string_format.cpp index a63b0167e..3f49a8052 100644 --- a/firmware/application/string_format.cpp +++ b/firmware/application/string_format.cpp @@ -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(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(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 +} diff --git a/firmware/application/string_format.hpp b/firmware/application/string_format.hpp index 98476fce1..2c043d06c 100644 --- a/firmware/application/string_format.hpp +++ b/firmware/application/string_format.hpp @@ -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 +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__*/ diff --git a/firmware/common/ui_painter.hpp b/firmware/common/ui_painter.hpp index c71c23032..5343826fe 100644 --- a/firmware/common/ui_painter.hpp +++ b/firmware/common/ui_painter.hpp @@ -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 { diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 422d9e7d3..8af6c4384 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -1813,7 +1813,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 +1868,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(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(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 +2054,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 +2082,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(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 **************************************************************/ diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index 2268f3f65..a79e2bf32 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -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 on_select{}; - std::function on_change{}; + std::function 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 { diff --git a/firmware/common/utility.hpp b/firmware/common/utility.hpp index 3da5dc841..50960615e 100644 --- a/firmware/common/utility.hpp +++ b/firmware/common/utility.hpp @@ -22,12 +22,13 @@ #ifndef __UTILITY_H__ #define __UTILITY_H__ -#include +#include +#include +#include #include #include -#include -#include #include +#include #define LOCATE_IN_RAM __attribute__((section(".ramtext"))) @@ -141,6 +142,16 @@ constexpr std::enable_if_t, bool> flags_enabled(TEnum val return (i_value & i_flags) == i_flags; } +// TODO: Constrain to integrals? +/* Converts an integer into a byte array. */ +template +constexpr std::array to_byte_array(T value) { + std::array 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 constexpr const T& clip(const T& value, const T& minimum, const T& maximum) { diff --git a/firmware/test/application/test_string_format.cpp b/firmware/test/application/test_string_format.cpp index 456efc20a..b6e2d086b 100644 --- a/firmware/test/application/test_string_format.cpp +++ b/firmware/test/application/test_string_format.cpp @@ -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(0xA), "0A"); + CHECK_EQ(to_string_hex(0xFE), "FE"); + CHECK_EQ(to_string_hex(0x00FE), "00FE"); + CHECK_EQ(to_string_hex(0xCAFE), "CAFE"); + CHECK_EQ(to_string_hex(0xCAFE), "0000CAFE"); + CHECK_EQ(to_string_hex(0xCAFEBEEF), "CAFEBEEF"); + CHECK_EQ(to_string_hex(0xCAFEBEEF), "00000000CAFEBEEF"); + CHECK_EQ(to_string_hex(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"); } diff --git a/firmware/test/application/test_utility.cpp b/firmware/test/application/test_utility.cpp index aea4c4233..d6b6f678c 100644 --- a/firmware/test/application/test_utility.cpp +++ b/firmware/test/application/test_utility.cpp @@ -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(0xAB); + REQUIRE_EQ(std::size(arr1), 1); + CHECK_EQ(arr1[0], 0xAB); + + auto arr2 = to_byte_array(0xABCD); + REQUIRE_EQ(std::size(arr2), 2); + CHECK_EQ(arr2[0], 0xAB); + CHECK_EQ(arr2[1], 0xCD); + + auto arr4 = to_byte_array(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); } \ No newline at end of file