mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-18 13:44:00 +00:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f66f438487 | |||
| 2fd3bf0136 | |||
| c3264f0d15 | |||
| 8d7fdeb633 | |||
| 69011754c9 | |||
| e50d8dc148 | |||
| 23c24355ab | |||
| 0dcf2af50d | |||
| cf07428a5d | |||
| 35b381ae77 | |||
| 0fc748a234 | |||
| 638ea78db9 | |||
| e81747ef7b | |||
| 89e24cb358 | |||
| 00667cecf9 | |||
| 53fcdedb88 | |||
| 5bdf9363e9 | |||
| 6b44a77ef6 | |||
| edc6dc819c | |||
| ffd72eb232 | |||
| feee1f7ab3 | |||
| 2d79cd4b24 | |||
| 637bcfdbc7 | |||
| 22c9dd31bf | |||
| 9e63d80667 | |||
| 67b5b57533 | |||
| c2314f4838 | |||
| 1cb682473a | |||
| 63dd85cbfc | |||
| 6dbc8460b4 | |||
| ec8bf04baa | |||
| a1189faad1 | |||
| ba9cc55b5a | |||
| c2e527bce2 | |||
| 98f3bf151f | |||
| 802a4e243b | |||
| 215ad4fd4c | |||
| 7150d95a98 | |||
| 80636c53da | |||
| 874d3c251c | |||
| 6c541af0fd | |||
| 4d1269051b | |||
| be1b2716d6 | |||
| 34d46a9d5d | |||
| cac3a7cf1e | |||
| 0e86e63e97 | |||
| c0ff5715de | |||
| 24ce1b9893 | |||
| edd8e7c3b9 |
@@ -47,6 +47,12 @@ jobs:
|
||||
run: mkdir ${{ github.workspace }}/build
|
||||
- name: Run the Docker image
|
||||
run: docker run -e VERSION_STRING=${{ steps.version_date.outputs.date }} -i -v ${{ github.workspace }}:/havoc portapack-dev
|
||||
- name: Download world map
|
||||
run: |
|
||||
wget https://github.com/eried/portapack-mayhem/releases/download/world_map/world_map.zip
|
||||
- name: Unzip world map
|
||||
run: |
|
||||
unzip world_map.zip -d sdcard/ADSB
|
||||
- name: Create Firmware ZIP
|
||||
run: |
|
||||
zip -j firmware.zip build/firmware/portapack-h1_h2-mayhem.bin && cd flashing && zip -r ../firmware.zip *
|
||||
|
||||
@@ -31,6 +31,12 @@ jobs:
|
||||
run: mkdir ${{ github.workspace }}/build
|
||||
- name: Run the Docker image
|
||||
run: docker run -e VERSION_STRING=${{ steps.version.outputs.version }} -i -v ${{ github.workspace }}:/havoc portapack-dev
|
||||
- name: Download world map
|
||||
run: |
|
||||
wget https://github.com/eried/portapack-mayhem/releases/download/world_map/world_map.zip
|
||||
- name: Unzip world map
|
||||
run: |
|
||||
unzip world_map.zip -d sdcard/ADSB
|
||||
- name: Create Firmware ZIP
|
||||
run: |
|
||||
zip -j firmware.zip build/firmware/portapack-h1_h2-mayhem.bin && cd flashing && zip -r ../firmware.zip *
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.6.0
|
||||
v1.7.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.7.0
|
||||
v1.7.1
|
||||
|
||||
+2
-1
@@ -18,7 +18,7 @@
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
cmake_minimum_required(VERSION 2.8.9)
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
cmake_policy(SET CMP0005 NEW)
|
||||
|
||||
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_CURRENT_LIST_DIR}/firmware/toolchain-arm-cortex-m.cmake)
|
||||
@@ -58,4 +58,5 @@ set(HACKRF_CPLD_XSVF_PATH ${HACKRF_PATH}/firmware/cpld/sgpio_if/${HACKRF_CPLD_XS
|
||||
set(HACKRF_FIRMWARE_DFU_IMAGE ${hackrf_usb_BINARY_DIR}/${HACKRF_FIRMWARE_DFU_FILENAME})
|
||||
set(HACKRF_FIRMWARE_BIN_IMAGE ${hackrf_usb_BINARY_DIR}/${HACKRF_FIRMWARE_BIN_FILENAME})
|
||||
|
||||
enable_testing()
|
||||
add_subdirectory(firmware)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
@@ -290,8 +290,8 @@ to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{description}
|
||||
Copyright (C) {year} {fullname}
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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
|
||||
@@ -329,11 +329,11 @@ necessary. Here is a sample; alter the names:
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
{signature of Ty Coon}, 1 April 1989
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
Public License instead of this License.
|
||||
@@ -1,7 +0,0 @@
|
||||
# License
|
||||
Portapack Mayhem is distributed under [GPL v3.0](LICENSE), however some sub projects might have different (GPL v3.0 compatible) licenses.
|
||||
|
||||
## Other Licenses (compatible with GPL v3.0)
|
||||
- Most of Portapack Mayhem is distributed under GPL-2.0-or-later. [license](LICENSE.GPL-2.0-or-later)
|
||||
- ChibiOS is distributed under GPL v3.0. [license](/firmware/chibios/license.txt)
|
||||
- Portapack-ChibiOS is distributed under Apache-2.0 [license](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ set(FIRMWARE_FILENAME ${FIRMWARE_NAME}.bin)
|
||||
|
||||
add_subdirectory(application)
|
||||
add_subdirectory(baseband)
|
||||
add_subdirectory(test)
|
||||
|
||||
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
|
||||
add_custom_command(
|
||||
@@ -52,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}
|
||||
|
||||
@@ -218,6 +218,7 @@ set(CPPSRC
|
||||
ui/ui_btngrid.cpp
|
||||
ui/ui_receiver.cpp
|
||||
ui/ui_rssi.cpp
|
||||
ui/ui_freqlist.cpp
|
||||
ui/ui_tv.cpp
|
||||
ui/ui_spectrum.cpp
|
||||
ui/ui_tabview.cpp
|
||||
|
||||
@@ -395,7 +395,7 @@ AISAppView::AISAppView(NavigationView& nav)
|
||||
receiver_model.set_tuning_frequency(tuning_frequency());
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
receiver_model.enable(); // Before using radio::enable(), but not updating Ant.DC-Bias.
|
||||
receiver_model.enable();
|
||||
|
||||
options_channel.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
this->on_frequency_changed(v);
|
||||
@@ -420,7 +420,7 @@ AISAppView::~AISAppView() {
|
||||
app_settings.rx_frequency = target_frequency_;
|
||||
settings.save("rx_ais", &app_settings);
|
||||
|
||||
receiver_model.disable(); // to switch off all, including DC bias.
|
||||
receiver_model.disable();
|
||||
|
||||
baseband::shutdown();
|
||||
}
|
||||
@@ -471,7 +471,7 @@ void AISAppView::on_frequency_changed(const uint32_t new_target_frequency) {
|
||||
|
||||
void AISAppView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
radio::set_tuning_frequency(tuning_frequency());
|
||||
receiver_model.set_tuning_frequency(tuning_frequency());
|
||||
}
|
||||
|
||||
uint32_t AISAppView::target_frequency() const {
|
||||
|
||||
@@ -52,7 +52,7 @@ class AMOptionsView : public View {
|
||||
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
5,
|
||||
6, // number of blanking characters
|
||||
{
|
||||
// using common messages from freqman.cpp
|
||||
}};
|
||||
|
||||
@@ -121,17 +121,7 @@ ERTAppView::ERTAppView(NavigationView&) {
|
||||
receiver_model.set_tuning_frequency(initial_target_frequency);
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
receiver_model.enable(); // Before using radio::enable(), but not updating Ant.DC-Bias.
|
||||
|
||||
/* radio::enable({
|
||||
initial_target_frequency,
|
||||
sampling_rate,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Receive,
|
||||
receiver_model.rf_amp(),
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga()),
|
||||
}); */
|
||||
receiver_model.enable();
|
||||
|
||||
logger = std::make_unique<ERTLogger>();
|
||||
if (logger) {
|
||||
@@ -143,7 +133,7 @@ ERTAppView::~ERTAppView() {
|
||||
// save app settings
|
||||
settings.save("rx_ert", &app_settings);
|
||||
|
||||
receiver_model.disable(); // to switch off all, including DC bias and change flag enabled_
|
||||
receiver_model.disable();
|
||||
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
@@ -141,15 +141,9 @@ void GpsSimAppView::start() {
|
||||
});
|
||||
}
|
||||
|
||||
rf_amp = (transmitter_model.rf_amp()); // recover rf_amp settings applied from ui_transmiter.cpp
|
||||
|
||||
radio::enable({receiver_model.tuning_frequency(),
|
||||
sample_rate,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Transmit,
|
||||
rf_amp, // previous code line : "receiver_model.rf_amp()," was passing the same rf_amp of all Receiver Apps
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga())});
|
||||
transmitter_model.set_sampling_rate(sample_rate);
|
||||
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
transmitter_model.enable();
|
||||
}
|
||||
|
||||
void GpsSimAppView::stop(const bool do_loop) {
|
||||
@@ -159,7 +153,7 @@ void GpsSimAppView::stop(const bool do_loop) {
|
||||
if (do_loop && check_loop.value()) {
|
||||
start();
|
||||
} else {
|
||||
radio::disable();
|
||||
transmitter_model.disable();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
}
|
||||
|
||||
@@ -224,7 +218,7 @@ GpsSimAppView::GpsSimAppView(
|
||||
}
|
||||
|
||||
GpsSimAppView::~GpsSimAppView() {
|
||||
radio::disable();
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit .
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
}
|
||||
|
||||
@@ -141,18 +141,9 @@ void ReplayAppView::start() {
|
||||
});
|
||||
}
|
||||
|
||||
// Enable Bias Tee if selected
|
||||
radio::set_antenna_bias(portapack::get_antenna_bias());
|
||||
|
||||
rf_amp = (transmitter_model.rf_amp()); // recover rf_amp settings applied from ui_transmiter.cpp
|
||||
|
||||
radio::enable({receiver_model.tuning_frequency(),
|
||||
sample_rate * 8,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Transmit,
|
||||
rf_amp, // previous code line : "receiver_model.rf_amp()," was passing the same rf_amp of all Receiver Apps
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga())});
|
||||
transmitter_model.set_sampling_rate(sample_rate * 8);
|
||||
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
transmitter_model.enable();
|
||||
|
||||
if (portapack::persistent_memory::stealth_mode()) {
|
||||
DisplaySleepMessage message;
|
||||
@@ -167,8 +158,7 @@ void ReplayAppView::stop(const bool do_loop) {
|
||||
if (do_loop && check_loop.value()) {
|
||||
start();
|
||||
} else {
|
||||
radio::set_antenna_bias(false); // Turn off Bias Tee
|
||||
radio::disable();
|
||||
transmitter_model.disable();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
}
|
||||
|
||||
@@ -233,7 +223,7 @@ ReplayAppView::ReplayAppView(
|
||||
}
|
||||
|
||||
ReplayAppView::~ReplayAppView() {
|
||||
radio::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 )
|
||||
|
||||
@@ -170,17 +170,7 @@ TPMSAppView::TPMSAppView(NavigationView&) {
|
||||
receiver_model.set_tuning_frequency(tuning_frequency());
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
receiver_model.enable(); // Before using radio::enable(), but not updating Ant.DC-Bias.
|
||||
|
||||
/* radio::enable({
|
||||
tuning_frequency(),
|
||||
sampling_rate,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Receive,
|
||||
receiver_model.rf_amp(),
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga()),
|
||||
}); */
|
||||
receiver_model.enable();
|
||||
|
||||
options_band.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
this->on_band_changed(v);
|
||||
@@ -212,8 +202,7 @@ TPMSAppView::~TPMSAppView() {
|
||||
app_settings.rx_frequency = target_frequency_;
|
||||
settings.save("rx_tpms", &app_settings);
|
||||
|
||||
receiver_model.disable(); // to switch off all, including DC bias and change flag enabled_
|
||||
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
@@ -258,7 +247,7 @@ void TPMSAppView::on_band_changed(const uint32_t new_band_frequency) {
|
||||
|
||||
void TPMSAppView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
radio::set_tuning_frequency(tuning_frequency());
|
||||
receiver_model.set_tuning_frequency(tuning_frequency());
|
||||
}
|
||||
|
||||
uint32_t TPMSAppView::target_frequency() const {
|
||||
|
||||
@@ -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, 8 * 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_;
|
||||
|
||||
@@ -95,10 +95,15 @@ void FreqManBaseView::change_category(int32_t category_id) {
|
||||
|
||||
last_category_id = current_category_id = category_id;
|
||||
|
||||
std::vector<freqman_entry>().swap(database);
|
||||
menu_view.set_db(database);
|
||||
|
||||
if (!load_freqman_file(file_list[categories[current_category_id].second], database))
|
||||
error_ = ERROR_ACCESS;
|
||||
else
|
||||
else {
|
||||
menu_view.set_db(database);
|
||||
refresh_list();
|
||||
}
|
||||
}
|
||||
|
||||
void FreqManBaseView::refresh_list() {
|
||||
@@ -108,20 +113,6 @@ void FreqManBaseView::refresh_list() {
|
||||
} else {
|
||||
if (on_refresh_widgets)
|
||||
on_refresh_widgets(false);
|
||||
|
||||
menu_view.clear();
|
||||
|
||||
for (size_t n = 0; n < database.size(); n++) {
|
||||
menu_view.add_item({freqman_item_string(database[n], 30),
|
||||
ui::Color::white(),
|
||||
nullptr,
|
||||
[this](KeyEvent) {
|
||||
if (on_select_frequency)
|
||||
on_select_frequency();
|
||||
}});
|
||||
}
|
||||
|
||||
menu_view.set_highlighted(0); // Refresh
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,18 +197,13 @@ FrequencyLoadView::FrequencyLoadView(
|
||||
// Resize menu view to fill screen
|
||||
menu_view.set_parent_rect({0, 3 * 8, 240, 30 * 8});
|
||||
|
||||
// Just to allow exit on left
|
||||
menu_view.on_left = [&nav, this]() {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
change_category(last_category_id);
|
||||
refresh_list();
|
||||
|
||||
on_select_frequency = [&nav, this]() {
|
||||
menu_view.on_select = [&nav, this](FreqManUIList&) {
|
||||
nav_.pop();
|
||||
|
||||
auto& entry = database[menu_view.highlighted_index()];
|
||||
auto& entry = database[menu_view.get_index()];
|
||||
|
||||
if (entry.type == RANGE) {
|
||||
// User chose a frequency range entry
|
||||
@@ -235,14 +221,14 @@ FrequencyLoadView::FrequencyLoadView(
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_edit_freq(rf::Frequency f) {
|
||||
database[menu_view.highlighted_index()].frequency_a = f;
|
||||
database[menu_view.get_index()].frequency_a = f;
|
||||
save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
refresh_list();
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_edit_desc(NavigationView& nav) {
|
||||
text_prompt(nav, desc_buffer, 28, [this](std::string& buffer) {
|
||||
database[menu_view.highlighted_index()].description = buffer;
|
||||
database[menu_view.get_index()].description = buffer;
|
||||
refresh_list();
|
||||
save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
});
|
||||
@@ -258,7 +244,7 @@ void FrequencyManagerView::on_new_category(NavigationView& nav) {
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_delete() {
|
||||
database.erase(database.begin() + menu_view.highlighted_index());
|
||||
database.erase(database.begin() + menu_view.get_index());
|
||||
save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
refresh_list();
|
||||
}
|
||||
@@ -292,15 +278,10 @@ FrequencyManagerView::FrequencyManagerView(
|
||||
&button_edit_desc,
|
||||
&button_delete});
|
||||
|
||||
// Just to allow exit on left
|
||||
menu_view.on_left = [&nav, this]() {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
change_category(last_category_id);
|
||||
refresh_list();
|
||||
|
||||
on_select_frequency = [this]() {
|
||||
menu_view.on_select = [this](FreqManUIList&) {
|
||||
button_edit_freq.focus();
|
||||
};
|
||||
|
||||
@@ -310,14 +291,14 @@ FrequencyManagerView::FrequencyManagerView(
|
||||
};
|
||||
|
||||
button_edit_freq.on_select = [this, &nav](Button&) {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(database[menu_view.highlighted_index()].frequency_a);
|
||||
auto new_view = nav.push<FrequencyKeypadView>(database[menu_view.get_index()].frequency_a);
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
on_edit_freq(f);
|
||||
};
|
||||
};
|
||||
|
||||
button_edit_desc.on_select = [this, &nav](Button&) {
|
||||
desc_buffer = database[menu_view.highlighted_index()].description;
|
||||
desc_buffer = database[menu_view.get_index()].description;
|
||||
on_edit_desc(nav);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "ui_freqlist.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -65,9 +66,9 @@ class FreqManBaseView : public View {
|
||||
14,
|
||||
{}};
|
||||
|
||||
MenuView menu_view{
|
||||
{0, 3 * 8, 240, 23 * 8},
|
||||
true};
|
||||
FreqManUIList menu_view{
|
||||
{0, 3 * 8, 240, 23 * 8}};
|
||||
|
||||
Text text_empty{
|
||||
{7 * 8, 12 * 8, 16 * 8, 16},
|
||||
"Empty category !",
|
||||
|
||||
@@ -286,7 +286,6 @@ void JammerView::stop_tx() {
|
||||
button_transmit.set_style(&style_val);
|
||||
button_transmit.set_text("START");
|
||||
transmitter_model.disable();
|
||||
radio::disable();
|
||||
baseband::set_jammer(false, JammerType::TYPE_FSK, 0);
|
||||
jamming = false;
|
||||
cooling = false;
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -29,10 +30,6 @@ using portapack::memory::map::backup_ram;
|
||||
|
||||
namespace ui {
|
||||
|
||||
bool LevelView::check_sd_card() {
|
||||
return (sd_card::status() == sd_card::Status::Mounted) ? true : false;
|
||||
}
|
||||
|
||||
void LevelView::focus() {
|
||||
button_frequency.focus();
|
||||
}
|
||||
@@ -74,12 +71,6 @@ LevelView::LevelView(NavigationView& nav)
|
||||
field_volume.on_change = [this](int32_t v) { this->on_headphone_volume_changed(v); };
|
||||
field_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99);
|
||||
|
||||
// Level directory
|
||||
if (check_sd_card()) { // Check to see if SD Card is mounted
|
||||
make_new_directory(u"/LEVEL");
|
||||
sd_card_mounted = true;
|
||||
}
|
||||
|
||||
change_mode(NFM_MODULATION); // Start on AM
|
||||
field_mode.set_by_value(NFM_MODULATION); // Reflect the mode into the manual selector
|
||||
|
||||
@@ -89,14 +80,12 @@ LevelView::LevelView(NavigationView& nav)
|
||||
button_frequency.set_text("<" + to_string_short_freq(freq) + " MHz>");
|
||||
|
||||
// load auto common app settings
|
||||
if (sd_card_mounted) {
|
||||
auto rc = settings.load("level", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
receiver_model.set_rf_amp(app_settings.rx_amp);
|
||||
}
|
||||
auto rc = settings.load("level", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
receiver_model.set_rf_amp(app_settings.rx_amp);
|
||||
}
|
||||
|
||||
button_frequency.on_select = [this, &nav](ButtonWithEncoder& button) {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -97,11 +98,9 @@ class LevelView : public View {
|
||||
size_t change_mode(freqman_index_t mod_type);
|
||||
void on_statistics_update(const ChannelStatistics& statistics);
|
||||
void set_display_freq(int64_t freq);
|
||||
bool check_sd_card();
|
||||
|
||||
int32_t db{0};
|
||||
long long int MAX_UFREQ = {7200000000}; // maximum usable freq
|
||||
bool sd_card_mounted = false;
|
||||
rf::Frequency freq = {0};
|
||||
|
||||
Labels labels{
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -45,18 +46,10 @@ void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint
|
||||
if (spectrum.db[bin - 120] > max_power)
|
||||
max_power = spectrum.db[bin - 120];
|
||||
}
|
||||
} else if (mode == LOOKING_GLASS_FASTSCAN) {
|
||||
} else {
|
||||
// view is made in multiple pass, use original bin picking
|
||||
// Center 12 bins are ignored (DC spike is blanked) Leftmost and rightmost 2 bins are ignored
|
||||
if (bin < 120) {
|
||||
if (spectrum.db[SPEC_NB_BINS - 2 - 120 + bin] > max_power)
|
||||
max_power = spectrum.db[SPEC_NB_BINS - 2 - 120 + bin];
|
||||
} else {
|
||||
if (spectrum.db[2 + bin - 120] > max_power)
|
||||
max_power = spectrum.db[2 + bin - 120];
|
||||
}
|
||||
} else // if( mode == LOOKING_GLASS_SLOWSCAN )
|
||||
{
|
||||
// FAST mode: center 12 bins are ignored in fast mode , (DC spike is blanked) leftmost and rightmost 2 bins are ignored
|
||||
// SLOW mode: leftmost 'offset' bins are ignored
|
||||
if (bin < 120) {
|
||||
if (spectrum.db[SPEC_NB_BINS - offset - 120 + bin] > max_power)
|
||||
max_power = spectrum.db[SPEC_NB_BINS - offset - 120 + bin];
|
||||
@@ -67,13 +60,19 @@ void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint
|
||||
}
|
||||
}
|
||||
|
||||
void GlassView::on_marker_change() {
|
||||
rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) {
|
||||
rf::Frequency freq_at_pos = 0;
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
marker = f_min + (marker_pixel_index * looking_glass_range) / SCREEN_W;
|
||||
} else // if( mode == LOOKING_GLASS_SLOWSCAN || mode == LOOKING_GLASS_FASTSCAN )
|
||||
{
|
||||
marker = f_min + (offset * each_bin_size) + (marker_pixel_index * looking_glass_range) / SCREEN_W;
|
||||
}
|
||||
// starting from the middle, minus 8 ignored bin on each side. Since pos is [-120,120] after the (pos - 120), it's divided by SCREEN_W(240)/2 => 120
|
||||
freq_at_pos = f_center_ini + ((pos - 120) * ((looking_glass_range - ((16 * looking_glass_range) / SPEC_NB_BINS)) / 2)) / (SCREEN_W / 2);
|
||||
} else
|
||||
freq_at_pos = f_min - (offset * each_bin_size) + (pos * looking_glass_range) / SCREEN_W;
|
||||
|
||||
return freq_at_pos;
|
||||
}
|
||||
|
||||
void GlassView::on_marker_change() {
|
||||
marker = get_freq_from_bin_pos(marker_pixel_index);
|
||||
button_marker.set_text(to_string_short_freq(marker));
|
||||
PlotMarker(marker_pixel_index); // Refresh marker on screen
|
||||
}
|
||||
@@ -96,9 +95,11 @@ void GlassView::adjust_range(int64_t* f_min, int64_t* f_max, int64_t width) {
|
||||
}
|
||||
|
||||
void GlassView::retune() {
|
||||
// Start a new sweep
|
||||
radio::set_tuning_frequency(f_center); // tune rx for this new slice directly, faster than using persistent memory saving
|
||||
chThdSleepMilliseconds(5);
|
||||
// Start a new sweep.
|
||||
// Tune rx for this new slice directly because the model
|
||||
// saves to persistent memory which is slower.
|
||||
radio::set_tuning_frequency(f_center);
|
||||
chThdSleepMilliseconds(5); // stabilize freq
|
||||
baseband::spectrum_streaming_start(); // Do the RX
|
||||
}
|
||||
|
||||
@@ -143,9 +144,8 @@ 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;
|
||||
max_freq_hold = get_freq_from_bin_pos(xpos);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -172,16 +172,12 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
|
||||
baseband::spectrum_streaming_stop();
|
||||
// Convert bins of this spectrum slice into a representative max_power and when enough, into pixels
|
||||
// we actually need SCREEN_W (240) of those bins
|
||||
for (bin = offset; bin < bin_length + offset; bin++) {
|
||||
for (bin = 0; bin < bin_length; bin++) {
|
||||
get_max_power(spectrum, bin, max_power);
|
||||
if (ignore_dc && bin == 119) {
|
||||
uint8_t next_max_power = 0;
|
||||
get_max_power(spectrum, 120, next_max_power);
|
||||
bins_Hz_size += 12 * each_bin_size; // add the ignored DC spike to "pixel fulfilled bag of Hz"
|
||||
max_power = (max_power + next_max_power) / 2;
|
||||
}
|
||||
bins_Hz_size += each_bin_size; // add this bin Hz count into the "pixel fulfilled bag of Hz"
|
||||
|
||||
bins_Hz_size += each_bin_size; // add this bin Hz count into the "pixel fulfilled bag of Hz"
|
||||
if (bins_Hz_size >= marker_pixel_step) // new pixel fullfilled
|
||||
{
|
||||
if (min_color_power < max_power)
|
||||
@@ -194,15 +190,21 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
|
||||
if (!pixel_index) // Received indication that a waterfall line has been completed
|
||||
{
|
||||
bins_Hz_size = 0; // Since this is an entire pixel line, we don't carry "Pixels into next bin"
|
||||
f_center = f_center_ini;
|
||||
retune();
|
||||
if (mode != LOOKING_GLASS_SINGLEPASS) {
|
||||
f_center = f_center_ini;
|
||||
retune();
|
||||
} else
|
||||
baseband::spectrum_streaming_start();
|
||||
return; // signal a new line
|
||||
}
|
||||
bins_Hz_size -= marker_pixel_step; // reset bins size, but carrying the eventual excess Hz into next pixel
|
||||
}
|
||||
}
|
||||
f_center += looking_glass_step;
|
||||
retune();
|
||||
if (mode != LOOKING_GLASS_SINGLEPASS) {
|
||||
f_center += looking_glass_step;
|
||||
retune();
|
||||
} else
|
||||
baseband::spectrum_streaming_start();
|
||||
}
|
||||
|
||||
void GlassView::on_hide() {
|
||||
@@ -216,45 +218,49 @@ void GlassView::on_show() {
|
||||
}
|
||||
|
||||
void GlassView::on_range_changed() {
|
||||
reset_live_view(false);
|
||||
reset_live_view(true);
|
||||
f_min = field_frequency_min.value();
|
||||
f_max = field_frequency_max.value();
|
||||
f_min = f_min * MHZ_DIV; // Transpose into full frequency realm
|
||||
f_max = f_max * MHZ_DIV;
|
||||
looking_glass_range = f_max - f_min;
|
||||
if (looking_glass_range < LOOKING_GLASS_SLICE_WIDTH_MAX) {
|
||||
mode = LOOKING_GLASS_SINGLEPASS;
|
||||
} else {
|
||||
mode = scan_type.selected_index_value();
|
||||
}
|
||||
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
if (looking_glass_range <= LOOKING_GLASS_SLICE_WIDTH_MAX) {
|
||||
adjust_range(&f_min, &f_max, SCREEN_W);
|
||||
looking_glass_range = f_max - f_min;
|
||||
// if the view is done in one pass, show it like in analog_audio_app
|
||||
mode = LOOKING_GLASS_SINGLEPASS;
|
||||
offset = 0;
|
||||
bin_length = SCREEN_W;
|
||||
ignore_dc = 0;
|
||||
} else if (mode == LOOKING_GLASS_FASTSCAN) {
|
||||
looking_glass_bandwidth = looking_glass_range;
|
||||
looking_glass_sampling_rate = looking_glass_bandwidth;
|
||||
each_bin_size = looking_glass_bandwidth / SCREEN_W;
|
||||
looking_glass_step = looking_glass_bandwidth;
|
||||
f_center_ini = f_min + (looking_glass_bandwidth / 2); // Initial center frequency for sweep
|
||||
} else { // if ( mode == LOOKING_GLASS_SLOWSCAN || mode == LOOKING_GLASS_FASTSCAN )
|
||||
// view is made in multiple pass, use original bin picking
|
||||
offset = 0;
|
||||
bin_length = SCREEN_W;
|
||||
ignore_dc = 1;
|
||||
} else // if( mode == LOOKING_GLASS_SLOWSCAN )
|
||||
{
|
||||
offset = 16;
|
||||
bin_length = 80;
|
||||
ignore_dc = 0;
|
||||
}
|
||||
each_bin_size = looking_glass_bandwidth / SPEC_NB_BINS;
|
||||
if (mode != LOOKING_GLASS_SINGLEPASS) {
|
||||
mode = scan_type.selected_index_value();
|
||||
if (mode == LOOKING_GLASS_FASTSCAN) {
|
||||
offset = 2;
|
||||
bin_length = SCREEN_W;
|
||||
ignore_dc = 1;
|
||||
} else { // if( mode == LOOKING_GLASS_SLOWSCAN )
|
||||
offset = 16;
|
||||
bin_length = 80;
|
||||
ignore_dc = 0;
|
||||
}
|
||||
adjust_range(&f_min, &f_max, SCREEN_W);
|
||||
looking_glass_range = f_max - f_min;
|
||||
looking_glass_bandwidth = LOOKING_GLASS_SLICE_WIDTH_MAX;
|
||||
looking_glass_sampling_rate = LOOKING_GLASS_SLICE_WIDTH_MAX;
|
||||
each_bin_size = LOOKING_GLASS_SLICE_WIDTH_MAX / SPEC_NB_BINS;
|
||||
looking_glass_step = (bin_length + (ignore_dc * 12)) * each_bin_size;
|
||||
} else {
|
||||
looking_glass_step = SPEC_NB_BINS * each_bin_size;
|
||||
f_center_ini = f_min - (offset * each_bin_size) + (looking_glass_bandwidth / 2); // Initial center frequency for sweep
|
||||
}
|
||||
adjust_range(&f_min, &f_max, SCREEN_W);
|
||||
looking_glass_range = f_max - f_min;
|
||||
marker_pixel_step = looking_glass_range / SCREEN_W; // Each pixel value in Hz
|
||||
search_span = looking_glass_range / MHZ_DIV;
|
||||
button_range.set_text(" "); // clear up to 6 chars
|
||||
marker_pixel_step = looking_glass_range / SCREEN_W; // Each pixel value in Hz
|
||||
//
|
||||
button_range.set_text(" "); // clear up to 6 chars
|
||||
if (locked_range) {
|
||||
button_range.set_text(">" + to_string_dec_uint(search_span) + "<");
|
||||
} else {
|
||||
@@ -264,18 +270,7 @@ void GlassView::on_range_changed() {
|
||||
pixel_index = 0; // reset pixel counter
|
||||
max_power = 0; // reset save max power level
|
||||
bins_Hz_size = 0; // reset amount of Hz filled up by pixels
|
||||
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
looking_glass_bandwidth = looking_glass_range;
|
||||
looking_glass_sampling_rate = looking_glass_bandwidth / 2;
|
||||
each_bin_size = looking_glass_bandwidth / SCREEN_W;
|
||||
} else // if ( mode == LOOKING_GLASS_SLOWSCAN || mode == LOOKING_GLASS_FASTSCAN )
|
||||
{
|
||||
looking_glass_sampling_rate = LOOKING_GLASS_SLICE_WIDTH_MAX;
|
||||
looking_glass_bandwidth = LOOKING_GLASS_SLICE_WIDTH_MAX;
|
||||
each_bin_size = looking_glass_bandwidth / SPEC_NB_BINS;
|
||||
}
|
||||
|
||||
//
|
||||
on_marker_change();
|
||||
|
||||
// set the sample rate and bandwidth
|
||||
@@ -283,9 +278,8 @@ void GlassView::on_range_changed() {
|
||||
receiver_model.set_baseband_bandwidth(looking_glass_bandwidth);
|
||||
|
||||
receiver_model.set_squelch_level(0);
|
||||
f_center_ini = f_min + (looking_glass_bandwidth / 2); // Initial center frequency for sweep
|
||||
f_center = f_center_ini; // Reset sweep into first slice
|
||||
baseband::set_spectrum(looking_glass_sampling_rate, field_trigger.value());
|
||||
f_center = f_center_ini; // Reset sweep into first slice
|
||||
baseband::set_spectrum(looking_glass_bandwidth, field_trigger.value());
|
||||
receiver_model.set_tuning_frequency(f_center); // tune rx for this slice
|
||||
}
|
||||
|
||||
@@ -301,6 +295,42 @@ void GlassView::PlotMarker(uint8_t pos) {
|
||||
portapack::display.fill_rectangle({pos, 106 + shift_y, 1, 2}, Color::red()); // Red marker bottom
|
||||
}
|
||||
|
||||
void GlassView::clip_min(int32_t v) {
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
if (v > 7200 - min_size) {
|
||||
v = 7200 - min_size;
|
||||
}
|
||||
if (v > (field_frequency_max.value() - min_size))
|
||||
field_frequency_max.set_value(v + min_size);
|
||||
if (locked_range)
|
||||
field_frequency_max.set_value(v + min_size);
|
||||
else
|
||||
field_frequency_min.set_value(v);
|
||||
on_range_changed();
|
||||
}
|
||||
|
||||
void GlassView::clip_max(int32_t v) {
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
if (v < min_size) {
|
||||
v = min_size;
|
||||
}
|
||||
if (v < (field_frequency_min.value() + min_size))
|
||||
field_frequency_min.set_value(v - min_size);
|
||||
if (locked_range)
|
||||
field_frequency_min.set_value(v - min_size);
|
||||
else
|
||||
field_frequency_max.set_value(v);
|
||||
on_range_changed();
|
||||
}
|
||||
|
||||
GlassView::GlassView(
|
||||
NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
@@ -328,21 +358,7 @@ GlassView::GlassView(
|
||||
load_Presets(); // Load available presets from TXT files (or default)
|
||||
|
||||
field_frequency_min.on_change = [this](int32_t v) {
|
||||
reset_live_view(true);
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
if (v > 7200 - min_size) {
|
||||
v = 7200 - min_size;
|
||||
field_frequency_min.set_value(v);
|
||||
}
|
||||
if (v > (field_frequency_max.value() - min_size))
|
||||
field_frequency_max.set_value(v + min_size);
|
||||
if (locked_range)
|
||||
field_frequency_max.set_value(v + min_size);
|
||||
on_range_changed();
|
||||
clip_min(v);
|
||||
};
|
||||
field_frequency_min.set_value(presets_db[0].min); // Defaults to first preset
|
||||
field_frequency_min.set_step(steps);
|
||||
@@ -350,37 +366,12 @@ GlassView::GlassView(
|
||||
field_frequency_min.on_select = [this, &nav](NumberField& field) {
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(field_frequency_min.value() * MHZ_DIV);
|
||||
new_view->on_changed = [this, &field](rf::Frequency f) {
|
||||
int32_t freq = f / MHZ_DIV;
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
if (freq > (7200 - min_size))
|
||||
freq = 7200 - min_size;
|
||||
field_frequency_min.set_value(freq);
|
||||
if (field_frequency_max.value() < (freq + min_size))
|
||||
field_frequency_max.set_value(freq + min_size);
|
||||
on_range_changed();
|
||||
clip_min(f / MHZ_DIV);
|
||||
};
|
||||
};
|
||||
|
||||
field_frequency_max.on_change = [this](int32_t v) {
|
||||
reset_live_view(true);
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
if (v < min_size) {
|
||||
v = min_size;
|
||||
field_frequency_max.set_value(v);
|
||||
}
|
||||
if (v < (field_frequency_min.value() + min_size))
|
||||
field_frequency_min.set_value(v - min_size);
|
||||
if (locked_range)
|
||||
field_frequency_min.set_value(v - min_size);
|
||||
on_range_changed();
|
||||
clip_max(v);
|
||||
};
|
||||
field_frequency_max.set_value(presets_db[0].max); // Defaults to first preset
|
||||
field_frequency_max.set_step(steps);
|
||||
@@ -388,18 +379,7 @@ GlassView::GlassView(
|
||||
field_frequency_max.on_select = [this, &nav](NumberField& field) {
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(field_frequency_max.value() * MHZ_DIV);
|
||||
new_view->on_changed = [this, &field](rf::Frequency f) {
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
int32_t freq = f / MHZ_DIV;
|
||||
if (freq < min_size)
|
||||
freq = min_size;
|
||||
field_frequency_max.set_value(freq);
|
||||
if (field_frequency_min.value() > (freq - min_size))
|
||||
field_frequency_min.set_value(freq - min_size);
|
||||
on_range_changed();
|
||||
clip_max(f / MHZ_DIV);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -503,7 +483,7 @@ GlassView::GlassView(
|
||||
};
|
||||
|
||||
field_trigger.on_change = [this](int32_t v) {
|
||||
baseband::set_spectrum(looking_glass_sampling_rate, v);
|
||||
baseband::set_spectrum(looking_glass_bandwidth, v);
|
||||
};
|
||||
field_trigger.set_value(32); // Defaults to 32, as normal triggering resolution
|
||||
|
||||
@@ -531,10 +511,10 @@ GlassView::GlassView(
|
||||
};
|
||||
|
||||
display.scroll_set_area(109, 319);
|
||||
baseband::set_spectrum(looking_glass_sampling_rate, field_trigger.value()); // trigger:
|
||||
// Discord User jteich: WidebandSpectrum::on_message to set the trigger value. In WidebandSpectrum::execute ,
|
||||
// it keeps adding the output of the fft to the buffer until "trigger" number of calls are made,
|
||||
// at which time it pushes the buffer up with channel_spectrum.feed
|
||||
baseband::set_spectrum(looking_glass_bandwidth, field_trigger.value()); // trigger:
|
||||
// Discord User jteich: WidebandSpectrum::on_message to set the trigger value. In WidebandSpectrum::execute ,
|
||||
// it keeps adding the output of the fft to the buffer until "trigger" number of calls are made,
|
||||
// at which time it pushes the buffer up with channel_spectrum.feed
|
||||
|
||||
marker_pixel_index = 120;
|
||||
on_range_changed();
|
||||
|
||||
@@ -89,7 +89,10 @@ class GlassView : public View {
|
||||
};
|
||||
|
||||
std::vector<preset_entry> presets_db{};
|
||||
void clip_min(int32_t v);
|
||||
void clip_max(int32_t v);
|
||||
void get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power);
|
||||
rf::Frequency get_freq_from_bin_pos(uint8_t pos);
|
||||
void on_marker_change();
|
||||
int64_t next_mult_of(int64_t num, int64_t multiplier);
|
||||
void adjust_range(int64_t* f_min, int64_t* f_max, int64_t width);
|
||||
|
||||
@@ -123,14 +123,15 @@ class MicTXView : public View {
|
||||
{{4 * 8, 10 * 8}, "LVL:", Color::light_grey()}, // we delete { {11 * 8, 5 * 8 }, "Amp:", Color::light_grey() },
|
||||
{{12 * 8, 10 * 8}, "ATT:", Color::light_grey()},
|
||||
{{20 * 8, 10 * 8}, "DEC:", Color::light_grey()},
|
||||
{{4 * 8, (13 * 8) - 2}, "TONE KEY:", Color::light_grey()},
|
||||
{{7 * 8, 23 * 8}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, 23 * 8}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, 25 * 8}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, 25 * 8}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, 27 * 8}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, 27 * 8}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, 27 * 8}, "AMP:", Color::light_grey()}};
|
||||
{{3 * 8, (13 * 8) - 5}, "TONE KEY:", Color::light_grey()},
|
||||
{{3 * 8, (18 * 8) - 1}, "======== Receiver ========", Color::green()},
|
||||
{{5 * 8, (23 * 8) + 2}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, (23 * 8) + 2}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, (25 * 8) + 2}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, (25 * 8) + 2}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, (27 * 8) + 2}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, (27 * 8) + 2}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, (27 * 8) + 2}, "AMP:", Color::light_grey()}};
|
||||
Labels labels_AK4951{
|
||||
{{3 * 8, 1 * 8}, "MIC-GAIN:", Color::light_grey()},
|
||||
{{17 * 8, 1 * 8}, "ALC", Color::light_grey()},
|
||||
@@ -141,14 +142,15 @@ class MicTXView : public View {
|
||||
{{4 * 8, 10 * 8}, "LVL:", Color::light_grey()}, // we delete { {11 * 8, 5 * 8 }, "Amp:", Color::light_grey() },
|
||||
{{12 * 8, 10 * 8}, "ATT:", Color::light_grey()},
|
||||
{{20 * 8, 10 * 8}, "DEC:", Color::light_grey()},
|
||||
{{4 * 8, (13 * 8) - 2}, "TONE KEY:", Color::light_grey()},
|
||||
{{(6 * 8) + 4, 23 * 8}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, 23 * 8}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, 25 * 8}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, 25 * 8}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, 27 * 8}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, 27 * 8}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, 27 * 8}, "AMP:", Color::light_grey()}};
|
||||
{{3 * 8, (13 * 8) - 5}, "TONE KEY:", Color::light_grey()},
|
||||
{{3 * 8, (18 * 8) - 1}, "======== Receiver ========", Color::green()},
|
||||
{{(5 * 8), (23 * 8) + 2}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, (23 * 8) + 2}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, (25 * 8) + 2}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, (25 * 8) + 2}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, (27 * 8) + 2}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, (27 * 8) + 2}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, (27 * 8) + 2}, "AMP:", Color::light_grey()}};
|
||||
|
||||
VuMeter vumeter{
|
||||
{0 * 8, 1 * 8, 2 * 8, 33 * 8},
|
||||
@@ -210,7 +212,7 @@ class MicTXView : public View {
|
||||
|
||||
OptionsField options_mode{
|
||||
{24 * 8, 5 * 8},
|
||||
4,
|
||||
6,
|
||||
{
|
||||
{"NFM/FM", 0},
|
||||
{" WFM ", 1},
|
||||
@@ -230,7 +232,7 @@ class MicTXView : public View {
|
||||
|
||||
OptionsField field_va{
|
||||
{17 * 8, 8 * 8},
|
||||
3,
|
||||
4,
|
||||
{{" OFF", 0},
|
||||
{" PTT", 1},
|
||||
{"AUTO", 2}}};
|
||||
@@ -255,30 +257,30 @@ class MicTXView : public View {
|
||||
' '};
|
||||
|
||||
OptionsField options_tone_key{
|
||||
{10 * 8, (15 * 8) - 2},
|
||||
{12 * 8, (13 * 8) - 5},
|
||||
23,
|
||||
{}};
|
||||
|
||||
Checkbox check_rogerbeep{
|
||||
{3 * 8, (16 * 8) + 7},
|
||||
{3 * 8, (14 * 8) + 4},
|
||||
10,
|
||||
"Roger beep",
|
||||
false};
|
||||
|
||||
Checkbox check_rxactive{
|
||||
{3 * 8, (21 * 8) - 4},
|
||||
18, // it was 8, but if it is string size should be 18
|
||||
"RX audio listening",
|
||||
{3 * 8, (21 * 8) - 7},
|
||||
8, // it was 18, but if it is string size should be 8
|
||||
"RX audio",
|
||||
false};
|
||||
|
||||
Checkbox check_common_freq_tx_rx{
|
||||
{18 * 8, (16 * 8) + 7},
|
||||
{18 * 8, (21 * 8) - 7},
|
||||
8,
|
||||
"F = F_RX",
|
||||
"F TX=RX",
|
||||
false};
|
||||
|
||||
NumberField field_volume{
|
||||
{11 * 8, 23 * 8},
|
||||
{9 * 8, (23 * 8) + 2},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
@@ -286,7 +288,7 @@ class MicTXView : public View {
|
||||
};
|
||||
|
||||
OptionsField field_rxbw{
|
||||
{19 * 8, 23 * 8},
|
||||
{19 * 8, (23 * 8) + 2},
|
||||
3,
|
||||
{
|
||||
{" NFM1:8k5 ", 0},
|
||||
@@ -295,7 +297,7 @@ class MicTXView : public View {
|
||||
}};
|
||||
|
||||
NumberField field_squelch{
|
||||
{23 * 8, 25 * 8},
|
||||
{23 * 8, (25 * 8) + 2},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
@@ -303,11 +305,11 @@ class MicTXView : public View {
|
||||
};
|
||||
|
||||
FrequencyField field_rxfrequency{
|
||||
{10 * 8, 25 * 8},
|
||||
{10 * 8, (25 * 8) + 2},
|
||||
};
|
||||
|
||||
NumberField field_rxlna{
|
||||
{9 * 8, 27 * 8},
|
||||
{9 * 8, (27 * 8) + 2},
|
||||
2,
|
||||
{0, 40},
|
||||
8,
|
||||
@@ -315,7 +317,7 @@ class MicTXView : public View {
|
||||
};
|
||||
|
||||
NumberField field_rxvga{
|
||||
{16 * 8, 27 * 8},
|
||||
{16 * 8, (27 * 8) + 2},
|
||||
2,
|
||||
{0, 62},
|
||||
2,
|
||||
@@ -323,7 +325,7 @@ class MicTXView : public View {
|
||||
};
|
||||
|
||||
NumberField field_rxamp{
|
||||
{24 * 8, 27 * 8},
|
||||
{24 * 8, (27 * 8) + 2},
|
||||
1,
|
||||
{0, 1},
|
||||
1,
|
||||
|
||||
@@ -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,8 +142,19 @@ bool PlaylistView::loop() const {
|
||||
void PlaylistView::toggle() {
|
||||
if (is_active()) {
|
||||
stop(false);
|
||||
} else {
|
||||
start();
|
||||
total_tracks = 0;
|
||||
track_number = 0;
|
||||
playlist_db.clear();
|
||||
playlist_masterdb.clear();
|
||||
} else { // Thanks kallanreed for providing this logic!
|
||||
total_tracks = 0;
|
||||
track_number = 0;
|
||||
playlist_db.clear();
|
||||
playlist_masterdb.clear();
|
||||
load_file(now_play_list_file);
|
||||
if (!playlist_db.empty()) {
|
||||
start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +175,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);
|
||||
@@ -178,37 +195,48 @@ void PlaylistView::start() {
|
||||
});
|
||||
}
|
||||
|
||||
// Enable Bias Tee if selected
|
||||
radio::set_antenna_bias(portapack::get_antenna_bias());
|
||||
|
||||
rf_amp = (transmitter_model.rf_amp()); // recover rf_amp settings applied from ui_transmiter.cpp
|
||||
|
||||
radio::enable({receiver_model.tuning_frequency(),
|
||||
sample_rate * 8,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Transmit,
|
||||
rf_amp, // previous code line : "receiver_model.rf_amp()," was passing the same rf_amp of all Receiver Apps
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga())});
|
||||
|
||||
// }
|
||||
transmitter_model.set_sampling_rate(sample_rate * 8);
|
||||
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
transmitter_model.enable();
|
||||
}
|
||||
|
||||
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();
|
||||
transmitter_model.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 {
|
||||
transmitter_model.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 +247,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 +268,8 @@ PlaylistView::PlaylistView(
|
||||
&tx_view, // this handles now the previous rfgain, rfamp
|
||||
&check_loop,
|
||||
&button_play,
|
||||
// &text_track,
|
||||
// TODO: add track number
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
@@ -264,15 +294,16 @@ 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);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
PlaylistView::~PlaylistView() {
|
||||
radio::disable();
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
@@ -287,7 +318,8 @@ void PlaylistView::on_hide() {
|
||||
void PlaylistView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
const ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
const ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(),
|
||||
new_parent_rect.height() - header_height};
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,13 @@ namespace ui {
|
||||
class PlaylistView : public View {
|
||||
public:
|
||||
PlaylistView(NavigationView& nav);
|
||||
|
||||
~PlaylistView();
|
||||
|
||||
void on_hide() override;
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Playlist"; };
|
||||
@@ -66,15 +69,14 @@ class PlaylistView : public View {
|
||||
static constexpr uint32_t baseband_bandwidth = 2500000;
|
||||
const size_t read_size{16384};
|
||||
const size_t buffer_count{3};
|
||||
|
||||
void load_file(std::filesystem::path playlist_path);
|
||||
void txtline_process(std::string&);
|
||||
void on_file_changed(std::filesystem::path new_file_path, rf::Frequency replay_frequency, uint32_t replay_sample_rate);
|
||||
void on_target_frequency_changed(rf::Frequency f);
|
||||
void on_tx_progress(const uint32_t progress);
|
||||
|
||||
void set_target_frequency(const rf::Frequency new_value);
|
||||
rf::Frequency target_frequency() const;
|
||||
|
||||
void toggle();
|
||||
void start();
|
||||
void stop(const bool do_loop);
|
||||
@@ -82,11 +84,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};
|
||||
size_t track_number{0};
|
||||
size_t total_tracks{0};
|
||||
std::filesystem::path now_play_list_file{};
|
||||
|
||||
Button button_open{
|
||||
{0 * 8, 0 * 16, 10 * 8, 2 * 16},
|
||||
@@ -125,6 +130,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.
|
||||
*
|
||||
@@ -29,7 +30,52 @@ using portapack::memory::map::backup_ram;
|
||||
|
||||
namespace ui {
|
||||
|
||||
bool ReconView::ReconSaveFreq(const std::string& freq_file_path, size_t freq_index, bool warn_if_exists) {
|
||||
void ReconView::clear_freqlist_for_ui_action() {
|
||||
audio::output::stop();
|
||||
// flag to detect and reload frequency_list
|
||||
freqlist_cleared_for_ui_action = true;
|
||||
// if in manual mode, there is enough memory to load freqman files, else we have to unload/reload
|
||||
if (!manual_mode) {
|
||||
// clear and shrink_to_fit are not enough to really start with a new, clean, empty vector
|
||||
// swap is the only way to achieve a perfect memory liberation
|
||||
std::vector<freqman_entry>().swap(frequency_list);
|
||||
} else {
|
||||
frequency_list.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
void ReconView::reset_indexes() {
|
||||
last_entry.modulation = -1;
|
||||
last_entry.bandwidth = -1;
|
||||
last_entry.step = -1;
|
||||
description = "...no description...";
|
||||
current_index = 0;
|
||||
}
|
||||
|
||||
void ReconView::colorize_waits() {
|
||||
// colorize wait on match
|
||||
if (wait == 0) {
|
||||
field_wait.set_style(&style_blue);
|
||||
} else if (wait >= 500) {
|
||||
field_wait.set_style(&style_white);
|
||||
} else if (wait > -500 && wait < 500) {
|
||||
field_wait.set_style(&style_red);
|
||||
} else if (wait <= -500) {
|
||||
field_wait.set_style(&style_green);
|
||||
}
|
||||
// colorize lock time if in SPARSE mode as in continuous the lock_wait time is disarmed at first lock count
|
||||
if (recon_match_mode == RECON_MATCH_SPARSE) {
|
||||
if ((recon_lock_duration / STATS_UPDATE_INTERVAL) <= recon_lock_nb_match) {
|
||||
field_lock_wait.set_style(&style_yellow);
|
||||
} else {
|
||||
field_lock_wait.set_style(&style_white);
|
||||
}
|
||||
} else {
|
||||
field_lock_wait.set_style(&style_white);
|
||||
}
|
||||
}
|
||||
|
||||
bool ReconView::recon_save_freq(const std::string& freq_file_path, size_t freq_index, bool warn_if_exists) {
|
||||
File recon_file;
|
||||
|
||||
if (frequency_list.size() == 0 || (frequency_list.size() && current_index > (int32_t)frequency_list.size()))
|
||||
@@ -81,7 +127,7 @@ bool ReconView::ReconSaveFreq(const std::string& freq_file_path, size_t freq_ind
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReconView::ReconSetupLoadStrings(const std::string& source, std::string& input_file, std::string& output_file, uint32_t& recon_lock_duration, uint32_t& recon_lock_nb_match, int32_t& recon_squelch_level, uint32_t& recon_match_mode, int32_t& wait, int32_t& volume) {
|
||||
bool ReconView::recon_load_config_from_sd() {
|
||||
File settings_file;
|
||||
size_t length, file_position = 0;
|
||||
char* pos;
|
||||
@@ -93,39 +139,37 @@ bool ReconView::ReconSetupLoadStrings(const std::string& source, std::string& in
|
||||
uint32_t nb_params = RECON_SETTINGS_NB_PARAMS;
|
||||
std::string params[RECON_SETTINGS_NB_PARAMS];
|
||||
|
||||
bool check_sd_card = (sd_card::status() == sd_card::Status::Mounted) ? true : false;
|
||||
make_new_directory(u"SETTINGS");
|
||||
|
||||
if (check_sd_card) {
|
||||
auto result = settings_file.open(source);
|
||||
if (!result.is_valid()) {
|
||||
while (it < nb_params) {
|
||||
// Read a 256 bytes block from file
|
||||
settings_file.seek(file_position);
|
||||
memset(file_data, 0, 257);
|
||||
auto read_size = settings_file.read(file_data, 256);
|
||||
if (read_size.is_error())
|
||||
break;
|
||||
file_position += 256;
|
||||
// Reset line_start to beginning of buffer
|
||||
line_start = file_data;
|
||||
auto result = settings_file.open(RECON_CFG_FILE);
|
||||
if (!result.is_valid()) {
|
||||
while (it < nb_params) {
|
||||
// Read a 256 bytes block from file
|
||||
settings_file.seek(file_position);
|
||||
memset(file_data, 0, 257);
|
||||
auto read_size = settings_file.read(file_data, 256);
|
||||
if (read_size.is_error())
|
||||
break;
|
||||
file_position += 256;
|
||||
// Reset line_start to beginning of buffer
|
||||
line_start = file_data;
|
||||
pos = line_start;
|
||||
while ((line_end = strstr(line_start, "\x0A"))) {
|
||||
length = line_end - line_start - 1;
|
||||
params[it] = string(pos, length);
|
||||
it++;
|
||||
line_start = line_end + 1;
|
||||
pos = line_start;
|
||||
while ((line_end = strstr(line_start, "\x0A"))) {
|
||||
length = line_end - line_start - 1;
|
||||
params[it] = string(pos, length);
|
||||
it++;
|
||||
line_start = line_end + 1;
|
||||
pos = line_start;
|
||||
if (line_start - file_data >= 256)
|
||||
break;
|
||||
if (it >= nb_params)
|
||||
break;
|
||||
}
|
||||
if (read_size.value() != 256)
|
||||
break; // End of file
|
||||
|
||||
// Restart at beginning of last incomplete line
|
||||
file_position -= (file_data + 256 - line_start);
|
||||
if (line_start - file_data >= 256)
|
||||
break;
|
||||
if (it >= nb_params)
|
||||
break;
|
||||
}
|
||||
if (read_size.value() != 256)
|
||||
break; // End of file
|
||||
|
||||
// Restart at beginning of last incomplete line
|
||||
file_position -= (file_data + 256 - line_start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +179,7 @@ bool ReconView::ReconSetupLoadStrings(const std::string& source, std::string& in
|
||||
output_file = "RECON_RESULTS";
|
||||
recon_lock_duration = RECON_MIN_LOCK_DURATION;
|
||||
recon_lock_nb_match = RECON_DEF_NB_MATCH;
|
||||
recon_squelch_level = -14;
|
||||
squelch = -14;
|
||||
recon_match_mode = RECON_MATCH_CONTINUOUS;
|
||||
wait = RECON_DEF_WAIT_DURATION;
|
||||
volume = 40;
|
||||
@@ -163,9 +207,9 @@ bool ReconView::ReconSetupLoadStrings(const std::string& source, std::string& in
|
||||
recon_lock_nb_match = RECON_DEF_NB_MATCH;
|
||||
|
||||
if (it > 4)
|
||||
recon_squelch_level = strtoll(params[4].c_str(), nullptr, 10);
|
||||
squelch = strtoll(params[4].c_str(), nullptr, 10);
|
||||
else
|
||||
recon_squelch_level = -14;
|
||||
squelch = -14;
|
||||
|
||||
if (it > 5)
|
||||
recon_match_mode = strtoll(params[5].c_str(), nullptr, 10);
|
||||
@@ -185,17 +229,19 @@ bool ReconView::ReconSetupLoadStrings(const std::string& source, std::string& in
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReconView::ReconSetupSaveStrings(const std::string& dest, const std::string& input_file, const std::string& output_file, uint32_t recon_lock_duration, uint32_t recon_lock_nb_match, int32_t recon_squelch_level, uint32_t recon_match_mode, int32_t wait, int32_t volume) {
|
||||
bool ReconView::recon_save_config_to_sd() {
|
||||
File settings_file;
|
||||
|
||||
auto result = settings_file.create(dest);
|
||||
make_new_directory(u"SETTINGS");
|
||||
|
||||
auto result = settings_file.create(RECON_CFG_FILE);
|
||||
if (result.is_valid())
|
||||
return false;
|
||||
settings_file.write_line(input_file);
|
||||
settings_file.write_line(output_file);
|
||||
settings_file.write_line(to_string_dec_uint(recon_lock_duration));
|
||||
settings_file.write_line(to_string_dec_uint(recon_lock_nb_match));
|
||||
settings_file.write_line(to_string_dec_int(recon_squelch_level));
|
||||
settings_file.write_line(to_string_dec_int(squelch));
|
||||
settings_file.write_line(to_string_dec_uint(recon_match_mode));
|
||||
settings_file.write_line(to_string_dec_int(wait));
|
||||
settings_file.write_line(to_string_dec_int(volume));
|
||||
@@ -207,10 +253,6 @@ void ReconView::audio_output_start() {
|
||||
this->on_headphone_volume_changed((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99);
|
||||
}
|
||||
|
||||
bool ReconView::check_sd_card() {
|
||||
return (sd_card::status() == sd_card::Status::Mounted) ? true : false;
|
||||
}
|
||||
|
||||
void ReconView::recon_redraw() {
|
||||
if (last_rssi_min != rssi.get_min() || last_rssi_med != rssi.get_avg() || last_rssi_max != rssi.get_max()) {
|
||||
last_rssi_min = rssi.get_min();
|
||||
@@ -220,18 +262,16 @@ void ReconView::recon_redraw() {
|
||||
}
|
||||
if (last_entry.frequency_a != freq) {
|
||||
last_entry.frequency_a = freq;
|
||||
big_display.set("FREQ: " + to_string_short_freq(freq) + " MHz");
|
||||
big_display.set("FREQ:" + to_string_short_freq(freq) + " MHz");
|
||||
}
|
||||
if (last_db != db || last_list_size != frequency_list.size() || last_freq_lock != freq_lock || last_nb_match != recon_lock_nb_match) {
|
||||
last_freq_lock = freq;
|
||||
last_list_size = frequency_list.size();
|
||||
last_db = db;
|
||||
if (last_nb_match != recon_lock_nb_match || last_freq_lock != freq_lock) {
|
||||
last_freq_lock = freq_lock;
|
||||
last_nb_match = recon_lock_nb_match;
|
||||
text_max.set("/" + to_string_dec_uint(frequency_list.size()) + " " + to_string_dec_int(db) + " db " + to_string_dec_uint(freq_lock) + "/" + to_string_dec_uint(recon_lock_nb_match));
|
||||
text_nb_locks.set(to_string_dec_uint(freq_lock) + "/" + to_string_dec_uint(recon_lock_nb_match));
|
||||
if (freq_lock == 0) {
|
||||
// NO FREQ LOCK, ONGOING STANDARD SCANNING
|
||||
big_display.set_style(&style_white);
|
||||
if (!userpause)
|
||||
if (recon)
|
||||
button_pause.set_text("<PAUSE>");
|
||||
else
|
||||
button_pause.set_text("<RESUME>");
|
||||
@@ -242,13 +282,17 @@ void ReconView::recon_redraw() {
|
||||
} else if (freq_lock >= recon_lock_nb_match) {
|
||||
big_display.set_style(&style_green);
|
||||
button_pause.set_text("<UNLOCK>");
|
||||
|
||||
// FREQ IS STRONG: GREEN and recon will pause when on_statistics_update()
|
||||
if ((!scanner_mode) && autosave && frequency_list.size() > 0) {
|
||||
ReconSaveFreq(freq_file_path, current_index, false);
|
||||
recon_save_freq(freq_file_path, current_index, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (last_db != db || last_list_size != frequency_list.size()) {
|
||||
last_list_size = frequency_list.size();
|
||||
last_db = db;
|
||||
text_max.set("/" + to_string_dec_uint(frequency_list.size()) + " " + to_string_dec_int(db) + " db");
|
||||
}
|
||||
}
|
||||
|
||||
void ReconView::handle_retune() {
|
||||
@@ -286,7 +330,7 @@ void ReconView::handle_retune() {
|
||||
if (last_index != current_index) {
|
||||
last_index = current_index;
|
||||
if ((int32_t)frequency_list.size() && current_index < (int32_t)frequency_list.size() && frequency_list[current_index].type == RANGE) {
|
||||
if (update_ranges) {
|
||||
if (update_ranges && !manual_mode) {
|
||||
button_manual_start.set_text(to_string_short_freq(frequency_list[current_index].frequency_a));
|
||||
frequency_range.min = frequency_list[current_index].frequency_a;
|
||||
if (frequency_list[current_index].frequency_b != 0) {
|
||||
@@ -324,9 +368,9 @@ void ReconView::focus() {
|
||||
}
|
||||
|
||||
ReconView::~ReconView() {
|
||||
ReconSetupSaveStrings("RECON/RECON.CFG", input_file, output_file, recon_lock_duration, recon_lock_nb_match, squelch, recon_match_mode, wait, field_volume.value());
|
||||
|
||||
// save app settings
|
||||
// save app config
|
||||
recon_save_config_to_sd();
|
||||
// save app common settings
|
||||
settings.save("recon", &app_settings);
|
||||
|
||||
audio::output::stop();
|
||||
@@ -345,13 +389,14 @@ ReconView::ReconView(NavigationView& nav)
|
||||
&field_squelch,
|
||||
&field_wait,
|
||||
&field_lock_wait,
|
||||
&button_recon_setup,
|
||||
&button_config,
|
||||
&button_scanner_mode,
|
||||
&button_looking_glass,
|
||||
&file_name,
|
||||
&rssi,
|
||||
&text_cycle,
|
||||
&text_max,
|
||||
&text_nb_locks,
|
||||
&desc_cycle,
|
||||
&big_display,
|
||||
&freq_stats,
|
||||
@@ -370,11 +415,6 @@ ReconView::ReconView(NavigationView& nav)
|
||||
&button_mic_app,
|
||||
&button_remove});
|
||||
|
||||
// Recon directory
|
||||
if (check_sd_card()) { // Check to see if SD Card is mounted
|
||||
make_new_directory(u"/RECON");
|
||||
sd_card_mounted = true;
|
||||
}
|
||||
def_step = 0;
|
||||
// HELPER: Pre-setting a manual range, based on stored frequency
|
||||
rf::Frequency stored_freq = persistent_memory::tuned_frequency();
|
||||
@@ -398,7 +438,6 @@ ReconView::ReconView(NavigationView& nav)
|
||||
load_ranges = persistent_memory::recon_load_ranges();
|
||||
load_hamradios = persistent_memory::recon_load_hamradios();
|
||||
update_ranges = persistent_memory::recon_update_ranges_when_recon();
|
||||
|
||||
field_volume.set_value(volume);
|
||||
if (sd_card_mounted) {
|
||||
// load auto common app settings
|
||||
@@ -412,6 +451,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
}
|
||||
|
||||
button_manual_start.on_select = [this, &nav](ButtonWithEncoder& button) {
|
||||
clear_freqlist_for_ui_action();
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(frequency_range.min);
|
||||
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||
frequency_range.min = f;
|
||||
@@ -420,6 +460,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
};
|
||||
|
||||
button_manual_end.on_select = [this, &nav](ButtonWithEncoder& button) {
|
||||
clear_freqlist_for_ui_action();
|
||||
auto new_view = nav.push<FrequencyKeypadView>(frequency_range.max);
|
||||
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||
frequency_range.max = f;
|
||||
@@ -495,7 +536,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
}
|
||||
button_pause.set_text("<PAUSE>"); // Show button for non continuous stop
|
||||
} else {
|
||||
if (userpause) {
|
||||
if (!recon) {
|
||||
recon_resume();
|
||||
} else {
|
||||
recon_pause();
|
||||
@@ -527,13 +568,28 @@ ReconView::ReconView(NavigationView& nav)
|
||||
};
|
||||
|
||||
button_mic_app.on_select = [this](Button&) {
|
||||
if (frequency_list.size() > 0 && current_index >= 0 && (unsigned)current_index < frequency_list.size()) {
|
||||
if (frequency_list[current_index].type == HAMRADIO) {
|
||||
// if it's a HAMRADIO entry, then frequency_a is the freq at which the repeater reveive, so we have to set it in transmit in mic app
|
||||
transmitter_model.set_tuning_frequency(frequency_list[current_index].frequency_a);
|
||||
// if it's a HAMRADIO entry, then frequency_b is the freq at which the repeater transmit, so we have to set it in receive in mic app
|
||||
receiver_model.set_tuning_frequency(frequency_list[current_index].frequency_b);
|
||||
} else {
|
||||
// it's single or range so we us actual tuned frequency
|
||||
transmitter_model.set_tuning_frequency(freq);
|
||||
receiver_model.set_tuning_frequency(freq);
|
||||
}
|
||||
}
|
||||
// there is no way yet to set modulation and bandwidth from Recon to MicApp
|
||||
nav_.pop();
|
||||
nav_.push<MicTXView>();
|
||||
};
|
||||
|
||||
button_remove.on_select = [this](ButtonWithEncoder&) {
|
||||
if (frequency_list.size() > 0) {
|
||||
if (scanner_mode) {
|
||||
if (!manual_mode) {
|
||||
// scanner or recon (!scanner) mode
|
||||
// in both we delete index from live view, but only remove in output file in scanner_mode
|
||||
if (current_index >= (int32_t)frequency_list.size()) {
|
||||
current_index = frequency_list.size() - 1;
|
||||
}
|
||||
@@ -559,11 +615,11 @@ ReconView::ReconView(NavigationView& nav)
|
||||
desc_cycle.set("...no description...");
|
||||
}
|
||||
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
|
||||
|
||||
}
|
||||
// also remove from output file if in scanner mode
|
||||
if (scanner_mode) {
|
||||
File freqman_file;
|
||||
|
||||
delete_file(freq_file_path);
|
||||
|
||||
auto result = freqman_file.create(freq_file_path);
|
||||
if (!result.is_valid()) {
|
||||
for (size_t n = 0; n < frequency_list.size(); n++) {
|
||||
@@ -573,7 +629,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else // RECON MODE / MANUAL, only remove matching from output
|
||||
} else if (manual_mode) // only remove from output
|
||||
{
|
||||
File recon_file;
|
||||
File tmp_recon_file;
|
||||
@@ -637,10 +693,10 @@ ReconView::ReconView(NavigationView& nav)
|
||||
};
|
||||
|
||||
button_manual_recon.on_select = [this](Button&) {
|
||||
button_remove.set_text("DELETE");
|
||||
scanner_mode = false;
|
||||
manual_mode = true;
|
||||
recon_pause();
|
||||
|
||||
if (!frequency_range.min || !frequency_range.max) {
|
||||
nav_.display_modal("Error", "Both START and END freqs\nneed a value");
|
||||
} else if (frequency_range.min > frequency_range.max) {
|
||||
@@ -648,17 +704,20 @@ ReconView::ReconView(NavigationView& nav)
|
||||
} else {
|
||||
audio::output::stop();
|
||||
|
||||
frequency_list.clear();
|
||||
// clear and shrink_to_fit are not enough to really start with a new, clean, empty vector
|
||||
// swap is the only way to achieve a perfect memory liberation
|
||||
std::vector<freqman_entry>().swap(frequency_list);
|
||||
|
||||
freqman_entry manual_freq_entry;
|
||||
|
||||
def_step = step_mode.selected_index(); // max range val
|
||||
|
||||
manual_freq_entry.type = RANGE;
|
||||
manual_freq_entry.description =
|
||||
"R " + to_string_short_freq(frequency_range.min) + ">" + to_string_short_freq(frequency_range.max) + " S" // current Manual range
|
||||
+ to_string_short_freq(freqman_entry_get_step_value(def_step)).erase(0, 1); // euquiq: lame kludge to reduce spacing in step freq
|
||||
manual_freq_entry.frequency_a = frequency_range.min; // min range val
|
||||
manual_freq_entry.frequency_b = frequency_range.max; // max range val
|
||||
to_string_short_freq(frequency_range.min).erase(0, 1) + ">" + to_string_short_freq(frequency_range.max).erase(0, 1) + " S:" // current Manual range
|
||||
+ freqman_entry_get_step_string_short(def_step); // euquiq: lame kludge to reduce spacing in step freq
|
||||
manual_freq_entry.frequency_a = frequency_range.min; // min range val
|
||||
manual_freq_entry.frequency_b = frequency_range.max; // max range val
|
||||
manual_freq_entry.modulation = -1;
|
||||
manual_freq_entry.bandwidth = -1;
|
||||
manual_freq_entry.step = def_step;
|
||||
@@ -677,7 +736,11 @@ ReconView::ReconView(NavigationView& nav)
|
||||
file_name.set_style(&style_white);
|
||||
file_name.set("MANUAL RANGE RECON");
|
||||
desc_cycle.set_style(&style_white);
|
||||
desc_cycle.set("MANUAL RANGE RECON");
|
||||
|
||||
last_entry.modulation = -1;
|
||||
last_entry.bandwidth = -1;
|
||||
last_entry.step = -1;
|
||||
last_index = -1;
|
||||
|
||||
current_index = 0;
|
||||
freq = manual_freq_entry.frequency_a;
|
||||
@@ -696,7 +759,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
button_dir.set_text("FW>");
|
||||
}
|
||||
timer = 0;
|
||||
if (userpause)
|
||||
if (!recon)
|
||||
recon_resume();
|
||||
};
|
||||
|
||||
@@ -724,7 +787,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
|
||||
button_add.on_select = [this](ButtonWithEncoder&) { // frequency_list[current_index]
|
||||
if (!scanner_mode) {
|
||||
ReconSaveFreq(freq_file_path, current_index, true);
|
||||
recon_save_freq(freq_file_path, current_index, true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -739,10 +802,12 @@ ReconView::ReconView(NavigationView& nav)
|
||||
scanner_mode = false;
|
||||
button_scanner_mode.set_style(&style_blue);
|
||||
button_scanner_mode.set_text("RECON");
|
||||
button_remove.set_text("DELETE");
|
||||
} else {
|
||||
scanner_mode = true;
|
||||
button_scanner_mode.set_style(&style_red);
|
||||
button_scanner_mode.set_text("SCANNER");
|
||||
button_scanner_mode.set_text("REMOVE");
|
||||
}
|
||||
frequency_file_load(true);
|
||||
if (autostart) {
|
||||
@@ -752,21 +817,19 @@ ReconView::ReconView(NavigationView& nav)
|
||||
}
|
||||
};
|
||||
|
||||
button_recon_setup.on_select = [this, &nav](Button&) {
|
||||
audio::output::stop();
|
||||
|
||||
frequency_list.clear();
|
||||
button_config.on_select = [this, &nav](Button&) {
|
||||
clear_freqlist_for_ui_action();
|
||||
|
||||
auto open_view = nav.push<ReconSetupView>(input_file, output_file, recon_lock_duration, recon_lock_nb_match, recon_match_mode);
|
||||
open_view->on_changed = [this](std::vector<std::string> result) {
|
||||
freqlist_cleared_for_ui_action = false;
|
||||
input_file = result[0];
|
||||
output_file = result[1];
|
||||
freq_file_path = "/FREQMAN/" + output_file + ".TXT";
|
||||
recon_lock_duration = strtol(result[2].c_str(), nullptr, 10);
|
||||
recon_lock_nb_match = strtol(result[3].c_str(), nullptr, 10);
|
||||
recon_match_mode = strtol(result[4].c_str(), nullptr, 10);
|
||||
|
||||
ReconSetupSaveStrings("RECON/RECON.CFG", input_file, output_file, recon_lock_duration, recon_lock_nb_match, squelch, recon_match_mode, wait, field_volume.value());
|
||||
recon_save_config_to_sd();
|
||||
|
||||
autosave = persistent_memory::recon_autosave_freqs();
|
||||
autostart = persistent_memory::recon_autostart_recon();
|
||||
@@ -775,48 +838,30 @@ ReconView::ReconView(NavigationView& nav)
|
||||
load_freqs = persistent_memory::recon_load_freqs();
|
||||
load_ranges = persistent_memory::recon_load_ranges();
|
||||
load_hamradios = persistent_memory::recon_load_hamradios();
|
||||
|
||||
update_ranges = persistent_memory::recon_update_ranges_when_recon();
|
||||
|
||||
field_wait.set_value(wait);
|
||||
field_lock_wait.set_value(recon_lock_duration);
|
||||
colorize_waits();
|
||||
|
||||
frequency_file_load(false);
|
||||
|
||||
if (autostart) {
|
||||
recon_resume();
|
||||
} else {
|
||||
recon_pause();
|
||||
}
|
||||
if (userpause != true) {
|
||||
recon_resume();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
field_wait.on_change = [this](int32_t v) {
|
||||
wait = v;
|
||||
if (wait == 0) {
|
||||
field_wait.set_style(&style_blue);
|
||||
} else if (wait >= 500) {
|
||||
field_wait.set_style(&style_white);
|
||||
} else if (wait > -500 && wait < 500) {
|
||||
field_wait.set_style(&style_red);
|
||||
} else if (wait <= -500) {
|
||||
field_wait.set_style(&style_green);
|
||||
}
|
||||
colorize_waits();
|
||||
};
|
||||
|
||||
field_lock_wait.on_change = [this](uint32_t v) {
|
||||
recon_lock_duration = v;
|
||||
if (recon_match_mode == RECON_MATCH_CONTINUOUS) {
|
||||
if ((v / STATS_UPDATE_INTERVAL) > recon_lock_nb_match) {
|
||||
field_lock_wait.set_style(&style_white);
|
||||
} else if ((v / STATS_UPDATE_INTERVAL) == recon_lock_nb_match) {
|
||||
field_lock_wait.set_style(&style_yellow);
|
||||
}
|
||||
} else // RECON_MATCH_SPARSE
|
||||
{
|
||||
field_lock_wait.set_style(&style_white);
|
||||
}
|
||||
colorize_waits();
|
||||
};
|
||||
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
@@ -833,12 +878,14 @@ ReconView::ReconView(NavigationView& nav)
|
||||
file_name.set("=>");
|
||||
|
||||
// Loading input and output file from settings
|
||||
ReconSetupLoadStrings("RECON/RECON.CFG", input_file, output_file, recon_lock_duration, recon_lock_nb_match, squelch, recon_match_mode, wait, volume);
|
||||
recon_load_config_from_sd();
|
||||
freq_file_path = "/FREQMAN/" + output_file + ".TXT";
|
||||
|
||||
field_squelch.set_value(squelch);
|
||||
field_wait.set_value(wait);
|
||||
field_lock_wait.set_value(recon_lock_duration);
|
||||
colorize_waits();
|
||||
|
||||
field_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99);
|
||||
|
||||
// fill modulation and step options
|
||||
@@ -860,7 +907,6 @@ ReconView::ReconView(NavigationView& nav)
|
||||
} else {
|
||||
recon_pause();
|
||||
}
|
||||
|
||||
recon_redraw();
|
||||
}
|
||||
|
||||
@@ -869,8 +915,10 @@ void ReconView::frequency_file_load(bool stop_all_before) {
|
||||
audio::output::stop();
|
||||
|
||||
def_step = step_mode.selected_index(); // use def_step from manual selector
|
||||
frequency_list.clear(); // clear the existing frequency list (expected behavior)
|
||||
std::string file_input = input_file; // default recon mode
|
||||
// clear and shrink_to_fit are not enough to really start with a new, clean, empty vector
|
||||
// swap is the only way to achieve a perfect memory liberation
|
||||
std::vector<freqman_entry>().swap(frequency_list); // clear the existing frequency list (expected behavior)
|
||||
std::string file_input = input_file; // default recon mode
|
||||
if (scanner_mode) {
|
||||
file_input = output_file;
|
||||
file_name.set_style(&style_red);
|
||||
@@ -881,9 +929,8 @@ void ReconView::frequency_file_load(bool stop_all_before) {
|
||||
button_scanner_mode.set_style(&style_blue);
|
||||
button_scanner_mode.set_text("RECON");
|
||||
}
|
||||
file_name.set_style(&style_white);
|
||||
desc_cycle.set_style(&style_white);
|
||||
if (!load_freqman_file_ex(file_input, frequency_list, load_freqs, load_ranges, load_hamradios)) {
|
||||
if (!load_freqman_file_ex(file_input, frequency_list, load_freqs, load_ranges, load_hamradios, RECON_FREQMAN_MAX_PER_FILE)) {
|
||||
file_name.set_style(&style_red);
|
||||
desc_cycle.set_style(&style_red);
|
||||
desc_cycle.set(" NO " + file_input + ".TXT FILE ...");
|
||||
@@ -896,7 +943,7 @@ void ReconView::frequency_file_load(bool stop_all_before) {
|
||||
desc_cycle.set("/0 no entries in list");
|
||||
file_name.set("BadOrEmpty " + file_input);
|
||||
} else {
|
||||
if (frequency_list.size() > FREQMAN_MAX_PER_FILE) {
|
||||
if (frequency_list.size() > RECON_FREQMAN_MAX_PER_FILE) {
|
||||
file_name.set_style(&style_yellow);
|
||||
desc_cycle.set_style(&style_yellow);
|
||||
}
|
||||
@@ -935,15 +982,10 @@ void ReconView::frequency_file_load(bool stop_all_before) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
last_entry.modulation = -1;
|
||||
last_entry.bandwidth = -1;
|
||||
last_entry.step = -1;
|
||||
|
||||
reset_indexes();
|
||||
step_mode.set_selected_index(def_step); // Impose the default step into the manual step selector
|
||||
receiver_model.enable();
|
||||
receiver_model.set_squelch_level(0);
|
||||
std::string description = "...no description...";
|
||||
current_index = 0;
|
||||
if (frequency_list.size() != 0) {
|
||||
switch (frequency_list[current_index].type) {
|
||||
case RANGE:
|
||||
@@ -958,7 +1000,7 @@ void ReconView::frequency_file_load(bool stop_all_before) {
|
||||
break;
|
||||
}
|
||||
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
|
||||
if (update_ranges) {
|
||||
if (update_ranges && !manual_mode) {
|
||||
button_manual_start.set_text(to_string_short_freq(frequency_list[current_index].frequency_a));
|
||||
frequency_range.min = frequency_list[current_index].frequency_a;
|
||||
if (frequency_list[current_index].frequency_b != 0) {
|
||||
@@ -977,8 +1019,20 @@ void ReconView::frequency_file_load(bool stop_all_before) {
|
||||
}
|
||||
|
||||
void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
// hack to reload the list if it was cleared by going into CONFIG
|
||||
if (freqlist_cleared_for_ui_action) {
|
||||
if (!manual_mode) {
|
||||
frequency_file_load(false);
|
||||
}
|
||||
if (autostart) {
|
||||
recon_resume();
|
||||
} else {
|
||||
recon_pause();
|
||||
}
|
||||
freqlist_cleared_for_ui_action = false;
|
||||
}
|
||||
db = statistics.max_db;
|
||||
if (!userpause) {
|
||||
if (recon) {
|
||||
if (!timer) {
|
||||
status = 0;
|
||||
continuous_lock = false;
|
||||
@@ -1177,7 +1231,6 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// send a pause message with the right freq
|
||||
if (has_looped && !continuous) {
|
||||
recon_pause();
|
||||
}
|
||||
@@ -1195,7 +1248,6 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
void ReconView::recon_pause() {
|
||||
timer = 0;
|
||||
freq_lock = 0;
|
||||
userpause = true;
|
||||
continuous_lock = false;
|
||||
recon = false;
|
||||
|
||||
@@ -1208,7 +1260,6 @@ void ReconView::recon_pause() {
|
||||
void ReconView::recon_resume() {
|
||||
timer = 0;
|
||||
freq_lock = 0;
|
||||
userpause = false;
|
||||
continuous_lock = false;
|
||||
recon = true;
|
||||
|
||||
@@ -1222,7 +1273,8 @@ void ReconView::on_index_delta(int32_t v) {
|
||||
if (v > 0) {
|
||||
fwd = true;
|
||||
button_dir.set_text("FW>");
|
||||
} else {
|
||||
}
|
||||
if (v < 0) {
|
||||
fwd = false;
|
||||
button_dir.set_text("<RW");
|
||||
}
|
||||
@@ -1237,7 +1289,8 @@ void ReconView::on_stepper_delta(int32_t v) {
|
||||
if (v > 0) {
|
||||
fwd = true;
|
||||
button_dir.set_text("FW>");
|
||||
} else {
|
||||
}
|
||||
if (v < 0) {
|
||||
fwd = false;
|
||||
button_dir.set_text("<RW");
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -42,6 +43,9 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
#define RECON_CFG_FILE "SETTINGS/recon.cfg"
|
||||
#define RECON_FREQMAN_MAX_PER_FILE 99 // maximum tested limit for load frequency from freqman to work
|
||||
|
||||
class ReconView : public View {
|
||||
public:
|
||||
ReconView(NavigationView& nav);
|
||||
@@ -98,6 +102,8 @@ class ReconView : public View {
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
void clear_freqlist_for_ui_action();
|
||||
void reset_indexes();
|
||||
void audio_output_start();
|
||||
bool check_sd_card();
|
||||
size_t change_mode(freqman_index_t mod_type);
|
||||
@@ -109,13 +115,13 @@ class ReconView : public View {
|
||||
void on_headphone_volume_changed(int32_t v);
|
||||
void on_index_delta(int32_t v);
|
||||
void on_stepper_delta(int32_t v);
|
||||
void colorize_waits();
|
||||
void recon_redraw();
|
||||
void handle_retune();
|
||||
void handle_coded_squelch(const uint32_t value);
|
||||
bool ReconSetupLoadStrings(const std::string& source, std::string& input_file, std::string& output_file, uint32_t& recon_lock_duration, uint32_t& recon_lock_nb_match, int32_t& recon_squelch_level, uint32_t& recon_match_mode, int32_t& wait, int32_t& volume);
|
||||
bool ReconSetupSaveStrings(const std::string& dest, const std::string& input_file, const std::string& output_file, uint32_t recon_lock_duration, uint32_t recon_lock_nb_match, int32_t recon_squelch_level, uint32_t recon_match_mode, int32_t wait, int32_t volume);
|
||||
bool ReconSaveFreq(const std::string& freq_file_path, size_t index, bool warn_if_exists);
|
||||
|
||||
bool recon_load_config_from_sd();
|
||||
bool recon_save_config_to_sd();
|
||||
bool recon_save_freq(const std::string& freq_file_path, size_t index, bool warn_if_exists);
|
||||
jammer::jammer_range_t frequency_range{false, 0, MAX_UFREQ}; // perfect for manual recon task too...
|
||||
int32_t squelch{0};
|
||||
int32_t db{0};
|
||||
@@ -123,10 +129,11 @@ class ReconView : public View {
|
||||
int32_t wait{RECON_DEF_WAIT_DURATION}; // in msec. if > 0 wait duration after a lock, if < 0 duration is set to 'wait' unless there is no more activity
|
||||
freqman_db frequency_list = {};
|
||||
int32_t current_index{0};
|
||||
bool userpause{false};
|
||||
bool continuous_lock{false};
|
||||
bool freqlist_cleared_for_ui_action{false}; // flag positioned by ui widgets to manage freqlist unload/load
|
||||
std::string input_file = {"RECON"};
|
||||
std::string output_file = {"RECON_RESULTS"};
|
||||
std::string description = {"...no description..."};
|
||||
bool autosave = {true};
|
||||
bool autostart = {true};
|
||||
bool continuous = {true};
|
||||
@@ -227,15 +234,21 @@ class ReconView : public View {
|
||||
};
|
||||
|
||||
RSSI rssi{
|
||||
{0 * 16, 2 * 16, SCREEN_W - 8 * 8 + 4, 16},
|
||||
{0 * 16, 2 * 16, SCREEN_W - 8 * 8 + 4, 14},
|
||||
};
|
||||
|
||||
ButtonWithEncoder text_cycle{
|
||||
{0, 3 * 16, 4 * 8, 16},
|
||||
""};
|
||||
|
||||
// "/XXX -XXX db" => 12 chars max
|
||||
Text text_max{
|
||||
{4 * 8, 3 * 16, SCREEN_W - 7 * 8 - 4 * 8, 16},
|
||||
{4 * 8, 3 * 16, 12 * 8, 16},
|
||||
};
|
||||
|
||||
// "XX/XX" => 5 chars max
|
||||
Text text_nb_locks{
|
||||
{16 * 8, 3 * 16, 5 * 8, 16},
|
||||
};
|
||||
|
||||
Text desc_cycle{
|
||||
@@ -263,7 +276,7 @@ class ReconView : public View {
|
||||
{12 * 8 + 4, 7 * 16, 14 * 8, 1 * 8},
|
||||
""};
|
||||
|
||||
Button button_recon_setup{
|
||||
Button button_config{
|
||||
{SCREEN_W - 7 * 8, 2 * 16, 7 * 8, 28},
|
||||
"CONFIG"};
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -92,7 +93,7 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
|
||||
};
|
||||
};
|
||||
|
||||
void ReconSetupViewMain::Save(std::string& input_file, std::string& output_file) {
|
||||
void ReconSetupViewMain::save(std::string& input_file, std::string& output_file) {
|
||||
persistent_memory::set_recon_autosave_freqs(checkbox_autosave_freqs.value());
|
||||
persistent_memory::set_recon_autostart_recon(checkbox_autostart_recon.value());
|
||||
persistent_memory::set_recon_continuous(checkbox_continuous.value());
|
||||
@@ -100,7 +101,7 @@ void ReconSetupViewMain::Save(std::string& input_file, std::string& output_file)
|
||||
input_file = _input_file;
|
||||
output_file = _output_file;
|
||||
};
|
||||
void ReconSetupViewMore::Save(uint32_t& recon_lock_duration, uint32_t& recon_lock_nb_match, uint32_t& recon_match_mode) {
|
||||
void ReconSetupViewMore::save(uint32_t& recon_lock_duration, uint32_t& recon_lock_nb_match, uint32_t& recon_match_mode) {
|
||||
persistent_memory::set_recon_load_freqs(checkbox_load_freqs.value());
|
||||
persistent_memory::set_recon_load_ranges(checkbox_load_ranges.value());
|
||||
persistent_memory::set_recon_load_hamradios(checkbox_load_hamradios.value());
|
||||
@@ -162,8 +163,8 @@ ReconSetupView::ReconSetupView(
|
||||
&button_save});
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
viewMain.Save(input_file, output_file);
|
||||
viewMore.Save(recon_lock_duration, recon_lock_nb_match, recon_match_mode);
|
||||
viewMain.save(input_file, output_file);
|
||||
viewMore.save(recon_lock_duration, recon_lock_nb_match, recon_match_mode);
|
||||
std::vector<std::string> messages;
|
||||
messages.push_back(input_file);
|
||||
messages.push_back(output_file);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -66,7 +67,7 @@ namespace ui {
|
||||
class ReconSetupViewMain : public View {
|
||||
public:
|
||||
ReconSetupViewMain(NavigationView& nav, Rect parent_rect, std::string input_file, std::string output_file);
|
||||
void Save(std::string& input_file, std::string& output_file);
|
||||
void save(std::string& input_file, std::string& output_file);
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
@@ -111,7 +112,7 @@ class ReconSetupViewMore : public View {
|
||||
public:
|
||||
ReconSetupViewMore(NavigationView& nav, Rect parent_rect, uint32_t _recon_lock_duration, uint32_t _recon_lock_nb_match, uint32_t _recon_match_mode);
|
||||
|
||||
void Save(uint32_t& recon_lock_duration, uint32_t& recon_lock_nb_match, uint32_t& recon_match_mode);
|
||||
void save(uint32_t& recon_lock_duration, uint32_t& recon_lock_nb_match, uint32_t& recon_match_mode);
|
||||
|
||||
void focus() override;
|
||||
|
||||
@@ -139,20 +140,20 @@ class ReconSetupViewMore : public View {
|
||||
{1 * 8, 102},
|
||||
3,
|
||||
"auto update m-ranges"};
|
||||
|
||||
NumberField field_recon_lock_duration{
|
||||
{1 * 8, 132}, // position X , Y
|
||||
4, // number of displayed digits (even empty)
|
||||
{STATS_UPDATE_INTERVAL, RECON_MAX_LOCK_DURATION}, // range of number
|
||||
STATS_UPDATE_INTERVAL, // rotary encoder increment
|
||||
' ', // filling character
|
||||
false // can loop
|
||||
};
|
||||
|
||||
Text text_recon_lock_duration{
|
||||
{1 * 8, 132, 22 * 8, 22},
|
||||
" ms (lock duration)"};
|
||||
NumberField field_recon_lock_duration{
|
||||
{1 * 8, 132}, // position X , Y
|
||||
4, // number of displayed digits (even empty)
|
||||
{-RECON_MAX_LOCK_DURATION, RECON_MAX_LOCK_DURATION}, // range of number
|
||||
STATS_UPDATE_INTERVAL, // rotary encoder increment
|
||||
' ', // filling character
|
||||
false // can loop
|
||||
};
|
||||
Text text_recon_lock_nb{
|
||||
{1 * 8, 162, 25 * 8, 22},
|
||||
" x (nb lock to match freq)"};
|
||||
|
||||
NumberField field_recon_lock_nb_match{
|
||||
{1 * 8, 162},
|
||||
4,
|
||||
@@ -160,6 +161,11 @@ class ReconSetupViewMore : public View {
|
||||
1,
|
||||
' ',
|
||||
false};
|
||||
|
||||
Text text_recon_lock_nb{
|
||||
{1 * 8, 162, 25 * 8, 22},
|
||||
" x (nb lock to match freq)"};
|
||||
|
||||
OptionsField field_recon_match_mode{
|
||||
{1 * 8, 192},
|
||||
20, // CONTINUOUS MATCH MODE / SPARSE TIMED MATCH MODE
|
||||
@@ -195,6 +201,7 @@ class ReconSetupView : public View {
|
||||
TabView tab_view{
|
||||
{"Main", Color::cyan(), &viewMain},
|
||||
{"More", Color::green(), &viewMore}};
|
||||
|
||||
Button button_save{
|
||||
{9 * 8, 255, 14 * 8, 40},
|
||||
"SAVE"};
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -195,6 +196,15 @@ SetRadioView::SetRadioView(
|
||||
check_bias.set_value(portapack::get_antenna_bias());
|
||||
check_bias.on_select = [this](Checkbox&, bool v) {
|
||||
portapack::set_antenna_bias(v);
|
||||
|
||||
// Update the radio.
|
||||
receiver_model.set_antenna_bias();
|
||||
transmitter_model.set_antenna_bias();
|
||||
// The models won't actually disable this if they are not 'enabled_'.
|
||||
// Be extra sure this is turned off.
|
||||
if (!v)
|
||||
radio::set_antenna_bias(false);
|
||||
|
||||
StatusRefreshMessage message{};
|
||||
EventDispatcher::send_message(message);
|
||||
};
|
||||
@@ -398,8 +408,8 @@ SetFrequencyCorrectionView::SetFrequencyCorrectionView(NavigationView& nav) {
|
||||
button_freq_rx_correction.on_select = [this, &nav](Button& button) {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(portapack::persistent_memory::config_converter_freq());
|
||||
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||
if (f > 4000000)
|
||||
f = 4000000;
|
||||
if (f >= MAX_FREQ_CORRECTION)
|
||||
f = MAX_FREQ_CORRECTION;
|
||||
portapack::persistent_memory::set_config_freq_rx_correction(f);
|
||||
// Retune to take converter change in account
|
||||
receiver_model.set_tuning_frequency(portapack::persistent_memory::tuned_frequency());
|
||||
@@ -411,8 +421,8 @@ SetFrequencyCorrectionView::SetFrequencyCorrectionView(NavigationView& nav) {
|
||||
button_freq_tx_correction.on_select = [this, &nav](Button& button) {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(portapack::persistent_memory::config_converter_freq());
|
||||
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||
if (f > 4000000)
|
||||
f = 4000000;
|
||||
if (f >= MAX_FREQ_CORRECTION)
|
||||
f = MAX_FREQ_CORRECTION;
|
||||
portapack::persistent_memory::set_config_freq_tx_correction(f);
|
||||
// Retune to take converter change in account
|
||||
receiver_model.set_tuning_frequency(portapack::persistent_memory::tuned_frequency());
|
||||
@@ -519,9 +529,9 @@ void SetPersistentMemoryView::focus() {
|
||||
button_return.focus();
|
||||
}
|
||||
|
||||
//
|
||||
// Audio settings
|
||||
//
|
||||
// ---------------------------------------------------------
|
||||
// Audio Settings
|
||||
// ---------------------------------------------------------
|
||||
SetAudioView::SetAudioView(NavigationView& nav) {
|
||||
add_children({&labels,
|
||||
&field_tone_mix,
|
||||
@@ -544,6 +554,9 @@ void SetAudioView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// QR Code Settings
|
||||
// ------------------------------------------------------
|
||||
SetQRCodeView::SetQRCodeView(NavigationView& nav) {
|
||||
add_children({&checkbox_bigger_qr,
|
||||
&button_save,
|
||||
@@ -565,6 +578,31 @@ void SetQRCodeView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Rotary Encoder Dial Settings
|
||||
// ---------------------------------------------------------
|
||||
SetEncoderDialView::SetEncoderDialView(NavigationView& nav) {
|
||||
add_children({&labels,
|
||||
&field_encoder_dial_sensitivity,
|
||||
&button_save,
|
||||
&button_cancel});
|
||||
|
||||
field_encoder_dial_sensitivity.set_by_value(persistent_memory::config_encoder_dial_sensitivity());
|
||||
|
||||
button_save.on_select = [&nav, this](Button&) {
|
||||
persistent_memory::set_encoder_dial_sensitivity(field_encoder_dial_sensitivity.selected_index_value());
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_cancel.on_select = [&nav, this](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
void SetEncoderDialView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Settings main menu
|
||||
// ---------------------------------------------------------
|
||||
@@ -583,6 +621,7 @@ SettingsMenuView::SettingsMenuView(NavigationView& nav) {
|
||||
{"FreqCorrection", 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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -33,6 +34,8 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
#define MAX_FREQ_CORRECTION 4294967295 // maximum possible for an uint32_t
|
||||
|
||||
struct SetDateTimeModel {
|
||||
uint16_t year;
|
||||
uint8_t month;
|
||||
@@ -429,6 +432,38 @@ class SetQRCodeView : public View {
|
||||
};
|
||||
};
|
||||
|
||||
using portapack::persistent_memory::encoder_dial_sensitivity;
|
||||
|
||||
class SetEncoderDialView : public View {
|
||||
public:
|
||||
SetEncoderDialView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Encoder Dial"; };
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{2 * 8, 3 * 16}, "Dial sensitivity:", Color::light_grey()},
|
||||
};
|
||||
|
||||
OptionsField field_encoder_dial_sensitivity{
|
||||
{20 * 8, 3 * 16},
|
||||
6,
|
||||
{{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
|
||||
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_MEDIUM},
|
||||
{"HIGH", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGH}}};
|
||||
|
||||
Button button_save{
|
||||
{2 * 8, 16 * 16, 12 * 8, 32},
|
||||
"Save"};
|
||||
|
||||
Button button_cancel{
|
||||
{16 * 8, 16 * 16, 12 * 8, 32},
|
||||
"Cancel",
|
||||
};
|
||||
};
|
||||
|
||||
class SetPersistentMemoryView : public View {
|
||||
public:
|
||||
SetPersistentMemoryView(NavigationView& nav);
|
||||
|
||||
@@ -107,7 +107,7 @@ SondeView::SondeView(NavigationView& nav) {
|
||||
receiver_model.set_tuning_frequency(tuning_frequency());
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
receiver_model.enable(); // Before using radio::enable(), but not updating Ant.DC-Bias.
|
||||
receiver_model.enable();
|
||||
|
||||
// QR code with geo URI
|
||||
button_see_qr.on_select = [this, &nav](Button&) {
|
||||
@@ -155,7 +155,7 @@ SondeView::~SondeView() {
|
||||
|
||||
baseband::set_pitch_rssi(0, false);
|
||||
|
||||
receiver_model.disable(); // to switch off all, including DC bias.
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
}
|
||||
@@ -258,8 +258,6 @@ void SondeView::on_headphone_volume_changed(int32_t v) {
|
||||
|
||||
void SondeView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
// radio::set_tuning_frequency(tuning_frequency());
|
||||
// we better remember the tuned frequency, by using this function instead:
|
||||
receiver_model.set_tuning_frequency(target_frequency_);
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ SpectrumPainterView::SpectrumPainterView(
|
||||
};
|
||||
};
|
||||
|
||||
tx_gain = 10;
|
||||
tx_gain = portapack::transmitter_model.tx_gain();
|
||||
field_rfgain.set_value(tx_gain); // Initial default value (-12 dB's max ).
|
||||
field_rfgain.on_change = [this](int32_t v) { // allow initial value change just after opened file.
|
||||
tx_gain = v;
|
||||
@@ -93,16 +93,9 @@ SpectrumPainterView::SpectrumPainterView(
|
||||
if (tx_mode == 0 && image_input_avaliable == false)
|
||||
return;
|
||||
|
||||
// Enable Bias Tee if selected
|
||||
radio::set_antenna_bias(portapack::get_antenna_bias());
|
||||
|
||||
radio::enable({portapack::receiver_model.tuning_frequency(),
|
||||
3072000U,
|
||||
1750000,
|
||||
rf::Direction::Transmit,
|
||||
rf_amp,
|
||||
static_cast<int8_t>(portapack::receiver_model.lna()),
|
||||
static_cast<int8_t>(portapack::receiver_model.vga())});
|
||||
portapack::transmitter_model.set_sampling_rate(3072000U);
|
||||
portapack::transmitter_model.set_baseband_bandwidth(1750000);
|
||||
portapack::transmitter_model.enable();
|
||||
|
||||
if (portapack::persistent_memory::stealth_mode()) {
|
||||
DisplaySleepMessage message;
|
||||
|
||||
@@ -81,19 +81,14 @@ TestView::TestView(NavigationView& nav) {
|
||||
if (logger)
|
||||
logger->append("saucepan.txt");
|
||||
|
||||
radio::enable({
|
||||
tuning_frequency(),
|
||||
sampling_rate,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Receive,
|
||||
receiver_model.rf_amp(),
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga()),
|
||||
});
|
||||
receiver_model.set_tuning_frequency(tuning_frequency());
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
TestView::~TestView() {
|
||||
radio::disable();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
@@ -127,8 +122,6 @@ void TestView::on_packet(const testapp::Packet& packet) {
|
||||
if (logger && logging)
|
||||
logger->log_raw_data(packet, raw_alt - cal_value);
|
||||
|
||||
// radio::disable();
|
||||
|
||||
/*text_serial.set(packet.serial_number());
|
||||
text_voltage.set(unit_auto_scale(packet.battery_voltage(), 2, 3) + "V");
|
||||
|
||||
@@ -139,7 +132,7 @@ void TestView::on_packet(const testapp::Packet& packet) {
|
||||
|
||||
void TestView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
radio::set_tuning_frequency(tuning_frequency());
|
||||
receiver_model.set_tuning_frequency(tuning_frequency());
|
||||
}
|
||||
|
||||
uint32_t TestView::tuning_frequency() const {
|
||||
|
||||
@@ -21,58 +21,53 @@
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_text_editor.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
|
||||
#include "log_file.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
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 {
|
||||
|
||||
TextEditorView::TextEditorView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children(
|
||||
{&button_open,
|
||||
&text_position});
|
||||
/* TextViewer *******************************************************/
|
||||
|
||||
TextViewer::TextViewer(Rect parent_rect)
|
||||
: Widget(parent_rect),
|
||||
max_line{static_cast<uint8_t>(parent_rect.height() / char_height)},
|
||||
max_col{static_cast<uint8_t>(parent_rect.width() / char_width)} {
|
||||
set_focusable(true);
|
||||
|
||||
// log_.append("LOGS/NOTEPAD.TXT");
|
||||
|
||||
button_open.on_select = [this](Button&) {
|
||||
auto open_view = nav_.push<FileLoadView>(".TXT");
|
||||
open_view->on_changed = [this](std::filesystem::path path) {
|
||||
open_file(path);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
void TextEditorView::on_focus() {
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
void TextEditorView::paint(Painter& painter) {
|
||||
void TextViewer::paint(Painter& painter) {
|
||||
auto first_line = paint_state_.first_line;
|
||||
auto first_col = paint_state_.first_col;
|
||||
|
||||
if (!paint_state_.has_file)
|
||||
if (!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;
|
||||
@@ -88,7 +83,7 @@ void TextEditorView::paint(Painter& painter) {
|
||||
paint_cursor(painter);
|
||||
}
|
||||
|
||||
bool TextEditorView::on_key(const KeyEvent key) {
|
||||
bool TextViewer::on_key(const KeyEvent key) {
|
||||
int16_t delta_col = 0;
|
||||
int16_t delta_line = 0;
|
||||
|
||||
@@ -100,19 +95,22 @@ bool TextEditorView::on_key(const KeyEvent key) {
|
||||
delta_line = -1;
|
||||
else if (key == KeyEvent::Down)
|
||||
delta_line = 1;
|
||||
/* else if (key == KeyEvent::Select)
|
||||
; // TODO: Edit/Menu */
|
||||
else if (key == KeyEvent::Select && on_select) {
|
||||
on_select();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Always allow cursor direction to be updated.
|
||||
cursor_.dir = delta_col != 0 ? ScrollDirection::Horizontal : ScrollDirection::Vertical;
|
||||
auto updated = apply_scrolling_constraints(delta_line, delta_col);
|
||||
|
||||
if (updated)
|
||||
refresh_ui();
|
||||
redraw();
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
bool TextEditorView::on_encoder(EncoderEvent delta) {
|
||||
bool TextViewer::on_encoder(EncoderEvent delta) {
|
||||
bool updated = false;
|
||||
|
||||
if (cursor_.dir == ScrollDirection::Horizontal)
|
||||
@@ -121,178 +119,109 @@ bool TextEditorView::on_encoder(EncoderEvent delta) {
|
||||
updated = apply_scrolling_constraints(delta, 0);
|
||||
|
||||
if (updated)
|
||||
refresh_ui();
|
||||
redraw();
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) {
|
||||
void TextViewer::redraw(bool redraw_text) {
|
||||
paint_state_.redraw_text = redraw_text;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
uint32_t TextViewer::offset() const {
|
||||
auto range = file_->line_range(cursor_.line);
|
||||
if (range)
|
||||
return range->start + col();
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t TextViewer::line_length() {
|
||||
return file_->line_length(cursor_.line);
|
||||
}
|
||||
|
||||
bool TextViewer::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) {
|
||||
if (!has_file())
|
||||
return false;
|
||||
|
||||
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;
|
||||
|
||||
if (on_cursor_moved)
|
||||
on_cursor_moved();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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();
|
||||
} else {
|
||||
button_open.focus();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) {
|
||||
auto r = screen_rect();
|
||||
auto& lines = info_.newlines;
|
||||
auto line_start = info_.line_start(line);
|
||||
char buffer[max_col + 1];
|
||||
|
||||
// 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 result = file_->get_text(line + i, col, buffer, max_col);
|
||||
|
||||
if (read_length > 0)
|
||||
if (result && *result > 0)
|
||||
painter.draw_string(
|
||||
{0, r.location().y() + (int)i * char_height},
|
||||
style_default, read(line_start + col, read_length));
|
||||
{0, r.top() + (int)i * char_height},
|
||||
style_text, {buffer, *result});
|
||||
|
||||
// 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. This is less visually jarring than full clear.
|
||||
int32_t clear_width = max_col - (result ? *result : 0);
|
||||
if (clear_width > 0)
|
||||
painter.fill_rectangle(
|
||||
{(max_col - clear_width) * char_width,
|
||||
r.top() + (int)i * char_height,
|
||||
clear_width * char_width, char_height},
|
||||
style_text.background);
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditorView::paint_cursor(Painter& painter) {
|
||||
void TextViewer::paint_cursor(Painter& painter) {
|
||||
if (!has_focus())
|
||||
return;
|
||||
|
||||
auto draw_cursor = [this, &painter](uint32_t line, uint16_t col, Color c) {
|
||||
auto r = screen_rect();
|
||||
line = line - paint_state_.first_line;
|
||||
@@ -300,23 +229,318 @@ void TextEditorView::paint_cursor(Painter& painter) {
|
||||
|
||||
painter.draw_rectangle(
|
||||
{(int)col * char_width - 1,
|
||||
r.location().y() + (int)line * char_height,
|
||||
r.top() + (int)line * char_height,
|
||||
char_width + 1, char_height},
|
||||
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);
|
||||
// Clear old cursor. CONSIDER: XOR cursor?
|
||||
draw_cursor(paint_state_.line, paint_state_.col, style_text.background);
|
||||
draw_cursor(cursor_.line, cursor_.col, style_text.foreground);
|
||||
paint_state_.line = cursor_.line;
|
||||
paint_state_.col = cursor_.col;
|
||||
}
|
||||
|
||||
uint16_t TextEditorView::line_length() const {
|
||||
return info_.line_length(cursor_.line);
|
||||
void TextViewer::reset_file(FileWrapper* file) {
|
||||
file_ = file;
|
||||
paint_state_.first_line = 0;
|
||||
paint_state_.first_col = 0;
|
||||
cursor_.line = 0;
|
||||
cursor_.col = 0;
|
||||
redraw(true);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
/* TextEditorMenu ***************************************************/
|
||||
|
||||
TextEditorMenu::TextEditorMenu()
|
||||
: View{{7 * 4, 9 * 4, 25 * 8, 25 * 8}} {
|
||||
add_children(
|
||||
{
|
||||
&rect_frame,
|
||||
&button_cut,
|
||||
&button_paste,
|
||||
&button_copy,
|
||||
&button_delline,
|
||||
&button_edit,
|
||||
&button_addline,
|
||||
&button_open,
|
||||
&button_save,
|
||||
&button_exit,
|
||||
});
|
||||
}
|
||||
|
||||
void TextEditorMenu::on_show() {
|
||||
hide_children(false);
|
||||
button_edit.focus();
|
||||
}
|
||||
|
||||
void TextEditorMenu::on_hide() {
|
||||
hide_children(true);
|
||||
}
|
||||
|
||||
void TextEditorMenu::hide_children(bool hidden) {
|
||||
for (auto child : children()) {
|
||||
child->hidden(hidden);
|
||||
}
|
||||
}
|
||||
|
||||
/* TextEditorView ***************************************************/
|
||||
|
||||
TextEditorView::TextEditorView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children(
|
||||
{
|
||||
&viewer,
|
||||
&menu,
|
||||
&button_menu,
|
||||
&text_position,
|
||||
&text_size,
|
||||
});
|
||||
|
||||
viewer.on_select = [this]() {
|
||||
// Treat as if menu button was pressed.
|
||||
if (button_menu.on_select)
|
||||
button_menu.on_select();
|
||||
};
|
||||
|
||||
viewer.on_cursor_moved = [this]() {
|
||||
update_position();
|
||||
};
|
||||
|
||||
menu.hidden(true);
|
||||
menu.on_cut() = [this]() {
|
||||
show_nyi();
|
||||
};
|
||||
menu.on_paste() = [this]() {
|
||||
show_nyi();
|
||||
};
|
||||
menu.on_copy() = [this]() {
|
||||
show_nyi();
|
||||
};
|
||||
|
||||
menu.on_delete_line() = [this]() {
|
||||
prepare_for_write();
|
||||
file_->delete_line(viewer.line());
|
||||
refresh_ui();
|
||||
hide_menu(true);
|
||||
};
|
||||
|
||||
menu.on_edit_line() = [this]() {
|
||||
show_edit_line();
|
||||
};
|
||||
|
||||
menu.on_add_line() = [this]() {
|
||||
prepare_for_write();
|
||||
|
||||
if (viewer.offset() < file_->size() - 1)
|
||||
file_->insert_line(viewer.line());
|
||||
else
|
||||
file_->insert_line(-1); // Add after last line.
|
||||
|
||||
refresh_ui();
|
||||
hide_menu(true);
|
||||
};
|
||||
|
||||
menu.on_open() = [this]() {
|
||||
/*show_save_prompt([this]() {
|
||||
show_file_picker();
|
||||
});*/
|
||||
// HACK: above should work but it's faulting.
|
||||
if (!file_dirty_) {
|
||||
show_file_picker();
|
||||
} else {
|
||||
show_save_prompt(nullptr);
|
||||
show_file_picker(false);
|
||||
}
|
||||
};
|
||||
|
||||
menu.on_save() = [this]() {
|
||||
save_temp_file();
|
||||
hide_menu(true);
|
||||
};
|
||||
|
||||
menu.on_exit() = [this]() {
|
||||
show_save_prompt([this]() {
|
||||
nav_.pop();
|
||||
});
|
||||
};
|
||||
|
||||
button_menu.on_select = [this]() {
|
||||
if (file_) {
|
||||
// Toggle menu.
|
||||
hide_menu(!menu.hidden());
|
||||
} else {
|
||||
show_file_picker();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TextEditorView::TextEditorView(NavigationView& nav, const fs::path& path)
|
||||
: TextEditorView(nav) {
|
||||
open_file(path);
|
||||
}
|
||||
|
||||
TextEditorView::~TextEditorView() {
|
||||
delete_temp_file();
|
||||
}
|
||||
|
||||
void TextEditorView::on_show() {
|
||||
if (file_)
|
||||
viewer.focus();
|
||||
else
|
||||
button_menu.focus();
|
||||
}
|
||||
|
||||
void TextEditorView::open_file(const fs::path& path) {
|
||||
file_.reset();
|
||||
viewer.clear_file();
|
||||
delete_temp_file();
|
||||
|
||||
path_ = {};
|
||||
file_dirty_ = false;
|
||||
has_temp_file_ = false;
|
||||
auto result = FileWrapper::open(path);
|
||||
|
||||
if (!result) {
|
||||
nav_.display_modal("Read Error", "Cannot open file:\n" + result.error().what());
|
||||
|
||||
} else {
|
||||
file_ = *std::move(result);
|
||||
path_ = path;
|
||||
viewer.set_file(*file_);
|
||||
}
|
||||
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
void TextEditorView::refresh_ui() {
|
||||
if (file_) {
|
||||
update_position();
|
||||
text_size.set(
|
||||
"Lines:" + to_string_dec_uint(file_->line_count()) +
|
||||
" (" + to_string_file_size(file_->size()) + ")");
|
||||
} else {
|
||||
text_position.set("");
|
||||
text_size.set("");
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditorView::update_position() {
|
||||
if (viewer.has_file()) {
|
||||
text_position.set(
|
||||
"Ln " + to_string_dec_uint(viewer.line() + 1) +
|
||||
", Col " + to_string_dec_uint(viewer.col() + 1));
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditorView::hide_menu(bool hidden) {
|
||||
menu.hidden(hidden);
|
||||
|
||||
// Only let the viewer be focused when the menu is
|
||||
// not shown, otherwise menu focus gets confusing.
|
||||
viewer.set_focusable(hidden);
|
||||
|
||||
if (hidden)
|
||||
viewer.focus();
|
||||
|
||||
viewer.redraw(true);
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void TextEditorView::show_file_picker(bool immediate) {
|
||||
// TODO: immediate is a hack until nav_.on_pop is fixed.
|
||||
auto open_view = immediate ? nav_.push<FileLoadView>("") : nav_.push_under_current<FileLoadView>("");
|
||||
|
||||
if (open_view) {
|
||||
open_view->on_changed = [this](std::filesystem::path path) {
|
||||
open_file(path);
|
||||
hide_menu();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditorView::show_edit_line() {
|
||||
auto str = file_->get_text(viewer.line(), 0, viewer.line_length());
|
||||
if (!str) {
|
||||
nav_.display_modal("Error", "Failed to get line text.");
|
||||
return;
|
||||
}
|
||||
|
||||
edit_line_buffer_ = *std::move(str);
|
||||
|
||||
text_prompt(
|
||||
nav_,
|
||||
edit_line_buffer_,
|
||||
viewer.col(),
|
||||
max_edit_length,
|
||||
[this](std::string& buffer) {
|
||||
auto range = file_->line_range(viewer.line());
|
||||
if (!range)
|
||||
return;
|
||||
|
||||
prepare_for_write();
|
||||
file_->replace_range(*range, buffer);
|
||||
});
|
||||
nav_.set_on_pop([this]() {
|
||||
edit_line_buffer_.clear();
|
||||
refresh_ui();
|
||||
hide_menu(true);
|
||||
});
|
||||
}
|
||||
|
||||
void TextEditorView::show_nyi() {
|
||||
nav_.display_modal("Soon...", "Coming soon.");
|
||||
}
|
||||
|
||||
void TextEditorView::show_save_prompt(std::function<void()> continuation) {
|
||||
if (!file_dirty_) {
|
||||
if (continuation)
|
||||
continuation();
|
||||
return;
|
||||
}
|
||||
|
||||
nav_.display_modal(
|
||||
"Save?", "Save changes?", YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice)
|
||||
save_temp_file();
|
||||
});
|
||||
nav_.set_on_pop(continuation);
|
||||
}
|
||||
|
||||
void TextEditorView::prepare_for_write() {
|
||||
file_dirty_ = true;
|
||||
|
||||
if (has_temp_file_)
|
||||
return;
|
||||
|
||||
// Copy to temp file on write.
|
||||
has_temp_file_ = true;
|
||||
delete_temp_file();
|
||||
copy_file(path_, get_temp_path());
|
||||
file_->assume_file(get_temp_path());
|
||||
}
|
||||
|
||||
void TextEditorView::delete_temp_file() const {
|
||||
auto temp_path = get_temp_path();
|
||||
if (!temp_path.empty()) {
|
||||
delete_file(temp_path);
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditorView::save_temp_file() {
|
||||
if (file_dirty_) {
|
||||
delete_file(path_);
|
||||
copy_file(get_temp_path(), path_);
|
||||
file_dirty_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
fs::path TextEditorView::get_temp_path() const {
|
||||
if (!path_.empty())
|
||||
return path_ + "~";
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
/* TODO:
|
||||
* - Busy indicator while reading files.
|
||||
*/
|
||||
|
||||
#ifndef __UI_TEXT_EDITOR_H__
|
||||
#define __UI_TEXT_EDITOR_H__
|
||||
|
||||
@@ -27,108 +31,73 @@
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
//#include "ui_textentry.hpp"
|
||||
|
||||
#include "file.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "file_wrapper.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ui {
|
||||
|
||||
enum class LineEnding : uint8_t {
|
||||
LF,
|
||||
CRLF
|
||||
};
|
||||
|
||||
enum class ScrollDirection : uint8_t {
|
||||
Vertical,
|
||||
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;
|
||||
|
||||
uint32_t line_count() const {
|
||||
return newlines.size();
|
||||
}
|
||||
|
||||
uint32_t line_start(uint32_t line) const {
|
||||
return line == 0 ? 0 : (newlines[line - 1] + 1);
|
||||
}
|
||||
|
||||
uint16_t line_length(uint32_t line) const {
|
||||
if (line >= line_count())
|
||||
return 0;
|
||||
|
||||
auto start = line_start(line);
|
||||
auto end = newlines[line];
|
||||
return end - start;
|
||||
}
|
||||
};
|
||||
|
||||
/*class TextViewer : public Widget {
|
||||
};*/
|
||||
|
||||
class TextEditorView : public View {
|
||||
/* Control that renders a text file. */
|
||||
class TextViewer : public Widget {
|
||||
public:
|
||||
TextEditorView(NavigationView& nav);
|
||||
// TextEditorView(NavigationView& nav, const std::filesystem::path& path);
|
||||
TextViewer(Rect parent_rect);
|
||||
|
||||
std::string title() const override {
|
||||
return "Notepad";
|
||||
};
|
||||
TextViewer(const TextViewer&) = delete;
|
||||
TextViewer(TextViewer&&) = delete;
|
||||
TextViewer& operator=(const TextViewer&) = delete;
|
||||
TextViewer& operator=(TextViewer&&) = delete;
|
||||
|
||||
std::function<void()> on_select{};
|
||||
std::function<void()> on_cursor_moved{};
|
||||
|
||||
void on_focus() override;
|
||||
void paint(Painter& painter) override;
|
||||
bool on_key(KeyEvent delta) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
|
||||
void redraw(bool redraw_text = false);
|
||||
|
||||
void set_file(FileWrapper& file) { reset_file(&file); }
|
||||
void clear_file() { reset_file(); }
|
||||
bool has_file() const { return file_ != nullptr; }
|
||||
|
||||
uint32_t line() const { return cursor_.line; }
|
||||
uint32_t col() const { return cursor_.col; }
|
||||
uint32_t offset() const;
|
||||
|
||||
// Gets the length of the current line.
|
||||
uint16_t line_length();
|
||||
|
||||
private:
|
||||
static constexpr uint8_t max_line = 32;
|
||||
static constexpr uint8_t max_col = 48;
|
||||
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{
|
||||
static constexpr Style style_text{
|
||||
.font = font::fixed_5x8,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::white(),
|
||||
};
|
||||
|
||||
const uint8_t max_line = 32;
|
||||
const uint8_t max_col = 48;
|
||||
|
||||
/* Returns true if the cursor was updated. */
|
||||
bool apply_scrolling_constraints(
|
||||
int16_t delta_line,
|
||||
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;
|
||||
void reset_file(FileWrapper* file = nullptr);
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
File file_{};
|
||||
FileInfo info_{};
|
||||
// LogFile log_{ };
|
||||
FileWrapper* file_{};
|
||||
|
||||
struct {
|
||||
// Previous cursor state.
|
||||
@@ -139,7 +108,6 @@ class TextEditorView : public View {
|
||||
uint32_t first_line{};
|
||||
uint16_t first_col{};
|
||||
bool redraw_text{true};
|
||||
bool has_file{false};
|
||||
} paint_state_{};
|
||||
|
||||
struct {
|
||||
@@ -147,21 +115,149 @@ class TextEditorView : public View {
|
||||
uint16_t col{};
|
||||
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) */
|
||||
/* Menu control for the TextEditor. */
|
||||
class TextEditorMenu : public View {
|
||||
public:
|
||||
TextEditorMenu();
|
||||
|
||||
// TODO: The scrollable view should be its own widget
|
||||
// otherwise control navigation doesn't work.
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
|
||||
Button button_open{
|
||||
{24 * 8, 34 * 8, 6 * 8, 4 * 8},
|
||||
"Open"};
|
||||
std::function<void()>& on_cut() { return button_cut.on_select; }
|
||||
std::function<void()>& on_paste() { return button_paste.on_select; }
|
||||
std::function<void()>& on_copy() { return button_copy.on_select; }
|
||||
|
||||
std::function<void()>& on_delete_line() { return button_delline.on_select; }
|
||||
std::function<void()>& on_edit_line() { return button_edit.on_select; }
|
||||
std::function<void()>& on_add_line() { return button_addline.on_select; }
|
||||
|
||||
std::function<void()>& on_open() { return button_open.on_select; }
|
||||
std::function<void()>& on_save() { return button_save.on_select; }
|
||||
std::function<void()>& on_exit() { return button_exit.on_select; }
|
||||
|
||||
private:
|
||||
void hide_children(bool hidden);
|
||||
|
||||
Rectangle rect_frame{
|
||||
{0 * 8, 0 * 8, 23 * 8, 23 * 8},
|
||||
Color::dark_grey()};
|
||||
|
||||
NewButton button_cut{
|
||||
{1 * 8, 1 * 8, 7 * 8, 7 * 8},
|
||||
"Cut",
|
||||
&bitmap_icon_cut,
|
||||
Color::dark_grey()};
|
||||
|
||||
NewButton button_paste{
|
||||
{8 * 8, 1 * 8, 7 * 8, 7 * 8},
|
||||
"Paste",
|
||||
&bitmap_icon_paste,
|
||||
Color::dark_grey()};
|
||||
|
||||
NewButton button_copy{
|
||||
{15 * 8, 1 * 8, 7 * 8, 7 * 8},
|
||||
"Copy",
|
||||
&bitmap_icon_copy,
|
||||
Color::dark_grey()};
|
||||
|
||||
NewButton button_delline{
|
||||
{1 * 8, 8 * 8, 7 * 8, 7 * 8},
|
||||
"-Line",
|
||||
&bitmap_icon_delete,
|
||||
Color::dark_red()};
|
||||
|
||||
NewButton button_edit{
|
||||
{8 * 8, 8 * 8, 7 * 8, 7 * 8},
|
||||
"Edit",
|
||||
&bitmap_icon_rename,
|
||||
Color::dark_blue()};
|
||||
|
||||
NewButton button_addline{
|
||||
{15 * 8, 8 * 8, 7 * 8, 7 * 8},
|
||||
"+Line",
|
||||
&bitmap_icon_scanner,
|
||||
Color::dark_blue()};
|
||||
|
||||
NewButton button_open{
|
||||
{1 * 8, 15 * 8, 7 * 8, 7 * 8},
|
||||
"Open",
|
||||
&bitmap_icon_load,
|
||||
Color::green()};
|
||||
|
||||
NewButton button_save{
|
||||
{8 * 8, 15 * 8, 7 * 8, 7 * 8},
|
||||
"Save",
|
||||
&bitmap_icon_save,
|
||||
Color::green()};
|
||||
|
||||
NewButton button_exit{
|
||||
{15 * 8, 15 * 8, 7 * 8, 7 * 8},
|
||||
"Exit",
|
||||
&bitmap_icon_previous,
|
||||
Color::dark_red()};
|
||||
};
|
||||
|
||||
/* View viewing and minor edits on a text file. */
|
||||
class TextEditorView : public View {
|
||||
public:
|
||||
TextEditorView(NavigationView& nav);
|
||||
TextEditorView(
|
||||
NavigationView& nav,
|
||||
const std::filesystem::path& path);
|
||||
~TextEditorView();
|
||||
|
||||
std::string title() const override {
|
||||
return "Notepad";
|
||||
};
|
||||
|
||||
void on_show() override;
|
||||
|
||||
private:
|
||||
static constexpr size_t max_edit_length = 1024;
|
||||
std::string edit_line_buffer_{};
|
||||
|
||||
void open_file(const std::filesystem::path& path);
|
||||
void save_file();
|
||||
void refresh_ui();
|
||||
void update_position();
|
||||
void hide_menu(bool hidden = true);
|
||||
void show_file_picker(bool immediate = true);
|
||||
void show_edit_line();
|
||||
void show_nyi();
|
||||
void show_save_prompt(std::function<void()> continuation);
|
||||
|
||||
void prepare_for_write();
|
||||
void create_temp_file() const;
|
||||
void delete_temp_file() const;
|
||||
void save_temp_file();
|
||||
std::filesystem::path get_temp_path() const;
|
||||
|
||||
NavigationView& nav_;
|
||||
std::unique_ptr<FileWrapper> file_{};
|
||||
std::filesystem::path path_{};
|
||||
bool file_dirty_{false};
|
||||
bool has_temp_file_{false};
|
||||
|
||||
TextViewer viewer{
|
||||
/* 272 = 320 - 16 (top bar) - 32 (bottom controls) */
|
||||
{0, 0, 240, 272}};
|
||||
|
||||
TextEditorMenu menu{};
|
||||
|
||||
NewButton button_menu{
|
||||
{26 * 8, 34 * 8, 4 * 8, 4 * 8},
|
||||
{},
|
||||
&bitmap_icon_controls,
|
||||
Color::dark_grey()};
|
||||
|
||||
Text text_position{
|
||||
{0 * 8, 36 * 8, 24 * 8, 2 * 8},
|
||||
{0 * 8, 34 * 8, 26 * 8, 2 * 8},
|
||||
""};
|
||||
|
||||
Text text_size{
|
||||
{0 * 8, 36 * 8, 26 * 8, 2 * 8},
|
||||
""};
|
||||
};
|
||||
|
||||
|
||||
@@ -2714,38 +2714,38 @@ static constexpr Bitmap bitmap_icon_remote{
|
||||
bitmap_icon_remote_data};
|
||||
|
||||
static constexpr uint8_t bitmap_icon_save_data[] = {
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x01,
|
||||
0x4E,
|
||||
0x05,
|
||||
0x91,
|
||||
0x03,
|
||||
0x3F,
|
||||
0x19,
|
||||
0x01,
|
||||
0xFC,
|
||||
0x07,
|
||||
0x0A,
|
||||
0x0A,
|
||||
0x0A,
|
||||
0x12,
|
||||
0xF2,
|
||||
0x21,
|
||||
0x02,
|
||||
0x20,
|
||||
0xF9,
|
||||
0xFF,
|
||||
0xF9,
|
||||
0xFF,
|
||||
0xFD,
|
||||
0x7F,
|
||||
0xFD,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0x3F,
|
||||
0xFF,
|
||||
0x3F,
|
||||
0xFF,
|
||||
0x1F,
|
||||
0xFF,
|
||||
0x02,
|
||||
0x20,
|
||||
0x02,
|
||||
0x20,
|
||||
0x02,
|
||||
0x20,
|
||||
0xFA,
|
||||
0x27,
|
||||
0xFA,
|
||||
0x2F,
|
||||
0x0A,
|
||||
0x28,
|
||||
0xFA,
|
||||
0x2F,
|
||||
0x0A,
|
||||
0x28,
|
||||
0xFA,
|
||||
0x2F,
|
||||
0xFC,
|
||||
0x1F,
|
||||
0x00,
|
||||
0x00,
|
||||
};
|
||||
static constexpr Bitmap bitmap_icon_save{
|
||||
{16, 16},
|
||||
|
||||
@@ -44,8 +44,9 @@ Optional<File::Error> File::open_fatfs(const std::filesystem::path& filename, BY
|
||||
}
|
||||
}
|
||||
|
||||
Optional<File::Error> File::open(const std::filesystem::path& filename) {
|
||||
return open_fatfs(filename, FA_READ);
|
||||
Optional<File::Error> File::open(const std::filesystem::path& filename, bool read_only) {
|
||||
BYTE mode = read_only ? FA_READ : FA_READ | FA_WRITE;
|
||||
return open_fatfs(filename, mode);
|
||||
}
|
||||
|
||||
Optional<File::Error> File::append(const std::filesystem::path& filename) {
|
||||
@@ -60,7 +61,7 @@ File::~File() {
|
||||
f_close(&f);
|
||||
}
|
||||
|
||||
File::Result<File::Size> File::read(void* const data, const Size bytes_to_read) {
|
||||
File::Result<File::Size> File::read(void* data, Size bytes_to_read) {
|
||||
UINT bytes_read = 0;
|
||||
const auto result = f_read(&f, data, bytes_to_read, &bytes_read);
|
||||
if (result == FR_OK) {
|
||||
@@ -70,7 +71,7 @@ File::Result<File::Size> File::read(void* const data, const Size bytes_to_read)
|
||||
}
|
||||
}
|
||||
|
||||
File::Result<File::Size> File::write(const void* const data, const Size bytes_to_write) {
|
||||
File::Result<File::Size> File::write(const void* data, Size bytes_to_write) {
|
||||
UINT bytes_written = 0;
|
||||
const auto result = f_write(&f, data, bytes_to_write, &bytes_written);
|
||||
if (result == FR_OK) {
|
||||
@@ -84,7 +85,7 @@ File::Result<File::Size> File::write(const void* const data, const Size bytes_to
|
||||
}
|
||||
}
|
||||
|
||||
File::Result<File::Offset> File::seek(const Offset new_position) {
|
||||
File::Result<File::Offset> File::seek(Offset new_position) {
|
||||
/* NOTE: Returns *old* position, not new position */
|
||||
const auto old_position = f_tell(&f);
|
||||
const auto result = f_lseek(&f, new_position);
|
||||
@@ -97,8 +98,17 @@ 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::Result<File::Offset> File::truncate() {
|
||||
const auto position = f_tell(&f);
|
||||
const auto result = f_truncate(&f);
|
||||
if (result != FR_OK) {
|
||||
return {static_cast<Error>(result)};
|
||||
}
|
||||
return {static_cast<File::Offset>(position)};
|
||||
}
|
||||
|
||||
File::Size File::size() const {
|
||||
return f_size(&f);
|
||||
}
|
||||
|
||||
Optional<File::Error> File::write_line(const std::string& s) {
|
||||
@@ -241,25 +251,26 @@ std::filesystem::filesystem_error rename_file(
|
||||
std::filesystem::filesystem_error copy_file(
|
||||
const std::filesystem::path& file_path,
|
||||
const std::filesystem::path& dest_path) {
|
||||
// Decent compromise between memory and speed.
|
||||
constexpr size_t buffer_size = 512;
|
||||
uint8_t buffer[buffer_size];
|
||||
File src;
|
||||
File dst;
|
||||
constexpr size_t buffer_size = 128;
|
||||
uint8_t buffer[buffer_size];
|
||||
|
||||
auto error = src.open(file_path);
|
||||
if (error.is_valid()) return error.value();
|
||||
if (error) return error.value();
|
||||
|
||||
error = dst.create(dest_path);
|
||||
if (error.is_valid()) return error.value();
|
||||
if (error) return error.value();
|
||||
|
||||
while (true) {
|
||||
auto result = src.read(buffer, buffer_size);
|
||||
if (result.is_error()) return result.error();
|
||||
|
||||
result = dst.write(buffer, result.value());
|
||||
result = dst.write(buffer, *result);
|
||||
if (result.is_error()) return result.error();
|
||||
|
||||
if (result.value() < buffer_size)
|
||||
if (*result < buffer_size)
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -288,6 +288,7 @@ class File {
|
||||
using Timestamp = uint32_t;
|
||||
using Error = std::filesystem::filesystem_error;
|
||||
|
||||
// TODO: move to common.
|
||||
template <typename T>
|
||||
struct Result {
|
||||
enum class Type {
|
||||
@@ -303,14 +304,26 @@ class File {
|
||||
return type == Type::Success;
|
||||
}
|
||||
|
||||
operator bool() const {
|
||||
return is_ok();
|
||||
}
|
||||
|
||||
bool is_error() const {
|
||||
return type == Type::Error;
|
||||
}
|
||||
|
||||
const T& value() const {
|
||||
const T& value() const& {
|
||||
return value_;
|
||||
}
|
||||
|
||||
const T& operator*() const& {
|
||||
return value_;
|
||||
}
|
||||
|
||||
T&& operator*() && {
|
||||
return std::move(value_);
|
||||
}
|
||||
|
||||
Error error() const {
|
||||
return error_;
|
||||
}
|
||||
@@ -318,9 +331,9 @@ class File {
|
||||
Result() = delete;
|
||||
|
||||
constexpr Result(
|
||||
T value)
|
||||
T&& value)
|
||||
: type{Type::Success},
|
||||
value_{value} {
|
||||
value_{std::forward<T>(value)} {
|
||||
}
|
||||
|
||||
constexpr Result(
|
||||
@@ -330,30 +343,38 @@ class File {
|
||||
}
|
||||
|
||||
~Result() {
|
||||
if (type == Type::Success) {
|
||||
if (is_ok())
|
||||
value_.~T();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
File(){};
|
||||
~File();
|
||||
|
||||
File(File&& other) {
|
||||
std::swap(f, other.f);
|
||||
}
|
||||
File& operator=(File&& other) {
|
||||
std::swap(f, other.f);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/* Prevent copies */
|
||||
File(const File&) = delete;
|
||||
File& operator=(const File&) = delete;
|
||||
|
||||
// TODO: Return Result<>.
|
||||
Optional<Error> open(const std::filesystem::path& filename);
|
||||
Optional<Error> open(const std::filesystem::path& filename, bool read_only = true);
|
||||
Optional<Error> append(const std::filesystem::path& filename);
|
||||
Optional<Error> create(const std::filesystem::path& filename);
|
||||
|
||||
Result<Size> read(void* const data, const Size bytes_to_read);
|
||||
Result<Size> write(const void* const data, const Size bytes_to_write);
|
||||
Result<Size> read(void* data, const Size bytes_to_read);
|
||||
Result<Size> write(const void* data, Size bytes_to_write);
|
||||
|
||||
Result<Offset> seek(const uint64_t Offset);
|
||||
Timestamp created_date();
|
||||
Size size();
|
||||
Result<Offset> seek(uint64_t Offset);
|
||||
Result<Offset> truncate();
|
||||
// Timestamp created_date() const;
|
||||
Size size() const;
|
||||
|
||||
template <size_t N>
|
||||
Result<Size> write(const std::array<uint8_t, N>& data) {
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
/*
|
||||
* 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 __FILE_WRAPPER_HPP__
|
||||
#define __FILE_WRAPPER_HPP__
|
||||
|
||||
#include "circular_buffer.hpp"
|
||||
#include "file.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
enum class LineEnding : uint8_t {
|
||||
LF,
|
||||
CRLF
|
||||
};
|
||||
|
||||
/* TODO:
|
||||
* - CRLF handling.
|
||||
* - Avoid full re-read on edits.
|
||||
* - Would need to read old/new text when editing to track newlines.
|
||||
* - How to surface errors? Exceptions?
|
||||
*/
|
||||
|
||||
/* FatFs docs http://elm-chan.org/fsw/ff/00index_e.html */
|
||||
|
||||
/* BufferType requires the following members
|
||||
* Size size()
|
||||
* Result<Size> read(void* data, Size bytes_to_read)
|
||||
* Result<Size> write(const void* data, Size bytes_to_write)
|
||||
* Result<Offset> seek(uint32_t offset)
|
||||
* Result<Offset> truncate()
|
||||
* Optional<Error> sync()
|
||||
*/
|
||||
|
||||
/* Wraps a buffer and provides an API for accessing lines efficiently. */
|
||||
template <typename BufferType, uint32_t CacheSize>
|
||||
class BufferWrapper {
|
||||
public:
|
||||
using Offset = uint32_t;
|
||||
using Line = uint32_t;
|
||||
using Column = uint32_t;
|
||||
using Range = struct {
|
||||
// Offset of the start, inclusive.
|
||||
Offset start;
|
||||
// Offset of the end, exclusive.
|
||||
Offset end;
|
||||
|
||||
Offset length() const { return end - start; }
|
||||
};
|
||||
|
||||
BufferWrapper(BufferType* buffer)
|
||||
: wrapped_{buffer} {
|
||||
initialize();
|
||||
}
|
||||
virtual ~BufferWrapper() {}
|
||||
|
||||
/* Prevent copies */
|
||||
BufferWrapper(const BufferWrapper&) = delete;
|
||||
BufferWrapper& operator=(const BufferWrapper&) = delete;
|
||||
|
||||
Optional<std::string> get_text(Line line, Column col, Offset length) {
|
||||
std::string buffer;
|
||||
buffer.resize(length);
|
||||
|
||||
auto result = get_text(line, col, &buffer[0], length);
|
||||
if (!result)
|
||||
return {};
|
||||
|
||||
buffer.resize(*result);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
Optional<Offset> get_text(Line line, Column col, char* output, Offset length) {
|
||||
auto range = line_range(line);
|
||||
int32_t to_read = length;
|
||||
|
||||
if (!range)
|
||||
return {};
|
||||
|
||||
// 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, output, to_read);
|
||||
}
|
||||
|
||||
/* Gets the size of the buffer in bytes. */
|
||||
File::Size size() const { return wrapped_->size(); }
|
||||
|
||||
/* Get the count of the lines in the buffer. */
|
||||
uint32_t line_count() const { return line_count_; }
|
||||
|
||||
/* Gets the range of the line if valid. */
|
||||
Optional<Range> line_range(Line line) {
|
||||
ensure_cached(line);
|
||||
|
||||
auto index = index_for_line(line);
|
||||
if (!index)
|
||||
return {};
|
||||
|
||||
auto start = *index == 0 ? start_offset_ : (newlines_[*index - 1] + 1);
|
||||
auto end = newlines_[*index] + 1;
|
||||
|
||||
return Range{start, end};
|
||||
}
|
||||
|
||||
/* Gets the length of the line, or 0 if invalid. */
|
||||
Offset line_length(Line line) {
|
||||
auto range = line_range(line);
|
||||
return range ? range->length() : 0;
|
||||
}
|
||||
|
||||
/* Gets the buffer offset of the line & col if valid. */
|
||||
Optional<Offset> get_offset(Line line, Column col) {
|
||||
auto range = line_range(line);
|
||||
|
||||
if (range)
|
||||
return range->start + col;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/* Gets the index of the first line in the cache.
|
||||
* Only really useful for unit testing or diagnostics. */
|
||||
Offset start_line() { return start_line_; };
|
||||
|
||||
/* Inserts a line before the specified line or at the
|
||||
* end of the buffer if line >= line_count. */
|
||||
void insert_line(Line line) {
|
||||
auto range = line_range(line);
|
||||
|
||||
if (range)
|
||||
replace_range({range->start, range->start}, "\n");
|
||||
else if (line >= line_count_)
|
||||
replace_range({(Offset)size(), (Offset)size()}, "\n");
|
||||
}
|
||||
|
||||
/* Deletes the specified line. */
|
||||
void delete_line(Line line) {
|
||||
auto range = line_range(line);
|
||||
|
||||
if (range)
|
||||
replace_range(*range, {});
|
||||
}
|
||||
|
||||
/* Replace the specified range with the string contents.
|
||||
* A range with start/end set to the same value will insert.
|
||||
* A range with an empty string will delete. */
|
||||
void replace_range(Range range, std::string_view value) {
|
||||
if (range.start > size() || range.end > size() || range.start > range.end)
|
||||
return;
|
||||
|
||||
/* If delta_length == 0, it's an overwrite. Could still have
|
||||
* added or removed newlines so caches will need to be rebuilt.
|
||||
* If delta_length > 0, the file needs to grow and content needs
|
||||
* to be shifted forward until the end of the range.
|
||||
* If delta_length < 0, the file needs to be truncated and the
|
||||
* content after the value needs to be shifted backward. */
|
||||
int32_t delta_length = value.length() - range.length();
|
||||
if (delta_length > 0)
|
||||
expand(range.end, delta_length);
|
||||
else if (delta_length < 0)
|
||||
shrink(range.end, delta_length);
|
||||
|
||||
write(range.start, value);
|
||||
wrapped_->sync();
|
||||
rebuild_cache();
|
||||
}
|
||||
|
||||
protected:
|
||||
BufferWrapper() {}
|
||||
|
||||
void set_buffer(BufferType* buffer) {
|
||||
wrapped_ = buffer;
|
||||
initialize();
|
||||
}
|
||||
|
||||
private:
|
||||
/* Number of newline offsets to cache. */
|
||||
static constexpr Offset max_newlines = CacheSize;
|
||||
|
||||
/* Size of stack buffer used for reading/writing. */
|
||||
static constexpr Offset buffer_size = 512;
|
||||
|
||||
void initialize() {
|
||||
start_offset_ = 0;
|
||||
start_line_ = 0;
|
||||
line_count_ = 0;
|
||||
rebuild_cache();
|
||||
}
|
||||
|
||||
void rebuild_cache() {
|
||||
newlines_.clear();
|
||||
|
||||
// Special case for empty files to keep them consistent.
|
||||
if (size() == 0) {
|
||||
line_count_ = 1;
|
||||
newlines_.push_back(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: think through this for edit cases.
|
||||
// E.g. don't read to end, maybe could specify
|
||||
// a range to re-read because it should be possible
|
||||
// to tell where the dirty regions are. After the
|
||||
// dirty region, it should be possible to fixup
|
||||
// the line_count data.
|
||||
// TODO: seems like shrink/expand could do this while
|
||||
// they are running.
|
||||
|
||||
line_count_ = start_line_;
|
||||
Offset offset = start_offset_;
|
||||
auto result = next_newline(offset);
|
||||
|
||||
while (result) {
|
||||
++line_count_;
|
||||
if (newlines_.size() < max_newlines)
|
||||
newlines_.push_back(*result);
|
||||
offset = *result + 1;
|
||||
result = next_newline(offset);
|
||||
}
|
||||
}
|
||||
|
||||
Optional<Offset> read(Offset offset, char* buffer, Offset length) {
|
||||
if (offset + length > size())
|
||||
return {};
|
||||
|
||||
wrapped_->seek(offset);
|
||||
|
||||
auto result = wrapped_->read(buffer, length);
|
||||
if (result.is_error())
|
||||
return {};
|
||||
|
||||
return *result;
|
||||
}
|
||||
|
||||
bool write(Offset offset, std::string_view value) {
|
||||
wrapped_->seek(offset);
|
||||
auto result = wrapped_->write(value.data(), value.length());
|
||||
|
||||
return result.is_ok();
|
||||
}
|
||||
|
||||
/* Returns the index of the line in the newline cache if valid. */
|
||||
Optional<Offset> index_for_line(Line line) const {
|
||||
if (line >= line_count_)
|
||||
return {};
|
||||
|
||||
Offset actual = line - start_line_;
|
||||
if (actual >= newlines_.size()) // NB: underflow wrap.
|
||||
return {};
|
||||
|
||||
return actual;
|
||||
}
|
||||
|
||||
/* Ensure specified line is in the newline cache. */
|
||||
void ensure_cached(Line line) {
|
||||
if (line >= line_count_)
|
||||
return;
|
||||
|
||||
auto index = index_for_line(line);
|
||||
if (index)
|
||||
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) {
|
||||
// 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) {
|
||||
start_line_++;
|
||||
start_offset_ = newlines_.front() + 1;
|
||||
newlines_.push_back(*offset);
|
||||
} /* else at the EOF. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Finding the first newline backward from offset. */
|
||||
Optional<Offset> previous_newline(Offset offset) {
|
||||
char buffer[buffer_size];
|
||||
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;
|
||||
|
||||
wrapped_->seek(offset);
|
||||
|
||||
auto result = wrapped_->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.
|
||||
}
|
||||
|
||||
/* Finding the first newline forward from offset. */
|
||||
Optional<Offset> next_newline(Offset offset) {
|
||||
// EOF, no more newlines to find.
|
||||
if (offset >= size())
|
||||
return {};
|
||||
|
||||
char buffer[buffer_size];
|
||||
wrapped_->seek(offset);
|
||||
|
||||
while (true) {
|
||||
auto result = wrapped_->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;
|
||||
}
|
||||
|
||||
// For consistency, treat the end of the file as a "newline".
|
||||
return size() - 1;
|
||||
}
|
||||
|
||||
/* Grow the file and move file content so that the
|
||||
* content at src is shifted forward by 'delta'. */
|
||||
void expand(Offset src, int32_t delta) {
|
||||
if (delta <= 0) // Not an expand.
|
||||
return;
|
||||
|
||||
char buffer[buffer_size];
|
||||
auto to_read = buffer_size;
|
||||
|
||||
// Number of bytes left to shift.
|
||||
Offset remaining = size() - src;
|
||||
Offset offset = size();
|
||||
|
||||
while (remaining > 0) {
|
||||
offset -= std::min(remaining, buffer_size);
|
||||
to_read = std::min(remaining, buffer_size);
|
||||
|
||||
wrapped_->seek(offset);
|
||||
auto result = wrapped_->read(buffer, to_read);
|
||||
if (result.is_error())
|
||||
break;
|
||||
|
||||
wrapped_->seek(offset + delta);
|
||||
result = wrapped_->write(buffer, *result);
|
||||
if (result.is_error())
|
||||
break;
|
||||
|
||||
remaining -= *result;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shrink the file and move file content so that the
|
||||
* content at src is shifted backward by 'delta'. */
|
||||
void shrink(Offset src, int32_t delta) {
|
||||
if (delta >= 0) // Not a shrink.
|
||||
return;
|
||||
|
||||
char buffer[buffer_size];
|
||||
auto offset = src;
|
||||
|
||||
while (true) {
|
||||
wrapped_->seek(offset);
|
||||
auto result = wrapped_->read(buffer, buffer_size);
|
||||
if (result.is_error())
|
||||
break;
|
||||
|
||||
wrapped_->seek(offset + delta);
|
||||
result = wrapped_->write(buffer, *result);
|
||||
|
||||
if (result.is_error() || *result < buffer_size)
|
||||
break;
|
||||
|
||||
offset += *result;
|
||||
}
|
||||
|
||||
// Delete the extra bytes at the end of the file.
|
||||
wrapped_->truncate();
|
||||
}
|
||||
|
||||
BufferType* wrapped_{};
|
||||
|
||||
/* Total number of lines in the buffer. */
|
||||
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_{};
|
||||
};
|
||||
|
||||
/* A BufferWrapper over a file. */
|
||||
class FileWrapper : public BufferWrapper<File, 64> {
|
||||
public:
|
||||
template <typename T>
|
||||
using Result = File::Result<T>;
|
||||
using Error = File::Error;
|
||||
static Result<std::unique_ptr<FileWrapper>> open(const std::filesystem::path& path) {
|
||||
auto fw = std::unique_ptr<FileWrapper>(new FileWrapper());
|
||||
auto error = fw->file_.open(path, /*read_only*/ false);
|
||||
|
||||
if (error)
|
||||
return *error;
|
||||
|
||||
fw->initialize();
|
||||
return fw;
|
||||
}
|
||||
|
||||
/* Underlying file. */
|
||||
File& file() { return file_; }
|
||||
|
||||
/* Swaps out the underlying file for the specified file.
|
||||
* The swapped file is expected have the same contents.
|
||||
* For copy-on-write scenario with a temp file. */
|
||||
bool assume_file(const std::filesystem::path& path) {
|
||||
File file;
|
||||
auto error = file.open(path, /*read_only*/ false);
|
||||
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
file_ = std::move(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
FileWrapper() {}
|
||||
void initialize() {
|
||||
set_buffer(&file_);
|
||||
}
|
||||
|
||||
File file_{};
|
||||
};
|
||||
|
||||
template <uint32_t CacheSize = 64, typename T>
|
||||
BufferWrapper<T, CacheSize> wrap_buffer(T& buffer) {
|
||||
return {&buffer};
|
||||
}
|
||||
|
||||
#endif // __FILE_WRAPPER_HPP__
|
||||
@@ -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.
|
||||
*
|
||||
@@ -102,10 +103,13 @@ std::vector<std::string> get_freqman_files() {
|
||||
};
|
||||
|
||||
bool load_freqman_file(std::string& file_stem, freqman_db& db) {
|
||||
return load_freqman_file_ex(file_stem, db, true, true, true);
|
||||
return load_freqman_file_ex(file_stem, db, true, true, true, FREQMAN_MAX_PER_FILE);
|
||||
}
|
||||
|
||||
bool load_freqman_file_ex(std::string& file_stem, freqman_db& db, bool load_freqs, bool load_ranges, bool load_hamradios) {
|
||||
bool load_freqman_file_ex(std::string& file_stem, freqman_db& db, bool load_freqs, bool load_ranges, bool load_hamradios, uint8_t max_num_freqs = FREQMAN_MAX_PER_FILE) {
|
||||
// swap with empty vector to ensure memory is immediately released
|
||||
std::vector<freqman_entry>().swap(db);
|
||||
|
||||
File freqman_file;
|
||||
size_t length, n = 0, file_position = 0;
|
||||
char* pos;
|
||||
@@ -113,35 +117,33 @@ bool load_freqman_file_ex(std::string& file_stem, freqman_db& db, bool load_freq
|
||||
char* line_end;
|
||||
std::string description;
|
||||
rf::Frequency frequency_a, frequency_b;
|
||||
char file_data[257];
|
||||
char file_data[FREQMAN_READ_BUF_SIZE + 1];
|
||||
freqman_entry_type type;
|
||||
freqman_index_t modulation = 0;
|
||||
freqman_index_t bandwidth = 0;
|
||||
freqman_index_t step = 0;
|
||||
freqman_index_t tone = 0;
|
||||
|
||||
db.clear();
|
||||
|
||||
auto result = freqman_file.open("FREQMAN/" + file_stem + ".TXT");
|
||||
if (result.is_valid())
|
||||
return false;
|
||||
|
||||
while (1) {
|
||||
// Read a 256 bytes block from file
|
||||
// Read a FREQMAN_READ_BUF_SIZE block from file
|
||||
freqman_file.seek(file_position);
|
||||
|
||||
memset(file_data, 0, 257);
|
||||
auto read_size = freqman_file.read(file_data, 256);
|
||||
memset(file_data, 0, FREQMAN_READ_BUF_SIZE + 1);
|
||||
auto read_size = freqman_file.read(file_data, FREQMAN_READ_BUF_SIZE);
|
||||
if (read_size.is_error())
|
||||
return false; // Read error
|
||||
|
||||
file_position += 256;
|
||||
file_position += FREQMAN_READ_BUF_SIZE;
|
||||
|
||||
// Reset line_start to beginning of buffer
|
||||
line_start = file_data;
|
||||
|
||||
// If EOF reached, insert 0x0A after, in case the last line doesn't have a C/R
|
||||
if (read_size.value() < 256)
|
||||
if (read_size.value() < FREQMAN_READ_BUF_SIZE)
|
||||
*(line_start + read_size.value()) = 0x0A;
|
||||
|
||||
// Look for complete lines in buffer
|
||||
@@ -221,23 +223,22 @@ bool load_freqman_file_ex(std::string& file_stem, freqman_db& db, bool load_freq
|
||||
pos += 2;
|
||||
length = std::min(strcspn(pos, ",\x0A"), (size_t)FREQMAN_DESC_MAX_LEN);
|
||||
description = string(pos, length);
|
||||
} else
|
||||
description = "-";
|
||||
}
|
||||
if ((type == SINGLE && load_freqs) || (type == RANGE && load_ranges) || (type == HAMRADIO && load_hamradios)) {
|
||||
db.push_back({frequency_a, frequency_b, description, type, modulation, bandwidth, step, tone});
|
||||
n++;
|
||||
if (n > FREQMAN_MAX_PER_FILE) return true;
|
||||
if (n > max_num_freqs) return true;
|
||||
}
|
||||
|
||||
line_start = line_end + 1;
|
||||
if (line_start - file_data >= 256) break;
|
||||
if (line_start - file_data >= FREQMAN_READ_BUF_SIZE) break;
|
||||
}
|
||||
|
||||
if (read_size.value() != 256)
|
||||
if (read_size.value() != FREQMAN_READ_BUF_SIZE)
|
||||
break; // End of file
|
||||
|
||||
// Restart at beginning of last incomplete line
|
||||
file_position -= (file_data + 256 - line_start);
|
||||
file_position -= (file_data + FREQMAN_READ_BUF_SIZE - line_start);
|
||||
}
|
||||
|
||||
/* populate implicitly specified modulation / bandwidth */
|
||||
@@ -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.
|
||||
*
|
||||
@@ -31,10 +32,11 @@
|
||||
#include "string_format.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
#define FREQMAN_DESC_MAX_LEN 24 // This is the number of characters that can be drawn in front of "R: TEXT..." before taking a full screen line
|
||||
#define FREQMAN_MAX_PER_FILE 115 // Maximum of entries we can read. This is a hardware limit
|
||||
// It was tested and lowered to leave a bit of space to the caller
|
||||
#define FREQMAN_MAX_PER_FILE_STR "115" // STRING OF FREQMAN_MAX_PER_FILE
|
||||
#define FREQMAN_DESC_MAX_LEN 24 // This is the number of characters that can be drawn in front of "R: TEXT..." before taking a full screen line
|
||||
#define FREQMAN_MAX_PER_FILE 60 // Maximum of entries we can read. This is a hardware limit
|
||||
// It was tested and lowered to leave a bit of space to the caller
|
||||
#define FREQMAN_MAX_PER_FILE_STR "60" // STRING OF FREQMAN_MAX_PER_FILE
|
||||
#define FREQMAN_READ_BUF_SIZE 96 // max freqman line size including desc is 90, +6 for a bit of space
|
||||
|
||||
using namespace ui;
|
||||
using namespace std;
|
||||
@@ -95,7 +97,7 @@ using freqman_db = std::vector<freqman_entry>;
|
||||
|
||||
std::vector<std::string> get_freqman_files();
|
||||
bool load_freqman_file(std::string& file_stem, freqman_db& db);
|
||||
bool load_freqman_file_ex(std::string& file_stem, freqman_db& db, bool load_freqs, bool load_ranges, bool load_hamradios);
|
||||
bool load_freqman_file_ex(std::string& file_stem, freqman_db& db, bool load_freqs, bool load_ranges, bool load_hamradios, uint8_t limit);
|
||||
bool get_freq_string(freqman_entry& entry, std::string& item_string);
|
||||
bool save_freqman_file(std::string& file_stem, freqman_db& db);
|
||||
bool create_freqman_file(std::string& file_stem, File& freqman_file);
|
||||
|
||||
@@ -23,17 +23,56 @@
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
// Returns TRUE if button state changed (after debouncing)
|
||||
bool Debounce::feed(const uint8_t bit) {
|
||||
history_ = (history_ << 1) | (bit & 1);
|
||||
|
||||
if (history_ == 0b00001111) {
|
||||
state_ = 1;
|
||||
return true;
|
||||
}
|
||||
if (history_ == 0b11110000) {
|
||||
state_ = 0;
|
||||
return true;
|
||||
// "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 {
|
||||
// Previous button state was 1 (pressed);
|
||||
// Has button been released for DEBOUNCE_COUNT ticks?
|
||||
if ((history_ & DEBOUNCE_MASK) == 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,14 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// consecutive # of times button input must be same (<=8)
|
||||
#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);
|
||||
@@ -32,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__*/
|
||||
|
||||
@@ -23,23 +23,75 @@
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
static const int8_t transition_map[] = {
|
||||
0, // 0000: noop
|
||||
0, // 0001: start
|
||||
0, // 0010: start
|
||||
0, // 0011: rate
|
||||
1, // 0100: end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
-1, // 0111: end
|
||||
-1, // 1000: end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
1, // 1011: end
|
||||
0, // 1100: rate
|
||||
0, // 1101: start
|
||||
0, // 1110: start
|
||||
0, // 1111: noop
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
// Now supporting multiple levels of rotary encoder dial sensitivity
|
||||
//
|
||||
// Portapack H2 normally has a 30-step encoder, meaning one step (pulse) every
|
||||
// 12 degrees of rotation.
|
||||
//
|
||||
// For each encoder "pulse" there are 4 state transitions, and we can choose
|
||||
// between looking at all of them (high sensitivity), half of them (medium/default),
|
||||
// or one quarter of them (low sensitivity).
|
||||
static const int8_t transition_map[][16] = {
|
||||
// Normal (Medium) Sensitivity -- default
|
||||
{
|
||||
0, // 0000: noop
|
||||
0, // 0001: ccw start
|
||||
0, // 0010: cw start
|
||||
0, // 0011: rate
|
||||
1, // 0100: cw end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
-1, // 0111: ccw end
|
||||
-1, // 1000: ccw end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
1, // 1011: cw end
|
||||
0, // 1100: rate
|
||||
0, // 1101: cw start
|
||||
0, // 1110: ccw start
|
||||
0, // 1111: noop
|
||||
},
|
||||
// Low Sensitivity
|
||||
{
|
||||
0, // 0000: noop
|
||||
0, // 0001: ccw start
|
||||
0, // 0010: cw start
|
||||
0, // 0011: rate
|
||||
1, // 0100: cw end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
0, // 0111: ccw end
|
||||
-1, // 1000: ccw end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
0, // 1011: cw end
|
||||
0, // 1100: rate
|
||||
0, // 1101: cw start
|
||||
0, // 1110: ccw start
|
||||
0, // 1111: noop
|
||||
},
|
||||
// High Sensitivity
|
||||
{
|
||||
0, // 0000: noop
|
||||
-1, // 0001: ccw start
|
||||
1, // 0010: cw start
|
||||
0, // 0011: rate
|
||||
1, // 0100: cw end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
-1, // 0111: ccw end
|
||||
-1, // 1000: ccw end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
1, // 1011: cw end
|
||||
0, // 1100: rate
|
||||
1, // 1101: cw start
|
||||
-1, // 1110: ccw start
|
||||
0, // 1111: noop
|
||||
},
|
||||
};
|
||||
|
||||
int_fast8_t Encoder::update(
|
||||
@@ -50,5 +102,6 @@ int_fast8_t Encoder::update(
|
||||
state <<= 1;
|
||||
state |= phase_1;
|
||||
|
||||
return transition_map[state & 0xf];
|
||||
// dial sensitivity setting is stored in pmem
|
||||
return transition_map[portapack::persistent_memory::config_encoder_dial_sensitivity()][state & 0xf];
|
||||
}
|
||||
|
||||
@@ -71,10 +71,10 @@ Samples get() {
|
||||
const auto yp_reg = LPC_ADC0->DR[portapack::adc0_touch_yp_input];
|
||||
const auto yn_reg = LPC_ADC0->DR[portapack::adc0_touch_yn_input];
|
||||
return {
|
||||
(xp_reg >> 6) & 0x3ff,
|
||||
(xn_reg >> 6) & 0x3ff,
|
||||
(yp_reg >> 6) & 0x3ff,
|
||||
(yn_reg >> 6) & 0x3ff,
|
||||
((xp_reg >> 6) & 0x3ff) * 16,
|
||||
((xn_reg >> 6) & 0x3ff) * 16,
|
||||
((yp_reg >> 6) & 0x3ff) * 16,
|
||||
((yn_reg >> 6) & 0x3ff) * 16,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -521,7 +521,8 @@ void shutdown(const bool leave_screen_on) {
|
||||
display.shutdown();
|
||||
}
|
||||
|
||||
radio::disable();
|
||||
receiver_model.disable();
|
||||
transmitter_model.disable();
|
||||
audio::shutdown();
|
||||
|
||||
hackrf::cpld::init_from_eeprom();
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
#include "clock_manager.hpp"
|
||||
#include "temperature_logger.hpp"
|
||||
|
||||
/* TODO: This would be better as a class to add
|
||||
* guardrails on setting properties. */
|
||||
namespace portapack {
|
||||
|
||||
extern portapack::IO io;
|
||||
@@ -59,6 +61,8 @@ extern bool antenna_bias;
|
||||
|
||||
extern TemperatureLogger temperature_logger;
|
||||
|
||||
/* Get or set the antenna_bias flag.
|
||||
* NB: Does not actually update the radio state. */
|
||||
void set_antenna_bias(const bool v);
|
||||
bool get_antenna_bias();
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ using namespace hackrf::one;
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
/* Direct access to the radio. Setting values incorrectly can damage
|
||||
* the device. Applications should use ReceiverModel or TransmitterModel
|
||||
* instead of calling these functions directly. */
|
||||
namespace radio {
|
||||
|
||||
static constexpr uint32_t ssp1_cpsr = 2;
|
||||
@@ -245,18 +248,7 @@ void set_antenna_bias(const bool on) {
|
||||
}
|
||||
}
|
||||
|
||||
void disable() {
|
||||
set_antenna_bias(false);
|
||||
baseband_codec.set_mode(max5864::Mode::Shutdown);
|
||||
second_if->set_mode(max2837::Mode::Standby);
|
||||
first_if.disable();
|
||||
set_rf_amp(false);
|
||||
|
||||
led_rx.off();
|
||||
led_tx.off();
|
||||
}
|
||||
|
||||
void enable(Configuration configuration) {
|
||||
/*void enable(Configuration configuration) {
|
||||
configure(configuration);
|
||||
}
|
||||
|
||||
@@ -268,6 +260,17 @@ void configure(Configuration configuration) {
|
||||
set_baseband_rate(configuration.baseband_rate);
|
||||
set_baseband_filter_bandwidth(configuration.baseband_filter_bandwidth);
|
||||
set_direction(configuration.direction);
|
||||
}*/
|
||||
|
||||
void disable() {
|
||||
set_antenna_bias(false);
|
||||
baseband_codec.set_mode(max5864::Mode::Shutdown);
|
||||
second_if->set_mode(max2837::Mode::Standby);
|
||||
first_if.disable();
|
||||
set_rf_amp(false);
|
||||
|
||||
led_rx.off();
|
||||
led_tx.off();
|
||||
}
|
||||
|
||||
namespace debug {
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
/* Direct access to the radio. Setting values incorrectly can damage
|
||||
* the device. Applications should use ReceiverModel or TransmitterModel
|
||||
* instead of calling these functions directly. */
|
||||
namespace radio {
|
||||
|
||||
struct Configuration {
|
||||
@@ -51,8 +54,9 @@ void set_baseband_filter_bandwidth(const uint32_t bandwidth_minimum);
|
||||
void set_baseband_rate(const uint32_t rate);
|
||||
void set_antenna_bias(const bool on);
|
||||
|
||||
void enable(Configuration configuration);
|
||||
void configure(Configuration configuration);
|
||||
/* Use ReceiverModel or TransmitterModel instead. */
|
||||
// void enable(Configuration configuration);
|
||||
// void configure(Configuration configuration);
|
||||
void disable();
|
||||
|
||||
namespace debug {
|
||||
|
||||
@@ -118,14 +118,14 @@ void ReceiverModel::set_vga(int32_t v_db) {
|
||||
update_vga();
|
||||
}
|
||||
|
||||
int32_t ReceiverModel::tx_gain() const {
|
||||
/*int32_t ReceiverModel::tx_gain() const {
|
||||
return tx_gain_db_;
|
||||
}
|
||||
|
||||
void ReceiverModel::set_tx_gain(int32_t v_db) {
|
||||
tx_gain_db_ = v_db;
|
||||
update_tx_gain();
|
||||
}
|
||||
}*/
|
||||
|
||||
uint32_t ReceiverModel::sampling_rate() const {
|
||||
return sampling_rate_;
|
||||
@@ -171,7 +171,7 @@ void ReceiverModel::enable() {
|
||||
update_rf_amp();
|
||||
update_lna();
|
||||
update_vga();
|
||||
update_tx_gain();
|
||||
// update_tx_gain();
|
||||
update_baseband_bandwidth();
|
||||
update_sampling_rate();
|
||||
update_modulation();
|
||||
@@ -185,7 +185,6 @@ void ReceiverModel::enable() {
|
||||
|
||||
void ReceiverModel::disable() {
|
||||
enabled_ = false;
|
||||
radio::set_antenna_bias(false);
|
||||
|
||||
// TODO: Responsibility for enabling/disabling the radio is muddy.
|
||||
// Some happens in ReceiverModel, some inside radio namespace.
|
||||
@@ -206,7 +205,7 @@ int32_t ReceiverModel::tuning_offset() {
|
||||
}
|
||||
|
||||
void ReceiverModel::update_tuning_frequency() {
|
||||
radio::set_tuning_frequency(persistent_memory::tuned_frequency());
|
||||
radio::set_tuning_frequency(persistent_memory::tuned_frequency() + tuning_offset());
|
||||
}
|
||||
|
||||
void ReceiverModel::update_antenna_bias() {
|
||||
@@ -230,9 +229,9 @@ void ReceiverModel::update_vga() {
|
||||
radio::set_vga_gain(vga_gain_db_);
|
||||
}
|
||||
|
||||
void ReceiverModel::update_tx_gain() {
|
||||
/*void ReceiverModel::update_tx_gain() {
|
||||
radio::set_tx_gain(tx_gain_db_);
|
||||
}
|
||||
}*/
|
||||
|
||||
void ReceiverModel::set_am_configuration(const size_t n) {
|
||||
if (n < am_configs.size()) {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "max283x.hpp"
|
||||
#include "volume.hpp"
|
||||
|
||||
// TODO: consider a base class for ReceiverModel & TransmitterModel.
|
||||
class ReceiverModel {
|
||||
public:
|
||||
enum class Mode {
|
||||
@@ -60,8 +61,9 @@ class ReceiverModel {
|
||||
int32_t vga() const;
|
||||
void set_vga(int32_t v_db);
|
||||
|
||||
int32_t tx_gain() const;
|
||||
void set_tx_gain(int32_t v_db);
|
||||
// TODO: Why does receiver need tx_gain?
|
||||
// int32_t tx_gain() const;
|
||||
// void set_tx_gain(int32_t v_db);
|
||||
|
||||
uint32_t sampling_rate() const;
|
||||
void set_sampling_rate(uint32_t v);
|
||||
@@ -87,7 +89,13 @@ class ReceiverModel {
|
||||
size_t wfm_configuration() const;
|
||||
void set_wfm_configuration(const size_t n);
|
||||
|
||||
void set_configuration_without_init(const Mode new_mode, const rf::Frequency new_frequency_step, const size_t new_am_config_index, const size_t new_nbfm_config_index, const size_t new_wfm_config_index, uint8_t new_squelch_level);
|
||||
void set_configuration_without_init(
|
||||
const Mode new_mode,
|
||||
const rf::Frequency new_frequency_step,
|
||||
const size_t new_am_config_index,
|
||||
const size_t new_nbfm_config_index,
|
||||
const size_t new_wfm_config_index,
|
||||
uint8_t new_squelch_level);
|
||||
|
||||
private:
|
||||
rf::Frequency frequency_step_{25000};
|
||||
@@ -96,7 +104,7 @@ class ReceiverModel {
|
||||
int32_t lna_gain_db_{32};
|
||||
uint32_t baseband_bandwidth_{max283x::filter::bandwidth_minimum};
|
||||
int32_t vga_gain_db_{32};
|
||||
int32_t tx_gain_db_{47};
|
||||
// int32_t tx_gain_db_{47};
|
||||
Mode mode_{Mode::NarrowbandFMAudio};
|
||||
uint32_t sampling_rate_{3072000};
|
||||
size_t am_config_index = 0;
|
||||
@@ -113,7 +121,7 @@ class ReceiverModel {
|
||||
void update_lna();
|
||||
void update_baseband_bandwidth();
|
||||
void update_vga();
|
||||
void update_tx_gain();
|
||||
// void update_tx_gain();
|
||||
void update_sampling_rate();
|
||||
void update_headphone_volume();
|
||||
|
||||
|
||||
@@ -34,16 +34,16 @@ Metrics calculate_metrics(const Frame& frame) {
|
||||
* fast *enough*?), so maybe leave it alone at least for now.
|
||||
*/
|
||||
|
||||
const auto x_max = frame.x.xp;
|
||||
const auto x_min = frame.x.xn;
|
||||
const auto x_max = frame.x.xp / 16;
|
||||
const auto x_min = frame.x.xn / 16;
|
||||
const auto x_range = x_max - x_min;
|
||||
const float x_position = (frame.x.yp + frame.x.yn) * 0.5f;
|
||||
const float x_position = (frame.x.yp / 16 + frame.x.yn / 16) * 0.5f;
|
||||
const float x_norm = (x_position - x_min) / x_range;
|
||||
|
||||
const auto y_max = frame.y.yn;
|
||||
const auto y_min = frame.y.yp;
|
||||
const auto y_max = frame.y.yn / 16;
|
||||
const auto y_min = frame.y.yp / 16;
|
||||
const auto y_range = y_max - y_min;
|
||||
const float y_position = (frame.y.xp + frame.y.xn) * 0.5f;
|
||||
const float y_position = (frame.y.xp / 16 + frame.y.xn / 16) * 0.5f;
|
||||
const float y_norm = (y_position - y_min) / y_range;
|
||||
|
||||
const auto z_max = frame.pressure.yp;
|
||||
|
||||
@@ -125,6 +125,7 @@ void TransmitterModel::enable() {
|
||||
update_vga();
|
||||
update_baseband_bandwidth();
|
||||
update_sampling_rate();
|
||||
update_tx_gain();
|
||||
|
||||
led_tx.on();
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
@@ -138,7 +139,6 @@ void TransmitterModel::enable() {
|
||||
|
||||
void TransmitterModel::disable() {
|
||||
enabled_ = false;
|
||||
radio::set_antenna_bias(false);
|
||||
|
||||
// TODO: Responsibility for enabling/disabling the radio is muddy.
|
||||
// Some happens in ReceiverModel, some inside radio namespace.
|
||||
|
||||
@@ -71,7 +71,8 @@ class TransmitterModel {
|
||||
uint32_t channel_bandwidth_{1};
|
||||
uint32_t baseband_bandwidth_{max2837::filter::bandwidth_minimum};
|
||||
int32_t vga_gain_db_{8};
|
||||
int32_t tx_gain_db_{47};
|
||||
/* 35 should give approx 1m transmission range. */
|
||||
int32_t tx_gain_db_{35};
|
||||
uint32_t sampling_rate_{3072000};
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
*
|
||||
* 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 "ui_freqlist.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ui {
|
||||
FreqManUIList::FreqManUIList(
|
||||
Rect parent_rect,
|
||||
bool instant_exec)
|
||||
: Widget{parent_rect},
|
||||
instant_exec_{instant_exec} {
|
||||
this->set_focusable(true);
|
||||
}
|
||||
|
||||
void FreqManUIList::set_highlighted_index(int index) {
|
||||
if ((unsigned)(current_index + index) >= freqlist_db.size())
|
||||
return;
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
if (current_index > 0)
|
||||
current_index--;
|
||||
}
|
||||
if (index >= freqlist_nb_lines) {
|
||||
index = freqlist_nb_lines - 1;
|
||||
if ((unsigned)(current_index + index) < freqlist_db.size())
|
||||
current_index++;
|
||||
else
|
||||
current_index = freqlist_db.size() - freqlist_nb_lines - 1;
|
||||
}
|
||||
highlighted_index = index;
|
||||
}
|
||||
|
||||
uint8_t FreqManUIList::get_index() {
|
||||
return current_index + highlighted_index;
|
||||
}
|
||||
|
||||
void FreqManUIList::paint(Painter& painter) {
|
||||
freqlist_nb_lines = 0;
|
||||
const auto r = screen_rect();
|
||||
uint8_t focused = has_focus();
|
||||
const Rect r_widget_screen{r.left() + focused, r.top() + focused, r.width() - 2 * focused, r.height() - 2 * +focused};
|
||||
painter.fill_rectangle(
|
||||
r_widget_screen,
|
||||
Color::black());
|
||||
// only return after clearing the screen so previous entries are not shown anymore
|
||||
if (freqlist_db.size() == 0)
|
||||
return;
|
||||
// coloration if file is too big
|
||||
Style* text_color = &style_default;
|
||||
if (freqlist_db.size() > FREQMAN_MAX_PER_FILE)
|
||||
text_color = &style_yellow;
|
||||
uint8_t nb_lines = 0;
|
||||
for (uint8_t it = current_index; it < freqlist_db.size(); it++) {
|
||||
uint8_t line_height = (int)nb_lines * char_height;
|
||||
if (line_height < (r.height() - char_height)) {
|
||||
std::string description = freqman_item_string(freqlist_db[it], 30);
|
||||
// line is within the widget
|
||||
painter.draw_string(
|
||||
{0, r.location().y() + (int)nb_lines * char_height},
|
||||
*text_color, description);
|
||||
if (nb_lines == highlighted_index) {
|
||||
const Rect r_highlighted_freq{0, r.location().y() + (int)nb_lines * char_height, 240, char_height};
|
||||
painter.draw_rectangle(
|
||||
r_highlighted_freq,
|
||||
Color::white());
|
||||
}
|
||||
nb_lines++;
|
||||
} else {
|
||||
// rect is filled, we can break
|
||||
break;
|
||||
}
|
||||
}
|
||||
freqlist_nb_lines = nb_lines;
|
||||
if (has_focus() || highlighted()) {
|
||||
const Rect r_focus{r.left(), r.top(), r.width(), r.height()};
|
||||
painter.draw_rectangle(
|
||||
r_focus,
|
||||
Color::white());
|
||||
}
|
||||
}
|
||||
|
||||
void FreqManUIList::set_db(freqman_db& db) {
|
||||
freqlist_db = db;
|
||||
current_index = 0;
|
||||
highlighted_index = 0;
|
||||
}
|
||||
|
||||
void FreqManUIList::on_focus() {
|
||||
if (on_highlight)
|
||||
on_highlight(*this);
|
||||
}
|
||||
|
||||
bool FreqManUIList::on_key(const KeyEvent key) {
|
||||
if (key == KeyEvent::Select) {
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (on_dir) {
|
||||
return on_dir(*this, key);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FreqManUIList::on_touch(const TouchEvent event) {
|
||||
switch (event.type) {
|
||||
case TouchEvent::Type::Start:
|
||||
set_highlighted(true);
|
||||
set_dirty();
|
||||
if (on_touch_press) {
|
||||
on_touch_press(*this);
|
||||
}
|
||||
if (on_select && instant_exec_) {
|
||||
on_select(*this);
|
||||
}
|
||||
return true;
|
||||
case TouchEvent::Type::End:
|
||||
set_highlighted(false);
|
||||
set_dirty();
|
||||
if (on_touch_release) {
|
||||
on_touch_release(*this);
|
||||
}
|
||||
if (on_select && !instant_exec_) {
|
||||
on_select(*this);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool FreqManUIList::on_encoder(EncoderEvent delta) {
|
||||
set_highlighted_index((int)highlighted_index + delta);
|
||||
set_dirty();
|
||||
return true;
|
||||
}
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
*
|
||||
* 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 __UI_FREQLIST_H__
|
||||
#define __UI_FREQLIST_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_font_fixed_5x8.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "event_m0.hpp"
|
||||
#include "message.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace ui {
|
||||
|
||||
class FreqManUIList : public Widget {
|
||||
public:
|
||||
std::function<void(FreqManUIList&)> on_select{};
|
||||
std::function<void(FreqManUIList&)> on_touch_release{}; // Executed when releasing touch, after on_select.
|
||||
std::function<void(FreqManUIList&)> on_touch_press{}; // Executed when touching, before on_select.
|
||||
std::function<bool(FreqManUIList&, KeyEvent)> on_dir{};
|
||||
std::function<void(FreqManUIList&)> on_highlight{};
|
||||
|
||||
FreqManUIList(Rect parent_rect, bool instant_exec); // instant_exec: Execute on_select when you touching instead of releasing
|
||||
FreqManUIList(
|
||||
Rect parent_rect)
|
||||
: FreqManUIList{parent_rect, false} {
|
||||
}
|
||||
FreqManUIList()
|
||||
: FreqManUIList{{}, {}} {
|
||||
}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void on_focus() override;
|
||||
bool on_key(const KeyEvent key) override;
|
||||
bool on_touch(const TouchEvent event) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
|
||||
void set_highlighted_index(int index); // set highlighted_index and return capped highlighted_index value to set in caller
|
||||
uint8_t get_index(); // return highlighed + index
|
||||
void set_db(freqman_db& db);
|
||||
|
||||
private:
|
||||
Style style_default{
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::white(),
|
||||
};
|
||||
Style style_yellow{
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::yellow(),
|
||||
};
|
||||
static constexpr int8_t char_height = 16;
|
||||
bool instant_exec_{false};
|
||||
freqman_db freqlist_db{};
|
||||
int current_index{0};
|
||||
int highlighted_index{0};
|
||||
int freqlist_nb_lines{0};
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
#endif /*__UI_FREQLIST_H__*/
|
||||
@@ -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"
|
||||
@@ -236,10 +236,8 @@ void SystemStatusView::refresh() {
|
||||
|
||||
if (portapack::clock_manager.get_reference().source == ClockManager::ReferenceSource::External) {
|
||||
button_clock_status.set_bitmap(&bitmap_icon_clk_ext);
|
||||
// button_bias_tee.set_foreground(ui::Color::green()); Typo?
|
||||
} else {
|
||||
button_clock_status.set_bitmap(&bitmap_icon_clk_int);
|
||||
// button_bias_tee.set_foreground(ui::Color::green());
|
||||
}
|
||||
|
||||
if (portapack::persistent_memory::clkout_enabled()) {
|
||||
@@ -307,11 +305,10 @@ void SystemStatusView::on_stealth() {
|
||||
}
|
||||
|
||||
void SystemStatusView::on_bias_tee() {
|
||||
if (!portapack::antenna_bias) {
|
||||
if (!portapack::get_antenna_bias()) {
|
||||
nav_.display_modal("Bias voltage", "Enable DC voltage on\nantenna connector?", YESNO, [this](bool v) {
|
||||
if (v) {
|
||||
portapack::set_antenna_bias(true);
|
||||
// radio::set_antenna_bias(true);
|
||||
receiver_model.set_antenna_bias();
|
||||
transmitter_model.set_antenna_bias();
|
||||
refresh();
|
||||
@@ -319,9 +316,12 @@ void SystemStatusView::on_bias_tee() {
|
||||
});
|
||||
} else {
|
||||
portapack::set_antenna_bias(false);
|
||||
// radio::set_antenna_bias(false);
|
||||
receiver_model.set_antenna_bias();
|
||||
transmitter_model.set_antenna_bias();
|
||||
|
||||
// Ensure this is disabled. The models don't actually
|
||||
// update the radio unless they are 'enabled_'.
|
||||
radio::set_antenna_bias(false);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
@@ -418,7 +418,7 @@ View* NavigationView::push_view(std::unique_ptr<View> new_view) {
|
||||
free_view();
|
||||
|
||||
const auto p = new_view.get();
|
||||
view_stack.emplace_back(std::move(new_view));
|
||||
view_stack.emplace_back(ViewState{std::move(new_view), {}});
|
||||
|
||||
update_view();
|
||||
|
||||
@@ -426,34 +426,14 @@ View* NavigationView::push_view(std::unique_ptr<View> new_view) {
|
||||
}
|
||||
|
||||
void NavigationView::pop() {
|
||||
if (view() == modal_view) {
|
||||
modal_view = nullptr;
|
||||
}
|
||||
|
||||
// Can't pop last item from stack.
|
||||
if (view_stack.size() > 1) {
|
||||
free_view();
|
||||
|
||||
view_stack.pop_back();
|
||||
|
||||
update_view();
|
||||
}
|
||||
pop(true);
|
||||
}
|
||||
|
||||
void NavigationView::pop_modal() {
|
||||
if (view() == modal_view) {
|
||||
modal_view = nullptr;
|
||||
}
|
||||
|
||||
// Pop modal view + underlying app view
|
||||
if (view_stack.size() > 2) {
|
||||
free_view();
|
||||
view_stack.pop_back();
|
||||
free_view();
|
||||
view_stack.pop_back();
|
||||
|
||||
update_view();
|
||||
}
|
||||
// Pop modal view + underlying app view.
|
||||
// TODO: this shouldn't be necessary.
|
||||
pop(false);
|
||||
pop(true);
|
||||
}
|
||||
|
||||
void NavigationView::display_modal(
|
||||
@@ -473,12 +453,31 @@ void NavigationView::display_modal(
|
||||
}
|
||||
}
|
||||
|
||||
void NavigationView::pop(bool update) {
|
||||
if (view() == modal_view) {
|
||||
modal_view = nullptr;
|
||||
}
|
||||
|
||||
// Can't pop last item from stack.
|
||||
if (view_stack.size() > 1) {
|
||||
auto on_pop = view_stack.back().on_pop;
|
||||
|
||||
free_view();
|
||||
view_stack.pop_back();
|
||||
|
||||
if (update)
|
||||
update_view();
|
||||
|
||||
if (on_pop) on_pop();
|
||||
}
|
||||
}
|
||||
|
||||
void NavigationView::free_view() {
|
||||
remove_child(view());
|
||||
}
|
||||
|
||||
void NavigationView::update_view() {
|
||||
const auto new_view = view_stack.back().get();
|
||||
const auto new_view = view_stack.back().view.get();
|
||||
|
||||
add_child(new_view);
|
||||
new_view->set_parent_rect({{0, 0}, size()});
|
||||
@@ -501,6 +500,18 @@ void NavigationView::focus() {
|
||||
}
|
||||
}
|
||||
|
||||
bool NavigationView::set_on_pop(std::function<void()> on_pop) {
|
||||
if (view_stack.size() <= 1)
|
||||
return false;
|
||||
|
||||
auto& top = view_stack.back();
|
||||
if (top.on_pop)
|
||||
return false;
|
||||
|
||||
top.on_pop = on_pop;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ReceiversMenuView *****************************************************/
|
||||
|
||||
ReceiversMenuView::ReceiversMenuView(NavigationView& nav) {
|
||||
@@ -563,7 +574,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>(); } },
|
||||
});
|
||||
|
||||
@@ -75,9 +75,11 @@ class NavigationView : public View {
|
||||
|
||||
// Pushes a new view under the current on the stack so the current view returns into this new one.
|
||||
template <class T, class... Args>
|
||||
void push_under_current(Args&&... args) {
|
||||
T* push_under_current(Args&&... args) {
|
||||
auto new_view = std::unique_ptr<View>(new T(*this, std::forward<Args>(args)...));
|
||||
view_stack.insert(view_stack.end() - 1, std::move(new_view));
|
||||
auto new_view_ptr = new_view.get();
|
||||
view_stack.insert(view_stack.end() - 1, ViewState{std::move(new_view), {}});
|
||||
return reinterpret_cast<T*>(new_view_ptr);
|
||||
}
|
||||
|
||||
template <class T, class... Args>
|
||||
@@ -97,12 +99,22 @@ class NavigationView : public View {
|
||||
|
||||
void focus() override;
|
||||
|
||||
/* Sets the 'on_pop' handler for the current view.
|
||||
* Returns true if the handler was bound successfully. */
|
||||
bool set_on_pop(std::function<void()> on_pop);
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<View>> view_stack{};
|
||||
struct ViewState {
|
||||
std::unique_ptr<View> view;
|
||||
std::function<void()> on_pop;
|
||||
};
|
||||
|
||||
std::vector<ViewState> view_stack{};
|
||||
Widget* modal_view{nullptr};
|
||||
|
||||
Widget* view() const;
|
||||
|
||||
void pop(bool update);
|
||||
void free_view();
|
||||
void update_view();
|
||||
View* push_view(std::unique_ptr<View> new_view);
|
||||
|
||||
@@ -807,10 +807,6 @@ void cpu_clock_init(void) {
|
||||
// CCU2_CLK_APLL_CFG = 0;
|
||||
// CCU2_CLK_SDIO_CFG = 0;
|
||||
#endif
|
||||
|
||||
if (detected_platform() == BOARD_ID_HACKRF1_R9) {
|
||||
clkin_detect_init();
|
||||
}
|
||||
}
|
||||
|
||||
clock_source_t activate_best_clock_source(void) {
|
||||
|
||||
@@ -175,6 +175,22 @@ uint8_t mode_sense6(msd_cbw_t* msd_cbw_data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t mode_sense10(msd_cbw_t* msd_cbw_data) {
|
||||
(void)msd_cbw_data;
|
||||
|
||||
scsi_mode_sense10_response_t ret = {
|
||||
.byte = {
|
||||
cpu_to_be16(sizeof(scsi_mode_sense6_response_t) - 2),
|
||||
0,
|
||||
0,
|
||||
0}};
|
||||
|
||||
memcpy(&usb_bulk_buffer[0], &ret, sizeof(scsi_mode_sense10_response_t));
|
||||
usb_send_bulk(&usb_bulk_buffer[0], sizeof(scsi_mode_sense10_response_t));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static data_request_t decode_data_request(const uint8_t* cmd) {
|
||||
data_request_t req;
|
||||
uint32_t lba;
|
||||
@@ -254,6 +270,10 @@ void scsi_command(msd_cbw_t* msd_cbw_data) {
|
||||
status = mode_sense6(msd_cbw_data);
|
||||
break;
|
||||
|
||||
case SCSI_CMD_MODE_SENSE_10:
|
||||
status = mode_sense10(msd_cbw_data);
|
||||
break;
|
||||
|
||||
case SCSI_CMD_READ_FORMAT_CAPACITIES:
|
||||
status = read_format_capacities(msd_cbw_data);
|
||||
break;
|
||||
|
||||
@@ -35,13 +35,11 @@
|
||||
#include "hackrf_core.h"
|
||||
#include "usb_bulk_buffer.h"
|
||||
|
||||
#define MSD_CBW_SIGNATURE 0x43425355
|
||||
#define MSD_CSW_SIGNATURE 0x53425355
|
||||
|
||||
#define SCSI_CMD_TEST_UNIT_READY 0x00
|
||||
#define SCSI_CMD_REQUEST_SENSE 0x03
|
||||
#define SCSI_CMD_INQUIRY 0x12
|
||||
#define SCSI_CMD_MODE_SENSE_6 0x1A
|
||||
#define SCSI_CMD_MODE_SENSE_10 0x5A
|
||||
#define SCSI_CMD_START_STOP_UNIT 0x1B
|
||||
#define SCSI_CMD_SEND_DIAGNOSTIC 0x1D
|
||||
#define SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL 0x1E
|
||||
@@ -81,8 +79,8 @@
|
||||
#define SCSI_ASENSEQ_INIT_COMMAND_REQUIRED 0x02
|
||||
#define SCSI_ASENSEQ_OPERATION_IN_PROGRESS 0x07
|
||||
|
||||
#define MSD_CBW_SIGNATURE 0x43425355
|
||||
#define MSD_CSW_SIGNATURE 0x53425355
|
||||
#define MSD_CBW_SIGNATURE 0x43425355 // USBC
|
||||
#define MSD_CSW_SIGNATURE 0x53425355 // USBS
|
||||
|
||||
#define USB_TRANSFER_SIZE 0x2000
|
||||
|
||||
@@ -136,6 +134,10 @@ typedef struct {
|
||||
uint8_t byte[4];
|
||||
} scsi_mode_sense6_response_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t byte[4];
|
||||
} scsi_mode_sense10_response_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t first_lba;
|
||||
uint16_t blk_cnt;
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
#include "usb_type.h"
|
||||
#include "usb_descriptor.h"
|
||||
|
||||
#define USB_VENDOR_ID (0x1D50)
|
||||
#define USB_VENDOR_ID (0x0781) /* SanDisk Corp. */
|
||||
|
||||
#ifdef HACKRF_ONE
|
||||
#define USB_PRODUCT_ID (0x6089)
|
||||
#define USB_PRODUCT_ID (0xa7a8) /* SD card reader */
|
||||
#elif JAWBREAKER
|
||||
#define USB_PRODUCT_ID (0x604B)
|
||||
#elif RAD1O
|
||||
@@ -37,7 +37,7 @@
|
||||
#define USB_PRODUCT_ID (0xFFFF)
|
||||
#endif
|
||||
|
||||
#define USB_API_VERSION (0x0107)
|
||||
#define USB_API_VERSION (0x0127) /* hardware revision */
|
||||
|
||||
#define USB_WORD(x) (x & 0xFF), ((x >> 8) & 0xFF)
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -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,26 @@ 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_; }
|
||||
operator bool() 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 std::move(value_); }
|
||||
T&& operator*() && { return std::move(value_); }
|
||||
|
||||
T* operator->() { return &value_; }
|
||||
const T* operator->() const { return &value_; }
|
||||
|
||||
private:
|
||||
T value_;
|
||||
|
||||
@@ -297,11 +297,14 @@ struct data_t {
|
||||
int64_t converter_frequency_offset;
|
||||
|
||||
// frequency correction
|
||||
int16_t frequency_rx_correction;
|
||||
uint32_t frequency_rx_correction;
|
||||
bool updown_frequency_rx_correction;
|
||||
int16_t frequency_tx_correction;
|
||||
uint32_t frequency_tx_correction;
|
||||
bool updown_frequency_tx_correction;
|
||||
|
||||
// Rotary encoder dial sensitivity (encoder.cpp/hpp)
|
||||
uint8_t encoder_dial_sensitivity;
|
||||
|
||||
constexpr data_t()
|
||||
: structure_version(data_structure_version_enum::VERSION_CURRENT),
|
||||
tuned_frequency(tuned_frequency_reset_value),
|
||||
@@ -337,7 +340,8 @@ struct data_t {
|
||||
frequency_rx_correction(0),
|
||||
updown_frequency_rx_correction(0),
|
||||
frequency_tx_correction(0),
|
||||
updown_frequency_tx_correction(0) {
|
||||
updown_frequency_tx_correction(0),
|
||||
encoder_dial_sensitivity(0) {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -795,19 +799,27 @@ bool config_freq_rx_correction_updown() {
|
||||
void set_freq_rx_correction_updown(bool v) {
|
||||
data->updown_frequency_rx_correction = v;
|
||||
}
|
||||
int16_t config_freq_tx_correction() {
|
||||
uint32_t config_freq_tx_correction() {
|
||||
return data->frequency_tx_correction;
|
||||
}
|
||||
int16_t config_freq_rx_correction() {
|
||||
uint32_t config_freq_rx_correction() {
|
||||
return data->frequency_rx_correction;
|
||||
}
|
||||
void set_config_freq_tx_correction(uint16_t v) {
|
||||
void set_config_freq_tx_correction(uint32_t v) {
|
||||
data->frequency_tx_correction = v;
|
||||
}
|
||||
void set_config_freq_rx_correction(uint16_t v) {
|
||||
void set_config_freq_rx_correction(uint32_t v) {
|
||||
data->frequency_rx_correction = v;
|
||||
}
|
||||
|
||||
// rotary encoder dial settings
|
||||
uint8_t config_encoder_dial_sensitivity() {
|
||||
return data->encoder_dial_sensitivity;
|
||||
}
|
||||
void set_encoder_dial_sensitivity(uint8_t v) {
|
||||
data->encoder_dial_sensitivity = v;
|
||||
}
|
||||
|
||||
// sd persisting settings
|
||||
int save_persistent_settings_to_file(std::string filename) {
|
||||
delete_file(filename);
|
||||
|
||||
@@ -99,6 +99,13 @@ struct backlight_config_t {
|
||||
bool _timeout_enabled;
|
||||
};
|
||||
|
||||
enum encoder_dial_sensitivity {
|
||||
DIAL_SENSITIVITY_MEDIUM = 0,
|
||||
DIAL_SENSITIVITY_LOW = 1,
|
||||
DIAL_SENSITIVITY_HIGH = 2,
|
||||
NUM_DIAL_SENSITIVITY
|
||||
};
|
||||
|
||||
namespace cache {
|
||||
|
||||
/* Set values in cache to sensible defaults. */
|
||||
@@ -189,16 +196,18 @@ bool config_freq_tx_correction_updown();
|
||||
void set_freq_tx_correction_updown(bool v);
|
||||
bool config_freq_rx_correction_updown();
|
||||
void set_freq_rx_correction_updown(bool v);
|
||||
int16_t config_freq_tx_correction();
|
||||
int16_t config_freq_rx_correction();
|
||||
void set_config_freq_tx_correction(uint16_t v);
|
||||
void set_config_freq_rx_correction(uint16_t v);
|
||||
uint32_t config_freq_tx_correction();
|
||||
uint32_t config_freq_rx_correction();
|
||||
void set_config_freq_tx_correction(uint32_t v);
|
||||
void set_config_freq_rx_correction(uint32_t v);
|
||||
void set_clock_hidden(bool v);
|
||||
void set_clock_with_date(bool v);
|
||||
void set_config_login(bool v);
|
||||
void set_config_speaker(bool v);
|
||||
void set_config_backlight_timer(const backlight_config_t& new_value);
|
||||
void set_disable_touchscreen(bool v);
|
||||
uint8_t config_encoder_dial_sensitivity();
|
||||
void set_encoder_dial_sensitivity(uint8_t v);
|
||||
|
||||
// uint8_t ui_config_textentry();
|
||||
// void set_config_textentry(uint8_t new_value);
|
||||
|
||||
@@ -35,18 +35,27 @@ Style Style::invert() const {
|
||||
.foreground = background};
|
||||
}
|
||||
|
||||
int Painter::draw_char(const Point p, const Style& style, const char c) {
|
||||
int Painter::draw_char(Point p, const Style& style, char c) {
|
||||
const auto glyph = style.font.glyph(c);
|
||||
display.draw_glyph(p, glyph, style.foreground, style.background);
|
||||
return glyph.advance().x();
|
||||
}
|
||||
|
||||
int Painter::draw_string(Point p, const Font& font, const Color foreground, const Color background, const std::string& text) {
|
||||
int Painter::draw_string(Point p, const Style& style, std::string_view text) {
|
||||
return draw_string(p, style.font, style.foreground, style.background, text);
|
||||
}
|
||||
|
||||
int Painter::draw_string(
|
||||
Point p,
|
||||
const Font& font,
|
||||
Color foreground,
|
||||
Color background,
|
||||
std::string_view text) {
|
||||
bool escape = false;
|
||||
size_t width = 0;
|
||||
Color pen = foreground;
|
||||
|
||||
for (const auto c : text) {
|
||||
for (auto c : text) {
|
||||
if (escape) {
|
||||
if (c <= 15)
|
||||
pen = term_colors[c & 15];
|
||||
@@ -65,48 +74,45 @@ int Painter::draw_string(Point p, const Font& font, const Color foreground, cons
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
int Painter::draw_string(Point p, const Style& style, const std::string& text) {
|
||||
return draw_string(p, style.font, style.foreground, style.background, text);
|
||||
}
|
||||
|
||||
void Painter::draw_bitmap(const Point p, const Bitmap& bitmap, const Color foreground, const Color background) {
|
||||
void Painter::draw_bitmap(Point p, const Bitmap& bitmap, Color foreground, Color background) {
|
||||
display.draw_bitmap(p, bitmap.size, bitmap.data, foreground, background);
|
||||
}
|
||||
|
||||
void Painter::draw_hline(Point p, int width, const Color c) {
|
||||
void Painter::draw_hline(Point p, int width, Color c) {
|
||||
display.fill_rectangle({p, {width, 1}}, c);
|
||||
}
|
||||
|
||||
void Painter::draw_vline(Point p, int height, const Color c) {
|
||||
void Painter::draw_vline(Point p, int height, Color c) {
|
||||
display.fill_rectangle({p, {1, height}}, c);
|
||||
}
|
||||
|
||||
void Painter::draw_rectangle(const Rect r, const Color c) {
|
||||
void Painter::draw_rectangle(Rect r, Color c) {
|
||||
draw_hline(r.location(), r.width(), c);
|
||||
draw_vline({r.left(), r.top() + 1}, r.height() - 2, c);
|
||||
draw_vline({r.left() + r.width() - 1, r.top() + 1}, r.height() - 2, c);
|
||||
draw_hline({r.left(), r.top() + r.height() - 1}, r.width(), c);
|
||||
}
|
||||
|
||||
void Painter::fill_rectangle(const Rect r, const Color c) {
|
||||
void Painter::fill_rectangle(Rect r, Color c) {
|
||||
display.fill_rectangle(r, c);
|
||||
}
|
||||
|
||||
void Painter::fill_rectangle_unrolled8(const Rect r, const Color c) {
|
||||
void Painter::fill_rectangle_unrolled8(Rect r, Color c) {
|
||||
display.fill_rectangle_unrolled8(r, c);
|
||||
}
|
||||
|
||||
void Painter::paint_widget_tree(Widget* const w) {
|
||||
void Painter::paint_widget_tree(Widget* w) {
|
||||
if (ui::is_dirty()) {
|
||||
paint_widget(w);
|
||||
ui::dirty_clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Painter::paint_widget(Widget* const w) {
|
||||
void Painter::paint_widget(Widget* w) {
|
||||
if (w->hidden()) {
|
||||
// Mark widget (and all children) as invisible.
|
||||
w->visible(false);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "ui.hpp"
|
||||
#include "ui_text.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -46,24 +46,24 @@ class Painter {
|
||||
Painter(const Painter&) = delete;
|
||||
Painter(Painter&&) = delete;
|
||||
|
||||
int draw_char(const Point p, const Style& style, const char c);
|
||||
int draw_char(Point p, const Style& style, char c);
|
||||
|
||||
int draw_string(Point p, const Font& font, const Color foreground, const Color background, const std::string& text);
|
||||
int draw_string(Point p, const Style& style, const std::string& text);
|
||||
int draw_string(Point p, const Style& style, std::string_view text);
|
||||
int draw_string(Point p, const Font& font, Color foreground, Color background, std::string_view text);
|
||||
|
||||
void draw_bitmap(const Point p, const Bitmap& bitmap, const Color background, const Color foreground);
|
||||
void draw_bitmap(Point p, const Bitmap& bitmap, Color background, Color foreground);
|
||||
|
||||
void draw_rectangle(const Rect r, const Color c);
|
||||
void fill_rectangle(const Rect r, const Color c);
|
||||
void fill_rectangle_unrolled8(const Rect r, const Color c);
|
||||
void draw_rectangle(Rect r, Color c);
|
||||
void fill_rectangle(Rect r, Color c);
|
||||
void fill_rectangle_unrolled8(Rect r, Color c);
|
||||
|
||||
void paint_widget_tree(Widget* const w);
|
||||
void paint_widget_tree(Widget* w);
|
||||
|
||||
void draw_hline(Point p, int width, const Color c);
|
||||
void draw_vline(Point p, int height, const Color c);
|
||||
void draw_hline(Point p, int width, Color c);
|
||||
void draw_vline(Point p, int height, Color c);
|
||||
|
||||
private:
|
||||
void paint_widget(Widget* const w);
|
||||
void paint_widget(Widget* w);
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 171 B After Width: | Height: | Size: 188 B |
@@ -0,0 +1,30 @@
|
||||
# Copyright (C) 2023 Bernd Herzog, 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.
|
||||
#
|
||||
|
||||
project(tests)
|
||||
|
||||
set(DOCTESTINC ${PROJECT_SOURCE_DIR}/include)
|
||||
|
||||
enable_testing()
|
||||
add_subdirectory(application)
|
||||
add_subdirectory(baseband)
|
||||
|
||||
add_custom_target(build_tests)
|
||||
add_dependencies(build_tests application_test baseband_test)
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright (C) 2023 Bernd Herzog, 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.
|
||||
#
|
||||
|
||||
project(application_test)
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
include(${CHIBIOS_PORTAPACK}/boards/PORTAPACK_APPLICATION/board.cmake)
|
||||
include(${CHIBIOS_PORTAPACK}/os/hal/platforms/LPC43xx_M0/platform.cmake)
|
||||
include(${CHIBIOS}/os/hal/hal.cmake)
|
||||
include(${CHIBIOS_PORTAPACK}/os/ports/GCC/ARMCMx/LPC43xx_M0/port.cmake)
|
||||
include(${CHIBIOS}/os/kernel/kernel.cmake)
|
||||
include(${CHIBIOS_PORTAPACK}/os/various/fatfs_bindings/fatfs.cmake)
|
||||
include(${CHIBIOS}/test/test.cmake)
|
||||
|
||||
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_file_wrapper.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_optional.cpp
|
||||
)
|
||||
|
||||
target_include_directories(application_test PRIVATE
|
||||
${DOCTESTINC}
|
||||
${PROJECT_SOURCE_DIR}/../../application
|
||||
${COMMON}
|
||||
${PORTINC}
|
||||
${KERNINC}
|
||||
${TESTINC}
|
||||
${HALINC}
|
||||
${PLATFORMINC}
|
||||
${BOARDINC}
|
||||
${CHIBIOS}/os/various
|
||||
${FATFSINC}
|
||||
${BASEBAND}
|
||||
)
|
||||
|
||||
target_compile_options(application_test PRIVATE
|
||||
-DLPC43XX
|
||||
-DLPC43XX_M0
|
||||
-D__NEWLIB__
|
||||
-DHACKRF_ONE
|
||||
-DTOOLCHAIN_GCC
|
||||
-DTOOLCHAIN_GCC_ARM
|
||||
-D_RANDOM_TCC=0
|
||||
-DVERSION_STRING=\"${VERSION}\"
|
||||
${USE_CPPOPT}
|
||||
${USE_OPT}
|
||||
${CPPWARN}
|
||||
)
|
||||
|
||||
add_test(NAME application_test
|
||||
COMMAND application_test
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include "doctest.h"
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
TEST_CASE("Testing test framework") {
|
||||
REQUIRE(1 == 1);
|
||||
REQUIRE_FALSE(1 == 2);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -0,0 +1,698 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "doctest.h"
|
||||
#include "file.hpp"
|
||||
#include "file_wrapper.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
/* Mocks the File interface with a backing string. */
|
||||
class MockFile {
|
||||
public:
|
||||
using Error = File::Error;
|
||||
using Offset = File::Offset;
|
||||
using Size = File::Size;
|
||||
template <typename T>
|
||||
using Result = File::Result<T>;
|
||||
|
||||
MockFile(std::string data)
|
||||
: data_{std::move(data)} {}
|
||||
|
||||
Size size() { return data_.size(); }
|
||||
|
||||
Result<Offset> seek(uint32_t offset) {
|
||||
if ((int32_t)offset < 0)
|
||||
return {static_cast<Error>(FR_BAD_SEEK)};
|
||||
|
||||
auto previous = offset_;
|
||||
|
||||
if (offset > size())
|
||||
data_.resize(offset);
|
||||
|
||||
offset_ = offset;
|
||||
return previous;
|
||||
}
|
||||
|
||||
Result<Offset> truncate() {
|
||||
data_.resize(offset_);
|
||||
return offset_;
|
||||
}
|
||||
|
||||
Result<Size> read(void* data, Size bytes_to_read) {
|
||||
if (offset_ + bytes_to_read > size())
|
||||
bytes_to_read = size() - offset_;
|
||||
|
||||
if (bytes_to_read == 0 || bytes_to_read > size()) // NB: underflow wrap
|
||||
return 0;
|
||||
|
||||
memcpy(data, &data_[offset_], bytes_to_read);
|
||||
offset_ += bytes_to_read;
|
||||
return bytes_to_read;
|
||||
}
|
||||
|
||||
Result<Size> write(const void* data, Size bytes_to_write) {
|
||||
auto new_offset = offset_ + bytes_to_write;
|
||||
if (new_offset >= size())
|
||||
data_.resize(new_offset);
|
||||
|
||||
memcpy(&data_[offset_], data, bytes_to_write);
|
||||
offset_ = new_offset;
|
||||
return bytes_to_write;
|
||||
}
|
||||
|
||||
Optional<Error> sync() {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string data_;
|
||||
uint32_t offset_{0};
|
||||
};
|
||||
|
||||
/* Verifies correctness of MockFile. */
|
||||
TEST_SUITE("Test MockFile") {
|
||||
SCENARIO("File size") {
|
||||
GIVEN("Empty string") {
|
||||
MockFile f{""};
|
||||
|
||||
THEN("size() should be 0.") {
|
||||
CHECK_EQ(f.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("Not empty string") {
|
||||
MockFile f{"abc"};
|
||||
|
||||
THEN("size() should be string length.") {
|
||||
CHECK_EQ(f.size(), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("File seek") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto init_size = f.size();
|
||||
|
||||
WHEN("seek()") {
|
||||
f.seek(4);
|
||||
THEN("offset_ should be updated.") {
|
||||
CHECK_EQ(f.offset_, 4);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() negative offset") {
|
||||
auto r = f.seek(-1);
|
||||
|
||||
THEN("Result should be bad_seek.") {
|
||||
CHECK(r.is_error());
|
||||
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() offset is size()") {
|
||||
auto r = f.seek(f.size());
|
||||
|
||||
THEN("File should not grow.") {
|
||||
CHECK(r.is_ok());
|
||||
CHECK_EQ(f.size(), init_size);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() offset > size()") {
|
||||
auto r = f.seek(f.size() + 1);
|
||||
|
||||
THEN("File should grow.") {
|
||||
CHECK(r.is_ok());
|
||||
CHECK_EQ(f.size(), init_size + 1);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() offset < size()") {
|
||||
auto r = f.seek(1);
|
||||
|
||||
THEN("Result should be ok.") {
|
||||
CHECK(r.is_ok());
|
||||
}
|
||||
|
||||
r = f.seek(3);
|
||||
|
||||
THEN("Result should be previous offset") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("File read") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
|
||||
const auto buf_len = 10;
|
||||
std::string buf;
|
||||
buf.resize(buf_len);
|
||||
|
||||
WHEN("Reading") {
|
||||
auto r = f.read(&buf[0], 3);
|
||||
|
||||
THEN("Result should be number of bytes read") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 3);
|
||||
}
|
||||
|
||||
buf.resize(*r);
|
||||
THEN("Buffer should contain read data") {
|
||||
CHECK_EQ(buf.length(), 3);
|
||||
CHECK_EQ(buf, "abc");
|
||||
}
|
||||
|
||||
r = f.read(&buf[0], 3);
|
||||
THEN("Reading should continue where it left off") {
|
||||
CHECK_EQ(buf.length(), 3);
|
||||
CHECK_EQ(buf, "\nde");
|
||||
}
|
||||
|
||||
r = f.read(&buf[0], 3);
|
||||
THEN("Reading should stop at the end of the file") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 1);
|
||||
|
||||
buf.resize(*r);
|
||||
CHECK_EQ(buf.length(), 1);
|
||||
CHECK_EQ(buf, "f");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading block larger than file size") {
|
||||
auto r = f.read(&buf[0], buf_len);
|
||||
buf.resize(*r);
|
||||
|
||||
THEN("It should read to file end.") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 7);
|
||||
CHECK_EQ(buf, f.data_);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("File write") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
|
||||
WHEN("Writing over existing region") {
|
||||
f.write("xyz", 3);
|
||||
|
||||
THEN("It should overwrite") {
|
||||
CHECK_EQ(f.data_, "xyz\ndef");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Writing over past end") {
|
||||
f.seek(f.size());
|
||||
f.write("xyz", 3);
|
||||
|
||||
THEN("It should extend file and write") {
|
||||
CHECK_EQ(f.size(), 10);
|
||||
CHECK_EQ(f.data_, "abc\ndefxyz");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This scenario was tested on device.
|
||||
SCENARIO("File truncate") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"hello world"};
|
||||
|
||||
WHEN("truncating at offset 5") {
|
||||
f.seek(5);
|
||||
f.truncate();
|
||||
|
||||
THEN("resulting file should be 'hello'.") {
|
||||
CHECK_EQ(f.size(), 5);
|
||||
CHECK_EQ(f.data_, "hello");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("File truncate") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto init_size = f.size();
|
||||
|
||||
WHEN("R/W pointer at end") {
|
||||
f.seek(f.size());
|
||||
f.truncate();
|
||||
|
||||
THEN("It should not change size.") {
|
||||
CHECK_EQ(f.size(), init_size);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("R/W pointer in middle") {
|
||||
f.seek(3);
|
||||
f.truncate();
|
||||
|
||||
THEN("It should change size.") {
|
||||
CHECK_EQ(f.size(), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_BEGIN("Test BufferWrapper");
|
||||
|
||||
TEST_CASE("It can wrap a MockFile.") {
|
||||
MockFile f{""};
|
||||
auto w = wrap_buffer(f);
|
||||
}
|
||||
|
||||
SCENARIO("Empty file") {
|
||||
GIVEN("An empty file") {
|
||||
MockFile f{""};
|
||||
auto w = wrap_buffer(f);
|
||||
|
||||
WHEN("Initializing") {
|
||||
CHECK_EQ(w.size(), 0);
|
||||
REQUIRE_EQ(w.line_count(), 1);
|
||||
}
|
||||
|
||||
WHEN("Getting line_range()") {
|
||||
auto r = w.line_range(0);
|
||||
CHECK(r);
|
||||
CHECK_EQ(r->start, 0);
|
||||
CHECK_EQ(r->end, 1);
|
||||
}
|
||||
|
||||
WHEN("Getting line_length()") {
|
||||
CHECK_EQ(w.line_length(0), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Basic file") {
|
||||
GIVEN("A file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
|
||||
WHEN("Initializing") {
|
||||
CHECK_EQ(w.size(), 7);
|
||||
REQUIRE_EQ(w.line_count(), 2);
|
||||
}
|
||||
|
||||
WHEN("Getting line_range()") {
|
||||
auto r = w.line_range(0);
|
||||
CHECK(r);
|
||||
CHECK_EQ(r->start, 0);
|
||||
CHECK_EQ(r->end, 4);
|
||||
|
||||
r = w.line_range(1);
|
||||
CHECK(r);
|
||||
CHECK_EQ(r->start, 4);
|
||||
CHECK_EQ(r->end, 7);
|
||||
}
|
||||
|
||||
WHEN("Getting line_length()") {
|
||||
CHECK_EQ(w.line_length(0), 4);
|
||||
CHECK_EQ(w.line_length(1), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Reading file lines.") {
|
||||
GIVEN("A valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
|
||||
WHEN("Reading a line") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(str->length(), 4); // Includes '\n'
|
||||
CHECK_EQ(*str, "abc\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading the last line") {
|
||||
auto str = w.get_text(w.line_count() - 1, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(str->length(), 3);
|
||||
CHECK_EQ(*str, "def");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading past the last line") {
|
||||
auto str = w.get_text(w.line_count(), 0, 10);
|
||||
|
||||
THEN("It should return empty value.") {
|
||||
REQUIRE(!str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Reading with cache miss.") {
|
||||
GIVEN("A valid file") {
|
||||
MockFile f{"abc\ndef\nghi\njkl\nmno"};
|
||||
constexpr uint32_t cache_size = 2;
|
||||
auto w = wrap_buffer<cache_size>(f);
|
||||
|
||||
CHECK_EQ(w.start_line(), 0);
|
||||
|
||||
WHEN("Reading a cached line") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "abc\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading line after last cached line.") {
|
||||
auto str = w.get_text(w.line_count() - 1, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "mno");
|
||||
}
|
||||
|
||||
THEN("It should push cache window forward to include line.") {
|
||||
CHECK_EQ(w.start_line(), w.line_count() - cache_size);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading line before first cached line.") {
|
||||
// First move cache forward to end.
|
||||
w.get_text(w.line_count() - 1, 0, 10);
|
||||
auto str = w.get_text(1, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "def\n");
|
||||
}
|
||||
|
||||
THEN("It should push cache window backward to include line.") {
|
||||
CHECK_EQ(w.start_line(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading line 0 before first cached line.") {
|
||||
// First move cache forward to end, then back to beginning.
|
||||
w.get_text(w.line_count() - 1, 0, 10);
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "abc\n");
|
||||
}
|
||||
|
||||
THEN("It should push cache window backward to include line.") {
|
||||
CHECK_EQ(w.start_line(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Replace range of same size.") {
|
||||
GIVEN("A file with lines") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Replacing range without changing size") {
|
||||
w.replace_range({0, 3}, "xyz");
|
||||
|
||||
CHECK_EQ("xyz\ndef", f.data_);
|
||||
|
||||
THEN("size should not change.") {
|
||||
CHECK_EQ(w.size(), init_size);
|
||||
}
|
||||
|
||||
THEN("text should be replaced.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "xyz\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Replace range that increases size.") {
|
||||
GIVEN("A file with lines") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Replacing range with larger size") {
|
||||
w.replace_range({0, 3}, "wxyz");
|
||||
|
||||
CHECK_EQ(f.data_, "wxyz\ndef");
|
||||
|
||||
THEN("size should be increased.") {
|
||||
CHECK_EQ(w.size(), init_size + 1);
|
||||
}
|
||||
|
||||
THEN("text should be replaced.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "wxyz\n");
|
||||
}
|
||||
|
||||
THEN("following text should not be modified.") {
|
||||
auto str = w.get_text(1, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "def");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("A file larger than internal buffer_size (512)") {
|
||||
std::string content = std::string(599, 'a');
|
||||
content.push_back('x');
|
||||
MockFile f{content};
|
||||
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Replacing range with larger size") {
|
||||
w.replace_range({0, 2}, "bbb");
|
||||
|
||||
THEN("size should be increased.") {
|
||||
CHECK_EQ(w.size(), init_size + 1);
|
||||
}
|
||||
|
||||
THEN("text should be replaced.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "bbbaaaaaaa");
|
||||
}
|
||||
|
||||
THEN("end text should be preserved.") {
|
||||
CHECK_EQ(f.data_.back(), 'x');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Replace range that decreases size.") {
|
||||
GIVEN("A file with lines") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Replacing range with smaller size") {
|
||||
w.replace_range({0, 3}, "yz");
|
||||
|
||||
CHECK_EQ(f.data_, "yz\ndef");
|
||||
|
||||
THEN("size should be decreased.") {
|
||||
CHECK_EQ(w.size(), init_size - 1);
|
||||
}
|
||||
|
||||
THEN("text should be replaced.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "yz\n");
|
||||
}
|
||||
|
||||
THEN("following text should not be modified.") {
|
||||
auto str = w.get_text(1, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "def");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("A file larger than internal buffer_size (512)") {
|
||||
std::string content = std::string(599, 'a');
|
||||
content.push_back('x');
|
||||
MockFile f{content};
|
||||
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Replacing range with smaller size") {
|
||||
w.replace_range({0, 10}, "b");
|
||||
|
||||
THEN("size should be decreased.") {
|
||||
CHECK_EQ(w.size(), init_size - 9);
|
||||
}
|
||||
|
||||
THEN("text should be replaced.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "baaaaaaaaa");
|
||||
}
|
||||
|
||||
THEN("end should be moved toward front.") {
|
||||
CHECK_EQ(f.data_.back(), 'x');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Insert line.") {
|
||||
GIVEN("A file with lines") {
|
||||
MockFile f{"abc\ndef\nghi\n"};
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Inserting before the first line") {
|
||||
w.insert_line(0);
|
||||
|
||||
THEN("should increment line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count + 1);
|
||||
CHECK_EQ(w.size(), init_size + 1);
|
||||
}
|
||||
|
||||
THEN("should insert empty line content.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
CHECK_EQ(*str, "\n");
|
||||
}
|
||||
|
||||
THEN("first line should be pushed down.") {
|
||||
auto str = w.get_text(1, 0, 10);
|
||||
CHECK_EQ(*str, "abc\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Inserting before the last line") {
|
||||
w.insert_line(2);
|
||||
|
||||
THEN("should increment line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count + 1);
|
||||
CHECK_EQ(w.size(), init_size + 1);
|
||||
}
|
||||
|
||||
THEN("should insert empty line content.") {
|
||||
auto str = w.get_text(2, 0, 10);
|
||||
CHECK_EQ(*str, "\n");
|
||||
}
|
||||
|
||||
THEN("last line should be pushed down.") {
|
||||
auto str = w.get_text(3, 0, 10);
|
||||
CHECK_EQ(*str, "ghi\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Inserting after the last line") {
|
||||
w.insert_line(w.line_count());
|
||||
|
||||
THEN("should increment line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count + 1);
|
||||
CHECK_EQ(w.size(), init_size + 1);
|
||||
}
|
||||
|
||||
THEN("should insert empty line content.") {
|
||||
auto str = w.get_text(3, 0, 10);
|
||||
CHECK_EQ(*str, "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Delete line.") {
|
||||
GIVEN("A file with lines") {
|
||||
MockFile f{"abc\ndef\nghi"};
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Deleting the first line") {
|
||||
w.delete_line(0);
|
||||
|
||||
THEN("should decrement line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count - 1);
|
||||
CHECK_EQ(w.size(), init_size - 4);
|
||||
}
|
||||
|
||||
THEN("should remove first line content.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
CHECK_EQ(*str, "def\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Deleting the middle line") {
|
||||
w.delete_line(1);
|
||||
|
||||
THEN("should decrement line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count - 1);
|
||||
CHECK_EQ(w.size(), init_size - 4);
|
||||
}
|
||||
|
||||
THEN("should remove middle line content.") {
|
||||
auto str = w.get_text(1, 0, 10);
|
||||
CHECK_EQ(*str, "ghi");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Deleting the last line") {
|
||||
w.delete_line(2);
|
||||
|
||||
THEN("should decrement line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count - 1);
|
||||
CHECK_EQ(w.size(), init_size - 3);
|
||||
}
|
||||
|
||||
THEN("should remove last line content.") {
|
||||
auto str = w.get_text(2, 0, 10);
|
||||
CHECK(!str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 "optional.hpp"
|
||||
|
||||
TEST_SUITE_BEGIN("optional");
|
||||
|
||||
TEST_CASE("Default instance should not be valid.") {
|
||||
Optional<int> o;
|
||||
REQUIRE_FALSE(o.is_valid());
|
||||
}
|
||||
|
||||
TEST_CASE("Instance with value should be valid.") {
|
||||
Optional<int> o{1};
|
||||
REQUIRE(o.is_valid());
|
||||
}
|
||||
|
||||
TEST_CASE("Instance with value should be return true.") {
|
||||
Optional<int> o{1};
|
||||
REQUIRE((bool)o);
|
||||
}
|
||||
|
||||
TEST_CASE("value() should return value.") {
|
||||
Optional<int> o{1};
|
||||
REQUIRE(o.value() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("operator* should return value.") {
|
||||
Optional<int> o{1};
|
||||
REQUIRE(*o == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("operator-> should return value members.") {
|
||||
struct S {
|
||||
int i;
|
||||
};
|
||||
|
||||
Optional<S> o{S{1}};
|
||||
REQUIRE(o->i == 1);
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (C) 2023 Bernd Herzog, 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.
|
||||
#
|
||||
|
||||
project(baseband_test)
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
include(${CHIBIOS_PORTAPACK}/boards/PORTAPACK_BASEBAND/board.cmake)
|
||||
include(${CHIBIOS_PORTAPACK}/os/hal/platforms/LPC43xx_M4/platform.cmake)
|
||||
include(${CHIBIOS}/os/hal/hal.cmake)
|
||||
include(${CHIBIOS_PORTAPACK}/os/ports/GCC/ARMCMx/LPC43xx_M4/port.cmake)
|
||||
include(${CHIBIOS}/os/kernel/kernel.cmake)
|
||||
include(${CHIBIOS}/test/test.cmake)
|
||||
|
||||
set(CMAKE_CXX_COMPILER g++)
|
||||
|
||||
add_executable(baseband_test EXCLUDE_FROM_ALL
|
||||
${PROJECT_SOURCE_DIR}/main.cpp
|
||||
${PROJECT_SOURCE_DIR}/dsp_fft_test.cpp
|
||||
${COMMON}/dsp_fft.cpp
|
||||
)
|
||||
|
||||
target_include_directories(baseband_test PRIVATE
|
||||
${DOCTESTINC}
|
||||
${COMMON}
|
||||
${PORTINC}
|
||||
${KERNINC}
|
||||
${TESTINC}
|
||||
${HALINC}
|
||||
${PLATFORMINC}
|
||||
${BOARDINC}
|
||||
${CHIBIOS}/os/various
|
||||
${BASEBAND}
|
||||
)
|
||||
|
||||
target_compile_options(baseband_test PRIVATE
|
||||
-DLPC43XX
|
||||
-DLPC43XX_M4
|
||||
-D__NEWLIB__
|
||||
-DHACKRF_ONE
|
||||
-DTOOLCHAIN_GCC
|
||||
-DTOOLCHAIN_GCC_ARM
|
||||
-D_RANDOM_TCC=0
|
||||
-DVERSION_STRING=\"${VERSION}\"
|
||||
)
|
||||
|
||||
add_test(NAME baseband_test
|
||||
COMMAND baseband_test
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* 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 "dsp_fft.hpp"
|
||||
#include "doctest.h"
|
||||
|
||||
TEST_CASE("ifft successfully calculates dc on zero frequency") {
|
||||
uint32_t fft_width = 8;
|
||||
complex16_t* v = new complex16_t[fft_width];
|
||||
complex16_t* tmp = new complex16_t[fft_width];
|
||||
|
||||
v[0] = {1024, 0}; // DC bin
|
||||
v[1] = {0, 0};
|
||||
v[2] = {0, 0};
|
||||
v[3] = {0, 0};
|
||||
v[4] = {0, 0};
|
||||
v[5] = {0, 0};
|
||||
v[6] = {0, 0};
|
||||
v[7] = {0, 0};
|
||||
|
||||
ifft<complex16_t>(v, fft_width, tmp);
|
||||
|
||||
CHECK(v[0].real() == 1024);
|
||||
CHECK(v[1].real() == 1024);
|
||||
CHECK(v[2].real() == 1024);
|
||||
CHECK(v[3].real() == 1024);
|
||||
CHECK(v[4].real() == 1024);
|
||||
CHECK(v[5].real() == 1024);
|
||||
CHECK(v[6].real() == 1024);
|
||||
CHECK(v[7].real() == 1024);
|
||||
|
||||
for (uint32_t i = 0; i < fft_width; i++)
|
||||
CHECK(v[i].imag() == 0);
|
||||
|
||||
free(v);
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
TEST_CASE("ifft successfully calculates pure sine of half the sample rate") {
|
||||
uint32_t fft_width = 8;
|
||||
complex16_t* v = new complex16_t[fft_width];
|
||||
complex16_t* tmp = new complex16_t[fft_width];
|
||||
|
||||
v[0] = {0, 0};
|
||||
v[1] = {0, 0};
|
||||
v[2] = {0, 0};
|
||||
v[3] = {0, 0};
|
||||
v[4] = {1024, 0}; // sample rate /2 bin
|
||||
v[5] = {0, 0};
|
||||
v[6] = {0, 0};
|
||||
v[7] = {0, 0};
|
||||
|
||||
ifft<complex16_t>(v, fft_width, tmp);
|
||||
|
||||
CHECK(v[0].real() == 1024);
|
||||
CHECK(v[1].real() == -1024);
|
||||
CHECK(v[2].real() == 1024);
|
||||
CHECK(v[3].real() == -1024);
|
||||
CHECK(v[4].real() == 1024);
|
||||
CHECK(v[5].real() == -1024);
|
||||
CHECK(v[6].real() == 1024);
|
||||
CHECK(v[7].real() == -1024);
|
||||
|
||||
for (uint32_t i = 0; i < fft_width; i++)
|
||||
CHECK(v[i].imag() == 0);
|
||||
|
||||
free(v);
|
||||
free(tmp);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include "doctest.h"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,6 @@ echo.
|
||||
pause
|
||||
|
||||
echo.
|
||||
dfu-util-static.exe --device 1fc9:000c --download hackrf_one_usb.dfu --reset
|
||||
"utils/dfu-util-static.exe" --device 1fc9:000c --download "utils/hackrf_one_usb.dfu" --reset
|
||||
echo.
|
||||
pause
|
||||
|
||||
+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