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_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() {

View File

@ -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{};

View File

@ -186,13 +186,12 @@ ADSBSquawkView::ADSBSquawkView(
&field_squawk});
}
void ADSBSquawkView::collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list) {
void ADSBSquawkView::collect_frames(const uint32_t /*ICAO_address*/, std::vector<ADSBFrame>& frame_list) {
if (!enabled) return;
ADSBFrame temp_frame;
(void)ICAO_address;
encode_frame_squawk(temp_frame, field_squawk.concatenate_4_octal_u16());
encode_frame_squawk(temp_frame, field_squawk.to_integer());
frame_list.emplace_back(temp_frame);
}
@ -277,7 +276,7 @@ ADSBTxView::~ADSBTxView() {
}
void ADSBTxView::generate_frames() {
const uint32_t ICAO_address = sym_icao.value_hex_u64();
const uint32_t ICAO_address = sym_icao.to_integer();
frames.clear();

View File

@ -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},

View File

@ -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;

View File

@ -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},

View File

@ -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);

View File

@ -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},
""};

View File

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

View File

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

View File

@ -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();

View File

@ -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},

View File

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

View File

@ -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},

View File

@ -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;

View File

@ -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"};

View File

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

View File

@ -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},

View File

@ -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

View File

@ -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 },

View File

@ -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();
};

View File

@ -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},

View File

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

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_uint(uint64_t n);
// TODO: Allow l=0 to not fill/justify? Already using this way in ui_spectrum.hpp...
std::string to_string_bin(const uint32_t n, const uint8_t l = 0);
std::string to_string_dec_uint(const uint32_t n, const int32_t l, const char fill = ' ');
std::string to_string_dec_int(const int32_t n, const int32_t l, const char fill = 0);
std::string to_string_decimal(float decimal, int8_t precision);
std::string to_string_hex(const uint64_t n, const int32_t l = 0);
std::string to_string_hex_array(uint8_t* const array, const int32_t l = 0);
std::string to_string_hex(uint64_t n, int32_t length);
std::string to_string_hex_array(uint8_t* array, int32_t length);
/* Helper to select length based on type size. */
template <typename T>
std::string to_string_hex(T n) {
return to_string_hex(n, sizeof(T) * 2); // Two digits/byte.
}
std::string to_string_freq(const uint64_t f);
std::string to_string_short_freq(const uint64_t f);
@ -78,4 +83,12 @@ std::string trim(std::string_view str); // Remove whitespace at ends.
std::string trimr(std::string_view str); // Remove trailing spaces
std::string truncate(std::string_view, size_t length);
/* Gets the int value for a character given the radix.
* e.g. '5' => 5, 'D' => 13. Out of bounds => 0. */
uint8_t char_to_uint(char c, uint8_t radix = 10);
/* Gets the int value for a character given the radix.
* e.g. 5 => '5', 13 => 'D'. Out of bounds => '\0'. */
char uint_to_char(uint8_t val, uint8_t radix = 10);
#endif /*__STRING_FORMAT_H__*/

View File

