mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-02 12:59:03 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22c9dd31bf | |||
| 9e63d80667 | |||
| 67b5b57533 | |||
| c2314f4838 | |||
| 1cb682473a | |||
| 63dd85cbfc | |||
| 6dbc8460b4 | |||
| ec8bf04baa | |||
| a1189faad1 | |||
| ba9cc55b5a | |||
| c2e527bce2 | |||
| 98f3bf151f | |||
| 802a4e243b |
@@ -45,10 +45,6 @@ Consider that the hardware and firmware has been created and maintain by a [lot]
|
||||
|
||||
To support the people behind the hardware, please buy a genuine [HackRF](https://greatscottgadgets.com/hackrf/) and [PortaPack](https://store.sharebrained.com/products/portapack-for-hackrf-one-kit).
|
||||
|
||||
As a last option, if you want to send money directly to me for getting more boards, antennas and such:
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CBPQA4HRRPJQ6&source=url)
|
||||
|
||||
## What if I really want something specific?
|
||||
If what you need can be relevant in general, you can [request a feature](https://github.com/eried/portapack-mayhem/issues/new?labels=enhancement&template=feature_request.md).
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ add_custom_target(
|
||||
|
||||
add_custom_target(
|
||||
program
|
||||
COMMAND dfu-util --device 1fc9:000c --download ${HACKRF_FIRMWARE_DFU_IMAGE}
|
||||
COMMAND dfu-util --device 1fc9:000c --download ${HACKRF_FIRMWARE_DFU_IMAGE} || (exit 0) # We need to add it for dfu-utils v.011 , (in v.09 it is not necessary)
|
||||
COMMAND sleep 3s
|
||||
COMMAND hackrf_spiflash -w ${FIRMWARE_FILENAME}
|
||||
DEPENDS ${FIRMWARE_FILENAME}
|
||||
|
||||
@@ -178,7 +178,7 @@ void AboutView::draw_demoglyph(ui::Point p, char ch, ui::Color* pal) {
|
||||
int16_t lbx, il;
|
||||
|
||||
// Map ASCII to font bitmap
|
||||
if ((ch >= 32) || (ch < 96))
|
||||
if ((ch >= 32) && (ch < 96))
|
||||
che = char_map[ch - 32];
|
||||
else
|
||||
che = 0xFF;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_text_editor.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "event_m0.hpp"
|
||||
@@ -510,16 +511,19 @@ FileManagerView::FileManagerView(
|
||||
refresh_widgets(v);
|
||||
};
|
||||
|
||||
add_children({&menu_view,
|
||||
&labels,
|
||||
&text_date,
|
||||
&button_rename,
|
||||
&button_delete,
|
||||
&button_cut,
|
||||
&button_copy,
|
||||
&button_paste,
|
||||
&button_new_dir,
|
||||
&button_new_file});
|
||||
add_children({
|
||||
&menu_view,
|
||||
&labels,
|
||||
&text_date,
|
||||
&button_rename,
|
||||
&button_delete,
|
||||
&button_cut,
|
||||
&button_copy,
|
||||
&button_paste,
|
||||
&button_new_dir,
|
||||
&button_new_file,
|
||||
&button_open_notepad,
|
||||
});
|
||||
|
||||
menu_view.on_highlight = [this]() {
|
||||
if (selected_is_valid())
|
||||
@@ -578,6 +582,14 @@ FileManagerView::FileManagerView(
|
||||
button_new_file.on_select = [this]() {
|
||||
on_new_file();
|
||||
};
|
||||
|
||||
button_open_notepad.on_select = [this]() {
|
||||
if (selected_is_valid() && !get_selected_entry().is_directory) {
|
||||
auto path = get_selected_full_path();
|
||||
nav_.replace<TextEditorView>(path);
|
||||
} else
|
||||
nav_.display_modal("Open in Notepad", "Can't open that in Notepad.");
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -77,7 +77,8 @@ class FileManBaseView : public View {
|
||||
{u".C8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
|
||||
{u".C16", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
|
||||
{u".WAV", &bitmap_icon_file_wav, ui::Color::dark_magenta()},
|
||||
{u"", &bitmap_icon_file, ui::Color::light_grey()} // NB: Must be last.
|
||||
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // PPL is the file extension for playlist app
|
||||
{u"", &bitmap_icon_file, ui::Color::light_grey()} // NB: Must be last.
|
||||
};
|
||||
|
||||
std::filesystem::path get_selected_full_path() const;
|
||||
@@ -120,7 +121,7 @@ class FileManBaseView : public View {
|
||||
""};
|
||||
|
||||
Button button_exit{
|
||||
{21 * 8, 34 * 8, 9 * 8, 32},
|
||||
{22 * 8, 34 * 8, 9 * 8, 32},
|
||||
"Exit"};
|
||||
};
|
||||
|
||||
@@ -263,6 +264,12 @@ class FileManagerView : public FileManBaseView {
|
||||
{},
|
||||
&bitmap_icon_new_file,
|
||||
Color::green()};
|
||||
|
||||
NewButton button_open_notepad{
|
||||
{0 * 8, 34 * 8, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_notepad,
|
||||
Color::orange()};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -40,7 +40,7 @@ class FlashUtilityView : public View {
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Flash Utility"; };
|
||||
std::string title() const override { return "FlashUtility"; }; // Removed the space because the title and speaker icon overlapped.
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2020 euquiq
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -143,9 +144,13 @@ void GlassView::add_spectrum_pixel(uint8_t power) {
|
||||
// save max powerwull freq
|
||||
if (spectrum_data[xpos] > max_freq_power) {
|
||||
max_freq_power = spectrum_data[xpos];
|
||||
max_freq_hold = f_center + ((looking_glass_range)*xpos) / SCREEN_W;
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
max_freq_hold = f_min + (xpos * looking_glass_range) / SCREEN_W;
|
||||
} else // if( mode == LOOKING_GLASS_SLOWSCAN || mode == LOOKING_GLASS_FASTSCAN )
|
||||
{
|
||||
max_freq_hold = f_min + (offset * each_bin_size) + (xpos * looking_glass_range) / SCREEN_W;
|
||||
}
|
||||
}
|
||||
|
||||
int16_t point = y_max_range.clip(((spectrum_data[xpos] - raw_min) * (320 - (108 + 16))) / raw_delta);
|
||||
uint8_t color_gradient = (point * 255) / 212;
|
||||
// clear if not in peak view
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include <unistd.h>
|
||||
#include <fstream>
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
@@ -53,11 +54,13 @@ void PlaylistView::load_file(std::filesystem::path playlist_path) {
|
||||
if ((int)one_char[0] >= ' ') {
|
||||
line += one_char[0];
|
||||
} else if (one_char[0] == '\n') {
|
||||
total_tracks++;
|
||||
txtline_process(line);
|
||||
line.clear();
|
||||
}
|
||||
}
|
||||
if (line.length() > 0) {
|
||||
total_tracks++;
|
||||
txtline_process(line);
|
||||
}
|
||||
}
|
||||
@@ -91,8 +94,10 @@ void PlaylistView::on_file_changed(std::filesystem::path new_file_path, rf::Freq
|
||||
File data_file;
|
||||
// Get file size
|
||||
auto data_open_error = data_file.open("/" + new_file_path.string());
|
||||
if (data_open_error.is_valid()) {
|
||||
file_error();
|
||||
if (!data_open_error.is_valid()) {
|
||||
track_number++;
|
||||
} else if (data_open_error.is_valid()) {
|
||||
file_error("C16 file\n" + new_file_path.string() + "\nread error.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,6 +114,7 @@ void PlaylistView::on_file_changed(std::filesystem::path new_file_path, rf::Freq
|
||||
progressbar.set_max(file_size);
|
||||
text_filename.set(file_path.filename().string().substr(0, 12));
|
||||
text_duration.set(to_string_time_ms(duration));
|
||||
// text_track.set(std::to_string(track_number) + "/" + std::to_string(total_tracks));
|
||||
|
||||
button_play.focus();
|
||||
}
|
||||
@@ -121,8 +127,8 @@ void PlaylistView::focus() {
|
||||
button_open.focus();
|
||||
}
|
||||
|
||||
void PlaylistView::file_error() {
|
||||
nav_.display_modal("Error", "File " + file_path.string() + " read error. " + file_path.string());
|
||||
void PlaylistView::file_error(std::string error_message) {
|
||||
nav_.display_modal("Error", "Error for \n" + file_path.string() + "\n" + error_message);
|
||||
}
|
||||
|
||||
bool PlaylistView::is_active() const {
|
||||
@@ -136,7 +142,16 @@ bool PlaylistView::loop() const {
|
||||
void PlaylistView::toggle() {
|
||||
if (is_active()) {
|
||||
stop(false);
|
||||
total_tracks = 0;
|
||||
track_number = 0;
|
||||
playlist_db.clear();
|
||||
playlist_masterdb.clear();
|
||||
} else {
|
||||
total_tracks = 0;
|
||||
track_number = 0;
|
||||
playlist_db.clear();
|
||||
playlist_masterdb.clear();
|
||||
load_file(now_play_list_file);
|
||||
start();
|
||||
}
|
||||
}
|
||||
@@ -158,7 +173,7 @@ void PlaylistView::start() {
|
||||
auto p = std::make_unique<FileReader>();
|
||||
auto open_error = p->open(file_path);
|
||||
if (open_error.is_valid()) {
|
||||
file_error();
|
||||
file_error("Illegal grammar");
|
||||
return; // Fixes TX bug if there's a file error
|
||||
} else {
|
||||
reader = std::move(p);
|
||||
@@ -198,17 +213,41 @@ void PlaylistView::stop(const bool do_loop) {
|
||||
if (is_active()) {
|
||||
replay_thread.reset();
|
||||
}
|
||||
if (do_loop) {
|
||||
if (playlist_db.size() > 0) {
|
||||
start();
|
||||
|
||||
// TODO: the logic here could be more beautiful but maybe they are all same for compiler anyway....
|
||||
// Notes of the logic here in case if it needed to be changed in the future:
|
||||
// 1. check_loop.value() is for the LOOP checkbox
|
||||
// 2. do_loop is a part of the replay thread, not a user - control thing.
|
||||
// 3. when (total_tracks >= track_number) is true, it means that the current track is not the last track.
|
||||
// Thus, (do_loop && (total_tracks != track_number)) is for the case when the start() func were called with true AND not the last track.
|
||||
// Which means it do loop until the last track.
|
||||
|
||||
if (check_loop.value()) {
|
||||
if (do_loop) {
|
||||
if (playlist_db.size() > 0) {
|
||||
start();
|
||||
} else {
|
||||
playlist_db = playlist_masterdb;
|
||||
start();
|
||||
}
|
||||
} else {
|
||||
playlist_db = playlist_masterdb;
|
||||
start();
|
||||
radio::set_antenna_bias(false); // Turn off Bias Tee
|
||||
radio::disable();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
}
|
||||
} else if (!check_loop.value()) {
|
||||
if (do_loop && (total_tracks >= track_number)) {
|
||||
if (playlist_db.size() > 0) {
|
||||
start();
|
||||
} else {
|
||||
playlist_db = playlist_masterdb;
|
||||
start();
|
||||
}
|
||||
} else {
|
||||
radio::set_antenna_bias(false); // Turn off Bias Tee
|
||||
radio::disable();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
}
|
||||
} else {
|
||||
radio::set_antenna_bias(false); // Turn off Bias Tee
|
||||
radio::disable();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
}
|
||||
|
||||
ready_signal = false;
|
||||
@@ -219,7 +258,7 @@ void PlaylistView::handle_replay_thread_done(const uint32_t return_code) {
|
||||
stop(true);
|
||||
} else if (return_code == ReplayThread::READ_ERROR) {
|
||||
stop(false);
|
||||
file_error();
|
||||
file_error("Replay thread read error");
|
||||
}
|
||||
|
||||
progressbar.set_value(0);
|
||||
@@ -240,6 +279,8 @@ PlaylistView::PlaylistView(
|
||||
&tx_view, // this handles now the previous rfgain, rfamp
|
||||
&check_loop,
|
||||
&button_play,
|
||||
// &text_track,
|
||||
// TODO: add track number
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
@@ -264,8 +305,9 @@ PlaylistView::PlaylistView(
|
||||
};
|
||||
|
||||
button_open.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".TXT");
|
||||
auto open_view = nav.push<FileLoadView>(".PPL");
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
now_play_list_file = new_file_path;
|
||||
load_file(new_file_path);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,11 +82,14 @@ class PlaylistView : public View {
|
||||
bool loop() const;
|
||||
void set_ready();
|
||||
void handle_replay_thread_done(const uint32_t return_code);
|
||||
void file_error();
|
||||
void file_error(std::string error_message);
|
||||
|
||||
std::filesystem::path file_path{};
|
||||
std::unique_ptr<ReplayThread> replay_thread{};
|
||||
bool ready_signal{false};
|
||||
int track_number{0};
|
||||
int total_tracks{0};
|
||||
std::filesystem::path now_play_list_file{};
|
||||
|
||||
Button button_open{
|
||||
{0 * 8, 0 * 16, 10 * 8, 2 * 16},
|
||||
@@ -125,6 +128,10 @@ class PlaylistView : public View {
|
||||
&bitmap_play,
|
||||
Color::green(),
|
||||
Color::black()};
|
||||
// TODO: add track number
|
||||
// Text text_track{
|
||||
// {18 * 8, 1 * 16, 12 * 8, 16},
|
||||
// "0/0"};
|
||||
|
||||
spectrum::WaterfallWidget waterfall{};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
@@ -744,9 +744,9 @@ void ScannerView::scan_resume() {
|
||||
}
|
||||
|
||||
void ScannerView::user_resume() {
|
||||
browse_timer = browse_wait * STATISTICS_UPDATES_PER_SEC; // Will trigger a scan_resume() on_statistics_update, also advancing to next freq.
|
||||
button_pause.set_text("<PAUSE>"); // Show button for pause, arrows indicate rotary encoder enabled for freq change
|
||||
userpause = false; // Resume scanning
|
||||
browse_timer = browse_wait * STATISTICS_UPDATES_PER_SEC + 1; // Will trigger a scan_resume() on_statistics_update, also advancing to next freq.
|
||||
button_pause.set_text("<PAUSE>"); // Show button for pause, arrows indicate rotary encoder enabled for freq change
|
||||
userpause = false; // Resume scanning
|
||||
}
|
||||
|
||||
void ScannerView::on_headphone_volume_changed(int32_t v) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_text_editor.hpp"
|
||||
|
||||
#include "log_file.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
@@ -31,29 +33,256 @@ template <typename T>
|
||||
T mid(const T& val1, const T& val2, const T& val3) {
|
||||
return std::max(val1, std::min(val2, val3));
|
||||
}
|
||||
|
||||
/*void log(const std::string& msg) {
|
||||
LogFile log{};
|
||||
log.append("LOGS/Notepad.txt");
|
||||
log.write_entry(msg);
|
||||
}*/
|
||||
} // namespace
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* FileWrapper ******************************************************/
|
||||
|
||||
FileWrapper::FileWrapper() {
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Error> FileWrapper::open(const fs::path& path) {
|
||||
file_ = File();
|
||||
auto result = file_.open(path);
|
||||
|
||||
if (!result.is_valid()) // No error
|
||||
initialize();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string FileWrapper::get_text(Offset line, Offset col, Offset length) {
|
||||
// TODO: better way to return errors.
|
||||
auto range = line_range(line);
|
||||
int32_t to_read = length;
|
||||
|
||||
if (!range.is_valid())
|
||||
return "[UNCACHED LINE]";
|
||||
|
||||
// Don't read past end of line.
|
||||
if (range->start + col + to_read >= range->end)
|
||||
to_read = range->end - col - range->start;
|
||||
|
||||
if (to_read <= 0)
|
||||
return {};
|
||||
|
||||
return read(range->start + col, to_read);
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Range> FileWrapper::line_range(Line line) {
|
||||
ensure_cached(line);
|
||||
|
||||
auto offset = offset_for_line(line);
|
||||
if (!offset.is_valid())
|
||||
return {};
|
||||
|
||||
auto start = *offset == 0 ? start_offset_ : (newlines_[*offset - 1] + 1);
|
||||
auto end = newlines_[*offset] + 1;
|
||||
|
||||
return {Range{start, end}};
|
||||
}
|
||||
|
||||
FileWrapper::Offset FileWrapper::line_length(Line line) {
|
||||
auto range = line_range(line);
|
||||
|
||||
if (range.is_valid())
|
||||
return range->end - range->start;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void FileWrapper::initialize() {
|
||||
start_offset_ = 0;
|
||||
start_line_ = 0;
|
||||
line_count_ = 0;
|
||||
newlines_.clear();
|
||||
line_ending_ = LineEnding::LF;
|
||||
|
||||
Offset offset = 0;
|
||||
auto result = next_newline(offset);
|
||||
|
||||
while (result.is_valid()) {
|
||||
++line_count_;
|
||||
if (newlines_.size() < max_newlines)
|
||||
newlines_.push_back(*result);
|
||||
offset = *result + 1;
|
||||
result = next_newline(offset);
|
||||
}
|
||||
}
|
||||
|
||||
std::string FileWrapper::read(Offset offset, Offset length) {
|
||||
// TODO: better way to return errors.
|
||||
if (offset + length > file_.size())
|
||||
return {"[BAD OFFSET]"};
|
||||
|
||||
std::string buffer(length, '\0');
|
||||
file_.seek(offset);
|
||||
|
||||
auto result = file_.read(&buffer[0], length);
|
||||
if (result.is_ok())
|
||||
buffer.resize(*result);
|
||||
else
|
||||
return result.error().what();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Offset> FileWrapper::offset_for_line(Line line) const {
|
||||
if (line >= line_count_)
|
||||
return {};
|
||||
|
||||
Offset actual = line - start_line_;
|
||||
if (actual < newlines_.size()) // NB: underflow wrap.
|
||||
return {actual};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void FileWrapper::ensure_cached(Line line) {
|
||||
if (line >= line_count_)
|
||||
return;
|
||||
|
||||
auto result = offset_for_line(line);
|
||||
if (result.is_valid())
|
||||
return;
|
||||
|
||||
if (line < start_line_) {
|
||||
while (line < start_line_ && start_offset_ >= 2) {
|
||||
// start_offset_ - 1 should be a newline. Need to
|
||||
// find the new value for start_offset_. start_line_
|
||||
// has to be > 0 to get into this block so there should
|
||||
// always be one newline before start_offset_.
|
||||
auto offset = previous_newline(start_offset_ - 2);
|
||||
newlines_.push_front(start_offset_ - 1);
|
||||
|
||||
if (!offset.is_valid()) {
|
||||
// Must be at beginning.
|
||||
start_line_ = 0;
|
||||
start_offset_ = 0;
|
||||
} else {
|
||||
// Found an previous newline, the new start_line_
|
||||
// starts at the newline offset + 1.
|
||||
start_line_--;
|
||||
start_offset_ = *offset + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (line >= start_line_ + newlines_.size()) {
|
||||
auto offset = next_newline(newlines_.back() + 1);
|
||||
if (offset.is_valid()) {
|
||||
start_line_++;
|
||||
start_offset_ = newlines_.front() + 1;
|
||||
newlines_.push_back(*offset);
|
||||
} /* else at the EOF. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Offset> FileWrapper::previous_newline(Offset start) {
|
||||
constexpr size_t buffer_size = 128;
|
||||
char buffer[buffer_size];
|
||||
Offset offset = start;
|
||||
auto to_read = buffer_size;
|
||||
|
||||
do {
|
||||
if (offset < to_read) {
|
||||
// NB: Char at 'offset' was read in the previous iteration.
|
||||
to_read = offset;
|
||||
offset = 0;
|
||||
} else
|
||||
offset -= to_read;
|
||||
|
||||
file_.seek(offset);
|
||||
|
||||
auto result = file_.read(buffer, to_read);
|
||||
if (result.is_error())
|
||||
break;
|
||||
|
||||
// Find newlines in the buffer backwards.
|
||||
for (int32_t i = *result - 1; i >= 0; --i) {
|
||||
switch (buffer[i]) {
|
||||
case '\n':
|
||||
return {offset + i};
|
||||
}
|
||||
}
|
||||
|
||||
if (offset == 0)
|
||||
break;
|
||||
|
||||
} while (true);
|
||||
|
||||
return {}; // Didn't find one.
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Offset> FileWrapper::next_newline(Offset start) {
|
||||
constexpr size_t buffer_size = 128;
|
||||
char buffer[buffer_size];
|
||||
Offset offset = start;
|
||||
|
||||
// EOF, nothing to do.
|
||||
if (start >= size())
|
||||
return {};
|
||||
|
||||
file_.seek(offset);
|
||||
|
||||
while (true) {
|
||||
auto result = file_.read(buffer, buffer_size);
|
||||
if (result.is_error())
|
||||
return {};
|
||||
|
||||
// Find newlines in the buffer.
|
||||
for (Offset i = 0; i < *result; ++i) {
|
||||
switch (buffer[i]) {
|
||||
case '\n':
|
||||
return {offset + i};
|
||||
}
|
||||
}
|
||||
|
||||
offset += *result;
|
||||
|
||||
if (*result < buffer_size)
|
||||
break;
|
||||
}
|
||||
|
||||
// Fake a newline at the end for consistency.
|
||||
return {offset};
|
||||
}
|
||||
|
||||
/* TextEditorView ***************************************************/
|
||||
|
||||
TextEditorView::TextEditorView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children(
|
||||
{&button_open,
|
||||
&text_position});
|
||||
{
|
||||
&button_open,
|
||||
&text_position,
|
||||
&text_size,
|
||||
});
|
||||
set_focusable(true);
|
||||
|
||||
// log_.append("LOGS/NOTEPAD.TXT");
|
||||
|
||||
button_open.on_select = [this](Button&) {
|
||||
auto open_view = nav_.push<FileLoadView>(".TXT");
|
||||
auto open_view = nav_.push<FileLoadView>("");
|
||||
open_view->on_changed = [this](std::filesystem::path path) {
|
||||
open_file(path);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
TextEditorView::TextEditorView(NavigationView& nav, const fs::path& path)
|
||||
: TextEditorView(nav) {
|
||||
open_file(path);
|
||||
}
|
||||
|
||||
void TextEditorView::on_focus() {
|
||||
refresh_ui();
|
||||
button_open.focus();
|
||||
}
|
||||
|
||||
void TextEditorView::paint(Painter& painter) {
|
||||
@@ -63,16 +292,19 @@ void TextEditorView::paint(Painter& painter) {
|
||||
if (!paint_state_.has_file)
|
||||
return;
|
||||
|
||||
// Move the viewport vertically.
|
||||
if (cursor_.line < first_line)
|
||||
first_line = cursor_.line;
|
||||
else if (cursor_.line >= first_line + max_line)
|
||||
first_line = cursor_.line - max_line + 1;
|
||||
|
||||
// Move the viewport horizontally.
|
||||
if (cursor_.col < first_col)
|
||||
first_col = cursor_.col;
|
||||
if (cursor_.col >= first_col + max_col)
|
||||
first_col = cursor_.col - max_col + 1;
|
||||
|
||||
// Viewport updated? Redraw text.
|
||||
if (first_line != paint_state_.first_line ||
|
||||
first_col != paint_state_.first_col) {
|
||||
paint_state_.first_line = first_line;
|
||||
@@ -129,28 +361,41 @@ bool TextEditorView::on_encoder(EncoderEvent delta) {
|
||||
bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) {
|
||||
int32_t new_line = cursor_.line + delta_line;
|
||||
int32_t new_col = cursor_.col + delta_col;
|
||||
|
||||
auto new_line_length = info_.line_length(new_line);
|
||||
int32_t new_line_length = file_.line_length(new_line);
|
||||
|
||||
if (new_col < 0)
|
||||
--new_line;
|
||||
else if (new_col > new_line_length && delta_line == 0) {
|
||||
// Only want to wrap if moving horizontally.
|
||||
else if (new_col >= new_line_length && delta_line == 0) {
|
||||
// Only wrap if moving horizontally.
|
||||
new_col = 0;
|
||||
++new_line;
|
||||
}
|
||||
|
||||
if (new_line < 0 || (uint32_t)new_line >= info_.line_count())
|
||||
// Snap to first/last to make navigating easier.
|
||||
if (new_line < 0 && new_col > 0) {
|
||||
new_line = 0;
|
||||
new_col = 0;
|
||||
} else if (new_line >= (int32_t)file_.line_count()) {
|
||||
auto last_line = file_.line_count() - 1;
|
||||
int32_t last_col = file_.line_length(last_line) - 1;
|
||||
|
||||
if (new_col < last_col) {
|
||||
new_line = last_line;
|
||||
new_col = last_col;
|
||||
}
|
||||
}
|
||||
|
||||
if (new_line < 0 || (uint32_t)new_line >= file_.line_count())
|
||||
return false;
|
||||
|
||||
new_line_length = info_.line_length(new_line);
|
||||
new_line_length = file_.line_length(new_line);
|
||||
|
||||
// TODO: don't wrap with encoder?
|
||||
// Wrap or clamp column.
|
||||
if (new_line_length == 0)
|
||||
new_col = 0;
|
||||
else if (new_col > new_line_length || new_col < 0)
|
||||
new_col = new_line_length;
|
||||
else if (new_col >= new_line_length || new_col < 0)
|
||||
new_col = new_line_length - 1;
|
||||
|
||||
cursor_.line = new_line;
|
||||
cursor_.col = new_col;
|
||||
@@ -161,134 +406,65 @@ bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t del
|
||||
void TextEditorView::refresh_ui() {
|
||||
if (paint_state_.has_file) {
|
||||
text_position.set(
|
||||
to_string_dec_uint(cursor_.col + 1) + ":" +
|
||||
to_string_dec_uint(cursor_.line + 1) + "/" +
|
||||
to_string_dec_uint(info_.line_count()) +
|
||||
(info_.truncated ? "*" : "") +
|
||||
" Size: " +
|
||||
to_string_file_size(info_.size));
|
||||
focus();
|
||||
"Ln " + to_string_dec_uint(cursor_.line + 1) +
|
||||
", Col " + to_string_dec_uint(cursor_.col + 1));
|
||||
text_size.set(
|
||||
"Lines:" + to_string_dec_uint(file_.line_count()) +
|
||||
" (" + to_string_file_size(file_.size()) + ")");
|
||||
} else {
|
||||
button_open.focus();
|
||||
// if (!button_open.has_focus())
|
||||
// button_open.focus();
|
||||
text_position.set("");
|
||||
text_size.set("");
|
||||
}
|
||||
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void TextEditorView::refresh_file_info() {
|
||||
constexpr size_t buffer_size = 128;
|
||||
char buffer[buffer_size];
|
||||
uint32_t base_offset = 0;
|
||||
|
||||
file_.seek(0);
|
||||
info_.newlines.clear();
|
||||
info_.line_ending = LineEnding::LF;
|
||||
info_.size = file_.size();
|
||||
info_.truncated = false;
|
||||
|
||||
while (true) {
|
||||
auto result = file_.read(buffer, buffer_size);
|
||||
if (result.is_error())
|
||||
break; // TODO: report error?
|
||||
|
||||
// TODO: CRLF state machine for cross block.
|
||||
for (uint32_t i = 0; i < result.value(); ++i) {
|
||||
switch (buffer[i]) {
|
||||
case '\n':
|
||||
info_.newlines.push_back(base_offset + i);
|
||||
}
|
||||
}
|
||||
|
||||
base_offset += result.value();
|
||||
|
||||
// Fake a newline at the end for consistency.
|
||||
// Could check if there already is a trailing newline, but it doesn't hurt.
|
||||
if (result.value() < buffer_size) {
|
||||
info_.newlines.push_back(base_offset);
|
||||
break;
|
||||
}
|
||||
|
||||
// HACK HACK: only show first 1000 lines for now.
|
||||
if (info_.newlines.size() >= 1000) {
|
||||
info_.truncated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
void TextEditorView::open_file(const fs::path& path) {
|
||||
// TODO: need a temp backing file for edits.
|
||||
|
||||
auto result = file_.open(path);
|
||||
paint_state_.has_file = !result.is_valid(); /* Has an error. */
|
||||
|
||||
if (paint_state_.has_file) {
|
||||
refresh_file_info();
|
||||
paint_state_.first_line = 0;
|
||||
paint_state_.first_col = 0;
|
||||
cursor_.line = 0;
|
||||
cursor_.col = 0;
|
||||
} else {
|
||||
nav_.display_modal("Read Error", "Cannot open file:\n" + result.value().what());
|
||||
paint_state_.has_file = false;
|
||||
}
|
||||
if (result.is_valid())
|
||||
nav_.display_modal("Read Error", "Cannot open file:\n" + result->what());
|
||||
|
||||
paint_state_.has_file = !result.is_valid(); // Has an error.
|
||||
paint_state_.first_line = 0;
|
||||
paint_state_.first_col = 0;
|
||||
cursor_.line = 0;
|
||||
cursor_.col = 0;
|
||||
|
||||
paint_state_.redraw_text = true;
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
std::string TextEditorView::read(uint32_t offset, uint32_t length) {
|
||||
if (offset >= info_.size)
|
||||
return {"[BAD OFFSET]"};
|
||||
|
||||
std::string buffer(length + 1, '\0');
|
||||
file_.seek(offset);
|
||||
|
||||
auto result = file_.read(&buffer[0], length);
|
||||
if (result.is_ok())
|
||||
/* resize? */;
|
||||
else
|
||||
return result.error().what();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void TextEditorView::paint_text(Painter& painter, uint32_t line, uint16_t col) {
|
||||
// TODO: A line cache would use more memory but save a lot of IO.
|
||||
// Only the new lines/characters would need to be refetched.
|
||||
|
||||
auto r = screen_rect();
|
||||
auto& lines = info_.newlines;
|
||||
auto line_start = info_.line_start(line);
|
||||
|
||||
// Draw the lines from the file
|
||||
for (uint32_t i = 0; i < max_line && i < lines.size(); ++i) {
|
||||
auto line_end = lines[line + i];
|
||||
int32_t read_length = max_col;
|
||||
for (auto i = 0u; i < max_line; ++i) {
|
||||
if (line + i >= file_.line_count())
|
||||
break;
|
||||
|
||||
// Don't read past end of the line.
|
||||
if (line_start + col + (uint32_t)read_length > line_end)
|
||||
read_length = line_end - col - line_start;
|
||||
auto str = file_.get_text(line + i, col, max_col);
|
||||
|
||||
if (read_length > 0)
|
||||
// Draw text.
|
||||
if (str.length() > 0)
|
||||
painter.draw_string(
|
||||
{0, r.location().y() + (int)i * char_height},
|
||||
style_default, read(line_start + col, read_length));
|
||||
style_default, str);
|
||||
|
||||
// Erase empty line sectons.
|
||||
if (read_length >= 0) {
|
||||
int32_t clear_width = max_col - read_length;
|
||||
if (clear_width > 0)
|
||||
painter.fill_rectangle(
|
||||
{(max_col - clear_width) * char_width,
|
||||
r.location().y() + (int)i * char_height,
|
||||
clear_width * char_width, char_height},
|
||||
style_default.background);
|
||||
}
|
||||
|
||||
line_start = lines[line + i] + 1 /* newline */;
|
||||
// Clear empty line sections.
|
||||
int32_t clear_width = max_col - str.length();
|
||||
if (clear_width > 0)
|
||||
painter.fill_rectangle(
|
||||
{(max_col - clear_width) * char_width,
|
||||
r.location().y() + (int)i * char_height,
|
||||
clear_width * char_width, char_height},
|
||||
style_default.background);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,9 +481,7 @@ void TextEditorView::paint_cursor(Painter& painter) {
|
||||
c);
|
||||
};
|
||||
|
||||
// TOOD: bug where cursor doesn't clear at EOF.
|
||||
// TODO: XOR cursor?
|
||||
|
||||
// Clear old cursor.
|
||||
draw_cursor(paint_state_.line, paint_state_.col, style_default.background);
|
||||
draw_cursor(cursor_.line, cursor_.col, style_default.foreground);
|
||||
@@ -315,8 +489,8 @@ void TextEditorView::paint_cursor(Painter& painter) {
|
||||
paint_state_.col = cursor_.col;
|
||||
}
|
||||
|
||||
uint16_t TextEditorView::line_length() const {
|
||||
return info_.line_length(cursor_.line);
|
||||
uint16_t TextEditorView::line_length() {
|
||||
return file_.line_length(cursor_.line);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
@@ -29,8 +29,9 @@
|
||||
#include "ui_widget.hpp"
|
||||
//#include "ui_textentry.hpp"
|
||||
|
||||
#include "circular_buffer.hpp"
|
||||
#include "file.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -47,44 +48,71 @@ enum class ScrollDirection : uint8_t {
|
||||
Horizontal
|
||||
};
|
||||
|
||||
// TODO: RAM is _very_ limited. Need to
|
||||
// rework this to not store every line.
|
||||
// Should be able to get away with only
|
||||
// abount one screen of lines so long as
|
||||
// you can't scroll more than one screen
|
||||
// at a time.
|
||||
struct FileInfo {
|
||||
/* Offsets of newlines. */
|
||||
std::vector<uint32_t> newlines;
|
||||
LineEnding line_ending;
|
||||
File::Size size;
|
||||
bool truncated;
|
||||
/* Wraps a file and provides an API for accessing lines efficiently. */
|
||||
class FileWrapper {
|
||||
public:
|
||||
using Error = std::filesystem::filesystem_error;
|
||||
using Offset = uint32_t; // TODO: make enums?
|
||||
using Line = uint32_t;
|
||||
using Column = uint32_t;
|
||||
using Range = struct {
|
||||
// Offset of the line start.
|
||||
Offset start;
|
||||
// Offset of one past the line end.
|
||||
Offset end;
|
||||
};
|
||||
|
||||
uint32_t line_count() const {
|
||||
return newlines.size();
|
||||
}
|
||||
FileWrapper();
|
||||
|
||||
uint32_t line_start(uint32_t line) const {
|
||||
return line == 0 ? 0 : (newlines[line - 1] + 1);
|
||||
}
|
||||
/* Prevent copies. */
|
||||
FileWrapper(const FileWrapper&) = delete;
|
||||
FileWrapper& operator=(const FileWrapper&) = delete;
|
||||
|
||||
uint16_t line_length(uint32_t line) const {
|
||||
if (line >= line_count())
|
||||
return 0;
|
||||
Optional<Error> open(const std::filesystem::path& path);
|
||||
std::string get_text(Line line, Column col, Offset length);
|
||||
|
||||
auto start = line_start(line);
|
||||
auto end = newlines[line];
|
||||
return end - start;
|
||||
}
|
||||
File::Size size() const { return file_.size(); }
|
||||
uint32_t line_count() const { return line_count_; }
|
||||
|
||||
Optional<Range> line_range(Line line);
|
||||
Offset line_length(Line line);
|
||||
|
||||
private:
|
||||
/* Number of newline offsets to cache. */
|
||||
static constexpr Offset max_newlines = 64;
|
||||
|
||||
void initialize();
|
||||
std::string read(Offset offset, Offset length = 30);
|
||||
|
||||
/* Returns the offset into the newline cache if valid. */
|
||||
Optional<Offset> offset_for_line(Line line) const;
|
||||
|
||||
/* Ensure specified line is in the newline cache. */
|
||||
void ensure_cached(Line line);
|
||||
|
||||
/* Helpers for finding the prev/next newline. */
|
||||
Optional<Offset> previous_newline(Offset start);
|
||||
Optional<Offset> next_newline(Offset start);
|
||||
|
||||
File file_{};
|
||||
|
||||
/* Total number of lines in the file. */
|
||||
Offset line_count_{0};
|
||||
|
||||
/* The offset and line of the newlines cache. */
|
||||
Offset start_offset_{0};
|
||||
Offset start_line_{0};
|
||||
|
||||
LineEnding line_ending_{LineEnding::LF};
|
||||
CircularBuffer<Offset, max_newlines + 1> newlines_{};
|
||||
};
|
||||
|
||||
/*class TextViewer : public Widget {
|
||||
};*/
|
||||
|
||||
class TextEditorView : public View {
|
||||
public:
|
||||
TextEditorView(NavigationView& nav);
|
||||
// TextEditorView(NavigationView& nav, const std::filesystem::path& path);
|
||||
TextEditorView(
|
||||
NavigationView& nav,
|
||||
const std::filesystem::path& path);
|
||||
|
||||
std::string title() const override {
|
||||
return "Notepad";
|
||||
@@ -101,7 +129,6 @@ class TextEditorView : public View {
|
||||
static constexpr int8_t char_width = 5;
|
||||
static constexpr int8_t char_height = 8;
|
||||
|
||||
// TODO: should these be common somewhere?
|
||||
static constexpr Style style_default{
|
||||
.font = font::fixed_5x8,
|
||||
.background = Color::black(),
|
||||
@@ -114,21 +141,17 @@ class TextEditorView : public View {
|
||||
int16_t delta_col);
|
||||
|
||||
void refresh_ui();
|
||||
void refresh_file_info();
|
||||
void open_file(const std::filesystem::path& path);
|
||||
std::string read(uint32_t offset, uint32_t length = 30);
|
||||
|
||||
void paint_text(Painter& painter, uint32_t line, uint16_t col);
|
||||
void paint_cursor(Painter& painter);
|
||||
|
||||
// Gets the length of the current line.
|
||||
uint16_t line_length() const;
|
||||
uint16_t line_length();
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
File file_{};
|
||||
FileInfo info_{};
|
||||
// LogFile log_{ };
|
||||
FileWrapper file_{};
|
||||
|
||||
struct {
|
||||
// Previous cursor state.
|
||||
@@ -148,11 +171,6 @@ class TextEditorView : public View {
|
||||
ScrollDirection dir{ScrollDirection::Vertical};
|
||||
} cursor_{};
|
||||
|
||||
/* 8px grid is 30 wide, 38 tall. */
|
||||
/* 16px font height or 19 rows. */
|
||||
/* Titlebar is 16px tall, so 18 rows left. */
|
||||
/* 240 x 320, (304 with titlebar) */
|
||||
|
||||
// TODO: The scrollable view should be its own widget
|
||||
// otherwise control navigation doesn't work.
|
||||
|
||||
@@ -161,6 +179,10 @@ class TextEditorView : public View {
|
||||
"Open"};
|
||||
|
||||
Text text_position{
|
||||
{0 * 8, 34 * 8, 24 * 8, 2 * 8},
|
||||
""};
|
||||
|
||||
Text text_size{
|
||||
{0 * 8, 36 * 8, 24 * 8, 2 * 8},
|
||||
""};
|
||||
};
|
||||
|
||||
@@ -97,8 +97,8 @@ File::Result<File::Offset> File::seek(const Offset new_position) {
|
||||
return {static_cast<File::Offset>(old_position)};
|
||||
}
|
||||
|
||||
File::Size File::size() {
|
||||
return {static_cast<File::Size>(f_size(&f))};
|
||||
File::Size File::size() const {
|
||||
return f_size(&f);
|
||||
}
|
||||
|
||||
Optional<File::Error> File::write_line(const std::string& s) {
|
||||
|
||||
@@ -311,6 +311,10 @@ class File {
|
||||
return value_;
|
||||
}
|
||||
|
||||
const T& operator*() const& {
|
||||
return value_;
|
||||
}
|
||||
|
||||
Error error() const {
|
||||
return error_;
|
||||
}
|
||||
@@ -339,6 +343,9 @@ class File {
|
||||
File(){};
|
||||
~File();
|
||||
|
||||
File(File&&) = default;
|
||||
File& operator=(File&&) = default;
|
||||
|
||||
/* Prevent copies */
|
||||
File(const File&) = delete;
|
||||
File& operator=(const File&) = delete;
|
||||
@@ -352,8 +359,8 @@ class File {
|
||||
Result<Size> write(const void* const data, const Size bytes_to_write);
|
||||
|
||||
Result<Offset> seek(const uint64_t Offset);
|
||||
Timestamp created_date();
|
||||
Size size();
|
||||
// Timestamp created_date() const;
|
||||
Size size() const;
|
||||
|
||||
template <size_t N>
|
||||
Result<Size> write(const std::array<uint8_t, N>& data) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -315,7 +316,6 @@ bool save_freqman_file(std::string& file_stem, freqman_db& db) {
|
||||
auto& entry = db[n];
|
||||
get_freq_string(entry, item_string);
|
||||
freqman_file.write_line(item_string);
|
||||
delete &item_string;
|
||||
}
|
||||
delete_file(freq_file_path);
|
||||
rename_file(tmp_freq_file_path, freq_file_path);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
@@ -27,11 +27,28 @@
|
||||
bool Debounce::feed(const uint8_t bit) {
|
||||
history_ = (history_ << 1) | (bit & 1);
|
||||
|
||||
// "Repeat" handling - simulated button release
|
||||
if (repeat_ctr_) {
|
||||
// Make sure the button is still being held continuously
|
||||
if (history_ == 0xFF) {
|
||||
// Simulate button press every REPEAT_SUBSEQUENT_DELAY ticks
|
||||
if (--repeat_ctr_ == 0) {
|
||||
state_ = !state_;
|
||||
repeat_ctr_ = REPEAT_SUBSEQUENT_DELAY / 2;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// It's a real button release; stop simulating
|
||||
repeat_ctr_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (state_ == 0) {
|
||||
// Previous button state was 0 (released);
|
||||
// Has button been held for DEBOUNCE_COUNT ticks?
|
||||
if ((history_ & DEBOUNCE_MASK) == DEBOUNCE_MASK) {
|
||||
state_ = 1;
|
||||
held_time_ = 0;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
@@ -41,6 +58,21 @@ bool Debounce::feed(const uint8_t bit) {
|
||||
state_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Repeat support is limited to the 4 directional buttons
|
||||
if (repeat_enabled_) {
|
||||
// Has button been held continuously for DEBOUNCE_REPEAT_DELAY?
|
||||
if (history_ == 0xFF) {
|
||||
if (++held_time_ == REPEAT_INITIAL_DELAY) {
|
||||
// Delay reached; trigger repeat code on NEXT tick
|
||||
repeat_ctr_ = 1;
|
||||
held_time_ = 0;
|
||||
}
|
||||
} else {
|
||||
// Button not continuously pressed; reset counter
|
||||
held_time_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
#define DEBOUNCE_COUNT 4
|
||||
#define DEBOUNCE_MASK ((1 << DEBOUNCE_COUNT) - 1)
|
||||
|
||||
// # of timer0 ticks before a held button starts being counted as repeated presses
|
||||
#define REPEAT_INITIAL_DELAY 250
|
||||
#define REPEAT_SUBSEQUENT_DELAY 92
|
||||
|
||||
class Debounce {
|
||||
public:
|
||||
bool feed(const uint8_t bit);
|
||||
@@ -36,9 +40,16 @@ class Debounce {
|
||||
return state_;
|
||||
}
|
||||
|
||||
void enable_repeat() {
|
||||
repeat_enabled_ = true;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t history_{0};
|
||||
uint8_t state_{0};
|
||||
bool repeat_enabled_{0};
|
||||
uint16_t repeat_ctr_{0};
|
||||
uint16_t held_time_{0};
|
||||
};
|
||||
|
||||
#endif /*__DEBOUNCE_H__*/
|
||||
|
||||
@@ -187,6 +187,10 @@ void controls_init() {
|
||||
*/
|
||||
gptStart(&GPTD1, &timer0_config);
|
||||
gptStartContinuous(&GPTD1, timer0_match_count);
|
||||
|
||||
// Enable repeat for directional switches only
|
||||
for (size_t i = 0; i < 4; i++)
|
||||
switch_debounce[i].enable_repeat();
|
||||
}
|
||||
|
||||
SwitchesState get_switches_state() {
|
||||
|
||||
@@ -338,11 +338,11 @@ const uint8_t fixed_5x8_glyph_data[] = {
|
||||
0x02,
|
||||
|
||||
// Index: 44 (0x2C) Char: 0x004C ('L')
|
||||
0x20,
|
||||
0x40,
|
||||
0x08,
|
||||
0x21,
|
||||
0x84,
|
||||
0x10,
|
||||
0xC2,
|
||||
0x01,
|
||||
0x03,
|
||||
|
||||
// Index: 45 (0x2D) Char: 0x004D ('M')
|
||||
0xA0,
|
||||
@@ -654,8 +654,8 @@ const uint8_t fixed_5x8_glyph_data[] = {
|
||||
|
||||
// Index: 89 (0x59) Char: 0x0079 ('y')
|
||||
0x00,
|
||||
0x00,
|
||||
0xE5,
|
||||
0x80,
|
||||
0xE4,
|
||||
0x90,
|
||||
0x01,
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
#include "ui_navigation.hpp"
|
||||
|
||||
//#include "modules.h"
|
||||
// #include "modules.h"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "event_m0.hpp"
|
||||
@@ -46,13 +46,13 @@
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "ui_jammer.hpp"
|
||||
//#include "ui_keyfob.hpp"
|
||||
// #include "ui_keyfob.hpp"
|
||||
#include "ui_lcr.hpp"
|
||||
#include "ui_mictx.hpp"
|
||||
#include "ui_morse.hpp"
|
||||
//#include "ui_numbers.hpp"
|
||||
//#include "ui_nuoptix.hpp"
|
||||
//#include "ui_playdead.hpp"
|
||||
// #include "ui_numbers.hpp"
|
||||
// #include "ui_nuoptix.hpp"
|
||||
// #include "ui_playdead.hpp"
|
||||
#include "ui_pocsag_tx.hpp"
|
||||
#include "ui_rds.hpp"
|
||||
#include "ui_remote.hpp"
|
||||
@@ -65,7 +65,7 @@
|
||||
#include "ui_siggen.hpp"
|
||||
#include "ui_sonde.hpp"
|
||||
#include "ui_sstvtx.hpp"
|
||||
//#include "ui_test.hpp"
|
||||
// #include "ui_test.hpp"
|
||||
#include "ui_text_editor.hpp"
|
||||
#include "ui_tone_search.hpp"
|
||||
#include "ui_touchtunes.hpp"
|
||||
@@ -76,7 +76,7 @@
|
||||
#include "ui_sd_over_usb.hpp"
|
||||
#include "ui_spectrum_painter.hpp"
|
||||
|
||||
//#include "acars_app.hpp"
|
||||
// #include "acars_app.hpp"
|
||||
#include "ais_app.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
#include "analog_tv_app.hpp"
|
||||
@@ -563,7 +563,7 @@ TransmittersMenuView::TransmittersMenuView(NavigationView& nav) {
|
||||
{"SSTV", ui::Color::green(), &bitmap_icon_sstv, [&nav]() { nav.push<SSTVTXView>(); }},
|
||||
{"TEDI/LCR", ui::Color::yellow(), &bitmap_icon_lcr, [&nav]() { nav.push<LCRView>(); }},
|
||||
{"TouchTune", ui::Color::yellow(), &bitmap_icon_remote, [&nav]() { nav.push<TouchTunesView>(); }},
|
||||
{"Playlist", ui::Color::yellow(), &bitmap_icon_remote, [&nav]() { nav.push<PlaylistView>(); }},
|
||||
{"Playlist", ui::Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<PlaylistView>(); }},
|
||||
{"S.Painter", ui::Color::orange(), &bitmap_icon_morse, [&nav]() { nav.push<SpectrumPainterView>(); }},
|
||||
//{ "Remote", ui::Color::dark_grey(), &bitmap_icon_remote, [&nav](){ nav.push<RemoteView>(); } },
|
||||
});
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __CIRCULAR_BUFFER_H__
|
||||
#define __CIRCULAR_BUFFER_H__
|
||||
|
||||
#include <stddef.h> // For size_t
|
||||
#include <memory>
|
||||
|
||||
/* Implements a fixed-size, circular buffer.
|
||||
* NB: Holds Capacity - 1 items.
|
||||
* There are no bounds checks on accessors so ensure there are
|
||||
* items in the buffer before accessing front/back/operator[]. */
|
||||
template <typename T, size_t Capacity>
|
||||
class CircularBuffer {
|
||||
public:
|
||||
CircularBuffer() = default;
|
||||
|
||||
CircularBuffer(const CircularBuffer&) = delete;
|
||||
CircularBuffer(CircularBuffer&&) = delete;
|
||||
CircularBuffer& operator=(const CircularBuffer&) = delete;
|
||||
CircularBuffer& operator=(CircularBuffer&&) = delete;
|
||||
|
||||
void push_front(T val) {
|
||||
head_ = head_ > 0 ? head_ - 1 : last_index;
|
||||
if (head_ == end_)
|
||||
pop_back_internal();
|
||||
|
||||
data_[head_] = std::move(val);
|
||||
}
|
||||
|
||||
void pop_front() {
|
||||
if (!empty())
|
||||
pop_front_internal();
|
||||
}
|
||||
|
||||
void push_back(T val) {
|
||||
data_[end_] = std::move(val);
|
||||
|
||||
end_ = end_ < last_index ? end_ + 1 : 0;
|
||||
if (head_ == end_)
|
||||
pop_front_internal();
|
||||
}
|
||||
|
||||
void pop_back() {
|
||||
if (!empty())
|
||||
pop_back_internal();
|
||||
}
|
||||
|
||||
T& operator[](size_t ix) & {
|
||||
ix += head_;
|
||||
if (ix >= Capacity)
|
||||
ix -= Capacity;
|
||||
return data_[ix];
|
||||
}
|
||||
|
||||
const T& operator[](size_t ix) const& {
|
||||
return const_cast<CircularBuffer*>(this)->operator[](ix);
|
||||
}
|
||||
|
||||
const T& front() const& {
|
||||
return data_[head_];
|
||||
}
|
||||
|
||||
const T& back() const& {
|
||||
auto end = end_ > 0 ? end_ - 1 : last_index;
|
||||
return data_[end];
|
||||
}
|
||||
|
||||
size_t size() const& {
|
||||
auto end = end_;
|
||||
if (end < head_)
|
||||
end += Capacity;
|
||||
return end - head_;
|
||||
}
|
||||
|
||||
bool empty() const {
|
||||
return head_ == end_;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
head_ = 0;
|
||||
end_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
void pop_front_internal() {
|
||||
head_ = head_ < last_index ? head_ + 1 : 0;
|
||||
}
|
||||
|
||||
void pop_back_internal() {
|
||||
end_ = end_ > 0 ? end_ - 1 : last_index;
|
||||
}
|
||||
|
||||
static constexpr size_t last_index = Capacity - 1;
|
||||
size_t head_{0};
|
||||
size_t end_{0};
|
||||
|
||||
T data_[Capacity]{};
|
||||
};
|
||||
|
||||
#endif /*__CIRCULAR_BUFFER_H__*/
|
||||
@@ -28,14 +28,27 @@ template <typename T>
|
||||
class Optional {
|
||||
public:
|
||||
constexpr Optional()
|
||||
: value_{}, valid_{false} {};
|
||||
: value_{}, valid_{false} {}
|
||||
constexpr Optional(const T& value)
|
||||
: value_{value}, valid_{true} {};
|
||||
: value_{value}, valid_{true} {}
|
||||
constexpr Optional(T&& value)
|
||||
: value_{std::move(value)}, valid_{true} {};
|
||||
: value_{std::move(value)}, valid_{true} {}
|
||||
|
||||
bool is_valid() const { return valid_; };
|
||||
const T& value() const { return value_; };
|
||||
bool is_valid() const { return valid_; }
|
||||
|
||||
// TODO: Throw if not valid?
|
||||
T& value() & { return value_; }
|
||||
T& operator*() & { return value_; }
|
||||
const T& value() const& { return value_; }
|
||||
const T& operator*() const& { return value_; }
|
||||
|
||||
T&& value() && { return value_; }
|
||||
T&& operator*() && { return value_; }
|
||||
const T&& value() const&& { return value_; }
|
||||
const T&& operator*() const&& { return value_; }
|
||||
|
||||
T* operator->() { return &value_; }
|
||||
const T* operator->() const { return &value_; }
|
||||
|
||||
private:
|
||||
T value_;
|
||||
|
||||
@@ -35,6 +35,7 @@ set(CMAKE_CXX_COMPILER g++)
|
||||
add_executable(application_test EXCLUDE_FROM_ALL
|
||||
${PROJECT_SOURCE_DIR}/main.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_basics.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_circular_buffer.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_optional.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright (C) 2023
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "doctest.h"
|
||||
#include "circular_buffer.hpp"
|
||||
|
||||
TEST_SUITE_BEGIN("circular buffer");
|
||||
|
||||
SCENARIO("Items can be pushed and popped from front and back.") {
|
||||
GIVEN("an empty buffer") {
|
||||
CircularBuffer<int, 5> cb;
|
||||
|
||||
REQUIRE(cb.empty());
|
||||
REQUIRE(cb.size() == 0);
|
||||
|
||||
WHEN("push_back()") {
|
||||
cb.push_back(1);
|
||||
|
||||
THEN("size should increase") {
|
||||
CHECK(cb.empty() == false);
|
||||
CHECK(cb.size() == 1);
|
||||
}
|
||||
|
||||
cb.push_back(2);
|
||||
|
||||
THEN("size should increase") {
|
||||
CHECK(cb.empty() == false);
|
||||
CHECK(cb.size() == 2);
|
||||
}
|
||||
|
||||
THEN("back() should be the last item pushed") {
|
||||
CHECK(cb.back() == 2);
|
||||
}
|
||||
|
||||
THEN("front() should be the first item pushed") {
|
||||
CHECK(cb.front() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("push_front()") {
|
||||
cb.push_front(3);
|
||||
|
||||
THEN("size should increase") {
|
||||
CHECK(cb.empty() == false);
|
||||
CHECK(cb.size() == 1);
|
||||
}
|
||||
|
||||
cb.push_front(4);
|
||||
|
||||
THEN("size should increase") {
|
||||
CHECK(cb.empty() == false);
|
||||
CHECK(cb.size() == 2);
|
||||
}
|
||||
|
||||
THEN("back() should be first item pushed") {
|
||||
CHECK(cb.back() == 3);
|
||||
}
|
||||
|
||||
THEN("front() should be the last item pushed") {
|
||||
CHECK(cb.front() == 4);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("pop_back()") {
|
||||
cb.pop_back();
|
||||
|
||||
THEN("size should not change") {
|
||||
REQUIRE(cb.empty());
|
||||
REQUIRE(cb.size() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("pop_front()") {
|
||||
cb.pop_back();
|
||||
|
||||
THEN("size should not change") {
|
||||
REQUIRE(cb.empty());
|
||||
REQUIRE(cb.size() == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("a buffer with items") {
|
||||
CircularBuffer<int, 5> cb;
|
||||
cb.push_back(1);
|
||||
cb.push_back(2);
|
||||
cb.push_back(3);
|
||||
|
||||
REQUIRE(!cb.empty());
|
||||
REQUIRE(cb.size() == 3);
|
||||
REQUIRE(cb.front() == 1);
|
||||
REQUIRE(cb.back() == 3);
|
||||
|
||||
WHEN("pop_back()") {
|
||||
cb.pop_back();
|
||||
|
||||
THEN("size should decrease") {
|
||||
CHECK(cb.empty() == false);
|
||||
CHECK(cb.size() == 2);
|
||||
}
|
||||
|
||||
THEN("back item should be removed") {
|
||||
CHECK(cb.back() == 2);
|
||||
}
|
||||
|
||||
THEN("front item should be unchanged") {
|
||||
CHECK(cb.front() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("pop_front()") {
|
||||
cb.pop_front();
|
||||
|
||||
THEN("size should decrease") {
|
||||
CHECK(cb.empty() == false);
|
||||
CHECK(cb.size() == 2);
|
||||
}
|
||||
|
||||
THEN("front item should be removed") {
|
||||
CHECK(cb.front() == 2);
|
||||
}
|
||||
|
||||
THEN("back item should be unchanged") {
|
||||
CHECK(cb.back() == 3);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("clear()") {
|
||||
cb.clear();
|
||||
|
||||
THEN("size should be empty") {
|
||||
CHECK(cb.empty());
|
||||
CHECK(cb.size() == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("a full buffer") {
|
||||
CircularBuffer<int, 4> cb;
|
||||
cb.push_back(1);
|
||||
cb.push_back(2);
|
||||
cb.push_back(3);
|
||||
|
||||
REQUIRE(!cb.empty());
|
||||
REQUIRE(cb.size() == 3);
|
||||
REQUIRE(cb.front() == 1);
|
||||
REQUIRE(cb.back() == 3);
|
||||
|
||||
WHEN("push_back()") {
|
||||
cb.push_back(4);
|
||||
|
||||
THEN("size should not be changed") {
|
||||
CHECK(!cb.empty());
|
||||
CHECK(cb.size() == 3);
|
||||
}
|
||||
|
||||
THEN("front should be popped") {
|
||||
CHECK(cb.front() == 2);
|
||||
}
|
||||
|
||||
THEN("back should be new value") {
|
||||
CHECK(cb.back() == 4);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("push_front()") {
|
||||
cb.push_front(4);
|
||||
|
||||
THEN("size should not be changed") {
|
||||
CHECK(!cb.empty());
|
||||
CHECK(cb.size() == 3);
|
||||
}
|
||||
|
||||
THEN("front should be new value") {
|
||||
CHECK(cb.front() == 4);
|
||||
}
|
||||
|
||||
THEN("back should be popped") {
|
||||
CHECK(cb.back() == 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Items can be accessed randomly") {
|
||||
GIVEN("buffer with items") {
|
||||
CircularBuffer<int, 4> cb;
|
||||
cb.push_front(1);
|
||||
cb.push_back(2);
|
||||
cb.push_back(3);
|
||||
|
||||
WHEN("accessing items") {
|
||||
THEN("front should be at 0") {
|
||||
CHECK(cb.front() == cb[0]);
|
||||
}
|
||||
|
||||
THEN("last should be at size() - 1") {
|
||||
CHECK(cb.back() == cb[cb.size() - 1]);
|
||||
}
|
||||
|
||||
THEN("all should be accessible") {
|
||||
// Assumes values are in order.
|
||||
for (size_t i = 0; i < cb.size(); i++)
|
||||
CHECK(cb[i] == i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("accessing items after push_front") {
|
||||
cb.push_front(4);
|
||||
|
||||
THEN("front should be at 0") {
|
||||
CHECK(cb.front() == cb[0]);
|
||||
}
|
||||
|
||||
THEN("last should be at size() - 1") {
|
||||
CHECK(cb.back() == cb[cb.size() - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("accessing items after push_back") {
|
||||
cb.push_back(4);
|
||||
|
||||
THEN("front should be at 0") {
|
||||
CHECK(cb.front() == cb[0]);
|
||||
}
|
||||
|
||||
THEN("last should be at size() - 1") {
|
||||
CHECK(cb.back() == cb[cb.size() - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
+1
-1
Submodule hackrf updated: ebe1ca003a...50c22aa05b
@@ -1,3 +1,2 @@
|
||||
##FREQ FILE SAMPLE RATE
|
||||
315000000,SAMPLES/TeslaChargePort_US.C16,500000
|
||||
433920000,SAMPLES/TeslaChargePort_EU_AU.C16,500000
|
||||
Reference in New Issue
Block a user