Add Remote App & UI updates. (#1451)

* Alpha order sub-menus
* WIP Getting Remote types outlined
* WIP building UI
* WIP adding RemoteButton control
* WIP Fix build
* WIP Basic editing support
* Border on the active button
* Make TxView2 sane
* Add easier RGB color creation from uint32
* Center some button icons
* WIP Remote - main UI
* WIP main UI mostly working, can send
* Add 'join' utility
* WIP save/load
* Pre-alloc buttons to prevent focus dangling
* Alpha order settings/debug pages
* Add UI for picking capture and set frequency
* WIP Getting really close now
* Fix path for init name
* Some fit & finish
This commit is contained in:
Kyle Reed 2023-09-18 14:22:46 -07:00 committed by GitHub
parent 537cf2e79b
commit fca373d936
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1205 additions and 312 deletions

View File

@ -24,9 +24,6 @@
#ifndef __GPS_SIM_APP_HPP__
#define __GPS_SIM_APP_HPP__
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui_widget.hpp"
@ -109,10 +106,8 @@ class GpsSimAppView : public View {
nav_};
TransmitterView2 tx_view{
// new handling of NumberField field_rfgain, NumberField field_rfamp
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Used in Replay / GPS Simul / Playlist App)
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
};
{11 * 8, 2 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{21 * 8, 2 * 16},

View File

@ -203,9 +203,6 @@ ReplayAppView::ReplayAppView(
ReplayAppView::~ReplayAppView() {
transmitter_model.disable();
display.fill_rectangle({0, 0, 240, 320}, Color::black()); // Solving sometimes visible bottom waterfall artifacts, clearing all LCD pixels.
chThdSleepMilliseconds(40); // (that happened sometimes if we interrupt the waterfall play at the beggining of the play around 25% and exit )
baseband::shutdown();
}

View File

@ -23,9 +23,6 @@
#ifndef __REPLAY_APP_HPP__
#define __REPLAY_APP_HPP__
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui_widget.hpp"
@ -99,16 +96,13 @@ class ReplayAppView : public View {
ProgressBar progressbar{
{18 * 8, 1 * 16, 12 * 8, 16}};
// TODO: Does this need to be a frequency field at all?
TxFrequencyField field_frequency{
{0 * 8, 2 * 16},
nav_};
TransmitterView2 tx_view{
// new handling of NumberField field_rfgain, NumberField field_rfamp
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Uused in Repay / GPS Simul / Playlist App)
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
};
{11 * 8, 2 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{21 * 8, 2 * 16},

View File

@ -398,17 +398,17 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
//{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }}, // temporarily disabled to conserve ROM space
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
{"Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugMemoryView>(); }},
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
//{ "Radio State", ui::Color::white(), nullptr, [&nav](){ nav.push<NotImplementedView>(); } },
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [&nav]() { nav.push<SDCardDebugView>(); }},
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
{"Temperature", ui::Color::dark_cyan(), &bitmap_icon_temperature, [&nav]() { nav.push<TemperatureView>(); }},
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
{"Touch Test", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugScreenTest>(); }},
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
//{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }}, // temporarily disabled to conserve ROM space
});
set_max_rows(2); // allow wider buttons
}

View File

@ -27,6 +27,7 @@
#include <algorithm>
#include "ui_fileman.hpp"
#include "ui_playlist.hpp"
#include "ui_remote.hpp"
#include "ui_ss_viewer.hpp"
#include "ui_text_editor.hpp"
#include "string_format.hpp"
@ -44,6 +45,7 @@ static const fs::path c16_ext{u".C16"};
static const fs::path cxx_ext{u".C*"};
static const fs::path png_ext{u".PNG"};
static const fs::path bmp_ext{u".BMP"};
static const fs::path rem_ext{u".REM"};
} // namespace ui
namespace {
@ -525,6 +527,9 @@ bool FileManagerView::handle_file_open() {
nav_.push<SplashViewer>(path);
reload_current();
return true;
} else if (path_iequal(rem_ext, ext)) {
nav_.push<RemoteView>(path);
return true;
}
return false;
@ -629,7 +634,7 @@ FileManagerView::FileManagerView(
button_open_notepad.on_select = [this]() {
if (selected_is_valid() && !get_selected_entry().is_directory) {
auto path = get_selected_full_path();
nav_.replace<TextEditorView>(path);
nav_.push<TextEditorView>(path);
} else
nav_.display_modal("Open in Notepad", "Can't open that in Notepad.");
};

View File

@ -77,7 +77,8 @@ class FileManBaseView : public View {
{u".C8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".C16", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".WAV", &bitmap_icon_file_wav, ui::Color::dark_magenta()},
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // PPL is the file extension for playlist app
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // Playlist/Replay
{u".REM", &bitmap_icon_remote, ui::Color::orange()}, // Remote
{u"", &bitmap_icon_file, ui::Color::light_grey()} // NB: Must be last.
};
@ -269,7 +270,8 @@ class FileManagerView : public FileManBaseView {
{4 * 8, 34 * 8, 4 * 8, 32},
{},
&bitmap_icon_options_datetime,
Color::orange()};
Color::orange(),
/*vcenter*/ true};
};
} /* namespace ui */

View File