@ -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 {

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 = 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<ui::Dim>(8 * length), 16}}},
length_{length},
type_{type} {
uint32_t c;
Type type,
bool explicit_edits)
: Widget{{parent_pos, {char_width * (int)length, 16}}},
type_{type},
explicit_edits_{explicit_edits} {
if (length == 0)
length = 1;
// Auto-init
if (type == SYMFIELD_OCT) {
for (c = 0; c < length; c++)
set_symbol_list(c, "01234567");
} else if (type == SYMFIELD_DEC) {
for (c = 0; c < length; c++)
set_symbol_list(c, "0123456789");
} else if (type == SYMFIELD_HEX) {
for (c = 0; c < length; c++)
set_symbol_list(c, "0123456789ABCDEF");
} else if (type == SYMFIELD_ALPHANUM) {
for (c = 0; c < length; c++)
set_symbol_list(c, " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
selected_ = length - 1;
value_.resize(length);
switch (type) {
case Type::Oct:
set_symbol_list("01234567");
break;
case Type::Dec:
set_symbol_list("0123456789");
break;
case Type::Hex:
set_symbol_list("0123456789ABCDEF");
break;
case Type::Alpha:
set_symbol_list(" 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
break;
default:
set_symbol_list("01");
break;
}
set_focusable(true);
}
uint16_t SymField::concatenate_4_octal_u16() {
// input array 4 octal digits , return 12 bits, same order, trippled A4-A2-A1|B4-B2-B1|C4-C2-C1|D4-D2-D1
uint32_t mul = 1;
uint16_t v = 0;
if (type_ == SYMFIELD_OCT) {
for (uint32_t c = 0; c < length_; c++) {
v += values_[(length_ - 1 - c)] * mul;
mul *= 8; // shift 3 bits to the right every new octal squawk digit
}
return v;
} else
return 0;
SymField::SymField(
Point parent_pos,
size_t length,
std::string symbol_list,
bool explicit_edits)
: SymField{parent_pos, length, Type::Custom, explicit_edits} {
set_symbol_list(std::move(symbol_list));
}
uint32_t SymField::value_dec_u32() {
uint32_t mul = 1, v = 0;
if (type_ == SYMFIELD_DEC) {
for (uint32_t c = 0; c < length_; c++) {
v += values_[(length_ - 1 - c)] * mul;
mul *= 10;
}
return v;
} else
char SymField::get_symbol(size_t index) const {
if (index >= value_.length())
return 0;
return value_[index];
}
uint64_t SymField::value_hex_u64() {
void SymField::set_symbol(size_t index, char symbol) {
if (index >= value_.length())
return;
set_symbol_internal(index, ensure_valid(symbol));
}
size_t SymField::get_offset(size_t index) const {
if (index >= value_.length())
return 0;
// NB: Linear search - symbol lists are small.
return symbols_.find(value_[index]);
}
void SymField::set_offset(size_t index, size_t offset) {
if (index >= value_.length() || offset >= symbols_.length())
return;
set_symbol_internal(index, symbols_[offset]);
}
void SymField::set_symbol_list(std::string symbol_list) {
if (symbol_list.length() == 0)
return;
symbols_ = std::move(symbol_list);
ensure_all_symbols();
}
void SymField::set_value(uint64_t value) {
auto v = value;
uint8_t radix = get_radix();
for (int i = value_.length() - 1; i >= 0; --i) {
uint8_t temp = v % radix;
value_[i] = uint_to_char(temp, radix);
v /= radix;
}
}
void SymField::set_value(std::string_view value) {
// Is new value too long?
// TODO: Truncate instead? Which end?
if (value.length() > value_.length())
return;
// Right-align string in field.
auto left_padding = value_.length() - value.length();
value_ = std::string(static_cast<size_t>(left_padding), '\0') + std::string{value};
ensure_all_symbols();
}
uint64_t SymField::to_integer() const {
uint64_t v = 0;
uint64_t mul = 1;
uint8_t radix = get_radix();
if (type_ != SYMFIELD_DEF) {
for (uint32_t c = 0; c < length_; c++)
v += (uint64_t)(values_[c]) << (4 * (length_ - 1 - c));
return v;
} else
return 0;
}
std::string SymField::value_string() {
std::string return_string{""};
if (type_ == SYMFIELD_ALPHANUM) {
for (uint32_t c = 0; c < length_; c++) {
return_string += symbol_list_[0][values_[c]];
}
for (int i = value_.length() - 1; i >= 0; --i) {
auto temp = char_to_uint(value_[i], radix);
v += temp * mul;
mul *= radix;
}
return return_string;
return v;
}
uint32_t SymField::get_sym(const uint32_t index) {
if (index >= length_) return 0;
return values_[index];
}
void SymField::set_sym(const uint32_t index, const uint32_t new_value) {
if (index >= length_) return;
uint32_t clipped_value = clip_value(index, new_value);
if (clipped_value != values_[index]) {
values_[index] = clipped_value;
if (on_change) {
on_change();
}
set_dirty();
}
}
void SymField::set_length(const uint32_t new_length) {
if ((new_length <= 32) && (new_length != length_)) {
prev_length_ = length_;
length_ = new_length;
// Clip eventual garbage from previous shorter word
for (size_t n = 0; n < length_; n++)
set_sym(n, values_[n]);
erase_prev_ = true;
set_dirty();
}
}
void SymField::set_symbol_list(const uint32_t index, const std::string symbol_list) {
if (index >= length_) return;
symbol_list_[index] = symbol_list;
// Re-clip symbol's value
set_sym(index, values_[index]);
const std::string& SymField::to_string() const {
return value_;
}
void SymField::paint(Painter& painter) {
Point pt_draw = screen_pos();
Point p = screen_pos();
if (erase_prev_) {
painter.fill_rectangle({pt_draw, {(int)prev_length_ * 8, 16}}, Color::black());
erase_prev_ = false;
}
for (size_t n = 0; n < value_.length(); n++) {
auto c = value_[n];
MutableStyle paint_style{style()};
for (size_t n = 0; n < length_; n++) {
const auto text = symbol_list_[n].substr(values_[n], 1);
// Only highlight while focused.
if (has_focus()) {
if (explicit_edits_) {
// Invert the whole field on focus if explicit edits is enabled.
paint_style.invert();
} else if (n == selected_) {
// Otherwise only highlight the selected symbol.
paint_style.invert();
}
const auto paint_style = (has_focus() && (n == selected_)) ? style().invert() : style();
if (editing_ && n == selected_) {
// Use 'bg_blue' style to indicate in editing mode.
paint_style.foreground = Color::white();
paint_style.background = Color::blue();
}
}
painter.draw_string(
pt_draw,
paint_style,
text);
pt_draw += {8, 0};
painter.draw_char(p, paint_style, c);
p += {8, 0};
}
}
bool SymField::on_key(const KeyEvent key) {
bool SymField::on_key(KeyEvent key) {
// If explicit edits are enabled, only Select is handled when not in edit mode.
if (explicit_edits_ && !editing_) {
switch (key) {
case KeyEvent::Select:
editing_ = true;
set_dirty();
return true;
default:
return false;
}
}
switch (key) {
case KeyEvent::Select:
if (on_select) {
on_select(*this);
return true;
}
break;
editing_ = !editing_;
set_dirty();
return true;
case KeyEvent::Left:
if (selected_ > 0) {
@ -2041,13 +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<int>(offset, 0, symbols_.length() - 1);
set_offset(selected_, offset);
return true;
}
bool SymField::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
bool SymField::on_touch(TouchEvent event) {
if (event.type == TouchEvent::Type::Start)
focus();
}
return true;
}
int32_t SymField::clip_value(const uint32_t index, const uint32_t value) {
size_t symbol_count = symbol_list_[index].length() - 1;
char SymField::ensure_valid(char symbol) const {
// NB: Linear search - symbol lists are small.
auto pos = symbols_.find(symbol);
return pos != std::string::npos ? symbol : symbols_[0];
}
if (value > symbol_count)
return symbol_count;
else
return value;
void SymField::ensure_all_symbols() {
auto temp = value_;
for (auto& c : value_)
c = ensure_valid(c);
if (temp != value_) {
if (on_change)
on_change(*this);
set_dirty();
}
}
void SymField::set_symbol_internal(size_t index, char symbol) {
if (value_[index] == symbol)
return;
value_[index] = symbol;
if (on_change)
on_change(*this);
set_dirty();
}
uint8_t SymField::get_radix() const {
switch (type_) {
case Type::Oct:
return 8;
case Type::Dec:
return 10;
case Type::Hex:
return 16;
default:
return 0;
}
}
/* Waveform **************************************************************/

View File

@ -751,52 +751,93 @@ class NumberField : public Widget {
const char fill_char;
int32_t value_{0};
bool can_loop{};
int32_t clip_value(int32_t value);
};
/* A widget that allows for character-by-character editing of its value. */
class SymField : public Widget {
public:
std::function<void(SymField&)> on_select{};
std::function<void()> on_change{};
std::function<void(SymField&)> on_change{};
enum symfield_type {
SYMFIELD_OCT,
SYMFIELD_DEC,
SYMFIELD_HEX,
SYMFIELD_ALPHANUM,
SYMFIELD_DEF // User DEFined
enum class Type {
Custom,
Oct,
Dec,
Hex,
Alpha,
};
SymField(Point parent_pos, size_t length, symfield_type type);
/* When "explicit_edits" is true, the field must be selected before it can
* be modified. This means that "slots" are not individually highlighted.
* This makes navigation on Views with SymFields easier because the
* whole control can be skipped over instead of one "slot" at a time. */
SymField(
Point parent_pos,
size_t length,
Type type = Type::Dec,
bool explicit_edits = false);
SymField(
Point parent_pos,
size_t length,
std::string symbol_list,
bool explicit_edits = false);
SymField(const SymField&) = delete;
SymField(SymField&&) = delete;
uint32_t get_sym(const uint32_t index);
void set_sym(const uint32_t index, const uint32_t new_value);
void set_length(const uint32_t new_length);
void set_symbol_list(const uint32_t index, const std::string symbol_list);
uint16_t concatenate_4_octal_u16();
uint32_t value_dec_u32();
uint64_t value_hex_u64();
std::string value_string();
/* Gets the symbol (character) at the specified index. */
char get_symbol(size_t index) const;
/* Sets the symbol (character) at the specified index. */
void set_symbol(size_t index, char symbol);
/* Gets the symbol's offset in the symbol list at the specified index. */
size_t get_offset(size_t index) const;
/* Sets the symbol's offset in the symbol list at the specified index. */
void set_offset(size_t index, size_t offset);
/* Sets the list of allowable symbols for the field. */
void set_symbol_list(std::string symbol_list);
/* Sets the integer value of the field. Only works for OCT/DEC/HEX. */
void set_value(uint64_t value);
/* Sets the string value of the field. Characters that are not in
* the symbol list will be set to the first symbol in the symbol list. */
void set_value(std::string_view value);
/* Gets the integer value of the field for OCT/DEC/HEX. */
uint64_t to_integer() const;
/* Gets the string value of the field. */
const std::string& to_string() const;
void paint(Painter& painter) override;
bool on_key(const KeyEvent key) override;
bool on_encoder(const EncoderEvent delta) override;
bool on_touch(const TouchEvent event) override;
bool on_key(KeyEvent key) override;
bool on_encoder(EncoderEvent delta) override;
bool on_touch(TouchEvent event) override;
private:
std::string symbol_list_[32] = {"01"}; // Failsafe init
uint32_t values_[32] = {0};
uint32_t selected_ = 0;
size_t length_, prev_length_ = 0;
bool erase_prev_ = false;
symfield_type type_{};
/* Ensure the specified symbol is in the symbol list. */
char ensure_valid(char symbol) const;
int32_t clip_value(const uint32_t index, const uint32_t value);
/* Ensures all the symbols are valid. */
void ensure_all_symbols();
/* Sets without validation and fires on_change if necessary. */
void set_symbol_internal(size_t index, char symbol);
/* Gets the radix for the current Type. */
uint8_t get_radix() const;
std::string symbols_{};
std::string value_{};
Type type_ = Type::Custom;
size_t selected_ = 0;
bool editing_ = false;
bool explicit_edits_ = true;
};
class Waveform : public Widget {

View File

@ -22,12 +22,13 @@
#ifndef __UTILITY_H__
#define __UTILITY_H__
#include <type_traits>
#include <algorithm>
#include <array>
#include <complex>
#include <cstdint>
#include <cstddef>
#include <algorithm>
#include <complex>
#include <memory>
#include <type_traits>
#define LOCATE_IN_RAM __attribute__((section(".ramtext")))
@ -141,6 +142,16 @@ constexpr std::enable_if_t<is_flags_type_v<TEnum>, bool> flags_enabled(TEnum val
return (i_value & i_flags) == i_flags;
}
// TODO: Constrain to integrals?
/* Converts an integer into a byte array. */
template <typename T, size_t N = sizeof(T)>
constexpr std::array<uint8_t, N> to_byte_array(T value) {
std::array<uint8_t, N> bytes{};
for (size_t i = 0; i < N; ++i)
bytes[i] = (value >> ((N - i - 1) * 8)) & 0xFF;
return bytes;
}
/* Returns value constrained to min and max. */
template <class T>
constexpr const T& clip(const T& value, const T& minimum, const T& maximum) {

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");
}
TEST_CASE("to_string_hex returns correct value.") {
CHECK_EQ(to_string_hex(0x0, 1), "0");
CHECK_EQ(to_string_hex(0x0, 4), "0000");
CHECK_EQ(to_string_hex(0x1, 1), "1");
CHECK_EQ(to_string_hex(0xA, 1), "A");
CHECK_EQ(to_string_hex(0xA0, 2), "A0");
CHECK_EQ(to_string_hex(0xA1, 2), "A1");
CHECK_EQ(to_string_hex(0xA1FE, 2), "FE"); // NB: Truncates front.
CHECK_EQ(to_string_hex(0x12345678, 8), "12345678");
CHECK_EQ(to_string_hex(0x9ABCDEF0, 8), "9ABCDEF0");
}
TEST_CASE("to_string_hex returns correct value based on type.") {
CHECK_EQ(to_string_hex<uint8_t>(0xA), "0A");
CHECK_EQ(to_string_hex<uint8_t>(0xFE), "FE");
CHECK_EQ(to_string_hex<uint16_t>(0x00FE), "00FE");
CHECK_EQ(to_string_hex<uint16_t>(0xCAFE), "CAFE");
CHECK_EQ(to_string_hex<uint32_t>(0xCAFE), "0000CAFE");
CHECK_EQ(to_string_hex<uint32_t>(0xCAFEBEEF), "CAFEBEEF");
CHECK_EQ(to_string_hex<uint64_t>(0xCAFEBEEF), "00000000CAFEBEEF");
CHECK_EQ(to_string_hex<uint64_t>(0xC0FFEE00CAFEBABE), "C0FFEE00CAFEBABE");
}
TEST_CASE("char_to_uint returns correct value.") {
CHECK_EQ(char_to_uint('0', 10), 0);
CHECK_EQ(char_to_uint('5', 10), 5);
CHECK_EQ(char_to_uint('8', 8), 0); // Bad value given radix.
CHECK_EQ(char_to_uint('8', 10), 8);
CHECK_EQ(char_to_uint('8', 16), 8);
CHECK_EQ(char_to_uint('A', 10), 0); // Bad value given radix.
CHECK_EQ(char_to_uint('A', 16), 0xA);
CHECK_EQ(char_to_uint('a', 16), 0xA); // Lowercase.
CHECK_EQ(char_to_uint('f', 16), 0xF); // Lowercase.
CHECK_EQ(char_to_uint('G', 16), 0); // Bad value given radix.
}
TEST_CASE("uint_to_char returns correct value.") {
CHECK_EQ(uint_to_char(0, 10), '0');
CHECK_EQ(uint_to_char(5, 10), '5');
CHECK_EQ(uint_to_char(8, 8), '\0'); // Bad value given radix.
CHECK_EQ(uint_to_char(8, 10), '8');
CHECK_EQ(uint_to_char(8, 16), '8');
CHECK_EQ(uint_to_char(10, 10), '\0'); // Bad value given radix.
CHECK_EQ(uint_to_char(10, 16), 'A');
CHECK_EQ(uint_to_char(15, 16), 'F');
CHECK_EQ(uint_to_char(16, 16), '\0'); // Bad value given radix.
}
TEST_CASE("trim removes whitespace.") {
CHECK(trim(" foo\n") == "foo");
}

View File

@ -71,4 +71,22 @@ TEST_CASE("ms_duration should return duration.") {
TEST_CASE("ms_duration not fault when passed zero.") {
CHECK_EQ(ms_duration(0, 0, 0), 0);
}
TEST_CASE("to_byte_array returns correct size and values.") {
auto arr1 = to_byte_array<uint8_t>(0xAB);
REQUIRE_EQ(std::size(arr1), 1);
CHECK_EQ(arr1[0], 0xAB);
auto arr2 = to_byte_array<uint16_t>(0xABCD);
REQUIRE_EQ(std::size(arr2), 2);
CHECK_EQ(arr2[0], 0xAB);
CHECK_EQ(arr2[1], 0xCD);
auto arr4 = to_byte_array<uint32_t>(0xABCD1234);
REQUIRE_EQ(std::size(arr4), 4);
CHECK_EQ(arr4[0], 0xAB);
CHECK_EQ(arr4[1], 0xCD);
CHECK_EQ(arr4[2], 0x12);
CHECK_EQ(arr4[3], 0x34);
}