mirror of
https://github.com/eried/portapack-mayhem.git
synced 2025-01-11 15:29:28 -05:00
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:
parent
537cf2e79b
commit
fca373d936
@ -24,9 +24,6 @@
|
|||||||
#ifndef __GPS_SIM_APP_HPP__
|
#ifndef __GPS_SIM_APP_HPP__
|
||||||
#define __GPS_SIM_APP_HPP__
|
#define __GPS_SIM_APP_HPP__
|
||||||
|
|
||||||
#define SHORT_UI true
|
|
||||||
#define NORMAL_UI false
|
|
||||||
|
|
||||||
#include "app_settings.hpp"
|
#include "app_settings.hpp"
|
||||||
#include "radio_state.hpp"
|
#include "radio_state.hpp"
|
||||||
#include "ui_widget.hpp"
|
#include "ui_widget.hpp"
|
||||||
@ -109,10 +106,8 @@ class GpsSimAppView : public View {
|
|||||||
nav_};
|
nav_};
|
||||||
|
|
||||||
TransmitterView2 tx_view{
|
TransmitterView2 tx_view{
|
||||||
// new handling of NumberField field_rfgain, NumberField field_rfamp
|
{11 * 8, 2 * 16},
|
||||||
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Used in Replay / GPS Simul / Playlist App)
|
/*short_ui*/ true};
|
||||||
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
|
|
||||||
};
|
|
||||||
|
|
||||||
Checkbox check_loop{
|
Checkbox check_loop{
|
||||||
{21 * 8, 2 * 16},
|
{21 * 8, 2 * 16},
|
||||||
|
@ -203,9 +203,6 @@ ReplayAppView::ReplayAppView(
|
|||||||
|
|
||||||
ReplayAppView::~ReplayAppView() {
|
ReplayAppView::~ReplayAppView() {
|
||||||
transmitter_model.disable();
|
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();
|
baseband::shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,9 +23,6 @@
|
|||||||
#ifndef __REPLAY_APP_HPP__
|
#ifndef __REPLAY_APP_HPP__
|
||||||
#define __REPLAY_APP_HPP__
|
#define __REPLAY_APP_HPP__
|
||||||
|
|
||||||
#define SHORT_UI true
|
|
||||||
#define NORMAL_UI false
|
|
||||||
|
|
||||||
#include "app_settings.hpp"
|
#include "app_settings.hpp"
|
||||||
#include "radio_state.hpp"
|
#include "radio_state.hpp"
|
||||||
#include "ui_widget.hpp"
|
#include "ui_widget.hpp"
|
||||||
@ -99,16 +96,13 @@ class ReplayAppView : public View {
|
|||||||
ProgressBar progressbar{
|
ProgressBar progressbar{
|
||||||
{18 * 8, 1 * 16, 12 * 8, 16}};
|
{18 * 8, 1 * 16, 12 * 8, 16}};
|
||||||
|
|
||||||
// TODO: Does this need to be a frequency field at all?
|
|
||||||
TxFrequencyField field_frequency{
|
TxFrequencyField field_frequency{
|
||||||
{0 * 8, 2 * 16},
|
{0 * 8, 2 * 16},
|
||||||
nav_};
|
nav_};
|
||||||
|
|
||||||
TransmitterView2 tx_view{
|
TransmitterView2 tx_view{
|
||||||
// new handling of NumberField field_rfgain, NumberField field_rfamp
|
{11 * 8, 2 * 16},
|
||||||
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Uused in Repay / GPS Simul / Playlist App)
|
/*short_ui*/ true};
|
||||||
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
|
|
||||||
};
|
|
||||||
|
|
||||||
Checkbox check_loop{
|
Checkbox check_loop{
|
||||||
{21 * 8, 2 * 16},
|
{21 * 8, 2 * 16},
|
||||||
|
@ -398,17 +398,17 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
|
|||||||
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
||||||
}
|
}
|
||||||
add_items({
|
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>(); }},
|
{"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>(); } },
|
//{ "Radio State", ui::Color::white(), nullptr, [&nav](){ nav.push<NotImplementedView>(); } },
|
||||||
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [&nav]() { nav.push<SDCardDebugView>(); }},
|
{"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>(); }},
|
{"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>(); }},
|
{"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
|
set_max_rows(2); // allow wider buttons
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include "ui_fileman.hpp"
|
#include "ui_fileman.hpp"
|
||||||
#include "ui_playlist.hpp"
|
#include "ui_playlist.hpp"
|
||||||
|
#include "ui_remote.hpp"
|
||||||
#include "ui_ss_viewer.hpp"
|
#include "ui_ss_viewer.hpp"
|
||||||
#include "ui_text_editor.hpp"
|
#include "ui_text_editor.hpp"
|
||||||
#include "string_format.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 cxx_ext{u".C*"};
|
||||||
static const fs::path png_ext{u".PNG"};
|
static const fs::path png_ext{u".PNG"};
|
||||||
static const fs::path bmp_ext{u".BMP"};
|
static const fs::path bmp_ext{u".BMP"};
|
||||||
|
static const fs::path rem_ext{u".REM"};
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@ -525,6 +527,9 @@ bool FileManagerView::handle_file_open() {
|
|||||||
nav_.push<SplashViewer>(path);
|
nav_.push<SplashViewer>(path);
|
||||||
reload_current();
|
reload_current();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (path_iequal(rem_ext, ext)) {
|
||||||
|
nav_.push<RemoteView>(path);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -629,7 +634,7 @@ FileManagerView::FileManagerView(
|
|||||||
button_open_notepad.on_select = [this]() {
|
button_open_notepad.on_select = [this]() {
|
||||||
if (selected_is_valid() && !get_selected_entry().is_directory) {
|
if (selected_is_valid() && !get_selected_entry().is_directory) {
|
||||||
auto path = get_selected_full_path();
|
auto path = get_selected_full_path();
|
||||||
nav_.replace<TextEditorView>(path);
|
nav_.push<TextEditorView>(path);
|
||||||
} else
|
} else
|
||||||
nav_.display_modal("Open in Notepad", "Can't open that in Notepad.");
|
nav_.display_modal("Open in Notepad", "Can't open that in Notepad.");
|
||||||
};
|
};
|
||||||
|
@ -77,7 +77,8 @@ class FileManBaseView : public View {
|
|||||||
{u".C8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
|
{u".C8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
|
||||||
{u".C16", &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".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.
|
{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},
|
{4 * 8, 34 * 8, 4 * 8, 32},
|
||||||
{},
|
{},
|
||||||
&bitmap_icon_options_datetime,
|
&bitmap_icon_options_datetime,
|
||||||
Color::orange()};
|
Color::orange(),
|
||||||
|
/*vcenter*/ true};
|
||||||
};
|
};
|
||||||
|
|
||||||
} /* namespace ui */
|
} /* namespace ui */
|
||||||
|
@ -633,7 +633,7 @@ MicTXView::MicTXView(
|
|||||||
|
|
||||||
MicTXView::~MicTXView() {
|
MicTXView::~MicTXView() {
|
||||||
audio::input::stop();
|
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();
|
transmitter_model.disable();
|
||||||
if (rx_enabled) // Also turn off audio rx if enabled
|
if (rx_enabled) // Also turn off audio rx if enabled
|
||||||
rxaudio(false);
|
rxaudio(false);
|
||||||
|
@ -25,9 +25,6 @@
|
|||||||
#ifndef __UI_MICTX_H__
|
#ifndef __UI_MICTX_H__
|
||||||
#define __UI_MICTX_H__
|
#define __UI_MICTX_H__
|
||||||
|
|
||||||
#define SHORT_UI true
|
|
||||||
#define NORMAL_UI false
|
|
||||||
|
|
||||||
#include "app_settings.hpp"
|
#include "app_settings.hpp"
|
||||||
#include "radio_state.hpp"
|
#include "radio_state.hpp"
|
||||||
#include "ui.hpp"
|
#include "ui.hpp"
|
||||||
@ -219,10 +216,8 @@ class MicTXView : public View {
|
|||||||
' '};
|
' '};
|
||||||
|
|
||||||
TransmitterView2 tx_view{
|
TransmitterView2 tx_view{
|
||||||
// new handling of NumberField field_rfgain, NumberField field_rfamp
|
{3 * 8, 5 * 8},
|
||||||
// 3*8, 2*8, SHORT_UI // x(columns), y (line) position. (used in Replay / GPS Simul / Playlist App's)
|
/*short_ui*/ false};
|
||||||
3 * 8, 2 * 8, NORMAL_UI // x(columns), y (line) position. (used in Mic App)
|
|
||||||
};
|
|
||||||
|
|
||||||
OptionsField options_mode{
|
OptionsField options_mode{
|
||||||
{24 * 8, 5 * 8},
|
{24 * 8, 5 * 8},
|
||||||
|
@ -386,6 +386,7 @@ PlaylistView::PlaylistView(
|
|||||||
&waterfall,
|
&waterfall,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ensure_directory(u"PLAYLIST");
|
||||||
waterfall.show_audio_spectrum_view(false);
|
waterfall.show_audio_spectrum_view(false);
|
||||||
|
|
||||||
field_frequency.set_value(100'000'000);
|
field_frequency.set_value(100'000'000);
|
||||||
|
@ -21,9 +21,6 @@
|
|||||||
* Boston, MA 02110-1301, USA.
|
* Boston, MA 02110-1301, USA.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#define SHORT_UI true
|
|
||||||
#define NORMAL_UI false
|
|
||||||
|
|
||||||
#include "app_settings.hpp"
|
#include "app_settings.hpp"
|
||||||
#include "bitmap.hpp"
|
#include "bitmap.hpp"
|
||||||
#include "file.hpp"
|
#include "file.hpp"
|
||||||
@ -63,7 +60,7 @@ class PlaylistView : public View {
|
|||||||
|
|
||||||
// More header == less spectrum view.
|
// More header == less spectrum view.
|
||||||
static constexpr ui::Dim header_height = 6 * 16;
|
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 {
|
struct playlist_entry {
|
||||||
std::filesystem::path path{};
|
std::filesystem::path path{};
|
||||||
@ -118,14 +115,11 @@ class PlaylistView : public View {
|
|||||||
Text text_sample_rate{
|
Text text_sample_rate{
|
||||||
{10 * 8, 1 * 16, 7 * 8, 16}};
|
{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{
|
ProgressBar progressbar_track{
|
||||||
{18 * 8, 1 * 16, 12 * 8, 8 + 1}};
|
{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{
|
ProgressBar progressbar_transmit{
|
||||||
{18 * 8, 3 * 8 - 1, 12 * 8, 8}};
|
{18 * 8, 3 * 8 - 1, 12 * 8, 8}};
|
||||||
|
|
||||||
@ -135,7 +129,8 @@ class PlaylistView : public View {
|
|||||||
// TODO: delay duration field.
|
// TODO: delay duration field.
|
||||||
|
|
||||||
TransmitterView2 tx_view{
|
TransmitterView2 tx_view{
|
||||||
9 * 8, 1 * 8, SHORT_UI};
|
{11 * 8, 2 * 16},
|
||||||
|
/*short_ui*/ true};
|
||||||
|
|
||||||
Checkbox check_loop{
|
Checkbox check_loop{
|
||||||
{21 * 8, 2 * 16},
|
{21 * 8, 2 * 16},
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
* Copyright (C) 2023 Kyle Reed
|
||||||
* Copyright (C) 2018 Furrtek
|
|
||||||
*
|
*
|
||||||
* This file is part of PortaPack.
|
* This file is part of PortaPack.
|
||||||
*
|
*
|
||||||
@ -22,30 +21,615 @@
|
|||||||
|
|
||||||
#include "ui_remote.hpp"
|
#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 "string_format.hpp"
|
||||||
|
#include "ui_fileman.hpp"
|
||||||
|
#include "ui_receiver.hpp"
|
||||||
|
#include "ui_textentry.hpp"
|
||||||
|
#include "utility.hpp"
|
||||||
|
|
||||||
using namespace portapack;
|
using namespace portapack;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
void RemoteView::focus() {
|
static constexpr uint8_t text_edit_max = 30;
|
||||||
button.focus();
|
|
||||||
|
/* 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() {
|
Optional<RemoteEntryModel> RemoteEntryModel::parse(std::string_view line) {
|
||||||
// transmitter_model.disable();
|
auto cols = split_string(line, ',');
|
||||||
// baseband::shutdown();
|
|
||||||
|
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(
|
/* RemoteModel *******************************************/
|
||||||
NavigationView& nav) {
|
|
||||||
add_children({&labels,
|
|
||||||
&button});
|
|
||||||
|
|
||||||
button.on_select = [this, &nav](Button&) {
|
bool RemoteModel::delete_entry(const RemoteEntryModel* entry) {
|
||||||
nav.pop();
|
// 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 */
|
} /* namespace ui */
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
* Copyright (C) 2023 Kyle Reed
|
||||||
* Copyright (C) 2018 Furrtek
|
|
||||||
*
|
*
|
||||||
* This file is part of PortaPack.
|
* This file is part of PortaPack.
|
||||||
*
|
*
|
||||||
@ -21,42 +20,370 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ui.hpp"
|
#include "ui.hpp"
|
||||||
|
#include "ui_navigation.hpp"
|
||||||
|
#include "ui_receiver.hpp"
|
||||||
|
#include "ui_spectrum.hpp"
|
||||||
#include "ui_transmitter.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 {
|
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 {
|
class RemoteView : public View {
|
||||||
public:
|
public:
|
||||||
RemoteView(NavigationView& nav);
|
RemoteView(NavigationView& nav);
|
||||||
|
RemoteView(NavigationView& nav, std::filesystem::path path);
|
||||||
~RemoteView();
|
~RemoteView();
|
||||||
|
|
||||||
|
RemoteView(const RemoteView&) = delete;
|
||||||
|
RemoteView& operator=(const RemoteView&) = delete;
|
||||||
|
|
||||||
|
std::string title() const override { return "Remote"; };
|
||||||
void focus() override;
|
void focus() override;
|
||||||
|
|
||||||
std::string title() const override { return "Custom remote"; };
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/*enum tx_modes {
|
/* Creates the dynamic buttons. */
|
||||||
IDLE = 0,
|
void create_buttons();
|
||||||
SINGLE,
|
/* Resets all the pointers to null entries. */
|
||||||
SCAN
|
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 {
|
static constexpr Dim button_rows = 4;
|
||||||
Point position;
|
static constexpr Dim button_cols = 3;
|
||||||
std::string text;
|
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{
|
NavigationView& nav_;
|
||||||
{{1 * 8, 0}, "Work in progress...", Color::light_grey()}};
|
RxRadioState radio_state_{};
|
||||||
|
|
||||||
Button button{
|
// Settings
|
||||||
{60, 64, 120, 32},
|
RemoteSettings settings_{};
|
||||||
"Exit"};
|
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 */
|
} /* namespace ui */
|
||||||
|
@ -654,17 +654,17 @@ SettingsMenuView::SettingsMenuView(NavigationView& nav) {
|
|||||||
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
||||||
}
|
}
|
||||||
add_items({
|
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>(); }},
|
{"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>(); }},
|
{"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>(); }},
|
{"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
|
set_max_rows(2); // allow wider buttons
|
||||||
}
|
}
|
||||||
|
@ -261,7 +261,8 @@ class TextEditorView : public View {
|
|||||||
{26 * 8, 34 * 8, 4 * 8, 4 * 8},
|
{26 * 8, 34 * 8, 4 * 8, 4 * 8},
|
||||||
{},
|
{},
|
||||||
&bitmap_icon_controls,
|
&bitmap_icon_controls,
|
||||||
Color::dark_grey()};
|
Color::dark_grey(),
|
||||||
|
/*vcenter*/ true};
|
||||||
|
|
||||||
Text text_position{
|
Text text_position{
|
||||||
{0 * 8, 34 * 8, 26 * 8, 2 * 8},
|
{0 * 8, 34 * 8, 26 * 8, 2 * 8},
|
||||||
|
@ -311,10 +311,10 @@ std::string to_string_file_size(uint32_t file_size) {
|
|||||||
return to_string_dec_uint(file_size) + suffix[suffix_index];
|
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};
|
const uint32_t powers_of_ten[5] = {1, 10, 100, 1000, 10000};
|
||||||
std::string string{""};
|
std::string string{""};
|
||||||
uint32_t prefix_index = base_nano;
|
uint32_t prefix_index = base_unit;
|
||||||
double integer_part;
|
double integer_part;
|
||||||
double fractional_part;
|
double fractional_part;
|
||||||
|
|
||||||
|
@ -76,7 +76,9 @@ std::string to_string_FAT_timestamp(const FATTimestamp& timestamp);
|
|||||||
// Gets a human readable file size string.
|
// Gets a human readable file size string.
|
||||||
std::string to_string_file_size(uint32_t file_size);
|
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);
|
double get_decimals(double num, int16_t mult, bool round = false);
|
||||||
|
|
||||||
std::string trim(std::string_view str); // Remove whitespace at ends.
|
std::string trim(std::string_view str); // Remove whitespace at ends.
|
||||||
|
@ -82,7 +82,8 @@ class AlphanumView : public TextEntryView {
|
|||||||
{192, 214, screen_width / 5, 38},
|
{192, 214, screen_width / 5, 38},
|
||||||
{},
|
{},
|
||||||
&bitmap_icon_shift,
|
&bitmap_icon_shift,
|
||||||
Color::dark_grey()};
|
Color::dark_grey(),
|
||||||
|
/*vcenter*/ true};
|
||||||
|
|
||||||
Labels labels{
|
Labels labels{
|
||||||
{{1 * 8, 33 * 8}, "Raw:", Color::light_grey()},
|
{{1 * 8, 33 * 8}, "Raw:", Color::light_grey()},
|
||||||
|
@ -32,8 +32,25 @@
|
|||||||
|
|
||||||
using namespace portapack;
|
using namespace portapack;
|
||||||
|
|
||||||
|
#define POWER_THRESHOLD_HIGH 47
|
||||||
|
#define POWER_THRESHOLD_MED 38
|
||||||
|
#define POWER_THRESHOLD_LOW 17
|
||||||
|
|
||||||
namespace ui {
|
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 *******************************************************/
|
/* TransmitterView *******************************************************/
|
||||||
|
|
||||||
void TransmitterView::paint(Painter& painter) {
|
void TransmitterView::paint(Painter& painter) {
|
||||||
@ -73,21 +90,13 @@ void TransmitterView::on_tx_amp_changed(bool rf_amp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TransmitterView::update_gainlevel_styles() {
|
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);
|
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) {
|
field_gain.set_style(style);
|
||||||
new_style_ptr = &style_power_high;
|
text_gain.set_style(style);
|
||||||
} else if (tot_gain > POWER_THRESHOLD_MED) {
|
field_amp.set_style(style);
|
||||||
new_style_ptr = &style_power_med;
|
text_amp.set_style(style);
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TransmitterView::set_transmitting(const bool transmitting) {
|
void TransmitterView::set_transmitting(const bool transmitting) {
|
||||||
@ -186,89 +195,66 @@ TransmitterView::TransmitterView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
TransmitterView::~TransmitterView() {
|
TransmitterView::~TransmitterView() {
|
||||||
|
// TODO: Does this make sense? Seems wrong to have
|
||||||
|
// what's basically a widget control the radio.
|
||||||
audio::output::stop();
|
audio::output::stop();
|
||||||
transmitter_model.disable();
|
transmitter_model.disable();
|
||||||
baseband::shutdown();
|
baseband::shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TransmitterView2 *******************************************************/
|
/* 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) {
|
TransmitterView2::TransmitterView2(Point pos, bool short_ui) {
|
||||||
transmitter_model.set_tx_gain(tx_gain);
|
// There are two modes, short and !short
|
||||||
update_gainlevel_styles();
|
// 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();
|
update_gainlevel_styles();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TransmitterView2::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);
|
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) {
|
text_labels.set_style(style);
|
||||||
new_style_ptr = &style_power_high;
|
field_gain.set_style(style);
|
||||||
} else if (tot_gain > POWER_THRESHOLD_MED) {
|
field_amp.set_style(style);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} /* namespace ui */
|
} /* namespace ui */
|
||||||
|
@ -26,9 +26,9 @@
|
|||||||
#include "ui.hpp"
|
#include "ui.hpp"
|
||||||
#include "ui_navigation.hpp"
|
#include "ui_navigation.hpp"
|
||||||
#include "ui_painter.hpp"
|
#include "ui_painter.hpp"
|
||||||
|
#include "ui_receiver.hpp"
|
||||||
#include "ui_styles.hpp"
|
#include "ui_styles.hpp"
|
||||||
#include "ui_widget.hpp"
|
#include "ui_widget.hpp"
|
||||||
#include "ui_receiver.hpp"
|
|
||||||
|
|
||||||
#include "rf_path.hpp"
|
#include "rf_path.hpp"
|
||||||
|
|
||||||
@ -37,10 +37,6 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#define POWER_THRESHOLD_HIGH 47
|
|
||||||
#define POWER_THRESHOLD_MED 38
|
|
||||||
#define POWER_THRESHOLD_LOW 17
|
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
class TXGainField : public NumberField {
|
class TXGainField : public NumberField {
|
||||||
@ -74,11 +70,8 @@ class TransmitterView : public View {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
const Style& style_start = Styles::green;
|
const Style& style_start = Styles::green;
|
||||||
const Style style_stop = Styles::red;
|
const Style& style_stop = Styles::red;
|
||||||
const Style style_locked = Styles::dark_grey;
|
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;
|
|
||||||
|
|
||||||
bool lock_{false};
|
bool lock_{false};
|
||||||
bool transmitting_{false};
|
bool transmitting_{false};
|
||||||
@ -134,60 +127,32 @@ class TransmitterView : public View {
|
|||||||
void update_gainlevel_styles(void);
|
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 {
|
class TransmitterView2 : public View {
|
||||||
public:
|
public:
|
||||||
TransmitterView2(const Coord x, const Coord y, bool short_UI);
|
TransmitterView2(Point pos, bool short_ui);
|
||||||
|
|
||||||
~TransmitterView2();
|
|
||||||
|
|
||||||
void on_show() override;
|
|
||||||
void paint(Painter& painter) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const Style& style_power_low = Styles::yellow;
|
Text text_labels{
|
||||||
const Style& style_power_med = Styles::orange;
|
{}, // Set in ctor.
|
||||||
const Style& style_power_high = Styles::red;
|
{}};
|
||||||
|
|
||||||
Text text_gain_amp{
|
|
||||||
{0, 3 * 8, 5 * 8, 1 * 16},
|
|
||||||
"Gain: Amp:"};
|
|
||||||
|
|
||||||
NumberField field_gain{
|
NumberField field_gain{
|
||||||
{5 * 8, 3 * 8},
|
{}, // Set in ctor.
|
||||||
2,
|
2,
|
||||||
{max2837::tx::gain_db_range.minimum, max2837::tx::gain_db_range.maximum},
|
{max2837::tx::gain_db_range.minimum, max2837::tx::gain_db_range.maximum},
|
||||||
max2837::tx::gain_db_step,
|
max2837::tx::gain_db_step,
|
||||||
' '};
|
' '};
|
||||||
|
|
||||||
NumberField field_amp{
|
NumberField field_amp{
|
||||||
{12 * 8, 3 * 8},
|
{}, // Set in ctor.
|
||||||
2,
|
2,
|
||||||
{0, 14},
|
{0, 14},
|
||||||
14,
|
14,
|
||||||
' '};
|
' '};
|
||||||
|
|
||||||
Text text_gain_amp_short_UI{
|
void update_gainlevel_styles();
|
||||||
{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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} /* namespace ui */
|
} /* namespace ui */
|
||||||
|
@ -302,17 +302,18 @@ void SystemStatusView::on_converter() {
|
|||||||
|
|
||||||
void SystemStatusView::on_bias_tee() {
|
void SystemStatusView::on_bias_tee() {
|
||||||
if (!portapack::get_antenna_bias()) {
|
if (!portapack::get_antenna_bias()) {
|
||||||
nav_.display_modal("Bias voltage",
|
nav_.display_modal(
|
||||||
"Enable DC voltage on\nantenna connector?",
|
"Bias voltage",
|
||||||
YESNO,
|
"Enable DC voltage on\nantenna connector?",
|
||||||
[this](bool v) {
|
YESNO,
|
||||||
if (v) {
|
[this](bool v) {
|
||||||
portapack::set_antenna_bias(true);
|
if (v) {
|
||||||
receiver_model.set_antenna_bias();
|
portapack::set_antenna_bias(true);
|
||||||
transmitter_model.set_antenna_bias();
|
receiver_model.set_antenna_bias();
|
||||||
refresh();
|
transmitter_model.set_antenna_bias();
|
||||||
}
|
refresh();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
portapack::set_antenna_bias(false);
|
portapack::set_antenna_bias(false);
|
||||||
receiver_model.set_antenna_bias();
|
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({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
||||||
}
|
}
|
||||||
add_items({
|
add_items({
|
||||||
|
// {"ACARS", Color::yellow(), &bitmap_icon_adsb, [&nav](){ nav.push<ACARSAppView>(); }},
|
||||||
{"ADS-B", Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBRxView>(); }},
|
{"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>(); }},
|
{"AFSK", Color::yellow(), &bitmap_icon_modem, [&nav]() { nav.push<AFSKRxView>(); }},
|
||||||
{"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push<BTLERxView>(); }},
|
{"AIS Boats", Color::green(), &bitmap_icon_ais, [&nav]() { nav.push<AISAppView>(); }},
|
||||||
{"NRF", Color::yellow(), &bitmap_icon_nrf, [&nav]() { nav.push<NRFRxView>(); }},
|
|
||||||
{"Audio", Color::green(), &bitmap_icon_speaker, [&nav]() { nav.push<AnalogAudioView>(); }},
|
|
||||||
{"Analog TV", Color::yellow(), &bitmap_icon_sstv, [&nav]() { nav.push<AnalogTvView>(); }},
|
{"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>(); }},
|
{"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>(); }},
|
{"POCSAG", Color::green(), &bitmap_icon_pocsag, [&nav]() { nav.push<POCSAGAppView>(); }},
|
||||||
{"Radiosnde", Color::green(), &bitmap_icon_sonde, [&nav]() { nav.push<SondeView>(); }},
|
{"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>(); }},
|
{"Recon", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<ReconView>(); }},
|
||||||
{"Level", Color::green(), &bitmap_icon_options_radio, [&nav]() { nav.push<LevelView>(); }},
|
{"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push<SearchView>(); }},
|
||||||
{"APRS", Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSRXView>(); }}
|
{"TPMS Cars", Color::green(), &bitmap_icon_tpms, [&nav]() { nav.push<TPMSAppView>(); }},
|
||||||
/*
|
// {"DMR", Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push<NotImplementedView>(); }},
|
||||||
{ "DMR", Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push<NotImplementedView>(); } },
|
// {"SIGFOX", Color::dark_grey(), &bitmap_icon_fox, [&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>(); }},
|
||||||
{ "LoRa", Color::dark_grey(), &bitmap_icon_lora, [&nav](){ nav.push<NotImplementedView>(); } },
|
// {"SSTV", Color::dark_grey(), &bitmap_icon_sstv, [&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>(); }},
|
||||||
{ "TETRA", Color::dark_grey(), &bitmap_icon_tetra, [&nav](){ nav.push<NotImplementedView>(); } },*/
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// set_highlighted(0); // Default selection is "Audio"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TransmittersMenuView **************************************************/
|
/* TransmittersMenuView **************************************************/
|
||||||
@ -531,26 +530,24 @@ TransmittersMenuView::TransmittersMenuView(NavigationView& nav) {
|
|||||||
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
||||||
}
|
}
|
||||||
add_items({
|
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>(); }},
|
{"APRS", ui::Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSTXView>(); }},
|
||||||
{"BHT Xy/EP", ui::Color::green(), &bitmap_icon_bht, [&nav]() { nav.push<BHTView>(); }},
|
{"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>(); }},
|
{"GPS Sim", ui::Color::green(), &bitmap_icon_gps_sim, [&nav]() { nav.push<GpsSimAppView>(); }},
|
||||||
{"Jammer", ui::Color::green(), &bitmap_icon_jammer, [&nav]() { nav.push<JammerView>(); }},
|
{"Jammer", ui::Color::green(), &bitmap_icon_jammer, [&nav]() { nav.push<JammerView>(); }},
|
||||||
//{ "Key fob", ui::Color::orange(), &bitmap_icon_keyfob, [&nav](){ nav.push<KeyfobView>(); } },
|
// { "Key fob", ui::Color::orange(), &bitmap_icon_keyfob, [&nav](){ nav.push<KeyfobView>(); }},
|
||||||
{"LGE tool", ui::Color::yellow(), &bitmap_icon_lge, [&nav]() { nav.push<LGEView>(); }},
|
{"LGE", ui::Color::yellow(), &bitmap_icon_lge, [&nav]() { nav.push<LGEView>(); }},
|
||||||
{"Morse", ui::Color::green(), &bitmap_icon_morse, [&nav]() { nav.push<MorseView>(); }},
|
{"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>(); }},
|
{"OOK", ui::Color::yellow(), &bitmap_icon_remote, [&nav]() { nav.push<EncodersView>(); }},
|
||||||
{"POCSAG", ui::Color::green(), &bitmap_icon_pocsag, [&nav]() { nav.push<POCSAGTXView>(); }},
|
{"POCSAG", ui::Color::green(), &bitmap_icon_pocsag, [&nav]() { nav.push<POCSAGTXView>(); }},
|
||||||
{"RDS", ui::Color::green(), &bitmap_icon_rds, [&nav]() { nav.push<RDSView>(); }},
|
{"RDS", ui::Color::green(), &bitmap_icon_rds, [&nav]() { nav.push<RDSView>(); }},
|
||||||
{"Soundbrd", ui::Color::green(), &bitmap_icon_soundboard, [&nav]() { nav.push<SoundBoardView>(); }},
|
{"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>(); }},
|
{"SSTV", ui::Color::green(), &bitmap_icon_sstv, [&nav]() { nav.push<SSTVTXView>(); }},
|
||||||
{"TEDI/LCR", ui::Color::yellow(), &bitmap_icon_lcr, [&nav]() { nav.push<LCRView>(); }},
|
{"TEDI/LCR", ui::Color::yellow(), &bitmap_icon_lcr, [&nav]() { nav.push<LCRView>(); }},
|
||||||
{"TouchTune", ui::Color::green(), &bitmap_icon_touchtunes, [&nav]() { nav.push<TouchTunesView>(); }},
|
{"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({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
||||||
}
|
}
|
||||||
add_items({
|
add_items({
|
||||||
//{ "Test app", Color::dark_grey(), nullptr, [&nav](){ nav.push<TestView>(); } },
|
{"Antenna Length", Color::green(), &bitmap_icon_tools_antenna, [&nav]() { nav.push<WhipCalcView>(); }},
|
||||||
{"Freq. manager", Color::green(), &bitmap_icon_freqman, [&nav]() { nav.push<FrequencyManagerView>(); }},
|
{"File Manager", Color::green(), &bitmap_icon_dir, [&nav]() { nav.push<FileManagerView>(); }},
|
||||||
{"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>(); }},
|
{"Notepad", Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<TextEditorView>(); }},
|
||||||
{"Signal gen", Color::green(), &bitmap_icon_cwgen, [&nav]() { nav.push<SigGenView>(); }},
|
{"SD Over USB", Color::yellow(), &bitmap_icon_hackrf, [&nav]() { nav.push<SdOverUsbView>(); }},
|
||||||
//{ "Tone search", Color::dark_grey(), nullptr, [&nav](){ nav.push<ToneSearchView>(); } },
|
{"Signal Gen", Color::green(), &bitmap_icon_cwgen, [&nav]() { nav.push<SigGenView>(); }},
|
||||||
{"Wav viewer", Color::yellow(), &bitmap_icon_soundboard, [&nav]() { nav.push<ViewWavView>(); }},
|
// {"Test App", Color::dark_grey(), nullptr, [&nav](){ nav.push<TestView>(); }},
|
||||||
{"Antenna length", Color::green(), &bitmap_icon_tools_antenna, [&nav]() { nav.push<WhipCalcView>(); }},
|
// {"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>(); }},
|
{"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
|
set_max_rows(2); // allow wider buttons
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SystemMenuView ********************************************************/
|
/* SystemMenuView ********************************************************/
|
||||||
|
|
||||||
void SystemMenuView::hackrf_mode(NavigationView& nav) {
|
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,
|
nav.push<ModalMessageView>(
|
||||||
[this](bool choice) {
|
"HackRF mode",
|
||||||
if (choice) {
|
" This mode enables HackRF\n functionality. To return,\n press the reset button.\n\n Switch to HackRF mode?",
|
||||||
EventDispatcher::request_stop();
|
YESNO,
|
||||||
}
|
[this](bool choice) {
|
||||||
});
|
if (choice) {
|
||||||
|
EventDispatcher::request_stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemMenuView::SystemMenuView(NavigationView& nav) {
|
SystemMenuView::SystemMenuView(NavigationView& nav) {
|
||||||
add_items({
|
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>(); }},
|
{"Receive", Color::cyan(), &bitmap_icon_receivers, [&nav]() { nav.push<ReceiversMenuView>(); }},
|
||||||
{"Transmit", Color::cyan(), &bitmap_icon_transmit, [&nav]() { nav.push<TransmittersMenuView>(); }},
|
{"Transmit", Color::cyan(), &bitmap_icon_transmit, [&nav]() { nav.push<TransmittersMenuView>(); }},
|
||||||
{"Capture", Color::red(), &bitmap_icon_capture, [&nav]() { nav.push<CaptureAppView>(); }},
|
{"Capture", Color::red(), &bitmap_icon_capture, [&nav]() { nav.push<CaptureAppView>(); }},
|
||||||
{"Replay", Color::green(), &bitmap_icon_replay, [&nav]() { nav.push<PlaylistView>(); }},
|
{"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>(); }},
|
{"Scanner", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<ScannerView>(); }},
|
||||||
{"Microphone", Color::green(), &bitmap_icon_microphone, [&nav]() { nav.push<MicTXView>(); }},
|
{"Microphone", Color::green(), &bitmap_icon_microphone, [&nav]() { nav.push<MicTXView>(); }},
|
||||||
{"Looking Glass", Color::green(), &bitmap_icon_looking, [&nav]() { nav.push<GlassView>(); }},
|
{"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>(); }},
|
{"Settings", Color::cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SettingsMenuView>(); }},
|
||||||
{"Debug", Color::light_grey(), &bitmap_icon_debug, [&nav]() { nav.push<DebugMenuView>(); }},
|
{"Debug", Color::light_grey(), &bitmap_icon_debug, [&nav]() { nav.push<DebugMenuView>(); }},
|
||||||
{"HackRF", Color::cyan(), &bitmap_icon_hackrf, [this, &nav]() { hackrf_mode(nav); }},
|
{"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_max_rows(2); // allow wider buttons
|
||||||
set_arrow_enabled(false);
|
set_arrow_enabled(false);
|
||||||
// set_highlighted(1); // Startup selection
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SystemView ************************************************************/
|
/* SystemView ************************************************************/
|
||||||
@ -623,19 +626,22 @@ SystemView::SystemView(
|
|||||||
constexpr Dim info_view_height = 16;
|
constexpr Dim info_view_height = 16;
|
||||||
|
|
||||||
add_child(&status_view);
|
add_child(&status_view);
|
||||||
status_view.set_parent_rect({{0, 0},
|
status_view.set_parent_rect(
|
||||||
{parent_rect.width(), status_view_height}});
|
{{0, 0},
|
||||||
|
{parent_rect.width(), status_view_height}});
|
||||||
status_view.on_back = [this]() {
|
status_view.on_back = [this]() {
|
||||||
this->navigation_view.pop();
|
this->navigation_view.pop();
|
||||||
};
|
};
|
||||||
|
|
||||||
add_child(&navigation_view);
|
add_child(&navigation_view);
|
||||||
navigation_view.set_parent_rect({{0, status_view_height},
|
navigation_view.set_parent_rect(
|
||||||
{parent_rect.width(), static_cast<Dim>(parent_rect.height() - status_view_height)}});
|
{{0, status_view_height},
|
||||||
|
{parent_rect.width(), static_cast<Dim>(parent_rect.height() - status_view_height)}});
|
||||||
|
|
||||||
add_child(&info_view);
|
add_child(&info_view);
|
||||||
info_view.set_parent_rect({{0, 19 * 16},
|
info_view.set_parent_rect(
|
||||||
{parent_rect.width(), info_view_height}});
|
{{0, 19 * 16},
|
||||||
|
{parent_rect.width(), info_view_height}});
|
||||||
|
|
||||||
navigation_view.on_view_changed = [this](const View& new_view) {
|
navigation_view.on_view_changed = [this](const View& new_view) {
|
||||||
if (!this->navigation_view.is_top()) {
|
if (!this->navigation_view.is_top()) {
|
||||||
@ -651,14 +657,6 @@ SystemView::SystemView(
|
|||||||
this->status_view.set_dirty();
|
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>();
|
navigation_view.push<SystemMenuView>();
|
||||||
|
|
||||||
if (pmem::config_splash()) {
|
if (pmem::config_splash()) {
|
||||||
@ -667,10 +665,6 @@ SystemView::SystemView(
|
|||||||
status_view.set_back_enabled(false);
|
status_view.set_back_enabled(false);
|
||||||
status_view.set_title_image_enabled(true);
|
status_view.set_title_image_enabled(true);
|
||||||
status_view.set_dirty();
|
status_view.set_dirty();
|
||||||
// else
|
|
||||||
// navigation_view.push<SystemMenuView>();
|
|
||||||
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Context& SystemView::context() const {
|
Context& SystemView::context() const {
|
||||||
@ -812,7 +806,7 @@ void ModalMessageView::paint(Painter& painter) {
|
|||||||
|
|
||||||
portapack::display.drawBMP({100, 48}, modal_warning_bmp, false);
|
portapack::display.drawBMP({100, 48}, modal_warning_bmp, false);
|
||||||
|
|
||||||
// Terrible...
|
// Break on lines.
|
||||||
while ((pos = message_.find("\n", start)) != std::string::npos) {
|
while ((pos = message_.find("\n", start)) != std::string::npos) {
|
||||||
painter.draw_string(
|
painter.draw_string(
|
||||||
{1 * 8, (Coord)(120 + (i * 16))},
|
{1 * 8, (Coord)(120 + (i * 16))},
|
||||||
|
@ -97,6 +97,14 @@ struct Color {
|
|||||||
return (v ^ 0xffff);
|
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() {
|
static constexpr Color black() {
|
||||||
return {0, 0, 0};
|
return {0, 0, 0};
|
||||||
}
|
}
|
||||||
|
@ -1139,9 +1139,9 @@ NewButton::NewButton(
|
|||||||
Color color,
|
Color color,
|
||||||
bool vertical_center)
|
bool vertical_center)
|
||||||
: Widget{parent_rect},
|
: Widget{parent_rect},
|
||||||
|
color_{color},
|
||||||
text_{text},
|
text_{text},
|
||||||
bitmap_{bitmap},
|
bitmap_{bitmap},
|
||||||
color_{color},
|
|
||||||
vertical_center_{vertical_center} {
|
vertical_center_{vertical_center} {
|
||||||
set_focusable(true);
|
set_focusable(true);
|
||||||
}
|
}
|
||||||
@ -1182,18 +1182,8 @@ void NewButton::paint(Painter& painter) {
|
|||||||
if (!bitmap_ && text_.empty())
|
if (!bitmap_ && text_.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Color bg, fg;
|
|
||||||
const auto r = screen_rect();
|
const auto r = screen_rect();
|
||||||
|
const Style style = paint_style();
|
||||||
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};
|
|
||||||
|
|
||||||
painter.draw_rectangle({r.location(), {r.width(), 1}}, Color::light_grey());
|
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());
|
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(
|
painter.fill_rectangle(
|
||||||
{r.left(), r.top() + 1, r.width() - 1, r.height() - 2},
|
{r.left(), r.top() + 1, r.width() - 1, r.height() - 2},
|
||||||
paint_style.background);
|
style.background);
|
||||||
|
|
||||||
int y = r.top();
|
int y = r.top();
|
||||||
if (bitmap_) {
|
if (bitmap_) {
|
||||||
@ -1213,18 +1203,32 @@ void NewButton::paint(Painter& painter) {
|
|||||||
bmp_pos,
|
bmp_pos,
|
||||||
*bitmap_,
|
*bitmap_,
|
||||||
color_,
|
color_,
|
||||||
bg);
|
style.background);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!text_.empty()) {
|
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(
|
painter.draw_string(
|
||||||
{r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
|
{r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
|
||||||
paint_style,
|
style,
|
||||||
text_);
|
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() {
|
void NewButton::on_focus() {
|
||||||
if (on_highlight)
|
if (on_highlight)
|
||||||
on_highlight(*this);
|
on_highlight(*this);
|
||||||
|
@ -479,10 +479,13 @@ class NewButton : public Widget {
|
|||||||
|
|
||||||
void paint(Painter& painter) override;
|
void paint(Painter& painter) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual Style paint_style();
|
||||||
|
Color color_;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string text_;
|
std::string text_;
|
||||||
const Bitmap* bitmap_;
|
const Bitmap* bitmap_;
|
||||||
Color color_;
|
|
||||||
bool vertical_center_{false};
|
bool vertical_center_{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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) {
|
uint32_t gcd(const uint32_t u, const uint32_t v) {
|
||||||
return gcd_top(u, 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;
|
||||||
|
}
|
||||||
|
@ -27,7 +27,9 @@
|
|||||||
#include <complex>
|
#include <complex>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <initializer_list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
|
||||||
#define LOCATE_IN_RAM __attribute__((section(".ramtext")))
|
#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__*/
|
#endif /*__UTILITY_H__*/
|
||||||
|
@ -47,6 +47,7 @@ add_executable(application_test EXCLUDE_FROM_ALL
|
|||||||
|
|
||||||
${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp
|
${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp
|
||||||
${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp
|
${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp
|
||||||
|
${PROJECT_SOURCE_DIR}/../../common/utility.cpp
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
${PROJECT_SOURCE_DIR}/../../application/file.cpp
|
${PROJECT_SOURCE_DIR}/../../application/file.cpp
|
||||||
|
@ -89,4 +89,11 @@ TEST_CASE("to_byte_array returns correct size and values.") {
|
|||||||
CHECK_EQ(arr4[1], 0xCD);
|
CHECK_EQ(arr4[1], 0xCD);
|
||||||
CHECK_EQ(arr4[2], 0x12);
|
CHECK_EQ(arr4[2], 0x12);
|
||||||
CHECK_EQ(arr4[3], 0x34);
|
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");
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user