@ -633,7 +633,7 @@ MicTXView::MicTXView(
MicTXView::~MicTXView() {
audio::input::stop();
transmitter_model.set_target_frequency(tx_frequency); // Save Tx frequency instead of Rx. Or maybe we need some "System Wide" changes to seperate Tx and Rx frequency.
transmitter_model.set_target_frequency(tx_frequency);
transmitter_model.disable();
if (rx_enabled) // Also turn off audio rx if enabled
rxaudio(false);

View File

@ -25,9 +25,6 @@
#ifndef __UI_MICTX_H__
#define __UI_MICTX_H__
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui.hpp"
@ -219,10 +216,8 @@ class MicTXView : public View {
' '};
TransmitterView2 tx_view{
// new handling of NumberField field_rfgain, NumberField field_rfamp
// 3*8, 2*8, SHORT_UI // x(columns), y (line) position. (used in Replay / GPS Simul / Playlist App's)
3 * 8, 2 * 8, NORMAL_UI // x(columns), y (line) position. (used in Mic App)
};
{3 * 8, 5 * 8},
/*short_ui*/ false};
OptionsField options_mode{
{24 * 8, 5 * 8},

View File

@ -386,6 +386,7 @@ PlaylistView::PlaylistView(
&waterfall,
});
ensure_directory(u"PLAYLIST");
waterfall.show_audio_spectrum_view(false);
field_frequency.set_value(100'000'000);

View File

@ -21,9 +21,6 @@
* Boston, MA 02110-1301, USA.
*/
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "bitmap.hpp"
#include "file.hpp"
@ -63,7 +60,7 @@ class PlaylistView : public View {
// More header == less spectrum view.
static constexpr ui::Dim header_height = 6 * 16;
static constexpr uint32_t baseband_bandwidth = 2500000;
static constexpr uint32_t baseband_bandwidth = 2'500'000;
struct playlist_entry {
std::filesystem::path path{};
@ -118,14 +115,11 @@ class PlaylistView : public View {
Text text_sample_rate{
{10 * 8, 1 * 16, 7 * 8, 16}};
/*v making there's 1px line (instead of two) between two progress bars,
* by letting 1px overlapped.
* So, since they overlapped 1px, they are visually same, and looks better.
*/
ProgressBar progressbar_track{
{18 * 8, 1 * 16, 12 * 8, 8 + 1}};
// (-1) to overlap with progressbar_track so there's
// only 1 pixel between them instead of 2.
ProgressBar progressbar_transmit{
{18 * 8, 3 * 8 - 1, 12 * 8, 8}};
@ -135,7 +129,8 @@ class PlaylistView : public View {
// TODO: delay duration field.
TransmitterView2 tx_view{
9 * 8, 1 * 8, SHORT_UI};
{11 * 8, 2 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{21 * 8, 2 * 16},

View File

@ -1,6 +1,5 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2018 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@ -22,30 +21,615 @@
#include "ui_remote.hpp"
#include "baseband_api.hpp"
#include "convert.hpp"
#include "file_reader.hpp"
#include "io_convert.hpp"
#include "irq_controls.hpp"
#include "oversample.hpp"
#include "string_format.hpp"
#include "ui_fileman.hpp"
#include "ui_receiver.hpp"
#include "ui_textentry.hpp"
#include "utility.hpp"
using namespace portapack;
namespace fs = std::filesystem;
namespace ui {
void RemoteView::focus() {
button.focus();
static constexpr uint8_t text_edit_max = 30;
/* RemoteEntryModel **************************************/
std::string RemoteEntryModel::to_string() const {
return join(',', {path.string(),
name,
to_string_dec_uint(icon),
to_string_dec_uint(bg_color),
to_string_dec_uint(fg_color),
to_string_dec_uint(metadata.center_frequency),
to_string_dec_uint(metadata.sample_rate)});
}
RemoteView::~RemoteView() {
// transmitter_model.disable();
// baseband::shutdown();
Optional<RemoteEntryModel> RemoteEntryModel::parse(std::string_view line) {
auto cols = split_string(line, ',');
if (cols.size() < 7)
return {};
RemoteEntryModel entry{};
entry.path = cols[0];
entry.name = std::string{cols[1]};
parse_int(cols[2], entry.icon);
parse_int(cols[3], entry.bg_color);
parse_int(cols[4], entry.fg_color);
parse_int(cols[5], entry.metadata.center_frequency);
parse_int(cols[6], entry.metadata.sample_rate);
return entry;
}
RemoteView::RemoteView(
NavigationView& nav) {
add_children({&labels,
&button});
/* RemoteModel *******************************************/
button.on_select = [this, &nav](Button&) {
nav.pop();
bool RemoteModel::delete_entry(const RemoteEntryModel* entry) {
// NB: expecting 'entry' to be a pointer to an entry in vector.
auto it = std::find_if(
entries.begin(), entries.end(),
[entry](auto& item) { return entry == &item; });
if (it == entries.end())
return false;
entries.erase(it);
return true;
}
bool RemoteModel::load(const std::filesystem::path& path) {
File f;
auto error = f.open(path);
if (error)
return false;
entries.clear();
bool first = true;
auto reader = FileLineReader(f);
for (const auto& line : reader) {
if (line.length() == 0 || line[0] == '#')
continue; // Empty or comment line.
// First line is the "name" field.
if (first) {
name = trim(line);
first = false;
continue;
}
// All the other lines are button entries.
auto entry = RemoteEntryModel::parse(line);
if (entry)
entries.push_back(*std::move(entry));
}
return true;
}
bool RemoteModel::save(const std::filesystem::path& path) {
File f;
auto error = f.create(path);
if (error)
return false;
f.write_line(name);
for (auto& entry : entries)
f.write_line(entry.to_string());
return true;
}
/* RemoteButton ******************************************/
RemoteButton::RemoteButton(Rect parent_rect, RemoteEntryModel* entry)
: NewButton{parent_rect, {}, nullptr},
entry_{nullptr} {
set_entry(entry);
// Forward to on_select2 -- this isn't ideal, but works for now.
on_select = [this]() {
if (on_select2)
on_select2(*this);
};
}
void RemoteButton::on_focus() {
// Enable long press on "Select".
SwitchesState config;
config[toUType(Switch::Sel)] = true;
set_switches_long_press_config(config);
}
void RemoteButton::on_blur() {
// Reset long press.
SwitchesState config{};
set_switches_long_press_config(config);
}
bool RemoteButton::on_key(KeyEvent key) {
if (key == KeyEvent::Select) {
if (key_is_long_pressed(key) && on_long_select) {
on_long_select(*this);
return true;
}
if (on_select2) {
on_select2(*this);
return true;
}
}
return false;
}
void RemoteButton::paint(Painter& painter) {
NewButton::paint(painter);
// Add a border on the highlighted button.
if (has_focus() || highlighted()) {
auto r = screen_rect();
painter.draw_rectangle(r, Color::white());
auto p = r.location() + Point{1, 1};
auto s = Size{r.size().width() - 2, r.size().height() - 2};
painter.draw_rectangle({p, s}, Color::light_grey());
}
};
RemoteEntryModel* RemoteButton::entry() {
return entry_;
}
void RemoteButton::set_entry(RemoteEntryModel* entry) {
entry_ = entry;
set_focusable(entry_ != nullptr);
hidden(entry_ == nullptr);
if (entry_) {
set_text(entry_->name);
set_bitmap(RemoteIcons::get(entry_->icon));
}
set_dirty();
}
Style RemoteButton::paint_style() {
if (!entry_)
return style();
MutableStyle s{style()};
s.foreground = RemoteColors::get(entry_->fg_color);
s.background = RemoteColors::get(entry_->bg_color);
if (has_focus() || highlighted())
s.invert();
// It's kind of a hack to set 'color_' here, but the base
// class' paint logic is kind of convoluted but isn't worth
// the extra bytes to copy and paste a paint override.
color_ = s.foreground;
return s;
}
/* RemoteEntryEditView ***********************************/
RemoteEntryEditView::RemoteEntryEditView(
NavigationView& nav,
RemoteEntryModel& entry)
: entry_{entry} {
add_children({
&labels,
&field_name,
&field_path,
&field_freq,
&text_rate,
&field_icon_index,
&field_fg_color_index,
&field_bg_color_index,
&button_preview,
&button_delete,
&button_done,
});
// TODO: It's time to make field bindings and clean this mess up.
field_name.on_change = [this](TextField& tf) {
entry_.name = tf.get_text();
button_preview.set_text(entry_.name);
};
field_name.on_select = [this, &nav](TextField& tf) {
temp_buffer_ = tf.get_text();
text_prompt(nav, temp_buffer_, text_edit_max,
[this, &tf](std::string& str) {
tf.set_text(str);
});
};
field_name.set_text(entry_.name);
field_path.on_select = [this, &nav](TextField&) {
auto open_view = nav.push<FileLoadView>(".C*");
open_view->push_dir(u"CAPTURES");
open_view->on_changed = [this](fs::path path) {
load_path(std::move(path));
refresh_ui();
};
};
field_freq.on_edit = [this, &nav]() {
auto freq_view = nav.push<FrequencyKeypadView>(entry_.metadata.center_frequency);
freq_view->on_changed = [this](rf::Frequency f) {
entry_.metadata.center_frequency = f;
field_freq.set_value(f);
};
};
field_icon_index.on_change = [this](int32_t v) {
entry_.icon = v;
button_preview.set_bitmap(RemoteIcons::get(v));
};
field_icon_index.set_value(entry.icon);
field_fg_color_index.on_change = [this](int32_t v) {
entry_.fg_color = v;
button_preview.set_color(RemoteColors::get(v));
};
field_fg_color_index.set_value(entry_.fg_color);
field_bg_color_index.on_change = [this](int32_t v) {
entry_.bg_color = v;
button_preview.set_dirty();
};
field_bg_color_index.set_value(entry_.bg_color);
button_delete.on_select = [this, &nav]() {
nav.display_modal(
"Delete?", " Delete this button?", YESNO,
[this, &nav](bool choice) {
if (choice) {
if (on_delete)
on_delete(entry_);
// Exit the edit view upon delete.
nav.set_on_pop([&nav]() { nav.pop(); });
}
});
};
button_done.on_select = [&nav](Button&) {
nav.pop();
};
refresh_ui();
}
void RemoteEntryEditView::focus() {
button_done.focus();
}
void RemoteEntryEditView::refresh_ui() {
field_path.set_text(entry_.path.filename().string());
field_freq.set_value(entry_.metadata.center_frequency);
text_rate.set(unit_auto_scale(entry_.metadata.sample_rate, 3, 0) + "Hz");
}
void RemoteEntryEditView::load_path(std::filesystem::path&& path) {
// Read metafile if it exists.
auto metadata_path = get_metadata_path(path);
auto metadata = read_metadata_file(metadata_path);
entry_.path = std::move(path);
// Use metadata if found, otherwise fallback to the TX frequency.
if (metadata)
entry_.metadata = *metadata;
else
entry_.metadata = {transmitter_model.target_frequency(), 500'000};
}
/* RemoteView ********************************************/
RemoteView::RemoteView(
NavigationView& nav)
: nav_{nav} {
baseband::run_image(portapack::spi_flash::image_tag_replay);
add_children({
&field_title,
&tx_view,
&check_loop,
&field_filename,
&button_add,
&button_new,
&button_open,
&waterfall,
});
create_buttons();
field_title.on_select = [this, &nav](TextField&) {
temp_buffer_ = model_.name;
text_prompt(nav_, temp_buffer_, text_edit_max, [this](std::string& new_name) {
model_.name = new_name;
refresh_ui();
set_needs_save();
});
};
field_filename.on_select = [this, &nav](TextField&) {
temp_buffer_ = remote_path_.stem().string();
text_prompt(nav_, temp_buffer_, text_edit_max, [this](std::string& new_name) {
rename_remote(new_name);
refresh_ui();
});
};
button_add.on_select = [this]() { add_button(); };
button_new.on_select = [this]() { new_remote(); };
button_open.on_select = [this]() { open_remote(); };
// Fill in the area between the remote buttons and bottom UI with waterfall.
Dim waterfall_top = buttons_top_.y() + button_area_height;
Dim waterfall_bottom = button_add.parent_rect().top();
Dim waterfall_height = waterfall_bottom - waterfall_top;
waterfall.set_parent_rect({0, waterfall_top, screen_width, waterfall_height});
ensure_directory(u"REMOTES");
// Load the previously loaded remote if exists.
if (!load_remote(settings_.remote_path))
init_remote();
refresh_ui();
}
RemoteView::RemoteView(NavigationView& nav, fs::path path)
: RemoteView(nav) {
load_remote(std::move(path));
refresh_ui();
}
RemoteView::~RemoteView() {
stop();
baseband::shutdown();
save_remote(/*show_error*/ false);
}
void RemoteView::focus() {
if (model_.entries.empty())
button_add.focus();
else
buttons_[0]->focus();
}
void RemoteView::create_buttons() {
// Handler callbacks.
auto handle_send = [this](RemoteButton& btn) {
if (btn.entry()->path.empty())
// No path set? Go to edit mode instead.
edit_button(btn);
else if (is_sending() && &btn == current_btn_)
// Pressed the same button again? Stop.
stop();
else
// Start sending.
send_button(btn);
};
auto handle_edit = [this](RemoteButton& btn) {
edit_button(btn);
};
// Create and add RemoteButtons for the whole grid.
for (size_t i = 0; i < max_buttons; ++i) {
Coord x = i % button_cols;
Coord y = i / button_cols;
Point pos = Point{x * button_width, y * button_height} + buttons_top_;
auto btn = std::make_unique<RemoteButton>(
Rect{pos, {button_width, button_height}},
nullptr);
btn->on_select2 = handle_send;
btn->on_long_select = handle_edit;
add_child(btn.get());
buttons_.push_back(std::move(btn));
}
}
void RemoteView::reset_buttons() {
// Whever the model's entries instance is invalidated,
// all the pointers in the buttons will end up dangling.
// TODO: This is pretty lame. Could maybe static alloc?
for (auto& btn : buttons_)
btn->set_entry(nullptr);
}
void RemoteView::refresh_ui() {
field_title.set_text(model_.name);
field_filename.set_text(remote_path_.stem().string());
// Update buttons from the model.
for (size_t i = 0; i < buttons_.size(); ++i) {
if (i < model_.entries.size())
buttons_[i]->set_entry(&model_.entries[i]);
else
buttons_[i]->set_entry(nullptr);
}
}
void RemoteView::add_button() {
if (model_.entries.size() >= max_buttons)
return;
// Don't let replay thread read the model while editing.
stop();
model_.entries.push_back({{}, "<EMPTY>", 0, 3, 1});
reset_buttons();
refresh_ui();
set_needs_save();
}
void RemoteView::edit_button(RemoteButton& btn) {
// Don't let replay thread read the model while editing.
stop();
auto edit_view = nav_.push<RemoteEntryEditView>(*btn.entry());
nav_.set_on_pop([this]() {
refresh_ui();
set_needs_save();
});
edit_view->on_delete = [this](RemoteEntryModel& to_delete) {
model_.delete_entry(&to_delete);
reset_buttons();
};
}
void RemoteView::send_button(RemoteButton& btn) {
// TODO: If this is called while is_sending() == true,
// it just stops and doesn't start the new button?
// Reset everything to prepare to send a file.
stop();
current_btn_ = &btn; // Stash for looping.
// Open the sample file to send.
auto reader = std::make_unique<FileConvertReader>();
auto error = reader->open(btn.entry()->path);
if (error) {
show_error("Can't open file:\n" + btn.entry()->path.stem().string());
return;
}
// Update the sample rate in proc_replay baseband.
baseband::set_sample_rate(
btn.entry()->metadata.sample_rate,
get_oversample_rate(btn.entry()->metadata.sample_rate));
// ReplayThread starts immediately on construction; must be set before creating.
transmitter_model.set_target_frequency(btn.entry()->metadata.center_frequency);
transmitter_model.set_sampling_rate(get_actual_sample_rate(btn.entry()->metadata.sample_rate));
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
transmitter_model.enable();
// ReplayThread reads the file and sends to the baseband.
replay_thread_ = std::make_unique<ReplayThread>(
std::move(reader),
/* read_size */ 0x4000,
/* buffer_count */ 3,
&ready_signal_,
[](uint32_t return_code) {
ReplayThreadDoneMessage message{return_code};
EventDispatcher::send_message(message);
});
}
void RemoteView::stop() {
// This terminates the underlying chThread.
replay_thread_.reset();
transmitter_model.disable();
ready_signal_ = false;
}
void RemoteView::new_remote() {
save_remote();
init_remote();
refresh_ui();
// View needs to redraw to hide old buttons.
set_dirty();
}
void RemoteView::open_remote() {
auto open_view = nav_.push<FileLoadView>(".REM");
open_view->push_dir(u"REMOTES");
open_view->on_changed = [this](fs::path path) {
save_remote();
load_remote(std::move(path));
refresh_ui();
;
};
}
void RemoteView::init_remote() {
model_ = {"<Unnamed Remote>", {}};
reset_buttons();
set_remote_path(next_filename_matching_pattern(u"/REMOTES/REMOTE_????.REM"));
set_needs_save(false);
if (remote_path_.empty())
show_error("Couldn't make new remote file.");
}
bool RemoteView::load_remote(fs::path&& path) {
set_remote_path(std::move(path));
set_needs_save(false);
reset_buttons();
return model_.load(remote_path_);
}
void RemoteView::save_remote(bool show_errors) {
if (!needs_save_)
return;
bool ok = model_.save(remote_path_);
if (!ok && show_errors)
show_error("Save failed for:\n" + remote_path_.stem().string());
set_needs_save(false);
}
void RemoteView::rename_remote(const std::string& new_name) {
auto folder = remote_path_.parent_path();
auto ext = remote_path_.extension();
auto new_path = folder / new_name + ext;
if (file_exists(new_path)) {
show_error("Remote " + new_name + " already exists");
return;
}
// Rename file if there is one.
if (fs::file_exists(remote_path_))
rename_file(remote_path_, new_path);
set_remote_path(std::move(new_path));
}
void RemoteView::handle_replay_thread_done(uint32_t return_code) {
if (return_code == ReplayThread::END_OF_FILE) {
if (check_loop.value() && current_btn_) {
send_button(*current_btn_);
return;
}
}
/*
// TODO: This can happen when stopping an in-progress Tx.
if (return_code == ReplayThread::READ_ERROR)
show_error("Bad capture file.");
*/
stop();
}
void RemoteView::set_remote_path(fs::path&& path) {
// Unfortunately, have to keep these two in sync because
// settings doesn't know about fs::path.
remote_path_ = std::move(path);
settings_.remote_path = remote_path_.string();
}
void RemoteView::show_error(const std::string& msg) const {
nav_.display_modal("Error", msg);
}
} /* namespace ui */

View File

@ -1,6 +1,5 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2018 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@ -21,42 +20,370 @@
*/
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_spectrum.hpp"
#include "ui_transmitter.hpp"
#include "transmitter_model.hpp"
#include "app_settings.hpp"
#include "baseband_api.hpp"
#include "bitmap.hpp"
#include "file.hpp"
#include "metadata_file.hpp"
#include "optional.hpp"
#include "radio_state.hpp"
#include "replay_thread.hpp"
#include <algorithm>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace ui {
/* Maps icon index to bitmap. */
class RemoteIcons {
public:
static constexpr const Bitmap* get(uint8_t index) {
if (index >= std::size(bitmaps_))
return bitmaps_[0];
return bitmaps_[index];
}
static constexpr size_t size() {
return std::size(bitmaps_);
}
private:
// NB: Icons need to be 16x16 or they won't fit corrently.
static constexpr std::array<const Bitmap*, 25> bitmaps_{
nullptr,
&bitmap_icon_fox,
&bitmap_icon_adsb,
&bitmap_icon_ais,
&bitmap_icon_aprs,
&bitmap_icon_btle,
&bitmap_icon_burger,
&bitmap_icon_camera,
&bitmap_icon_cwgen,
&bitmap_icon_dmr,
&bitmap_icon_file_image,
&bitmap_icon_lge,
&bitmap_icon_looking,
&bitmap_icon_memory,
&bitmap_icon_morse,
&bitmap_icon_nrf,
&bitmap_icon_notepad,
&bitmap_icon_rds,
&bitmap_icon_remote,
&bitmap_icon_setup,
&bitmap_icon_sleep,
&bitmap_icon_sonde,
&bitmap_icon_stealth,
&bitmap_icon_tetra,
&bitmap_icon_temperature};
};
// TODO: Use RGB colors instead?
/* Maps color index to color. */
class RemoteColors {
public:
static constexpr Color get(uint8_t index) {
if (index >= std::size(colors_))
return colors_[0];
return colors_[index];
}
static constexpr size_t size() {
return std::size(colors_);
}
private:
static constexpr std::array<Color, 21> colors_{
Color::black(), // 0
Color::white(), // 1
Color::darker_grey(), // 2
Color::dark_grey(), // 3
Color::grey(), // 4
Color::light_grey(), // 5
Color::red(), // 6
Color::orange(), // 7
Color::yellow(), // 8
Color::green(), // 9
Color::blue(), // 10
Color::cyan(), // 11
Color::magenta(), // 12
Color::dark_red(), // 13
Color::dark_orange(), // 14
Color::dark_yellow(), // 15
Color::dark_green(), // 16
Color::dark_blue(), // 17
Color::dark_cyan(), // 18
Color::dark_magenta(), // 19
Color::purple()}; // 20
};
/* Data model for a remote entry. */
struct RemoteEntryModel {
std::filesystem::path path{};
std::string name{};
uint8_t icon = 0;
uint8_t bg_color = 0;
uint8_t fg_color = 0;
capture_metadata metadata{};
// TODO: start/end position for trimming.
std::string to_string() const;
static Optional<RemoteEntryModel> parse(std::string_view line);
};
/* Data model for a remote. */
struct RemoteModel {
std::string name{};
std::vector<RemoteEntryModel> entries{};
bool delete_entry(const RemoteEntryModel* entry);
bool load(const std::filesystem::path& path);
bool save(const std::filesystem::path& path);
};
/* Button for the remote UI. */
class RemoteButton : public NewButton {
public:
std::function<void(RemoteButton&)> on_select2{};
std::function<void(RemoteButton&)> on_long_select{};
RemoteButton(Rect parent_rect, RemoteEntryModel* entry);
RemoteButton(const RemoteButton&) = delete;
RemoteButton& operator=(const RemoteButton&) = delete;
void on_focus() override;
void on_blur() override;
bool on_key(KeyEvent key) override;
void paint(Painter& painter) override;
RemoteEntryModel* entry();
void set_entry(RemoteEntryModel* entry);
protected:
Style paint_style() override;
private:
// Hide because it's not used.
using NewButton::on_select;
RemoteEntryModel* entry_;
};
/* Settings container for remote. */
struct RemoteSettings {
std::string remote_path{};
};
/* View for editing a remote entry button. */
class RemoteEntryEditView : public View {
public:
std::function<void(RemoteEntryModel&)> on_delete{};
RemoteEntryEditView(NavigationView& nav, RemoteEntryModel& entry);
std::string title() const override { return "Edit Button"; };
void focus() override;
private:
RemoteEntryModel& entry_;
std::string temp_buffer_{};
void refresh_ui();
void load_path(std::filesystem::path&& path);
Labels labels{
{{2 * 8, 1 * 16}, "Name:", Color::light_grey()},
{{2 * 8, 2 * 16}, "Path:", Color::light_grey()},
{{2 * 8, 3 * 16}, "Freq:", Color::light_grey()},
{{17 * 8, 3 * 16}, "MHz", Color::light_grey()},
{{2 * 8, 4 * 16}, "Rate:", Color::light_grey()},
{{2 * 8, 5 * 16}, "Icon:", Color::light_grey()},
{{2 * 8, 6 * 16}, "FG Color:", Color::light_grey()},
{{2 * 8, 7 * 16}, "BG Color:", Color::light_grey()},
{{8 * 8, 9 * 16}, "Button preview", Color::light_grey()},
};
TextField field_name{{8 * 8, 1 * 16, 20 * 8, 1 * 16}, {}};
TextField field_path{{8 * 8, 2 * 16, 20 * 8, 1 * 16}, {}};
FrequencyField field_freq{{8 * 8, 3 * 16}};
Text text_rate{{8 * 8, 4 * 16, 20 * 8, 1 * 16}, {}};
NumberField field_icon_index{
{8 * 8, 5 * 16},
2,
{0, RemoteIcons::size() - 1},
/*step*/ 1,
/*fill*/ ' ',
/*loop*/ true};
NumberField field_fg_color_index{
{11 * 8, 6 * 16},
2,
{0, RemoteColors::size() - 1},
/*step*/ 1,
/*fill*/ ' ',
/*loop*/ true};
NumberField field_bg_color_index{
{11 * 8, 7 * 16},
2,
{0, RemoteColors::size() - 1},
/*step*/ 1,
/*fill*/ ' ',
/*loop*/ true};
RemoteButton button_preview{
{10 * 8, 11 * 16 - 8, 10 * 8, 50},
&entry_};
NewButton button_delete{
{2 * 8, 16 * 16, 4 * 8, 2 * 16},
{},
&bitmap_icon_trash,
Color::red()};
Button button_done{{11 * 8, 16 * 16, 8 * 8, 2 * 16}, "Done"};
};
/* App that allows for buttons to be bound to captures for playback. */
class RemoteView : public View {
public:
RemoteView(NavigationView& nav);
RemoteView(NavigationView& nav, std::filesystem::path path);
~RemoteView();
RemoteView(const RemoteView&) = delete;
RemoteView& operator=(const RemoteView&) = delete;
std::string title() const override { return "Remote"; };
void focus() override;
std::string title() const override { return "Custom remote"; };
private:
/*enum tx_modes {
IDLE = 0,
SINGLE,
SCAN
};
/* Creates the dynamic buttons. */
void create_buttons();
/* Resets all the pointers to null entries. */
void reset_buttons();
void refresh_ui();
tx_modes tx_mode = IDLE;
void add_button();
void edit_button(RemoteButton& btn);
void send_button(RemoteButton& btn);
void stop();
void new_remote();
void open_remote();
void init_remote();
bool load_remote(std::filesystem::path&& path);
void save_remote(bool show_errors = true);
void rename_remote(const std::string& new_name);
void handle_replay_thread_done(uint32_t return_code);
void set_needs_save(bool v = true) { needs_save_ = v; }
void set_remote_path(std::filesystem::path&& path);
bool is_sending() const { return replay_thread_ != nullptr; }
void show_error(const std::string& msg) const;
struct remote_layout_t {
Point position;
std::string text;
};
static constexpr Dim button_rows = 4;
static constexpr Dim button_cols = 3;
static constexpr uint8_t max_buttons = button_rows * button_cols;
static constexpr Dim button_area_height = 200;
static constexpr Dim button_width = screen_width / button_cols;
static constexpr Dim button_height = button_area_height / button_rows;
const std::array<remote_layout_t, 32> remote_layout { };*/
// This value is mysterious... why?
static constexpr uint32_t baseband_bandwidth = 2'500'000;
Labels labels{
{{1 * 8, 0}, "Work in progress...", Color::light_grey()}};
NavigationView& nav_;
RxRadioState radio_state_{};
Button button{
{60, 64, 120, 32},
"Exit"};
// Settings
RemoteSettings settings_{};
app_settings::SettingsManager app_settings_{
"tx_remote"sv,
app_settings::Mode::TX,
{
{"remote_path"sv, &settings_.remote_path},
}};
RemoteModel model_{};
bool needs_save_ = false;
std::string temp_buffer_{};
std::filesystem::path remote_path_{};
RemoteButton* current_btn_{};
const Point buttons_top_{0, 20};
std::vector<std::unique_ptr<RemoteButton>> buttons_{};
std::unique_ptr<ReplayThread> replay_thread_{};
bool ready_signal_{}; // Used to signal ReplayThread ready.
TextField field_title{
{0 * 8, 0 * 16 + 2, 30 * 8, 1 * 16},
{}};
TransmitterView2 tx_view{
{0 * 8, 17 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{10 * 8, 17 * 16},
4,
"Loop",
/*small*/ true};
TextField field_filename{
{0 * 8, 18 * 16, 17 * 8, 1 * 16},
{}};
NewButton button_add{
{17 * 8 + 4, 17 * 16, 4 * 8, 2 * 16},
"",
&bitmap_icon_add,
Color::orange(),
/*vcenter*/ true};
NewButton button_new{
{22 * 8, 17 * 16, 4 * 8, 2 * 16},
"",
&bitmap_icon_new_file,
Color::dark_blue(),
/*vcenter*/ true};
NewButton button_open{
{26 * 8, 17 * 16, 4 * 8, 2 * 16},
"",
&bitmap_icon_load,
Color::dark_blue(),
/*vcenter*/ true};
spectrum::WaterfallView waterfall{};
MessageHandlerRegistration message_handler_replay_thread_error{
Message::ID::ReplayThreadDone,
[this](const Message* p) {
auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
handle_replay_thread_done(message.return_code);
}};
MessageHandlerRegistration message_handler_fifo_signal{
Message::ID::RequestSignal,
[this](const Message* p) {
auto message = static_cast<const RequestSignalMessage*>(p);
if (message->signal == RequestSignalMessage::Signal::FillRequest) {
ready_signal_ = true;
}
}};
};
} /* namespace ui */

View File

@ -654,17 +654,17 @@ SettingsMenuView::SettingsMenuView(NavigationView& nav) {
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
{"App Settings", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetAppSettingsView>(); }},
{"Audio", ui::Color::dark_cyan(), &bitmap_icon_speaker, [&nav]() { nav.push<SetAudioView>(); }},
{"Calibration", ui::Color::dark_cyan(), &bitmap_icon_options_touch, [&nav]() { nav.push<TouchCalibrationView>(); }},
{"Converter", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetConverterSettingsView>(); }},
{"Date/Time", ui::Color::dark_cyan(), &bitmap_icon_options_datetime, [&nav]() { nav.push<SetDateTimeView>(); }},
{"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetEncoderDialView>(); }},
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetFrequencyCorrectionView>(); }},
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<SetPersistentMemoryView>(); }},
{"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push<SetQRCodeView>(); }},
{"Radio", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetRadioView>(); }},
{"User Interface", ui::Color::dark_cyan(), &bitmap_icon_options_ui, [&nav]() { nav.push<SetUIView>(); }},
{"Date/Time", ui::Color::dark_cyan(), &bitmap_icon_options_datetime, [&nav]() { nav.push<SetDateTimeView>(); }},
{"Calibration", ui::Color::dark_cyan(), &bitmap_icon_options_touch, [&nav]() { nav.push<TouchCalibrationView>(); }},
{"App Settings", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetAppSettingsView>(); }},
{"Converter", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetConverterSettingsView>(); }},
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetFrequencyCorrectionView>(); }},
{"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push<SetQRCodeView>(); }},
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<SetPersistentMemoryView>(); }},
{"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetEncoderDialView>(); }},
});
set_max_rows(2); // allow wider buttons
}

View File

@ -261,7 +261,8 @@ class TextEditorView : public View {
{26 * 8, 34 * 8, 4 * 8, 4 * 8},
{},
&bitmap_icon_controls,
Color::dark_grey()};
Color::dark_grey(),
/*vcenter*/ true};
Text text_position{
{0 * 8, 34 * 8, 26 * 8, 2 * 8},

View File

@ -311,10 +311,10 @@ std::string to_string_file_size(uint32_t file_size) {
return to_string_dec_uint(file_size) + suffix[suffix_index];
}
std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precision) {
std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision) {
const uint32_t powers_of_ten[5] = {1, 10, 100, 1000, 10000};
std::string string{""};
uint32_t prefix_index = base_nano;
uint32_t prefix_index = base_unit;
double integer_part;
double fractional_part;

View File

@ -76,7 +76,9 @@ std::string to_string_FAT_timestamp(const FATTimestamp& timestamp);
// Gets a human readable file size string.
std::string to_string_file_size(uint32_t file_size);
std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precision);
/* Scales 'n' to be a value less than 1000. 'base_unit' is the index of the unit from
* 'unit_prefix' that 'n' is in initially. 3 is the index of the '1s' unit. */
std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision);
double get_decimals(double num, int16_t mult, bool round = false);
std::string trim(std::string_view str); // Remove whitespace at ends.

View File

@ -82,7 +82,8 @@ class AlphanumView : public TextEntryView {
{192, 214, screen_width / 5, 38},
{},
&bitmap_icon_shift,
Color::dark_grey()};
Color::dark_grey(),
/*vcenter*/ true};
Labels labels{
{{1 * 8, 33 * 8}, "Raw:", Color::light_grey()},

View File

@ -32,8 +32,25 @@
using namespace portapack;
#define POWER_THRESHOLD_HIGH 47
#define POWER_THRESHOLD_MED 38
#define POWER_THRESHOLD_LOW 17
namespace ui {
/* Gets a style indicating total TX gain level. */
static const Style* get_style_for_gain(uint8_t tot_gain) {
if (tot_gain > POWER_THRESHOLD_HIGH) return &Styles::red;
if (tot_gain > POWER_THRESHOLD_MED)
return &Styles::orange;
if (tot_gain > POWER_THRESHOLD_LOW)
return &Styles::yellow;
return nullptr; // Uses default.
}
/* TransmitterView *******************************************************/
void TransmitterView::paint(Painter& painter) {
@ -73,21 +90,13 @@ void TransmitterView::on_tx_amp_changed(bool rf_amp) {
}
void TransmitterView::update_gainlevel_styles() {
const Style* new_style_ptr = NULL;
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
auto style = get_style_for_gain(tot_gain);
if (tot_gain > POWER_THRESHOLD_HIGH) {
new_style_ptr = &style_power_high;
} else if (tot_gain > POWER_THRESHOLD_MED) {
new_style_ptr = &style_power_med;
} else if (tot_gain > POWER_THRESHOLD_LOW) {
new_style_ptr = &style_power_low;
}
field_gain.set_style(new_style_ptr);
text_gain.set_style(new_style_ptr);
field_amp.set_style(new_style_ptr);
text_amp.set_style(new_style_ptr);
field_gain.set_style(style);
text_gain.set_style(style);
field_amp.set_style(style);
text_amp.set_style(style);
}
void TransmitterView::set_transmitting(const bool transmitting) {
@ -186,89 +195,66 @@ TransmitterView::TransmitterView(
}
TransmitterView::~TransmitterView() {
// TODO: Does this make sense? Seems wrong to have
// what's basically a widget control the radio.
audio::output::stop();
transmitter_model.disable();
baseband::shutdown();
}
/* TransmitterView2 *******************************************************/
/* Simpler transmitter view that only renders TX Gain and Amp.
* There are two modes, NORMAL_UI and SHORT_UI. SHORT_UI abbreviates control labels. */
void TransmitterView2::paint(Painter&) {
// All widgets paint themselves. Don't let base paint.
}
void TransmitterView2::on_tx_gain_changed(int32_t tx_gain) {
transmitter_model.set_tx_gain(tx_gain);
update_gainlevel_styles();
}
TransmitterView2::TransmitterView2(Point pos, bool short_ui) {
// There are two modes, short and !short
// Short: "G:XX A:YY"
// !Short: "Gain:XX Amp:YY"
Dim width = short_ui ? (9 * 8) : (14 * 8);
set_parent_rect({pos, {width, 16}});
add_children({
&text_labels,
&field_gain,
&field_amp,
});
// Set up controls depending UI mode.
text_labels.set(short_ui ? "G: A:" : "Gain: Amp:");
text_labels.set_parent_rect(
short_ui
? Rect{0 * 8, 0 * 16, 7 * 8, 1 * 16}
: Rect{0 * 8, 0 * 16, 12 * 8, 1 * 16});
field_gain.set_parent_rect(
short_ui
? Rect{2 * 8, 0 * 16, 2 * 8, 1 * 16}
: Rect{5 * 8, 0 * 16, 2 * 8, 1 * 16});
field_amp.set_parent_rect(
short_ui
? Rect{7 * 8, 0 * 16, 2 * 8, 1 * 16}
: Rect{12 * 8, 0 * 16, 2 * 8, 1 * 16});
field_gain.set_value(transmitter_model.tx_gain());
field_gain.on_change = [this](uint32_t tx_gain) {
transmitter_model.set_tx_gain(tx_gain);
update_gainlevel_styles();
};
field_amp.set_value(transmitter_model.rf_amp() ? 14 : 0);
field_amp.on_change = [this](uint32_t rf_amp) {
transmitter_model.set_rf_amp(rf_amp > 0);
update_gainlevel_styles();
};
void TransmitterView2::on_tx_amp_changed(bool rf_amp) {
transmitter_model.set_rf_amp(rf_amp);
update_gainlevel_styles();
}
void TransmitterView2::update_gainlevel_styles() {
const Style* new_style_ptr = NULL;
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
auto style = get_style_for_gain(tot_gain);
if (tot_gain > POWER_THRESHOLD_HIGH) {
new_style_ptr = &style_power_high;
} else if (tot_gain > POWER_THRESHOLD_MED) {
new_style_ptr = &style_power_med;
} else if (tot_gain > POWER_THRESHOLD_LOW) {
new_style_ptr = &style_power_low;
}
field_gain.set_style(new_style_ptr);
text_gain_amp.set_style(new_style_ptr);
field_amp.set_style(new_style_ptr);
field_gain_short_UI.set_style(new_style_ptr);
text_gain_amp_short_UI.set_style(new_style_ptr);
field_amp_short_UI.set_style(new_style_ptr);
}
void TransmitterView2::on_show() {
field_gain.set_value(transmitter_model.tx_gain());
field_amp.set_value(transmitter_model.rf_amp() ? 14 : 0);
field_gain_short_UI.set_value(transmitter_model.tx_gain());
field_amp_short_UI.set_value(transmitter_model.rf_amp() ? 14 : 0);
update_gainlevel_styles();
}
TransmitterView2::TransmitterView2(const Coord x, const Coord y, bool short_UI) {
set_parent_rect({x, y, 20 * 8, 1 * 8});
add_children({
&(short_UI ? text_gain_amp_short_UI : text_gain_amp),
&(short_UI ? field_gain_short_UI : field_gain),
&(short_UI ? field_amp_short_UI : field_amp),
});
if (short_UI) {
field_gain_short_UI.on_change = [this](uint32_t tx_gain) {
on_tx_gain_changed(tx_gain);
};
field_amp_short_UI.on_change = [this](uint32_t rf_amp) {
on_tx_amp_changed((bool)rf_amp);
};
} else {
field_gain.on_change = [this](uint32_t tx_gain) {
on_tx_gain_changed(tx_gain);
};
field_amp.on_change = [this](uint32_t rf_amp) {
on_tx_amp_changed((bool)rf_amp);
};
}
}
TransmitterView2::~TransmitterView2() {
audio::output::stop();
transmitter_model.disable();
baseband::shutdown();
text_labels.set_style(style);
field_gain.set_style(style);
field_amp.set_style(style);
}
} /* namespace ui */

View File

@ -26,9 +26,9 @@
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_painter.hpp"
#include "ui_receiver.hpp"
#include "ui_styles.hpp"
#include "ui_widget.hpp"
#include "ui_receiver.hpp"
#include "rf_path.hpp"
@ -37,10 +37,6 @@
#include <algorithm>
#include <functional>
#define POWER_THRESHOLD_HIGH 47
#define POWER_THRESHOLD_MED 38
#define POWER_THRESHOLD_LOW 17
namespace ui {
class TXGainField : public NumberField {
@ -74,11 +70,8 @@ class TransmitterView : public View {
private:
const Style& style_start = Styles::green;
const Style style_stop = Styles::red;
const Style style_locked = Styles::dark_grey;
const Style style_power_low = Styles::yellow;
const Style style_power_med = Styles::orange;
const Style style_power_high = Styles::red;
const Style& style_stop = Styles::red;
const Style& style_locked = Styles::dark_grey;
bool lock_{false};
bool transmitting_{false};
@ -134,60 +127,32 @@ class TransmitterView : public View {
void update_gainlevel_styles(void);
};
/* Simpler transmitter view that only renders TX Gain and Amp.
* When short_UI is set it abbreviates control labels. */
class TransmitterView2 : public View {
public:
TransmitterView2(const Coord x, const Coord y, bool short_UI);
~TransmitterView2();
void on_show() override;
void paint(Painter& painter) override;
TransmitterView2(Point pos, bool short_ui);
private:
const Style& style_power_low = Styles::yellow;
const Style& style_power_med = Styles::orange;
const Style& style_power_high = Styles::red;
Text text_gain_amp{
{0, 3 * 8, 5 * 8, 1 * 16},
"Gain: Amp:"};
Text text_labels{
{}, // Set in ctor.
{}};
NumberField field_gain{
{5 * 8, 3 * 8},
{}, // Set in ctor.
2,
{max2837::tx::gain_db_range.minimum, max2837::tx::gain_db_range.maximum},
max2837::tx::gain_db_step,
' '};
NumberField field_amp{
{12 * 8, 3 * 8},
{}, // Set in ctor.
2,
{0, 14},
14,
' '};
Text text_gain_amp_short_UI{
{0, (3 * 8), 5 * 8, 1 * 16},
"Gain A:"};
NumberField field_gain_short_UI{
{(4 * 8) + 2, 3 * 8},
2,
{max2837::tx::gain_db_range.minimum, max2837::tx::gain_db_range.maximum},
max2837::tx::gain_db_step,
' '};
NumberField field_amp_short_UI{
{(9 * 8) - 2, 3 * 8},
2,
{0, 14},
14,
' '};
void on_tx_gain_changed(int32_t tx_gain);
void on_tx_amp_changed(bool rf_amp);
void update_gainlevel_styles(void);
void update_gainlevel_styles();
};
} /* namespace ui */

View File

@ -302,17 +302,18 @@ void SystemStatusView::on_converter() {
void SystemStatusView::on_bias_tee() {
if (!portapack::get_antenna_bias()) {
nav_.display_modal("Bias voltage",
"Enable DC voltage on\nantenna connector?",
YESNO,
[this](bool v) {
if (v) {
portapack::set_antenna_bias(true);
receiver_model.set_antenna_bias();
transmitter_model.set_antenna_bias();
refresh();
}
});
nav_.display_modal(
"Bias voltage",
"Enable DC voltage on\nantenna connector?",
YESNO,
[this](bool v) {
if (v) {
portapack::set_antenna_bias(true);
receiver_model.set_antenna_bias();
transmitter_model.set_antenna_bias();
refresh();
}
});
} else {
portapack::set_antenna_bias(false);
receiver_model.set_antenna_bias();
@ -498,30 +499,28 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) {
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
// {"ACARS", Color::yellow(), &bitmap_icon_adsb, [&nav](){ nav.push<ACARSAppView>(); }},
{"ADS-B", Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBRxView>(); }},
//{ "ACARS", Color::yellow(), &bitmap_icon_adsb, [&nav](){ nav.push<ACARSAppView>(); }},
{"AIS Boats", Color::green(), &bitmap_icon_ais, [&nav]() { nav.push<AISAppView>(); }},
{"AFSK", Color::yellow(), &bitmap_icon_modem, [&nav]() { nav.push<AFSKRxView>(); }},
{"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push<BTLERxView>(); }},
{"NRF", Color::yellow(), &bitmap_icon_nrf, [&nav]() { nav.push<NRFRxView>(); }},
{"Audio", Color::green(), &bitmap_icon_speaker, [&nav]() { nav.push<AnalogAudioView>(); }},
{"AIS Boats", Color::green(), &bitmap_icon_ais, [&nav]() { nav.push<AISAppView>(); }},
{"Analog TV", Color::yellow(), &bitmap_icon_sstv, [&nav]() { nav.push<AnalogTvView>(); }},
{"APRS", Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSRXView>(); }},
{"Audio", Color::green(), &bitmap_icon_speaker, [&nav]() { nav.push<AnalogAudioView>(); }},
{"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push<BTLERxView>(); }},
{"ERT Meter", Color::green(), &bitmap_icon_ert, [&nav]() { nav.push<ERTAppView>(); }},
{"Level", Color::green(), &bitmap_icon_options_radio, [&nav]() { nav.push<LevelView>(); }},
{"NRF", Color::yellow(), &bitmap_icon_nrf, [&nav]() { nav.push<NRFRxView>(); }},
{"POCSAG", Color::green(), &bitmap_icon_pocsag, [&nav]() { nav.push<POCSAGAppView>(); }},
{"Radiosnde", Color::green(), &bitmap_icon_sonde, [&nav]() { nav.push<SondeView>(); }},
{"TPMS Cars", Color::green(), &bitmap_icon_tpms, [&nav]() { nav.push<TPMSAppView>(); }},
{"Recon", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<ReconView>(); }},
{"Level", Color::green(), &bitmap_icon_options_radio, [&nav]() { nav.push<LevelView>(); }},
{"APRS", Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSRXView>(); }}
/*
{ "DMR", Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push<NotImplementedView>(); } },
{ "SIGFOX", Color::dark_grey(), &bitmap_icon_fox, [&nav](){ nav.push<NotImplementedView>(); } }, // SIGFRXView
{ "LoRa", Color::dark_grey(), &bitmap_icon_lora, [&nav](){ nav.push<NotImplementedView>(); } },
{ "SSTV", Color::dark_grey(), &bitmap_icon_sstv, [&nav](){ nav.push<NotImplementedView>(); } },
{ "TETRA", Color::dark_grey(), &bitmap_icon_tetra, [&nav](){ nav.push<NotImplementedView>(); } },*/
{"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push<SearchView>(); }},
{"TPMS Cars", Color::green(), &bitmap_icon_tpms, [&nav]() { nav.push<TPMSAppView>(); }},
// {"DMR", Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push<NotImplementedView>(); }},
// {"SIGFOX", Color::dark_grey(), &bitmap_icon_fox, [&nav](){ nav.push<NotImplementedView>(); }},
// {"LoRa", Color::dark_grey(), &bitmap_icon_lora, [&nav](){ nav.push<NotImplementedView>(); }},
// {"SSTV", Color::dark_grey(), &bitmap_icon_sstv, [&nav](){ nav.push<NotImplementedView>(); }},
// {"TETRA", Color::dark_grey(), &bitmap_icon_tetra, [&nav](){ nav.push<NotImplementedView>(); }},
});
// set_highlighted(0); // Default selection is "Audio"
}
/* TransmittersMenuView **************************************************/
@ -531,26 +530,24 @@ TransmittersMenuView::TransmittersMenuView(NavigationView& nav) {
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
{"ADS-B [S]", ui::Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBTxView>(); }},
{"ADS-B", ui::Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBTxView>(); }},
{"APRS", ui::Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSTXView>(); }},
{"BHT Xy/EP", ui::Color::green(), &bitmap_icon_bht, [&nav]() { nav.push<BHTView>(); }},
{"BurgerPgr", ui::Color::yellow(), &bitmap_icon_burger, [&nav]() { nav.push<CoasterPagerView>(); }},
{"GPS Sim", ui::Color::green(), &bitmap_icon_gps_sim, [&nav]() { nav.push<GpsSimAppView>(); }},
{"Jammer", ui::Color::green(), &bitmap_icon_jammer, [&nav]() { nav.push<JammerView>(); }},
//{ "Key fob", ui::Color::orange(), &bitmap_icon_keyfob, [&nav](){ nav.push<KeyfobView>(); } },
{"LGE tool", ui::Color::yellow(), &bitmap_icon_lge, [&nav]() { nav.push<LGEView>(); }},
// { "Key fob", ui::Color::orange(), &bitmap_icon_keyfob, [&nav](){ nav.push<KeyfobView>(); }},
{"LGE", ui::Color::yellow(), &bitmap_icon_lge, [&nav]() { nav.push<LGEView>(); }},
{"Morse", ui::Color::green(), &bitmap_icon_morse, [&nav]() { nav.push<MorseView>(); }},
{"BurgerPgr", ui::Color::yellow(), &bitmap_icon_burger, [&nav]() { nav.push<CoasterPagerView>(); }},
//{ "Nuoptix DTMF", ui::Color::green(), &bitmap_icon_nuoptix, [&nav](){ nav.push<NuoptixView>(); } },
// { "Nuoptix DTMF", ui::Color::green(), &bitmap_icon_nuoptix, [&nav](){ nav.push<NuoptixView>(); }},
{"OOK", ui::Color::yellow(), &bitmap_icon_remote, [&nav]() { nav.push<EncodersView>(); }},
{"POCSAG", ui::Color::green(), &bitmap_icon_pocsag, [&nav]() { nav.push<POCSAGTXView>(); }},
{"RDS", ui::Color::green(), &bitmap_icon_rds, [&nav]() { nav.push<RDSView>(); }},
{"Soundbrd", ui::Color::green(), &bitmap_icon_soundboard, [&nav]() { nav.push<SoundBoardView>(); }},
{"S.Painter", ui::Color::orange(), &bitmap_icon_paint, [&nav]() { nav.push<SpectrumPainterView>(); }},
{"SSTV", ui::Color::green(), &bitmap_icon_sstv, [&nav]() { nav.push<SSTVTXView>(); }},
{"TEDI/LCR", ui::Color::yellow(), &bitmap_icon_lcr, [&nav]() { nav.push<LCRView>(); }},
{"TouchTune", ui::Color::green(), &bitmap_icon_touchtunes, [&nav]() { nav.push<TouchTunesView>(); }},
//{"Playlist", ui::Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<PlaylistView>(); }},
{"S.Painter", ui::Color::orange(), &bitmap_icon_paint, [&nav]() { nav.push<SpectrumPainterView>(); }},
//{ "Remote", ui::Color::dark_grey(), &bitmap_icon_remote, [&nav](){ nav.push<RemoteView>(); } },
});
}
@ -561,41 +558,47 @@ UtilitiesMenuView::UtilitiesMenuView(NavigationView& nav) {
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
//{ "Test app", Color::dark_grey(), nullptr, [&nav](){ nav.push<TestView>(); } },
{"Freq. manager", Color::green(), &bitmap_icon_freqman, [&nav]() { nav.push<FrequencyManagerView>(); }},
{"File manager", Color::green(), &bitmap_icon_dir, [&nav]() { nav.push<FileManagerView>(); }},
{"Antenna Length", Color::green(), &bitmap_icon_tools_antenna, [&nav]() { nav.push<WhipCalcView>(); }},
{"File Manager", Color::green(), &bitmap_icon_dir, [&nav]() { nav.push<FileManagerView>(); }},
{"Freq. Manager", Color::green(), &bitmap_icon_freqman, [&nav]() { nav.push<FrequencyManagerView>(); }},
{"Notepad", Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<TextEditorView>(); }},
{"Signal gen", Color::green(), &bitmap_icon_cwgen, [&nav]() { nav.push<SigGenView>(); }},
//{ "Tone search", Color::dark_grey(), nullptr, [&nav](){ nav.push<ToneSearchView>(); } },
{"Wav viewer", Color::yellow(), &bitmap_icon_soundboard, [&nav]() { nav.push<ViewWavView>(); }},
{"Antenna length", Color::green(), &bitmap_icon_tools_antenna, [&nav]() { nav.push<WhipCalcView>(); }},
{"SD Over USB", Color::yellow(), &bitmap_icon_hackrf, [&nav]() { nav.push<SdOverUsbView>(); }},
{"Signal Gen", Color::green(), &bitmap_icon_cwgen, [&nav]() { nav.push<SigGenView>(); }},
// {"Test App", Color::dark_grey(), nullptr, [&nav](){ nav.push<TestView>(); }},
// {"Tone Search", Color::dark_grey(), nullptr, [&nav](){ nav.push<ToneSearchView>(); }},
{"Wav View", Color::yellow(), &bitmap_icon_soundboard, [&nav]() { nav.push<ViewWavView>(); }},
{"Wipe SD card", Color::red(), &bitmap_icon_tools_wipesd, [&nav]() { nav.push<WipeSDView>(); }},
// Dangerous apps.
{"Flash Utility", Color::red(), &bitmap_icon_temperature, [&nav]() { nav.push<FlashUtilityView>(); }},
{"SD over USB", Color::yellow(), &bitmap_icon_hackrf, [&nav]() { nav.push<SdOverUsbView>(); }},
{"Wipe SD card", Color::red(), &bitmap_icon_tools_wipesd, [&nav]() { nav.push<WipeSDView>(); }},
});
set_max_rows(2); // allow wider buttons
}
/* SystemMenuView ********************************************************/
void SystemMenuView::hackrf_mode(NavigationView& nav) {
nav.push<ModalMessageView>("HackRF mode", " This mode enables HackRF\n functionality. To return,\n press the reset button.\n\n Switch to HackRF mode?", YESNO,
[this](bool choice) {
if (choice) {
EventDispatcher::request_stop();
}
});
nav.push<ModalMessageView>(
"HackRF mode",
" This mode enables HackRF\n functionality. To return,\n press the reset button.\n\n Switch to HackRF mode?",
YESNO,
[this](bool choice) {
if (choice) {
EventDispatcher::request_stop();
}
});
}
SystemMenuView::SystemMenuView(NavigationView& nav) {
add_items({
//{ "Play dead", Color::red(), &bitmap_icon_playdead, [&nav](){ nav.push<PlayDeadView>(); } },
// {"Play dead", Color::red(), &bitmap_icon_playdead, [&nav]() { nav.push<PlayDeadView>(); }},
{"Receive", Color::cyan(), &bitmap_icon_receivers, [&nav]() { nav.push<ReceiversMenuView>(); }},
{"Transmit", Color::cyan(), &bitmap_icon_transmit, [&nav]() { nav.push<TransmittersMenuView>(); }},
{"Capture", Color::red(), &bitmap_icon_capture, [&nav]() { nav.push<CaptureAppView>(); }},
{"Replay", Color::green(), &bitmap_icon_replay, [&nav]() { nav.push<PlaylistView>(); }},
{"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push<SearchView>(); }},
// {"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push<SearchView>(); }},
{"Remote", ui::Color::green(), &bitmap_icon_remote, [&nav]() { nav.push<RemoteView>(); }},
{"Scanner", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<ScannerView>(); }},
{"Microphone", Color::green(), &bitmap_icon_microphone, [&nav]() { nav.push<MicTXView>(); }},
{"Looking Glass", Color::green(), &bitmap_icon_looking, [&nav]() { nav.push<GlassView>(); }},
@ -603,11 +606,11 @@ SystemMenuView::SystemMenuView(NavigationView& nav) {
{"Settings", Color::cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SettingsMenuView>(); }},
{"Debug", Color::light_grey(), &bitmap_icon_debug, [&nav]() { nav.push<DebugMenuView>(); }},
{"HackRF", Color::cyan(), &bitmap_icon_hackrf, [this, &nav]() { hackrf_mode(nav); }},
//{ "About", Color::cyan(), nullptr, [&nav](){ nav.push<AboutView>(); } }
// {"About", Color::cyan(), nullptr, [&nav]() { nav.push<AboutView>(); }},
});
set_max_rows(2); // allow wider buttons
set_arrow_enabled(false);
// set_highlighted(1); // Startup selection
}
/* SystemView ************************************************************/
@ -623,19 +626,22 @@ SystemView::SystemView(
constexpr Dim info_view_height = 16;
add_child(&status_view);
status_view.set_parent_rect({{0, 0},
{parent_rect.width(), status_view_height}});
status_view.set_parent_rect(
{{0, 0},
{parent_rect.width(), status_view_height}});
status_view.on_back = [this]() {
this->navigation_view.pop();
};
add_child(&navigation_view);
navigation_view.set_parent_rect({{0, status_view_height},
{parent_rect.width(), static_cast<Dim>(parent_rect.height() - status_view_height)}});
navigation_view.set_parent_rect(
{{0, status_view_height},
{parent_rect.width(), static_cast<Dim>(parent_rect.height() - status_view_height)}});
add_child(&info_view);
info_view.set_parent_rect({{0, 19 * 16},
{parent_rect.width(), info_view_height}});
info_view.set_parent_rect(
{{0, 19 * 16},
{parent_rect.width(), info_view_height}});
navigation_view.on_view_changed = [this](const View& new_view) {
if (!this->navigation_view.is_top()) {
@ -651,14 +657,6 @@ SystemView::SystemView(
this->status_view.set_dirty();
};
// pmem::set_playdead_sequence(0x8D1);
// Initial view
/*if ((pmem::playing_dead() == 0x5920C1DF) || // Enable code
(pmem::ui_config() & 16)) { // Login option
navigation_view.push<PlayDeadView>();
} else {*/
navigation_view.push<SystemMenuView>();
if (pmem::config_splash()) {
@ -667,10 +665,6 @@ SystemView::SystemView(
status_view.set_back_enabled(false);
status_view.set_title_image_enabled(true);
status_view.set_dirty();
// else
// navigation_view.push<SystemMenuView>();
//}
}
Context& SystemView::context() const {
@ -812,7 +806,7 @@ void ModalMessageView::paint(Painter& painter) {
portapack::display.drawBMP({100, 48}, modal_warning_bmp, false);
// Terrible...
// Break on lines.
while ((pos = message_.find("\n", start)) != std::string::npos) {
painter.draw_string(
{1 * 8, (Coord)(120 + (i * 16))},

View File

@ -97,6 +97,14 @@ struct Color {
return (v ^ 0xffff);
}
/* Converts a 32-bit color into a 16-bit color.
* High byte is ignored. */
static constexpr Color RGB(uint32_t rgb) {
return {static_cast<uint8_t>((rgb >> 16) & 0xff),
static_cast<uint8_t>((rgb >> 8) & 0xff),
static_cast<uint8_t>(rgb & 0xff)};
}
static constexpr Color black() {
return {0, 0, 0};
}

View File

@ -1139,9 +1139,9 @@ NewButton::NewButton(
Color color,
bool vertical_center)
: Widget{parent_rect},
color_{color},
text_{text},
bitmap_{bitmap},
color_{color},
vertical_center_{vertical_center} {
set_focusable(true);
}
@ -1182,18 +1182,8 @@ void NewButton::paint(Painter& painter) {
if (!bitmap_ && text_.empty())
return;
Color bg, fg;
const auto r = screen_rect();
if (has_focus() || highlighted()) {
bg = style().foreground;
fg = Color::black();
} else {
bg = Color::grey();
fg = style().foreground;
}
const Style paint_style = {style().font, bg, fg};
const Style style = paint_style();
painter.draw_rectangle({r.location(), {r.width(), 1}}, Color::light_grey());
painter.draw_rectangle({r.left(), r.top() + r.height() - 1, r.width(), 1}, Color::dark_grey());
@ -1201,7 +1191,7 @@ void NewButton::paint(Painter& painter) {
painter.fill_rectangle(
{r.left(), r.top() + 1, r.width() - 1, r.height() - 2},
paint_style.background);
style.background);
int y = r.top();
if (bitmap_) {
@ -1213,18 +1203,32 @@ void NewButton::paint(Painter& painter) {
bmp_pos,
*bitmap_,
color_,
bg);
style.background);
}
if (!text_.empty()) {
const auto label_r = paint_style.font.size_of(text_);
const auto label_r = style.font.size_of(text_);
painter.draw_string(
{r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
paint_style,
style,
text_);
}
}
Style NewButton::paint_style() {
MutableStyle s{style()};
if (has_focus() || highlighted()) {
s.background = style().foreground;
s.foreground = Color::black();
} else {
s.background = Color::grey();
s.foreground = style().foreground;
}
return s;
}
void NewButton::on_focus() {
if (on_highlight)
on_highlight(*this);

View File

@ -479,10 +479,13 @@ class NewButton : public Widget {
void paint(Painter& painter) override;
protected:
virtual Style paint_style();
Color color_;
private:
std::string text_;
const Bitmap* bitmap_;
Color color_;
bool vertical_center_{false};
};

View File

@ -213,3 +213,25 @@ static constexpr uint32_t gcd_top(const uint32_t u, const uint32_t v) {
uint32_t gcd(const uint32_t u, const uint32_t v) {
return gcd_top(u, v);
}
std::string join(char c, std::initializer_list<std::string_view> strings) {
std::string result;
size_t total_size = strings.size();
for (auto s : strings)
total_size += s.size();
result.reserve(total_size);
bool first = true;
for (auto s : strings) {
if (!first)
result += c;
else
first = false;
result += s;
}
return result;
}

View File

@ -27,7 +27,9 @@
#include <complex>
#include <cstdint>
#include <cstddef>
#include <initializer_list>
#include <memory>
#include <string_view>
#include <type_traits>
#define LOCATE_IN_RAM __attribute__((section(".ramtext")))
@ -213,4 +215,6 @@ struct range_t {
}
};
std::string join(char c, std::initializer_list<std::string_view> strings);
#endif /*__UTILITY_H__*/

View File

@ -47,6 +47,7 @@ add_executable(application_test EXCLUDE_FROM_ALL
${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp
${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp
${PROJECT_SOURCE_DIR}/../../common/utility.cpp
# Dependencies
${PROJECT_SOURCE_DIR}/../../application/file.cpp

View File

@ -90,3 +90,10 @@ TEST_CASE("to_byte_array returns correct size and values.") {
CHECK_EQ(arr4[2], 0x12);
CHECK_EQ(arr4[3], 0x34);
}
TEST_CASE("join will join strings") {
CHECK_EQ(join(',', {}), "");
CHECK_EQ(join(',', {"a"}), "a");
CHECK_EQ(join('-', {"a", "b"}), "a-b");
CHECK_EQ(join(',', {"a", "b", "c"}), "a,b,c");
}