Compare commits

...

12 Commits

Author SHA1 Message Date
Bernd Herzog abb42e8df4 added executeable bit for new scripts (#1471) 2023-10-03 08:17:45 +02:00
Bernd Herzog 7fdb1af69d External apps (#1469)
* implemented external app build

* added some ui stuff for testing

* added pacman game

* wired key to pacman game

* fixed pacman drawing issue

* changed afsk rx app to be external

* fixed ui::NavigationView initialization for external apps

* refactoring

* refactoring

* moved m4 image to external app

* added script for external app deployment

* refactoring

* implemented dynamic app listing

* added color to app icon

* improved app loading

* added external apps to sd card content

* refactoring

* review findings

* typo

* review findings

* improved memory management of bitmaps
2023-10-02 20:19:22 +02:00
Kyle Reed 78713cc2af Search cleanup, more binder support (#1467) 2023-10-01 09:04:37 -07:00
Kyle Reed 951890eaff Add Binder helper methods (#1465)
* Add binder, fix focus on remote btn delete
* Use binder for freqman edit
2023-09-29 18:55:27 +02:00
Kyle Reed fb00bfac3f Nav stack cleanup (#1460)
* Minor nav stack fixes
* Nav stack cleanup
* Additional cleanup, fix notepad crash
* Fix abort/cancel
* Fix for nasty focus bug
* Format
2023-09-27 21:03:02 +02:00
Kyle Reed a6a1483083 Improved trimming (#1458)
* Add threshold UI
* WIP Better trimming
* Rewrite mostly done WIP
* WIP - trim idea
* WIP threshold trimming
* WIP with new design
* Cleanup
2023-09-23 21:56:37 +02:00
Kyle Reed ef03f020ce IQ Trim app and Capture auto-trim (#1456)
* Add IQ Trim app WIP
* Add 'trim' checkbox to capture
* Implement trimming
* Finish auto-trim
* Stray //
2023-09-21 17:43:10 +02:00
Kyle Reed 070eb4003e Remove proc pocsag & add keep/drop filtering (#1453)
* Remove proc_pocsag from build, use new proc in app
* Add support for keep/drop filtering
2023-09-19 11:46:30 +02:00
Kyle Reed fca373d936 Add Remote App & UI updates. (#1451)
* Alpha order sub-menus
* WIP Getting Remote types outlined
* WIP building UI
* WIP adding RemoteButton control
* WIP Fix build
* WIP Basic editing support
* Border on the active button
* Make TxView2 sane
* Add easier RGB color creation from uint32
* Center some button icons
* WIP Remote - main UI
* WIP main UI mostly working, can send
* Add 'join' utility
* WIP save/load
* Pre-alloc buttons to prevent focus dangling
* Alpha order settings/debug pages
* Add UI for picking capture and set frequency
* WIP Getting really close now
* Fix path for init name
* Some fit & finish
2023-09-18 23:22:46 +02:00
Kyle Reed 537cf2e79b Fix console scroll - comment on how scrolling works (#1448)
* Force console scroll area to be multiple of line height. Tons of comments.
* Resize POCSAG console to fill height.
* Make scoll behavior comment clearer
2023-09-14 20:20:40 +02:00
Kyle Reed af424aa5f8 SymField rewrite (#1444)
* First WIP symfield

* Cleanup widget code

* Rebase and format

* Fix 'to_integer' bug, fix siggen UI.

* to_string_hex fix, unit tests for string code

* Pass instance in callback

* Fix on_change callbacks

* Fix keyfob (not active)

* to_byte_array, coaster tx cleanup

* Add to_byte_array tests

* Changes in ui_numbers

* Fix ui_encoders

* Format

* Fix modemsetup view's symfields

* Remove header

* Format
2023-09-12 12:38:19 -07:00
Kyle Reed 70e0f2913f App Settings w/out radio settings, Settings UI cleanup (#1443)
* Make app settings more consistent

* Allow app settings to always work for custom settings
2023-09-10 17:04:20 -07:00
119 changed files with 13400 additions and 1554 deletions
+2 -2
View File
@@ -49,7 +49,7 @@ jobs:
run: docker run -e VERSION_STRING=${{ steps.version_date.outputs.date }} -i -v ${{ github.workspace }}:/havoc portapack-dev
- name: Create Small SD Card ZIP - No World Map
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && zip -r sdcard-no-map.zip sdcard
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && zip -r sdcard-no-map.zip sdcard
- name: Download world map
run: |
wget https://github.com/eried/portapack-mayhem/releases/download/world_map/world_map.zip
@@ -61,7 +61,7 @@ jobs:
zip -j firmware.zip build/firmware/portapack-h1_h2-mayhem.bin && cd flashing && zip -r ../firmware.zip *
- name: Create SD Card ZIP
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && zip -r sdcard.zip sdcard
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && zip -r sdcard.zip sdcard
- name: Create changelog
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
+2 -2
View File
@@ -33,7 +33,7 @@ jobs:
run: docker run -e VERSION_STRING=${{ steps.version.outputs.version }} -i -v ${{ github.workspace }}:/havoc portapack-dev
- name: Create Small SD Card ZIP - No World Map
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && zip -r sdcard-no-map.zip sdcard
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && zip -r sdcard-no-map.zip sdcard
- name: Download world map
run: |
wget https://github.com/eried/portapack-mayhem/releases/download/world_map/world_map.zip
@@ -45,7 +45,7 @@ jobs:
zip -j firmware.zip build/firmware/portapack-h1_h2-mayhem.bin && cd flashing && zip -r ../firmware.zip *
- name: Create SD Card ZIP
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && zip -r sdcard.zip sdcard
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && zip -r sdcard.zip sdcard
- name: Create changelog
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
+6
View File
@@ -42,6 +42,12 @@ if ("$ENV{VERSION_STRING}" STREQUAL "")
endif (GIT_VERSION_FOUND)
endif()
execute_process(
COMMAND bash "-c" "echo ${VERSION} | md5sum | head -c 8"
OUTPUT_VARIABLE VERSION_MD5
)
set(VERSION_MD5 "0x${VERSION_MD5}")
set(LICENSE_PATH ${CMAKE_CURRENT_LIST_DIR}/LICENSE)
set(HARDWARE_PATH ${CMAKE_CURRENT_LIST_DIR}/hardware)
+10 -2
View File
@@ -26,6 +26,7 @@ set(CHIBIOS ${PROJECT_SOURCE_DIR}/chibios)
set(CHIBIOS_PORTAPACK ${PROJECT_SOURCE_DIR}/chibios-portapack)
set(EXTRACT_CPLD_DATA ${PROJECT_SOURCE_DIR}/tools/extract_cpld_data.py)
set(EXPORT_EXTERNAL_APP_IMAGES ${PROJECT_SOURCE_DIR}/tools/export_external_apps.py)
set(MAKE_SPI_IMAGE ${PROJECT_SOURCE_DIR}/tools/make_spi_image.py)
set(MAKE_IMAGE_CHUNK ${PROJECT_SOURCE_DIR}/tools/make_image_chunk.py)
set(LZ4 lz4)
@@ -55,8 +56,15 @@ add_custom_target(
program
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}
COMMAND hackrf_spiflash -i -R -w ${FIRMWARE_FILENAME}
DEPENDS firmware ${FIRMWARE_FILENAME}
)
add_custom_target(
program-external-apps
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND ${PROJECT_SOURCE_DIR}/tools/copy_external_apps.sh
DEPENDS program
)
# TODO: Bad hack to fix location of LICENSE file for tar.
+39 -33
View File
@@ -32,7 +32,7 @@ include(CheckCXXCompilerFlag)
project(application)
# Compiler options here.
set(USE_OPT "-Os -g --specs=nano.specs")
set(USE_OPT "-Os -g --specs=nano.specs -specs=nosys.specs")
# C specific options here (added to USE_OPT).
set(USE_COPT "-std=gnu99")
@@ -97,6 +97,7 @@ 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)
include(external/external.cmake)
# Define linker script file here
set(LDSCRIPT ${PORTLD}/LPC43xx_M0.ld)
@@ -182,6 +183,7 @@ set(CPPSRC
io_convert.cpp
io_file.cpp
io_wave.cpp
iq_trim.cpp
irq_controls.cpp
irq_lcd_frame.cpp
irq_rtc.cpp
@@ -238,43 +240,63 @@ set(CPPSRC
ui/ui_textentry.cpp
ui/ui_tone_key.cpp
ui/ui_transmitter.cpp
apps/acars_app.cpp
apps/ais_app.cpp
apps/analog_audio_app.cpp
apps/analog_tv_app.cpp
apps/capture_app.cpp
apps/ert_app.cpp
apps/gps_sim_app.cpp
apps/lge_app.cpp
apps/lge_app.cpp
apps/pocsag_app.cpp
# apps/replay_app.cpp
apps/soundboard_app.cpp
apps/tpms_app.cpp
apps/tpms_app.cpp
apps/ui_about_simple.cpp
apps/ui_adsb_rx.cpp
apps/ui_adsb_tx.cpp
apps/ui_afsk_rx.cpp
apps/ui_aprs_rx.cpp
apps/ui_btle_rx.cpp
apps/ui_nrf_rx.cpp
apps/ui_aprs_tx.cpp
apps/ui_bht_tx.cpp
apps/ui_dfu_menu.cpp
apps/ui_btle_rx.cpp
apps/ui_coasterp.cpp
apps/ui_debug.cpp
apps/ui_dfu_menu.cpp
apps/ui_encoders.cpp
apps/ui_fileman.cpp
apps/ui_flash_utility.cpp
apps/ui_sd_over_usb.cpp
apps/ui_freqman.cpp
apps/ui_iq_trim.cpp
apps/ui_jammer.cpp
#apps/ui_keyfob.cpp
# apps/ui_keyfob.cpp
apps/ui_lcr.cpp
apps/lge_app.cpp
apps/ui_level.cpp
apps/ui_looking_glass_app.cpp
apps/ui_mictx.cpp
apps/ui_modemsetup.cpp
apps/ui_morse.cpp
apps/ui_nrf_rx.cpp
# apps/ui_nuoptix.cpp
apps/ui_playlist.cpp
apps/ui_pocsag_tx.cpp
apps/ui_rds.cpp
apps/ui_recon_settings.cpp
apps/ui_recon.cpp
apps/ui_remote.cpp
apps/ui_scanner.cpp
apps/ui_search.cpp
apps/ui_sd_over_usb.cpp
apps/ui_sd_wipe.cpp
apps/ui_search.cpp
apps/ui_settings.cpp
apps/ui_siggen.cpp
apps/ui_sonde.cpp
apps/ui_sstvtx.cpp
apps/ui_spectrum_painter_image.cpp
apps/ui_spectrum_painter_text.cpp
apps/ui_spectrum_painter.cpp
apps/ui_ss_viewer.cpp
apps/ui_sstvtx.cpp
# apps/ui_test.cpp
apps/ui_text_editor.cpp
apps/ui_tone_search.cpp
@@ -282,26 +304,6 @@ set(CPPSRC
apps/ui_touchtunes.cpp
apps/ui_view_wav.cpp
apps/ui_whipcalc.cpp
apps/acars_app.cpp
apps/ais_app.cpp
apps/analog_audio_app.cpp
apps/analog_tv_app.cpp
apps/capture_app.cpp
apps/ert_app.cpp
apps/lge_app.cpp
apps/pocsag_app.cpp
# apps/replay_app.cpp
apps/ui_playlist.cpp
apps/gps_sim_app.cpp
apps/soundboard_app.cpp
apps/ui_recon.cpp
apps/ui_recon_settings.cpp
apps/ui_level.cpp
apps/tpms_app.cpp
apps/tpms_app.cpp
apps/ui_spectrum_painter.cpp
apps/ui_spectrum_painter_image.cpp
apps/ui_spectrum_painter_text.cpp
protocols/aprs.cpp
protocols/ax25.cpp
protocols/bht.cpp
@@ -320,6 +322,9 @@ set(CPPSRC
${CPLD_20150901_DATA_CPP}
${CPLD_20170522_DATA_CPP}
${HACKRF_CPLD_DATA_CPP}
ui_external_items_menu_loader.cpp
${EXTCPPSRC}
)
# C sources to be compiled in ARM mode regardless of the global setting.
@@ -395,7 +400,7 @@ set(CPPWARN "-Wall -Wextra -Wno-psabi")
# List all default C defines here, like -D_DEBUG=1
# TODO: Switch -DCRT0_INIT_DATA depending on load from RAM or SPIFI?
# NOTE: _RANDOM_TCC to kill a GCC 4.9.3 error with std::max argument types
set(DDEFS "-DLPC43XX -DLPC43XX_M0 -D__NEWLIB__ -DHACKRF_ONE -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"'")
set(DDEFS "-DLPC43XX -DLPC43XX_M0 -D__NEWLIB__ -DHACKRF_ONE -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"' -DVERSION_MD5=${VERSION_MD5}")
# List all default ASM defines here, like -D_DEBUG=1
set(DADEFS)
@@ -464,12 +469,13 @@ set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT})
add_definitions(${DEFS})
include_directories(. ${INCDIR})
link_directories(${LLIBDIR})
target_link_libraries(${PROJECT_NAME}.elf ${LIBS})
target_link_libraries(${PROJECT_NAME}.elf ${LIBS} "-L${CMAKE_CURRENT_LIST_DIR}/external")
target_link_libraries(${PROJECT_NAME}.elf -Wl,-Map=${PROJECT_NAME}.map)
add_custom_command(
OUTPUT ${PROJECT_NAME}.bin
COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin
COMMAND ${CMAKE_OBJCOPY} -v -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin --remove-section=.external_app_*
COMMAND ${EXPORT_EXTERNAL_APP_IMAGES} ${PROJECT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_OBJCOPY} ${EXTAPPLIST}
DEPENDS ${PROJECT_NAME}.elf
)
+44 -35
View File
@@ -244,55 +244,64 @@ SettingsManager::SettingsManager(
: app_name_{app_name},
settings_{},
bindings_{},
loaded_{false} {
loaded_{false},
radio_loaded_{false} {
settings_.mode = mode;
settings_.options = options;
if (!portapack::persistent_memory::load_app_settings())
return;
// Pre-alloc enough for app settings and additional settings.
additional_settings.reserve(17 + additional_settings.size());
bindings_ = std::move(additional_settings);
// Transmitter model settings.
if (flags_enabled(mode, Mode::TX)) {
bindings_.emplace_back("tx_frequency"sv, &settings_.tx_frequency);
bindings_.emplace_back("tx_amp"sv, &settings_.tx_amp);
bindings_.emplace_back("tx_gain"sv, &settings_.tx_gain);
bindings_.emplace_back("channel_bandwidth"sv, &settings_.channel_bandwidth);
}
// Additional settings should always be loaded because apps now rely
// on being able to store UI settings, config, etc. The radio settings
// are only loaded if the global option has been enabled.
// Add the radio setting bindings if either load or save are enabled.
if (portapack::persistent_memory::load_app_settings() ||
portapack::persistent_memory::save_app_settings()) {
// Transmitter model settings.
if (flags_enabled(mode, Mode::TX)) {
bindings_.emplace_back("tx_frequency"sv, &settings_.tx_frequency);
bindings_.emplace_back("tx_amp"sv, &settings_.tx_amp);
bindings_.emplace_back("tx_gain"sv, &settings_.tx_gain);
bindings_.emplace_back("channel_bandwidth"sv, &settings_.channel_bandwidth);
}
// Receiver model settings.
if (flags_enabled(mode, Mode::RX)) {
bindings_.emplace_back("rx_frequency"sv, &settings_.rx_frequency);
bindings_.emplace_back("lna"sv, &settings_.lna);
bindings_.emplace_back("vga"sv, &settings_.vga);
bindings_.emplace_back("rx_amp"sv, &settings_.rx_amp);
bindings_.emplace_back("modulation"sv, &settings_.modulation);
bindings_.emplace_back("am_config_index"sv, &settings_.am_config_index);
bindings_.emplace_back("nbfm_config_index"sv, &settings_.nbfm_config_index);
bindings_.emplace_back("wfm_config_index"sv, &settings_.wfm_config_index);
bindings_.emplace_back("squelch"sv, &settings_.squelch);
}
// Receiver model settings.
if (flags_enabled(mode, Mode::RX)) {
bindings_.emplace_back("rx_frequency"sv, &settings_.rx_frequency);
bindings_.emplace_back("lna"sv, &settings_.lna);
bindings_.emplace_back("vga"sv, &settings_.vga);
bindings_.emplace_back("rx_amp"sv, &settings_.rx_amp);
bindings_.emplace_back("modulation"sv, &settings_.modulation);
bindings_.emplace_back("am_config_index"sv, &settings_.am_config_index);
bindings_.emplace_back("nbfm_config_index"sv, &settings_.nbfm_config_index);
bindings_.emplace_back("wfm_config_index"sv, &settings_.wfm_config_index);
bindings_.emplace_back("squelch"sv, &settings_.squelch);
}
// Common model settings.
bindings_.emplace_back("baseband_bandwidth"sv, &settings_.baseband_bandwidth);
bindings_.emplace_back("sampling_rate"sv, &settings_.sampling_rate);
bindings_.emplace_back("step"sv, &settings_.step);
bindings_.emplace_back("volume"sv, &settings_.volume);
// Common model settings.
bindings_.emplace_back("baseband_bandwidth"sv, &settings_.baseband_bandwidth);
bindings_.emplace_back("sampling_rate"sv, &settings_.sampling_rate);
bindings_.emplace_back("step"sv, &settings_.step);
bindings_.emplace_back("volume"sv, &settings_.volume);
}
loaded_ = load_settings(app_name_, bindings_);
if (loaded_)
// Only copy to the radio if load was successful.
if (loaded_ && portapack::persistent_memory::load_app_settings()) {
radio_loaded_ = true;
copy_to_radio_model(settings_);
}
SettingsManager::~SettingsManager() {
if (portapack::persistent_memory::save_app_settings()) {
copy_from_radio_model(settings_);
save_settings(app_name_, bindings_);
}
}
SettingsManager::~SettingsManager() {
// Only save radio settings when the option is enabled.
if (portapack::persistent_memory::save_app_settings())
copy_from_radio_model(settings_);
save_settings(app_name_, bindings_);
}
} // namespace app_settings
+7 -1
View File
@@ -150,7 +150,9 @@ void copy_to_radio_model(const AppSettings& settings);
/* Copies common values from the receiver/transmitter models. */
void copy_from_radio_model(AppSettings& settings);
/* RAII wrapper for automatically loading and saving radio settings for an app.
/* RAII wrapper for automatically loading and saving settings for an app.
* "Additional" settings are always loaded, but radio settings are conditionally
* saved/loaded if the global app settings options are enabled.
* NB: This should be added to a class before any LNA/VGA controls so that
* the receiver/transmitter models are set before the control ctors run. */
class SettingsManager {
@@ -167,6 +169,9 @@ class SettingsManager {
/* True if settings were successfully loaded from file. */
bool loaded() const { return loaded_; }
/* True if radio settings were successfully loaded from file. */
bool radio_loaded() const { return radio_loaded_; }
Mode mode() const { return settings_.mode; }
AppSettings& raw() { return settings_; }
@@ -176,6 +181,7 @@ class SettingsManager {
AppSettings settings_;
SettingBindings bindings_;
bool loaded_;
bool radio_loaded_;
};
} // namespace app_settings
+1 -1
View File
@@ -382,7 +382,7 @@ AISAppView::AISAppView(NavigationView& nav)
recent_entry_detail_view.hidden(true);
if (!settings_.loaded())
if (!settings_.radio_loaded())
receiver_model.set_target_frequency(initial_target_frequency);
receiver_model.enable();
+6 -1
View File
@@ -44,6 +44,7 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
&field_vga,
&option_bandwidth,
&option_format,
&check_trim,
&record_view,
&waterfall,
});
@@ -59,6 +60,10 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
record_view.set_file_type((RecordView::FileType)file_type);
};
check_trim.on_select = [this](Checkbox&, bool v) {
record_view.set_auto_trim(v);
};
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
option_bandwidth.on_change = [this](size_t, uint32_t bandwidth) {
/* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
@@ -92,7 +97,7 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
};
receiver_model.enable();
option_bandwidth.set_by_value(500000); // better by_value than by option_bandwidth.set_selected_index(4), Preselected default option 500kHz.
option_bandwidth.set_by_value(500000);
record_view.on_error = [&nav](std::string message) {
nav.display_modal("Error", message);
@@ -92,6 +92,12 @@ class CaptureAppView : public View {
{{"C16", RecordView::FileType::RawS16},
{"C8", RecordView::FileType::RawS8}}};
Checkbox check_trim{
{23 * 8, 1 * 16},
4,
"Trim",
/*small*/ true};
RecordView record_view{
{0 * 8, 2 * 16, 30 * 8, 1 * 16},
u"BBD_????.*",
+1 -1
View File
@@ -110,7 +110,7 @@ ERTAppView::ERTAppView(NavigationView&) {
&recent_entries_view,
});
if (!settings_.loaded())
if (!settings_.radio_loaded())
receiver_model.set_target_frequency(initial_target_frequency);
receiver_model.enable();
+1 -1
View File
@@ -173,7 +173,7 @@ GpsSimAppView::GpsSimAppView(
&waterfall,
});
if (!settings_.loaded()) {
if (!settings_.radio_loaded()) {
field_frequency.set_value(initial_target_frequency);
transmitter_model.set_sampling_rate(2600000);
}
+2 -7
View File
@@ -24,9 +24,6 @@
#ifndef __GPS_SIM_APP_HPP__
#define __GPS_SIM_APP_HPP__
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui_widget.hpp"
@@ -109,10 +106,8 @@ class GpsSimAppView : public View {
nav_};
TransmitterView2 tx_view{
// new handling of NumberField field_rfgain, NumberField field_rfamp
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Used in Replay / GPS Simul / Playlist App)
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
};
{11 * 8, 2 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{21 * 8, 2 * 16},
+59 -25
View File
@@ -54,34 +54,32 @@ POCSAGSettingsView::POCSAGSettingsView(
POCSAGSettings& settings)
: settings_{settings} {
add_children(
{&check_beta,
{&labels,
&check_log,
&check_log_raw,
&check_small_font,
&check_hide_bad,
&check_hide_addr_only,
&check_ignore,
&field_ignore,
&opt_filter_mode,
&field_filter_address,
&button_save});
check_beta.set_value(settings_.use_new_proc);
check_log.set_value(settings_.enable_logging);
check_log_raw.set_value(settings_.enable_raw_log);
check_small_font.set_value(settings_.enable_small_font);
check_hide_bad.set_value(settings_.hide_bad_data);
check_hide_addr_only.set_value(settings_.hide_addr_only);
check_ignore.set_value(settings_.enable_ignore);
field_ignore.set_value(settings_.address_to_ignore);
opt_filter_mode.set_by_value(settings_.filter_mode);
field_filter_address.set_value(settings_.filter_address);
button_save.on_select = [this, &nav](Button&) {
settings_.use_new_proc = check_beta.value();
settings_.enable_logging = check_log.value();
settings_.enable_raw_log = check_log_raw.value();
settings_.enable_small_font = check_small_font.value();
settings_.hide_bad_data = check_hide_bad.value();
settings_.hide_addr_only = check_hide_addr_only.value();
settings_.enable_ignore = check_ignore.value();
settings_.address_to_ignore = field_ignore.value();
settings_.filter_mode = opt_filter_mode.selected_index_value();
settings_.filter_address = field_filter_address.to_integer();
nav.pop();
};
@@ -89,10 +87,7 @@ POCSAGSettingsView::POCSAGSettingsView(
POCSAGAppView::POCSAGAppView(NavigationView& nav)
: nav_{nav} {
if (settings_.use_new_proc)
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
else
baseband::run_image(portapack::spi_flash::image_tag_pocsag);
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
add_children(
{&rssi,
@@ -108,15 +103,19 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
&widget_baud,
&widget_bits,
&widget_frames,
&button_ignore_last,
&button_filter_last,
&button_config,
&console});
// No app settings, use fallbacks.
// No app settings, use fallbacks from pmem.
if (!app_settings_.loaded()) {
settings_.filter_address = pmem::pocsag_ignore_address();
settings_.filter_mode = (settings_.filter_address == 0)
? FILTER_NONE
: FILTER_DROP;
}
if (!app_settings_.radio_loaded()) {
field_frequency.set_value(initial_target_frequency);
settings_.address_to_ignore = pmem::pocsag_ignore_address();
settings_.enable_ignore = settings_.address_to_ignore > 0;
}
logger.append(LOG_ROOT_DIR "/POCSAG.TXT");
@@ -126,9 +125,11 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
receiver_model.set_squelch_level(v);
};
button_ignore_last.on_select = [this](Button&) {
settings_.enable_ignore = true;
settings_.address_to_ignore = last_address;
button_filter_last.on_select = [this](Button&) {
if (settings_.filter_mode == FILTER_NONE)
settings_.filter_mode = FILTER_DROP;
settings_.filter_address = last_address;
refresh_ui();
};
button_config.on_select = [this](Button&) {
@@ -151,16 +152,49 @@ POCSAGAppView::~POCSAGAppView() {
receiver_model.disable();
baseband::shutdown();
// Save pmem settings. TODO: Even needed anymore?
pmem::set_pocsag_ignore_address(settings_.address_to_ignore);
pmem::set_pocsag_last_address(pocsag_state.address); // For POCSAG TX.
// Save pmem settings.
pmem::set_pocsag_ignore_address(settings_.filter_address);
pmem::set_pocsag_last_address(last_address); // For POCSAG TX.
}
void POCSAGAppView::refresh_ui() {
// Set console font style.
console.set_style(
settings_.enable_small_font
? &Styles::white_small
: &Styles::white);
// Update filter button text.
std::string btn_text = "Filter Last";
switch (settings_.filter_mode) {
case FILTER_DROP:
btn_text = "Ignore Last";
break;
case FILTER_KEEP:
btn_text = "Keep Last";
break;
case FILTER_NONE:
default:
btn_text = "Filter Last";
break;
}
button_filter_last.set_text(btn_text);
}
bool POCSAGAppView::ignore_address(uint32_t address) const {
switch (settings_.filter_mode) {
case FILTER_DROP:
return address == settings_.filter_address;
case FILTER_KEEP:
return address != settings_.filter_address;
case FILTER_NONE:
default:
return false;
}
}
void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefix) {
@@ -173,8 +207,8 @@ void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefi
return;
}
// Ignored address.
if (ignore() && pocsag_state.address == settings_.address_to_ignore) {
// Ignore address? TODO: could filter earlier.
if (ignore_address(pocsag_state.address)) {
console.write("\n" STR_COLOR_CYAN + prefix + " Ignored: " + to_string_dec_uint(pocsag_state.address));
last_address = pocsag_state.address;
return;
+30 -25
View File
@@ -114,15 +114,20 @@ class FrameIndicator : public Widget {
bool has_sync_ = false;
};
enum POCSAGFilter : uint8_t {
FILTER_NONE,
FILTER_DROP,
FILTER_KEEP
};
struct POCSAGSettings {
bool enable_small_font = false;
bool enable_logging = false;
bool enable_raw_log = false;
bool enable_ignore = false;
bool hide_bad_data = false;
bool hide_addr_only = false;
uint32_t address_to_ignore = 0;
bool use_new_proc = false;
uint8_t filter_mode = false;
uint32_t filter_address = 0;
};
class POCSAGSettingsView : public View {
@@ -135,11 +140,10 @@ class POCSAGSettingsView : public View {
private:
POCSAGSettings& settings_;
Checkbox check_beta{
{0 * 8 + 2, 18 * 16 - 4},
6,
"Beta",
true /*small*/};
Labels labels{
{{2 * 8, 12 * 16}, "Filter Mode:", Color::light_grey()},
{{2 * 8, 13 * 16}, "Filter Addr:", Color::light_grey()},
};
Checkbox check_log{
{2 * 8, 2 * 16},
@@ -166,17 +170,18 @@ class POCSAGSettingsView : public View {
22,
"Hide Addr Only"};
Checkbox check_ignore{
{2 * 8, 12 * 16},
22,
"Enable Ignored Address"};
OptionsField opt_filter_mode{
{15 * 8, 12 * 16},
4,
{{"None", FILTER_NONE},
{"Drop", FILTER_DROP},
{"Keep", FILTER_KEEP}}};
NumberField field_ignore{
{7 * 8, 13 * 16 + 8},
SymField field_filter_address{
{15 * 8, 13 * 16},
7,
{0, 9999999},
1,
'0'};
SymField::Type::Dec,
true /*explicit_edit*/};
Button button_save{
{11 * 8, 16 * 16, 10 * 8, 2 * 16},
@@ -195,7 +200,6 @@ class POCSAGAppView : public View {
static constexpr uint32_t initial_target_frequency = 466'175'000;
bool logging() const { return settings_.enable_logging; };
bool logging_raw() const { return settings_.enable_raw_log; };
bool ignore() const { return settings_.enable_ignore; };
bool hide_bad_data() const { return settings_.hide_bad_data; };
bool hide_addr_only() const { return settings_.hide_addr_only; };
@@ -211,19 +215,19 @@ class POCSAGAppView : public View {
{"small_font"sv, &settings_.enable_small_font},
{"enable_logging"sv, &settings_.enable_logging},
{"enable_raw_log"sv, &settings_.enable_raw_log},
{"enable_ignore"sv, &settings_.enable_ignore},
{"address_to_ignore"sv, &settings_.address_to_ignore},
{"filter_mode"sv, &settings_.filter_mode},
{"filter_address"sv, &settings_.filter_address},
{"hide_bad_data"sv, &settings_.hide_bad_data},
{"hide_addr_only"sv, &settings_.hide_addr_only},
{"use_new_proc"sv, &settings_.use_new_proc},
}};
void refresh_ui();
bool ignore_address(uint32_t address) const;
void handle_decoded(Timestamp timestamp, const std::string& prefix);
void on_packet(const POCSAGPacketMessage* message);
void on_stats(const POCSAGStatsMessage* stats);
uint32_t last_address = 0xFFFFFFFF;
uint32_t last_address = 0;
pocsag::EccContainer ecc{};
pocsag::POCSAGState pocsag_state{&ecc};
POCSAGLogger logger{};
@@ -274,16 +278,17 @@ class POCSAGAppView : public View {
BaudIndicator widget_baud{
{8 * 9 + 1, 1 * 16 + 2}};
Button button_ignore_last{
Button button_filter_last{
{10 * 8, 1 * 16, 12 * 8, 20},
"Ignore Last"};
"Filter Last"};
Button button_config{
{22 * 8, 1 * 16, 8 * 8, 20},
"Config"};
// 54 == status bar (16) + top controls (2 * 16 + 6).
Console console{
{0, 2 * 16 + 6, screen_width, screen_height - 56}};
{0, 2 * 16 + 6, screen_width, screen_height - 54}};
MessageHandlerRegistration message_handler_packet{
Message::ID::POCSAGPacket,
-3
View File
@@ -203,9 +203,6 @@ ReplayAppView::ReplayAppView(
ReplayAppView::~ReplayAppView() {
transmitter_model.disable();
display.fill_rectangle({0, 0, 240, 320}, Color::black()); // Solving sometimes visible bottom waterfall artifacts, clearing all LCD pixels.
chThdSleepMilliseconds(40); // (that happened sometimes if we interrupt the waterfall play at the beggining of the play around 25% and exit )
baseband::shutdown();
}
+2 -8
View File
@@ -23,9 +23,6 @@
#ifndef __REPLAY_APP_HPP__
#define __REPLAY_APP_HPP__
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui_widget.hpp"
@@ -99,16 +96,13 @@ class ReplayAppView : public View {
ProgressBar progressbar{
{18 * 8, 1 * 16, 12 * 8, 16}};
// TODO: Does this need to be a frequency field at all?
TxFrequencyField field_frequency{
{0 * 8, 2 * 16},
nav_};
TransmitterView2 tx_view{
// new handling of NumberField field_rfgain, NumberField field_rfamp
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Uused in Repay / GPS Simul / Playlist App)
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
};
{11 * 8, 2 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{21 * 8, 2 * 16},
+1 -1
View File
@@ -157,7 +157,7 @@ TPMSAppView::TPMSAppView(NavigationView&) {
&field_vga,
&recent_entries_view});
if (!settings_.loaded())
if (!settings_.radio_loaded())
receiver_model.set_target_frequency(initial_target_frequency);
receiver_model.enable();
+3 -4
View File
@@ -186,13 +186,12 @@ ADSBSquawkView::ADSBSquawkView(
&field_squawk});
}
void ADSBSquawkView::collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list) {
void ADSBSquawkView::collect_frames(const uint32_t /*ICAO_address*/, std::vector<ADSBFrame>& frame_list) {
if (!enabled) return;
ADSBFrame temp_frame;
(void)ICAO_address;
encode_frame_squawk(temp_frame, field_squawk.concatenate_4_octal_u16());
encode_frame_squawk(temp_frame, field_squawk.to_integer());
frame_list.emplace_back(temp_frame);
}
@@ -277,7 +276,7 @@ ADSBTxView::~ADSBTxView() {
}
void ADSBTxView::generate_frames() {
const uint32_t ICAO_address = sym_icao.value_hex_u64();
const uint32_t ICAO_address = sym_icao.to_integer();
frames.clear();
+2 -2
View File
@@ -135,7 +135,7 @@ class ADSBSquawkView : public OptionTabView {
SymField field_squawk{
{10 * 8, 2 * 16},
4,
SymField::SYMFIELD_OCT};
SymField::Type::Oct};
};
class ADSBTXThread {
@@ -235,7 +235,7 @@ class ADSBTxView : public View {
SymField sym_icao{
{10 * 8, 4 * 8},
6,
SymField::SYMFIELD_HEX};
SymField::Type::Hex};
Text text_frame{
{1 * 8, 29 * 8, 14 * 8, 16},
+3 -2
View File
@@ -48,9 +48,10 @@ APRSTXView::~APRSTXView() {
}
void APRSTXView::start_tx() {
// TODO: Clean up this API to take string_views to avoid allocations.
make_aprs_frame(
sym_source.value_string().c_str(), num_ssid_source.value(),
sym_dest.value_string().c_str(), num_ssid_dest.value(),
sym_source.to_string().c_str(), num_ssid_source.value(),
sym_dest.to_string().c_str(), num_ssid_dest.value(),
payload);
// uint8_t * bb_data_ptr = shared_memory.bb_data.data;
+3 -2
View File
@@ -68,7 +68,8 @@ class APRSTXView : public View {
SymField sym_source{
{7 * 8, 1 * 16},
6,
SymField::SYMFIELD_ALPHANUM};
SymField::Type::Alpha};
NumberField num_ssid_source{
{19 * 8, 1 * 16},
2,
@@ -79,7 +80,7 @@ class APRSTXView : public View {
SymField sym_dest{
{7 * 8, 2 * 16},
6,
SymField::SYMFIELD_ALPHANUM};
SymField::Type::Alpha};
NumberField num_ssid_dest{
{19 * 8, 2 * 16},
+13 -29
View File
@@ -42,12 +42,12 @@ CoasterPagerView::~CoasterPagerView() {
}
void CoasterPagerView::generate_frame() {
uint8_t frame[19];
uint32_t c;
constexpr uint8_t frame_bytes = 19;
uint8_t frame[frame_bytes];
// Preamble (8 bytes)
for (c = 0; c < 8; c++)
frame[c] = 0x55; // Isn't this 0xAA ?
for (uint8_t c = 0; c < 8; c++)
frame[c] = 0x55;
// Sync word
frame[8] = 0x2D;
@@ -57,11 +57,11 @@ void CoasterPagerView::generate_frame() {
frame[10] = 8;
// Data
for (c = 0; c < 8; c++)
frame[c + 11] = (sym_data.get_sym(c * 2) << 4) | sym_data.get_sym(c * 2 + 1);
auto data_bytes = to_byte_array(sym_data.to_integer());
memcpy(&frame[11], data_bytes.data(), data_bytes.size());
// Copy for baseband
memcpy(shared_memory.bb_data.data, frame, 19);
memcpy(shared_memory.bb_data.data, frame, frame_bytes);
}
void CoasterPagerView::start_tx() {
@@ -72,12 +72,7 @@ void CoasterPagerView::start_tx() {
baseband::set_fsk_data(19 * 8, 2280000 / 1000, 5000, 32);
}
void CoasterPagerView::on_tx_progress(const uint32_t progress, const bool done) {
(void)progress;
uint16_t address = 0;
uint32_t c;
void CoasterPagerView::on_tx_progress(const uint32_t /*progress*/, const bool done) {
if (done) {
if (tx_mode == SINGLE) {
transmitter_model.disable();
@@ -85,18 +80,12 @@ void CoasterPagerView::on_tx_progress(const uint32_t progress, const bool done)
tx_view.set_transmitting(false);
} else if (tx_mode == SCAN) {
// Increment address
for (c = 0; c < 4; c++) {
address <<= 4;
address |= sym_data.get_sym(12 + c);
}
uint64_t data = sym_data.to_integer();
uint16_t address = data & 0xFFFF;
address++;
for (c = 0; c < 4; c++) {
sym_data.set_sym(15 - c, address & 0x0F);
address >>= 4;
}
data = (data & 0xFFFFFFFFFFFF0000) + address;
sym_data.set_value(data);
start_tx();
}
@@ -104,9 +93,6 @@ void CoasterPagerView::on_tx_progress(const uint32_t progress, const bool done)
}
CoasterPagerView::CoasterPagerView(NavigationView& nav) {
const uint8_t data_init[8] = {0x44, 0x01, 0x3B, 0x30, 0x30, 0x30, 0x34, 0xBC};
uint32_t c;
baseband::run_image(portapack::spi_flash::image_tag_fsktx);
add_children({&labels,
@@ -115,9 +101,7 @@ CoasterPagerView::CoasterPagerView(NavigationView& nav) {
&text_message,
&tx_view});
// Bytes to nibbles
for (c = 0; c < 16; c++)
sym_data.set_sym(c, (data_init[c >> 1] >> ((c & 1) ? 0 : 4)) & 0x0F);
sym_data.set_value(0x44013B30303034BC);
checkbox_scan.set_value(false);
+7 -5
View File
@@ -68,17 +68,19 @@ class CoasterPagerView : public View {
SymField sym_data{
{7 * 8, 8 * 8},
16, // 14 ? 12 ?
SymField::SYMFIELD_HEX};
16,
SymField::Type::Hex};
Checkbox checkbox_scan{
{10 * 8, 14 * 8},
4,
"Scan"};
/*ProgressBar progressbar {
{ 5 * 8, 12 * 16, 20 * 8, 16 },
};*/
/*
ProgressBar progressbar {
{ 5 * 8, 12 * 16, 20 * 8, 16 }};
*/
Text text_message{
{5 * 8, 13 * 16, 20 * 8, 16},
""};
+6 -6
View File
@@ -398,17 +398,17 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
//{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }}, // temporarily disabled to conserve ROM space
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
{"Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugMemoryView>(); }},
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
//{ "Radio State", ui::Color::white(), nullptr, [&nav](){ nav.push<NotImplementedView>(); } },
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [&nav]() { nav.push<SDCardDebugView>(); }},
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
{"Temperature", ui::Color::dark_cyan(), &bitmap_icon_temperature, [&nav]() { nav.push<TemperatureView>(); }},
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
{"Touch Test", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugScreenTest>(); }},
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
//{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }}, // temporarily disabled to conserve ROM space
});
set_max_rows(2); // allow wider buttons
}
+36 -27
View File
@@ -49,7 +49,6 @@ EncodersConfigView::EncodersConfigView(
&field_clk_step,
&field_frameduration,
&field_frameduration_step,
&symfield_word,
&text_format,
&waveform});
@@ -64,10 +63,6 @@ EncodersConfigView::EncodersConfigView(
options_enctype.set_options(enc_options);
options_enctype.set_selected_index(0);
symfield_word.on_change = [this]() {
generate_frame();
};
// Selecting input clock changes symbol and word duration
field_clk.on_change = [this](int32_t value) {
// value is in kHz, new_value is in us
@@ -98,33 +93,46 @@ void EncodersConfigView::focus() {
}
void EncodersConfigView::on_type_change(size_t index) {
std::string format_string = "";
size_t word_length;
char symbol_type;
// Remove existing SymField controls.
for (auto& symfield : symfields_word)
remove_child(symfield.get());
symfields_word.clear();
encoder_def = &encoder_defs[index];
field_clk.set_value(encoder_def->default_speed / 1000);
field_repeat_min.set_value(encoder_def->repeat_min);
// SymField setup
word_length = encoder_def->word_length;
symfield_word.set_length(word_length);
size_t n = 0, i = 0;
while (n < word_length) {
symbol_type = encoder_def->word_format[i++];
if (symbol_type == 'A') {
symfield_word.set_symbol_list(n++, encoder_def->address_symbols);
format_string += 'A';
} else if (symbol_type == 'D') {
symfield_word.set_symbol_list(n++, encoder_def->data_symbols);
format_string += 'D';
// Add new SymFields.
Point pos{2 * 8, 9 * 8};
std::string format_string;
uint8_t word_length = encoder_def->word_length;
auto on_change_handler = [this](SymField&) {
generate_frame();
};
for (uint8_t i = 0; i < word_length; i++) {
auto symbol_type = encoder_def->word_format[i];
symfields_word.push_back(std::make_unique<SymField>(pos, 1));
auto& symfield = symfields_word.back();
symfield->on_change = on_change_handler;
switch (symbol_type) {
case 'A':
symfield->set_symbol_list(encoder_def->address_symbols);
format_string += 'A';
break;
case 'D':
symfield->set_symbol_list(encoder_def->data_symbols);
format_string += 'D';
break;
}
add_child(symfield.get());
pos += Point{8, 0};
}
// Ugly :( Pad to erase
format_string.append(24 - format_string.size(), ' ');
text_format.set(format_string);
generate_frame();
@@ -146,17 +154,18 @@ void EncodersConfigView::draw_waveform() {
}
void EncodersConfigView::generate_frame() {
size_t i = 0;
frame_fragments.clear();
size_t i = 0;
for (auto c : encoder_def->word_format) {
if (c == 'S')
frame_fragments += encoder_def->sync;
else if (!c)
else if (c == '\0')
break;
else
frame_fragments += encoder_def->bit_format[symfield_word.get_sym(i++)];
else {
auto offset = symfields_word[i++]->get_offset(0);
frame_fragments += encoder_def->bit_format[offset];
}
}
draw_waveform();
+4 -4
View File
@@ -29,6 +29,9 @@
#include "app_settings.hpp"
#include "radio_state.hpp"
#include <memory>
#include <vector>
using namespace encoders;
namespace ui {
@@ -113,10 +116,7 @@ class EncodersConfigView : public View {
{"100", 100},
{"1000", 1000}}};
SymField symfield_word{
{2 * 8, 9 * 8},
20,
SymField::SYMFIELD_DEF};
std::vector<std::unique_ptr<SymField>> symfields_word{};
Text text_format{
{2 * 8, 11 * 8, 24 * 8, 16},
+20 -12
View File
@@ -27,6 +27,7 @@
#include <algorithm>
#include "ui_fileman.hpp"
#include "ui_playlist.hpp"
#include "ui_remote.hpp"
#include "ui_ss_viewer.hpp"
#include "ui_text_editor.hpp"
#include "string_format.hpp"
@@ -44,6 +45,7 @@ static const fs::path c16_ext{u".C16"};
static const fs::path cxx_ext{u".C*"};
static const fs::path png_ext{u".PNG"};
static const fs::path bmp_ext{u".BMP"};
static const fs::path rem_ext{u".REM"};
} // namespace ui
namespace {
@@ -106,14 +108,17 @@ bool partner_file_prompt(
if (partner.empty())
return false;
nav.push_under_current<ModalMessageView>(
"Partner File",
partner.filename().string() + "\n" + action_name + " this file too?",
YESNO,
[&nav, partner, on_partner_action](bool choice) {
if (on_partner_action)
on_partner_action(partner, choice);
});
// Display the continuation UI once the current has been popped.
nav.set_on_pop([&nav, partner, action_name, on_partner_action] {
nav.display_modal(
"Partner File",
partner.filename().string() + "\n" + action_name + " this file too?",
YESNO,
[&nav, partner, on_partner_action](bool choice) {
if (on_partner_action)
on_partner_action(partner, choice);
});
});
return true;
}
@@ -187,8 +192,8 @@ FileManBaseView::FileManBaseView(
&text_current,
&button_exit});
button_exit.on_select = [this, &nav](Button&) {
nav.pop();
button_exit.on_select = [this](Button&) {
nav_.pop();
};
if (!sdcIsCardInserted(&SDCD1)) {
@@ -323,9 +328,9 @@ FileLoadView::FileLoadView(
if (get_selected_entry().is_directory) {
push_dir(get_selected_entry().path);
} else {
nav_.pop();
if (on_changed)
on_changed(get_selected_full_path());
nav_.pop();
}
};
}
@@ -525,6 +530,9 @@ bool FileManagerView::handle_file_open() {
nav_.push<SplashViewer>(path);
reload_current();
return true;
} else if (path_iequal(rem_ext, ext)) {
nav_.push<RemoteView>(path);
return true;
}
return false;
@@ -629,7 +637,7 @@ FileManagerView::FileManagerView(
button_open_notepad.on_select = [this]() {
if (selected_is_valid() && !get_selected_entry().is_directory) {
auto path = get_selected_full_path();
nav_.replace<TextEditorView>(path);
nav_.push<TextEditorView>(path);
} else
nav_.display_modal("Open in Notepad", "Can't open that in Notepad.");
};
+4 -2
View File
@@ -77,7 +77,8 @@ class FileManBaseView : public View {
{u".C8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".C16", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".WAV", &bitmap_icon_file_wav, ui::Color::dark_magenta()},
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // PPL is the file extension for playlist app
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // Playlist/Replay
{u".REM", &bitmap_icon_remote, ui::Color::orange()}, // Remote
{u"", &bitmap_icon_file, ui::Color::light_grey()} // NB: Must be last.
};
@@ -269,7 +270,8 @@ class FileManagerView : public FileManBaseView {
{4 * 8, 34 * 8, 4 * 8, 32},
{},
&bitmap_icon_options_datetime,
Color::orange()};
Color::orange(),
/*vcenter*/ true};
};
} /* namespace ui */
+22 -74
View File
@@ -23,6 +23,7 @@
#include "ui_freqman.hpp"
#include "binder.hpp"
#include "event_m0.hpp"
#include "portapack.hpp"
#include "rtc_time.hpp"
@@ -92,9 +93,9 @@ void FreqManBaseView::focus() {
// TODO: Shouldn't be on focus.
if (error_ == ERROR_ACCESS) {
nav_.display_modal("Error", "File access error", ABORT, nullptr);
nav_.display_modal("Error", "File access error", ABORT);
} else if (error_ == ERROR_NOFILES) {
nav_.display_modal("Error", "No database files\nin /FREQMAN", ABORT, nullptr);
nav_.display_modal("Error", "No database files\nin /FREQMAN", ABORT);
} else {
options_category.focus();
}
@@ -152,28 +153,14 @@ FrequencySaveView::FrequencySaveView(
add_children(
{&labels,
&big_display,
&button_clear,
&button_edit,
&button_save,
&text_description});
&field_description});
entry_.type = freqman_type::Single;
entry_.frequency_a = value;
entry_.description = to_string_timestamp(rtc_time::now());
refresh_ui();
button_clear.on_select = [this, &nav](Button&) {
entry_.description = "";
refresh_ui();
};
button_edit.on_select = [this, &nav](Button&) {
temp_buffer_ = entry_.description;
text_prompt(nav_, temp_buffer_, desc_edit_max, [this](std::string& new_desc) {
entry_.description = new_desc;
refresh_ui();
});
};
bind(field_description, entry_.description, nav);
button_save.on_select = [this, &nav](Button&) {
db_.insert_entry(db_.entry_count(), entry_);
@@ -181,9 +168,13 @@ FrequencySaveView::FrequencySaveView(
};
}
void FrequencySaveView::focus() {
refresh_ui();
FreqManBaseView::focus();
}
void FrequencySaveView::refresh_ui() {
big_display.set(entry_.frequency_a);
text_description.set(entry_.description);
}
/* FrequencyLoadView *************************************/
@@ -376,69 +367,26 @@ FrequencyEditView::FrequencyEditView(
field_step.options().insert(
field_step.options().begin(), {"None", -1});
field_type.set_by_value((int32_t)entry_.type);
field_type.on_change = [this](size_t, auto value) {
entry_.type = static_cast<freqman_type>(value);
bind(field_type, entry_.type, [this](auto) {
refresh_ui();
};
});
// TODO: this pattern should be able to be wrapped up.
field_freq_a.set_value(entry_.frequency_a);
field_freq_a.on_change = [this](rf::Frequency f) {
entry_.frequency_a = f;
bind(field_freq_a, entry_.frequency_a, nav, [this](auto) {
refresh_ui();
};
field_freq_a.on_edit = [this]() {
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_a.value());
freq_view->on_changed = [this](rf::Frequency f) {
field_freq_a.set_value(f);
};
};
});
field_freq_b.set_value(entry_.frequency_b);
field_freq_b.on_change = [this](rf::Frequency f) {
entry_.frequency_b = f;
bind(field_freq_b, entry_.frequency_b, nav, [this](auto) {
refresh_ui();
};
field_freq_b.on_edit = [this]() {
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_b.value());
freq_view->on_changed = [this](rf::Frequency f) {
field_freq_b.set_value(f);
};
};
});
field_modulation.set_by_value((int32_t)entry_.modulation);
field_modulation.on_change = [this](size_t, auto value) {
entry_.modulation = static_cast<freqman_index_t>(value);
bind(field_modulation, entry_.modulation, [this](auto) {
populate_bandwidth_options();
};
});
field_bandwidth.set_by_value((int32_t)entry_.bandwidth);
field_bandwidth.on_change = [this](size_t, auto value) {
entry_.bandwidth = static_cast<freqman_index_t>(value);
};
field_step.set_by_value((int32_t)entry_.step);
field_step.on_change = [this](size_t, auto value) {
entry_.step = static_cast<freqman_index_t>(value);
};
field_tone.set_by_value((int32_t)entry_.tone);
field_tone.on_change = [this](size_t, auto value) {
entry_.tone = static_cast<freqman_index_t>(value);
};
field_description.set_text(entry_.description);
field_description.on_change = [this](TextField& tf) {
entry_.description = tf.get_text();
};
field_description.on_select = [this](TextField& tf) {
temp_buffer_ = tf.get_text();
text_prompt(nav_, temp_buffer_, FreqManBaseView::desc_edit_max,
[this, &tf](std::string& new_desc) {
tf.set_text(new_desc);
});
};
bind(field_bandwidth, entry_.bandwidth);
bind(field_step, entry_.step);
bind(field_tone, entry_.tone);
bind(field_description, entry_.description, nav_);
button_save.on_select = [this](Button&) {
if (on_save)
+4 -10
View File
@@ -87,9 +87,9 @@ class FrequencySaveView : public FreqManBaseView {
public:
FrequencySaveView(NavigationView& nav, const rf::Frequency value);
std::string title() const override { return "Save freq"; }
void focus() override;
private:
std::string temp_buffer_{};
freqman_entry entry_{};
void refresh_ui();
@@ -101,15 +101,9 @@ class FrequencySaveView : public FreqManBaseView {
Labels labels{
{{0 * 8, 6 * 16}, "Description:", Color::white()}};
Text text_description{{0 * 8, 7 * 16, 30 * 8, 1 * 16}};
Button button_clear{
{4 * 8, 10 * 16, 10 * 8, 2 * 16},
"Clear"};
Button button_edit{
{16 * 8, 10 * 16, 10 * 8, 2 * 16},
"Edit"};
TextField field_description{
{0 * 8, 7 * 16, 30 * 8, 1 * 16},
""};
Button button_save{
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
+194
View File
@@ -0,0 +1,194 @@
/*
* 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 "ui_iq_trim.hpp"
#include "complex.hpp"
#include "portapack.hpp"
#include "ui_fileman.hpp"
using namespace portapack;
namespace fs = std::filesystem;
namespace ui {
IQTrimView::IQTrimView(NavigationView& nav)
: nav_{nav} {
add_children({
&labels,
&field_path,
&field_start,
&field_end,
&text_samples,
&text_max,
&field_cutoff,
&button_trim,
});
field_path.on_select = [this](TextField&) {
auto open_view = nav_.push<FileLoadView>(".C*");
open_view->push_dir(u"CAPTURES");
open_view->on_changed = [this](fs::path path) {
path_ = std::move(path);
profile_capture();
compute_range();
refresh_ui();
};
};
text_samples.set_style(&Styles::light_grey);
text_max.set_style(&Styles::light_grey);
field_start.on_change = [this](int32_t v) {
if (field_end.value() < v)
field_end.set_value(v, false);
set_dirty();
};
field_end.on_change = [this](int32_t v) {
if (field_start.value() > v)
field_start.set_value(v, false);
set_dirty();
};
field_cutoff.set_value(7); // 7% of max is a good default.
field_cutoff.on_change = [this](int32_t) {
compute_range();
refresh_ui();
};
button_trim.on_select = [this](Button&) {
if (trim_capture()) {
profile_capture();
compute_range();
refresh_ui();
}
};
}
void IQTrimView::paint(Painter& painter) {
if (info_) {
uint32_t power_cutoff = field_cutoff.value() * static_cast<uint64_t>(info_->max_power) / 100;
// Draw power buckets.
for (size_t i = 0; i < power_buckets_.size(); ++i) {
auto power = power_buckets_[i].power;
uint8_t amp = 0;
if (power > power_cutoff && info_->max_power > 0)
amp = (255ULL * power) / info_->max_power;
painter.draw_vline(
pos_lines + Point{(int)i, 0},
height_lines,
Color(amp, amp, amp));
}
// Draw trim range edges.
int start_x = screen_width * field_start.value() / info_->sample_count;
int end_x = screen_width * field_end.value() / info_->sample_count;
painter.draw_vline(
pos_lines + Point{start_x, 0},
height_lines,
Color::dark_green());
painter.draw_vline(
pos_lines + Point{end_x, 0},
height_lines,
Color::dark_red());
}
}
void IQTrimView::focus() {
field_path.focus();
}
void IQTrimView::refresh_ui() {
field_path.set_text(path_.filename().string());
text_samples.set(to_string_dec_uint(info_->sample_count));
text_max.set(to_string_dec_uint(info_->max_power));
set_dirty();
}
void IQTrimView::update_range_controls(iq::TrimRange trim_range) {
auto max_range = info_ ? info_->sample_count : 0;
auto step = info_ ? info_->sample_count / screen_width : 0;
field_start.set_range(0, max_range);
field_start.set_step(step);
field_end.set_range(0, max_range);
field_end.set_step(step);
field_start.set_value(trim_range.start_sample, false);
field_end.set_value(trim_range.end_sample, false);
}
void IQTrimView::profile_capture() {
power_buckets_ = {};
iq::PowerBuckets buckets{
.p = power_buckets_.data(),
.size = power_buckets_.size()};
progress_ui.show_reading();
info_ = iq::profile_capture(path_, buckets);
progress_ui.clear();
}
void IQTrimView::compute_range() {
if (!info_)
return;
iq::PowerBuckets buckets{
.p = power_buckets_.data(),
.size = power_buckets_.size()};
auto trim_range = iq::compute_trim_range(*info_, buckets, field_cutoff.value());
update_range_controls(trim_range);
}
bool IQTrimView::trim_capture() {
if (!info_) {
nav_.display_modal("Error", "Open a file first.");
return false;
}
bool trimmed = false;
iq::TrimRange trim_range{
static_cast<uint32_t>(field_start.value()),
static_cast<uint32_t>(field_end.value()),
info_->sample_size};
if (trim_range.start_sample >= trim_range.end_sample) {
nav_.display_modal("Error", "Invalid trimming range.");
return false;
}
progress_ui.show_trimming();
trimmed = iq::trim_capture_with_range(path_, trim_range, progress_ui.get_callback());
progress_ui.clear();
if (!trimmed)
nav_.display_modal("Error", "Trimming failed.");
return trimmed;
}
} // namespace ui
+152
View File
@@ -0,0 +1,152 @@
/*
* 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 __UI_IQ_TRIM_H__
#define __UI_IQ_TRIM_H__
#include "file.hpp"
#include "iq_trim.hpp"
#include "optional.hpp"
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_styles.hpp"
#include "ui_widget.hpp"
#include <array>
namespace ui {
/* Helper for drawing progress related to IQ trimming. */
/* TODO: Ideally this would all be done on second thread. */
class TrimProgressUI {
public:
void show_reading() {
clear();
p.draw_string({6 * 8, 5 * 16}, Styles::yellow, "Reading Capture...");
}
void show_trimming() {
clear();
p.draw_string({5 * 8, 5 * 16}, Styles::yellow, "Trimming Capture...");
}
void show_progress(uint8_t percent) {
auto width = percent * screen_width / 100;
p.draw_hline({0, 6 * 16 + 2}, width, Color::yellow());
}
void clear() {
p.fill_rectangle({0 * 8, 4 * 16, screen_width, 3 * 16}, Color::black());
}
auto get_callback() {
return [this](uint8_t percent) { show_progress(percent); };
}
private:
Painter p{};
};
class IQTrimView : public View {
public:
IQTrimView(NavigationView& nav);
std::string title() const override { return "IQ Trim"; }
void paint(Painter& painter) override;
void focus() override;
private:
/* Update controls with latest values. */
void refresh_ui();
/* Update the start/end controls with trim range info. */
void update_range_controls(iq::TrimRange trim_range);
/* Collect capture info and samples to draw the UI. */
void profile_capture();
/* Determine the start and end buckets based on the cutoff. */
void compute_range();
/* Trims the capture file based on the settings. */
bool trim_capture();
NavigationView& nav_;
std::filesystem::path path_{};
Optional<iq::CaptureInfo> info_{};
std::array<iq::PowerBuckets::Bucket, screen_width> power_buckets_{};
TrimProgressUI progress_ui{};
Labels labels{
{{0 * 8, 0 * 16}, "Capture File:", Color::light_grey()},
{{0 * 8, 6 * 16}, "Start :", Color::light_grey()},
{{0 * 8, 7 * 16}, "End :", Color::light_grey()},
{{0 * 8, 8 * 16}, "Samples:", Color::light_grey()},
{{0 * 8, 9 * 16}, "Max Pwr:", Color::light_grey()},
{{0 * 8, 10 * 16}, "Cutoff :", Color::light_grey()},
{{12 * 8, 10 * 16}, "%", Color::light_grey()},
};
TextField field_path{
{0 * 8, 1 * 16, 30 * 8, 1 * 16},
"Open File..."};
Point pos_lines{0 * 8, 4 * 16};
Dim height_lines{2 * 16};
NumberField field_start{
{9 * 8, 6 * 16},
10,
{0, 0},
1,
' '};
NumberField field_end{
{9 * 8, 7 * 16},
10,
{0, 0},
1,
' '};
Text text_samples{
{9 * 8, 8 * 16, 10 * 8, 1 * 16},
"0"};
Text text_max{
{9 * 8, 9 * 16, 10 * 8, 1 * 16},
"0"};
NumberField field_cutoff{
{9 * 8, 10 * 16},
3,
{1, 100},
1,
' '};
Button button_trim{
{20 * 8, 16 * 16, 8 * 8, 2 * 16},
"Trim"};
};
} /* namespace ui */
#endif /*__UI_IQ_TRIM_H__*/
+6 -16
View File
@@ -97,12 +97,12 @@ size_t KeyfobView::generate_frame() {
uint64_t payload;
// Symfield word to frame
payload = field_payload_a.value_hex_u64();
payload = field_payload_a.to_integer();
for (size_t i = 0; i < 5; i++) {
frame[4 - i] = payload & 0xFF;
payload >>= 8;
}
payload = field_payload_b.value_hex_u64();
payload = field_payload_b.to_integer();
for (size_t i = 0; i < 5; i++) {
frame[9 - i] = payload & 0xFF;
payload >>= 8;
@@ -136,9 +136,6 @@ void KeyfobView::focus() {
}
KeyfobView::~KeyfobView() {
// save app settings
settings.save("tx_keyfob", &app_settings);
transmitter_model.disable();
baseband::shutdown();
}
@@ -167,12 +164,12 @@ void KeyfobView::on_make_change(size_t index) {
// DEBUG
void KeyfobView::update_symfields() {
for (size_t i = 0; i < 5; i++) {
field_payload_a.set_sym(i << 1, frame[i] >> 4);
field_payload_a.set_sym((i << 1) + 1, frame[i] & 0x0F);
field_payload_a.set_offset(i << 1, frame[i] >> 4);
field_payload_a.set_offset((i << 1) + 1, frame[i] & 0x0F);
}
for (size_t i = 0; i < 5; i++) {
field_payload_b.set_sym(i << 1, frame[5 + i] >> 4);
field_payload_b.set_sym((i << 1) + 1, frame[5 + i] & 0x0F);
field_payload_b.set_offset(i << 1, frame[5 + i] >> 4);
field_payload_b.set_offset((i << 1) + 1, frame[5 + i] & 0x0F);
}
}
@@ -212,13 +209,6 @@ KeyfobView::KeyfobView(
&progressbar,
&tx_view});
// load app settings
auto rc = settings.load("tx_keyfob", &app_settings);
if (rc == SETTINGS_OK) {
transmitter_model.set_rf_amp(app_settings.tx_amp);
transmitter_model.set_tx_gain(app_settings.tx_gain);
}
frame[0] = 0x55;
update_symfields();
+3 -3
View File
@@ -48,7 +48,7 @@ class KeyfobView : public View {
OOK_SAMPLERATE /* sampling rate */
};
app_settings::SettingsManager settings_{
"tx_keyfob", , app_settings::Mode::TX};
"tx_keyfob", app_settings::Mode::TX};
// 1013210ns / bit
static constexpr uint32_t subaru_samples_per_bit = (OOK_SAMPLERATE * 0.00101321);
@@ -98,11 +98,11 @@ class KeyfobView : public View {
SymField field_payload_a{
{2 * 8, 5 * 16},
10,
SymField::SYMFIELD_HEX};
SymField::Type::Hex};
SymField field_payload_b{
{13 * 8, 5 * 16},
10,
SymField::SYMFIELD_HEX};
SymField::Type::Hex};
Text text_status{
{2 * 8, 13 * 16, 128, 16},
+1 -1
View File
@@ -633,7 +633,7 @@ MicTXView::MicTXView(
MicTXView::~MicTXView() {
audio::input::stop();
transmitter_model.set_target_frequency(tx_frequency); // Save Tx frequency instead of Rx. Or maybe we need some "System Wide" changes to seperate Tx and Rx frequency.
transmitter_model.set_target_frequency(tx_frequency);
transmitter_model.disable();
if (rx_enabled) // Also turn off audio rx if enabled
rxaudio(false);
+2 -7
View File
@@ -25,9 +25,6 @@
#ifndef __UI_MICTX_H__
#define __UI_MICTX_H__
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui.hpp"
@@ -219,10 +216,8 @@ class MicTXView : public View {
' '};
TransmitterView2 tx_view{
// new handling of NumberField field_rfgain, NumberField field_rfamp
// 3*8, 2*8, SHORT_UI // x(columns), y (line) position. (used in Replay / GPS Simul / Playlist App's)
3 * 8, 2 * 8, NORMAL_UI // x(columns), y (line) position. (used in Mic App)
};
{3 * 8, 5 * 8},
/*short_ui*/ false};
OptionsField options_mode{
{24 * 8, 5 * 8},
+23 -25
View File
@@ -23,7 +23,6 @@
#include "ui_modemsetup.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
using namespace portapack;
@@ -41,15 +40,20 @@ ModemSetupView::ModemSetupView(
using options_t = std::vector<option_t>;
options_t modem_options;
add_children({&labels,
&field_baudrate,
&field_mark,
&field_space,
&field_repeat,
&options_modem,
&button_set_modem,
&sym_format,
&button_save});
add_children({
&labels,
&field_baudrate,
&field_mark,
&field_space,
&field_repeat,
&options_modem,
&button_set_modem,
&sym_format_data,
&sym_format_parity,
&sym_format_stop,
&sym_format_msb,
&button_save,
});
// Only list AFSK modems for now
for (size_t i = 0; i < MODEM_DEF_COUNT; i++) {
@@ -59,15 +63,10 @@ ModemSetupView::ModemSetupView(
options_modem.set_options(modem_options);
options_modem.set_selected_index(0);
sym_format.set_symbol_list(0, "6789"); // Data bits
sym_format.set_symbol_list(1, "NEo"); // Parity
sym_format.set_symbol_list(2, "012"); // Stop bits
sym_format.set_symbol_list(3, "ML"); // MSB/LSB first
sym_format.set_sym(0, persistent_memory::serial_format().data_bits - 6);
sym_format.set_sym(1, persistent_memory::serial_format().parity);
sym_format.set_sym(2, persistent_memory::serial_format().stop_bits);
sym_format.set_sym(3, persistent_memory::serial_format().bit_order);
sym_format_data.set_offset(0, persistent_memory::serial_format().data_bits - 6);
sym_format_parity.set_offset(0, persistent_memory::serial_format().parity);
sym_format_stop.set_offset(0, persistent_memory::serial_format().stop_bits);
sym_format_msb.set_offset(0, persistent_memory::serial_format().bit_order);
field_mark.set_value(persistent_memory::afsk_mark_freq());
field_space.set_value(persistent_memory::afsk_space_freq());
@@ -84,18 +83,17 @@ ModemSetupView::ModemSetupView(
};
button_save.on_select = [this, &nav](Button&) {
serial_format_t serial_format;
persistent_memory::set_afsk_mark(field_mark.value());
persistent_memory::set_afsk_space(field_space.value());
persistent_memory::set_modem_baudrate(field_baudrate.value());
persistent_memory::set_modem_repeat(field_repeat.value());
serial_format.data_bits = sym_format.get_sym(0) + 6;
serial_format.parity = (parity_enum)sym_format.get_sym(1);
serial_format.stop_bits = sym_format.get_sym(2);
serial_format.bit_order = (order_enum)sym_format.get_sym(3);
serial_format_t serial_format{};
serial_format.data_bits = sym_format_data.get_offset(0) + 6;
serial_format.parity = (parity_enum)sym_format_parity.get_offset(0);
serial_format.stop_bits = sym_format_stop.get_offset(0);
serial_format.bit_order = (order_enum)sym_format_msb.get_offset(0);
persistent_memory::set_serial_format(serial_format);
+18 -3
View File
@@ -79,10 +79,25 @@ class ModemSetupView : public View {
7,
{}};
SymField sym_format{
SymField sym_format_data{
{16 * 8, 22 * 8},
4,
SymField::SYMFIELD_DEF};
1,
"6789"};
SymField sym_format_parity{
{17 * 8, 22 * 8},
1,
"NEo"};
SymField sym_format_stop{
{18 * 8, 22 * 8},
1,
"012"};
SymField sym_format_msb{
{19 * 8, 22 * 8},
1,
"ML"};
Button button_set_modem{
{23 * 8, 6 * 8 - 4, 6 * 8, 24},
+17 -18
View File
@@ -21,6 +21,7 @@
*/
#include "ui_numbers.hpp"
#include "ui_styles.hpp"
#include "string_format.hpp"
#include "portapack.hpp"
@@ -38,7 +39,7 @@ namespace ui {
void NumbersStationView::focus() {
if (file_error)
nav_.display_modal("No voices", "No valid voices found in\nthe /numbers directory.", ABORT, nullptr);
nav_.display_modal("No voices", "No valid voices found in\nthe /numbers directory.", ABORT);
else
button_exit.focus();
}
@@ -75,7 +76,7 @@ void NumbersStationView::prepare_audio() {
return;
}
code = symfield_code.get_sym(code_index);
code = symfield_code.get_offset(code_index);
if (code >= 10) {
memset(audio_buffer, 0, 1024);
@@ -144,7 +145,7 @@ void NumbersStationView::on_tick_second() {
armed_blink = not armed_blink;
if (armed_blink)
check_armed.set_style(&style_red);
check_armed.set_style(&Styles::red);
else
check_armed.set_style(&style());
@@ -152,14 +153,12 @@ void NumbersStationView::on_tick_second() {
}
void NumbersStationView::on_voice_changed(size_t index) {
std::string code_list = "";
std::string code_list;
for (const auto& wavs : voices[index].available_wavs)
code_list += wavs.code;
for (uint32_t c = 0; c < 25; c++)
symfield_code.set_symbol_list(c, code_list);
symfield_code.set_symbol_list(code_list);
current_voice = &voices[index];
}
@@ -265,17 +264,17 @@ NumbersStationView::NumbersStationView(
};
// DEBUG
symfield_code.set_sym(0, 10);
symfield_code.set_sym(1, 3);
symfield_code.set_sym(2, 4);
symfield_code.set_sym(3, 11);
symfield_code.set_sym(4, 6);
symfield_code.set_sym(5, 1);
symfield_code.set_sym(6, 9);
symfield_code.set_sym(7, 7);
symfield_code.set_sym(8, 8);
symfield_code.set_sym(9, 0);
symfield_code.set_sym(10, 12); // End
symfield_code.set_offset(0, 10);
symfield_code.set_offset(1, 3);
symfield_code.set_offset(2, 4);
symfield_code.set_offset(3, 11);
symfield_code.set_offset(4, 6);
symfield_code.set_offset(5, 1);
symfield_code.set_offset(6, 9);
symfield_code.set_offset(7, 7);
symfield_code.set_offset(8, 8);
symfield_code.set_offset(9, 0);
symfield_code.set_offset(10, 12); // End
/*
rtc::RTC datetime;
+8 -5
View File
@@ -142,16 +142,19 @@ class NumbersStationView : public View {
SymField symfield_code{
{1 * 8, 10 * 8},
25,
SymField::SYMFIELD_DEF};
SymField::Type::Custom};
Checkbox check_armed{
{2 * 8, 13 * 16},
5,
"Armed"};
/*Button button_tx_now {
{ 18 * 8, 13 * 16, 10 * 8, 32 },
"TX now"
};*/
/*
Button button_tx_now {
{ 18 * 8, 13 * 16, 10 * 8, 32 },
"TX now"};
*/
Button button_exit{
{21 * 8, 16 * 16, 64, 32},
"Exit"};
@@ -386,6 +386,7 @@ PlaylistView::PlaylistView(
&waterfall,
});
ensure_directory(u"PLAYLIST");
waterfall.show_audio_spectrum_view(false);
field_frequency.set_value(100'000'000);
+5 -10
View File
@@ -21,9 +21,6 @@
* Boston, MA 02110-1301, USA.
*/
#define SHORT_UI true
#define NORMAL_UI false
#include "app_settings.hpp"
#include "bitmap.hpp"
#include "file.hpp"
@@ -63,7 +60,7 @@ class PlaylistView : public View {
// More header == less spectrum view.
static constexpr ui::Dim header_height = 6 * 16;
static constexpr uint32_t baseband_bandwidth = 2500000;
static constexpr uint32_t baseband_bandwidth = 2'500'000;
struct playlist_entry {
std::filesystem::path path{};
@@ -118,14 +115,11 @@ class PlaylistView : public View {
Text text_sample_rate{
{10 * 8, 1 * 16, 7 * 8, 16}};
/*v making there's 1px line (instead of two) between two progress bars,
* by letting 1px overlapped.
* So, since they overlapped 1px, they are visually same, and looks better.
*/
ProgressBar progressbar_track{
{18 * 8, 1 * 16, 12 * 8, 8 + 1}};
// (-1) to overlap with progressbar_track so there's
// only 1 pixel between them instead of 2.
ProgressBar progressbar_transmit{
{18 * 8, 3 * 8 - 1, 12 * 8, 8}};
@@ -135,7 +129,8 @@ class PlaylistView : public View {
// TODO: delay duration field.
TransmitterView2 tx_view{
9 * 8, 1 * 8, SHORT_UI};
{11 * 8, 2 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{21 * 8, 2 * 16},
+2 -7
View File
@@ -56,7 +56,7 @@ bool POCSAGTXView::start_tx() {
pocsag::BitRate bitrate;
std::vector<uint32_t> codewords;
address = field_address.value_dec_u32();
address = field_address.to_integer();
if (address > 0x1FFFFFU) {
nav_.display_modal("Bad address", "Address must be less\nthan 2097152.");
return false;
@@ -137,12 +137,7 @@ POCSAGTXView::POCSAGTXView(
options_bitrate.set_selected_index(1); // 1200bps
options_type.set_selected_index(0); // Address only
// TODO: set_value for whole symfield
uint32_t reload_address = persistent_memory::pocsag_last_address();
for (uint32_t c = 0; c < 7; c++) {
field_address.set_sym(6 - c, reload_address % 10);
reload_address /= 10;
}
field_address.set_value(persistent_memory::pocsag_last_address());
options_type.on_change = [this](size_t, int32_t i) {
if (i == 2)
+1 -2
View File
@@ -94,8 +94,7 @@ class POCSAGTXView : public View {
SymField field_address{
{11 * 8, 6 * 8},
7,
SymField::SYMFIELD_DEC};
7};
OptionsField options_type{
{11 * 8, 8 * 8},
+4 -7
View File
@@ -164,7 +164,7 @@ RDSView::~RDSView() {
}
void RDSView::start_tx() {
rds_flags.PI_code = sym_pi_code.value_hex_u64();
rds_flags.PI_code = sym_pi_code.to_integer();
rds_flags.PTY = options_pty.selected_index_value();
rds_flags.DI = view_PSN.mono_stereo ? 1 : 0;
rds_flags.TP = check_TP.value();
@@ -212,12 +212,9 @@ RDSView::RDSView(
check_TP.set_value(true);
sym_pi_code.set_sym(0, 0xF);
sym_pi_code.set_sym(1, 0x3);
sym_pi_code.set_sym(2, 0xE);
sym_pi_code.set_sym(3, 0x0);
sym_pi_code.on_change = [this]() {
rds_flags.PI_code = sym_pi_code.value_hex_u64();
sym_pi_code.set_value(0xF3E0);
sym_pi_code.on_change = [this](SymField&) {
rds_flags.PI_code = sym_pi_code.to_integer();
};
options_pty.set_selected_index(0); // None
+1 -1
View File
@@ -287,7 +287,7 @@ class RDSView : public View {
SymField sym_pi_code{
{13 * 8, 28 + 16},
4,
SymField::SYMFIELD_HEX};
SymField::Type::Hex};
/*OptionsField options_coverage {
{ 17 * 8, 32 + 8 },
+1 -2
View File
@@ -459,8 +459,7 @@ ReconView::ReconView(NavigationView& nav)
rssi.set_focusable(true);
rssi.set_peak(true, 500);
rssi.on_select = [this](RSSI&) {
nav_.pop();
nav_.push<LevelView>();
nav_.replace<LevelView>();
};
// TODO: *BUG* Both transmitter_model and receiver_model share the same pmem setting for target_frequency.
+574 -14
View File
@@ -1,6 +1,5 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2018 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -22,30 +21,591 @@
#include "ui_remote.hpp"
#include "baseband_api.hpp"
#include "binder.hpp"
#include "convert.hpp"
#include "file_reader.hpp"
#include "io_convert.hpp"
#include "irq_controls.hpp"
#include "oversample.hpp"
#include "string_format.hpp"
#include "ui_fileman.hpp"
#include "ui_receiver.hpp"
#include "ui_textentry.hpp"
#include "utility.hpp"
using namespace portapack;
namespace fs = std::filesystem;
namespace ui {
void RemoteView::focus() {
button.focus();
static constexpr uint8_t text_edit_max = 30;
/* RemoteEntryModel **************************************/
std::string RemoteEntryModel::to_string() const {
return join(',', {path.string(),
name,
to_string_dec_uint(icon),
to_string_dec_uint(bg_color),
to_string_dec_uint(fg_color),
to_string_dec_uint(metadata.center_frequency),
to_string_dec_uint(metadata.sample_rate)});
}
RemoteView::~RemoteView() {
// transmitter_model.disable();
// baseband::shutdown();
Optional<RemoteEntryModel> RemoteEntryModel::parse(std::string_view line) {
auto cols = split_string(line, ',');
if (cols.size() < 7)
return {};
RemoteEntryModel entry{};
entry.path = cols[0];
entry.name = std::string{cols[1]};
parse_int(cols[2], entry.icon);
parse_int(cols[3], entry.bg_color);
parse_int(cols[4], entry.fg_color);
parse_int(cols[5], entry.metadata.center_frequency);
parse_int(cols[6], entry.metadata.sample_rate);
return entry;
}
RemoteView::RemoteView(
NavigationView& nav) {
add_children({&labels,
&button});
/* RemoteModel *******************************************/
button.on_select = [this, &nav](Button&) {
nav.pop();
bool RemoteModel::delete_entry(const RemoteEntryModel* entry) {
// NB: expecting 'entry' to be a pointer to an entry in vector.
auto it = std::find_if(
entries.begin(), entries.end(),
[entry](auto& item) { return entry == &item; });
if (it == entries.end())
return false;
entries.erase(it);
return true;
}
bool RemoteModel::load(const std::filesystem::path& path) {
File f;
auto error = f.open(path);
if (error)
return false;
entries.clear();
bool first = true;
auto reader = FileLineReader(f);
for (const auto& line : reader) {
if (line.length() == 0 || line[0] == '#')
continue; // Empty or comment line.
// First line is the "name" field.
if (first) {
name = trim(line);
first = false;
continue;
}
// All the other lines are button entries.
auto entry = RemoteEntryModel::parse(line);
if (entry)
entries.push_back(*std::move(entry));
}
return true;
}
bool RemoteModel::save(const std::filesystem::path& path) {
File f;
auto error = f.create(path);
if (error)
return false;
f.write_line(name);
for (auto& entry : entries)
f.write_line(entry.to_string());
return true;
}
/* RemoteButton ******************************************/
RemoteButton::RemoteButton(Rect parent_rect, RemoteEntryModel* entry)
: NewButton{parent_rect, {}, nullptr},
entry_{nullptr} {
set_entry(entry);
// Forward to on_select2 -- this isn't ideal, but works for now.
on_select = [this]() {
if (on_select2)
on_select2(*this);
};
}
void RemoteButton::on_focus() {
// Enable long press on "Select".
SwitchesState config;
config[toUType(Switch::Sel)] = true;
set_switches_long_press_config(config);
}
void RemoteButton::on_blur() {
// Reset long press.
SwitchesState config{};
set_switches_long_press_config(config);
}
bool RemoteButton::on_key(KeyEvent key) {
if (key == KeyEvent::Select) {
if (key_is_long_pressed(key) && on_long_select) {
on_long_select(*this);
return true;
}
if (on_select2) {
on_select2(*this);
return true;
}
}
return false;
}
void RemoteButton::paint(Painter& painter) {
NewButton::paint(painter);
// Add a border on the highlighted button.
if (has_focus() || highlighted()) {
auto r = screen_rect();
painter.draw_rectangle(r, Color::white());
auto p = r.location() + Point{1, 1};
auto s = Size{r.size().width() - 2, r.size().height() - 2};
painter.draw_rectangle({p, s}, Color::light_grey());
}
};
RemoteEntryModel* RemoteButton::entry() {
return entry_;
}
void RemoteButton::set_entry(RemoteEntryModel* entry) {
entry_ = entry;
set_focusable(entry_ != nullptr);
hidden(entry_ == nullptr);
if (entry_) {
set_text(entry_->name);
set_bitmap(RemoteIcons::get(entry_->icon));
}
set_dirty();
}
Style RemoteButton::paint_style() {
if (!entry_)
return style();
MutableStyle s{style()};
s.foreground = RemoteColors::get(entry_->fg_color);
s.background = RemoteColors::get(entry_->bg_color);
if (has_focus() || highlighted())
s.invert();
// It's kind of a hack to set 'color_' here, but the base
// class' paint logic is kind of convoluted but isn't worth
// the extra bytes to copy and paste a paint override.
color_ = s.foreground;
return s;
}
/* RemoteEntryEditView ***********************************/
RemoteEntryEditView::RemoteEntryEditView(
NavigationView& nav,
RemoteEntryModel& entry)
: entry_{entry} {
add_children({
&labels,
&field_name,
&field_path,
&field_freq,
&text_rate,
&field_icon_index,
&field_fg_color_index,
&field_bg_color_index,
&button_preview,
&button_delete,
&button_done,
});
bind(field_name, entry_.name, nav, [this](auto& v) {
button_preview.set_text(v);
});
field_path.on_select = [this, &nav](TextField&) {
auto open_view = nav.push<FileLoadView>(".C*");
open_view->push_dir(u"CAPTURES");
open_view->on_changed = [this](fs::path path) {
load_path(std::move(path));
refresh_ui();
};
};
bind(field_freq, entry_.metadata.center_frequency, nav);
bind(field_icon_index, entry_.icon, [this](auto v) {
button_preview.set_bitmap(RemoteIcons::get(v));
});
bind(field_fg_color_index, entry_.fg_color, [this](auto v) {
button_preview.set_color(RemoteColors::get(v));
});
bind(field_bg_color_index, entry_.bg_color, [this](auto) {
button_preview.set_dirty();
});
button_delete.on_select = [this, &nav]() {
nav.display_modal(
"Delete?", " Delete this button?", YESNO,
[this, &nav](bool choice) {
if (choice) {
if (on_delete)
on_delete(entry_);
// Exit the edit view upon delete.
nav.set_on_pop([&nav]() { nav.pop(); });
}
});
};
button_done.on_select = [&nav](Button&) {
nav.pop();
};
refresh_ui();
}
void RemoteEntryEditView::focus() {
button_done.focus();
}
void RemoteEntryEditView::refresh_ui() {
field_path.set_text(entry_.path.filename().string());
field_freq.set_value(entry_.metadata.center_frequency);
text_rate.set(unit_auto_scale(entry_.metadata.sample_rate, 3, 0) + "Hz");
}
void RemoteEntryEditView::load_path(std::filesystem::path&& path) {
// Read metafile if it exists.
auto metadata_path = get_metadata_path(path);
auto metadata = read_metadata_file(metadata_path);
entry_.path = std::move(path);
// Use metadata if found, otherwise fallback to the TX frequency.
if (metadata)
entry_.metadata = *metadata;
else
entry_.metadata = {transmitter_model.target_frequency(), 500'000};
}
/* RemoteView ********************************************/
RemoteView::RemoteView(
NavigationView& nav)
: nav_{nav} {
baseband::run_image(portapack::spi_flash::image_tag_replay);
add_children({
&field_title,
&tx_view,
&check_loop,
&field_filename,
&button_add,
&button_new,
&button_open,
&waterfall,
});
create_buttons();
field_title.on_select = [this, &nav](TextField&) {
temp_buffer_ = model_.name;
text_prompt(nav_, temp_buffer_, text_edit_max, [this](std::string& new_name) {
model_.name = new_name;
refresh_ui();
set_needs_save();
});
};
field_filename.on_select = [this, &nav](TextField&) {
temp_buffer_ = remote_path_.stem().string();
text_prompt(nav_, temp_buffer_, text_edit_max, [this](std::string& new_name) {
rename_remote(new_name);
refresh_ui();
});
};
button_add.on_select = [this]() { add_button(); };
button_new.on_select = [this]() { new_remote(); };
button_open.on_select = [this]() { open_remote(); };
// Fill in the area between the remote buttons and bottom UI with waterfall.
Dim waterfall_top = buttons_top_.y() + button_area_height;
Dim waterfall_bottom = button_add.parent_rect().top();
Dim waterfall_height = waterfall_bottom - waterfall_top;
waterfall.set_parent_rect({0, waterfall_top, screen_width, waterfall_height});
ensure_directory(u"REMOTES");
// Load the previously loaded remote if exists.
if (!load_remote(settings_.remote_path))
init_remote();
refresh_ui();
}
RemoteView::RemoteView(NavigationView& nav, fs::path path)
: RemoteView(nav) {
load_remote(std::move(path));
refresh_ui();
}
RemoteView::~RemoteView() {
stop();
baseband::shutdown();
save_remote(/*show_error*/ false);
}
void RemoteView::focus() {
if (model_.entries.empty())
button_add.focus();
else
buttons_[0]->focus();
}
void RemoteView::create_buttons() {
// Handler callbacks.
auto handle_send = [this](RemoteButton& btn) {
if (btn.entry()->path.empty())
// No path set? Go to edit mode instead.
edit_button(btn);
else if (is_sending() && &btn == current_btn_)
// Pressed the same button again? Stop.
stop();
else
// Start sending.
send_button(btn);
};
auto handle_edit = [this](RemoteButton& btn) {
edit_button(btn);
};
// Create and add RemoteButtons for the whole grid.
for (size_t i = 0; i < max_buttons; ++i) {
Coord x = i % button_cols;
Coord y = i / button_cols;
Point pos = Point{x * button_width, y * button_height} + buttons_top_;
auto btn = std::make_unique<RemoteButton>(
Rect{pos, {button_width, button_height}},
nullptr);
btn->on_select2 = handle_send;
btn->on_long_select = handle_edit;
add_child(btn.get());
buttons_.push_back(std::move(btn));
}
}
void RemoteView::reset_buttons() {
// Whever the model's entries instance is invalidated,
// all the pointers in the buttons will end up dangling.
// TODO: This is pretty lame. Could maybe static alloc?
for (auto& btn : buttons_)
btn->set_entry(nullptr);
}
void RemoteView::refresh_ui() {
field_title.set_text(model_.name);
field_filename.set_text(remote_path_.stem().string());
// Update buttons from the model.
for (size_t i = 0; i < buttons_.size(); ++i) {
if (i < model_.entries.size())
buttons_[i]->set_entry(&model_.entries[i]);
else
buttons_[i]->set_entry(nullptr);
}
}
void RemoteView::add_button() {
if (model_.entries.size() >= max_buttons)
return;
// Don't let replay thread read the model while editing.
stop();
model_.entries.push_back({{}, "<EMPTY>", 0, 3, 1});
reset_buttons();
refresh_ui();
set_needs_save();
}
void RemoteView::edit_button(RemoteButton& btn) {
// Don't let replay thread read the model while editing.
stop();
auto edit_view = nav_.push<RemoteEntryEditView>(*btn.entry());
nav_.set_on_pop([this]() {
refresh_ui();
set_needs_save();
focus(); // Need to refocus after refreshing the buttons.
});
edit_view->on_delete = [this](RemoteEntryModel& to_delete) {
model_.delete_entry(&to_delete);
reset_buttons();
};
}
void RemoteView::send_button(RemoteButton& btn) {
// TODO: If this is called while is_sending() == true,
// it just stops and doesn't start the new button?
// Reset everything to prepare to send a file.
stop();
current_btn_ = &btn; // Stash for looping.
// Open the sample file to send.
auto reader = std::make_unique<FileConvertReader>();
auto error = reader->open(btn.entry()->path);
if (error) {
show_error("Can't open file:\n" + btn.entry()->path.stem().string());
return;
}
// Update the sample rate in proc_replay baseband.
baseband::set_sample_rate(
btn.entry()->metadata.sample_rate,
get_oversample_rate(btn.entry()->metadata.sample_rate));
// ReplayThread starts immediately on construction; must be set before creating.
transmitter_model.set_target_frequency(btn.entry()->metadata.center_frequency);
transmitter_model.set_sampling_rate(get_actual_sample_rate(btn.entry()->metadata.sample_rate));
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
transmitter_model.enable();
// ReplayThread reads the file and sends to the baseband.
replay_thread_ = std::make_unique<ReplayThread>(
std::move(reader),
/* read_size */ 0x4000,
/* buffer_count */ 3,
&ready_signal_,
[](uint32_t return_code) {
ReplayThreadDoneMessage message{return_code};
EventDispatcher::send_message(message);
});
}
void RemoteView::stop() {
// This terminates the underlying chThread.
replay_thread_.reset();
transmitter_model.disable();
ready_signal_ = false;
}
void RemoteView::new_remote() {
save_remote();
init_remote();
refresh_ui();
// View needs to redraw to hide old buttons.
set_dirty();
}
void RemoteView::open_remote() {
auto open_view = nav_.push<FileLoadView>(".REM");
open_view->push_dir(u"REMOTES");
open_view->on_changed = [this](fs::path path) {
save_remote();
load_remote(std::move(path));
refresh_ui();
};
}
void RemoteView::init_remote() {
model_ = {"<Unnamed Remote>", {}};
reset_buttons();
set_remote_path(next_filename_matching_pattern(u"/REMOTES/REMOTE_????.REM"));
set_needs_save(false);
if (remote_path_.empty())
show_error("Couldn't make new remote file.");
}
bool RemoteView::load_remote(fs::path&& path) {
set_remote_path(std::move(path));
set_needs_save(false);
reset_buttons();
return model_.load(remote_path_);
}
void RemoteView::save_remote(bool show_errors) {
if (!needs_save_)
return;
bool ok = model_.save(remote_path_);
if (!ok && show_errors)
show_error("Save failed for:\n" + remote_path_.stem().string());
set_needs_save(false);
}
void RemoteView::rename_remote(const std::string& new_name) {
auto folder = remote_path_.parent_path();
auto ext = remote_path_.extension();
auto new_path = folder / new_name + ext;
if (file_exists(new_path)) {
show_error("Remote " + new_name + " already exists");
return;
}
// Rename file if there is one.
if (fs::file_exists(remote_path_))
rename_file(remote_path_, new_path);
set_remote_path(std::move(new_path));
}
void RemoteView::handle_replay_thread_done(uint32_t return_code) {
if (return_code == ReplayThread::END_OF_FILE) {
if (check_loop.value() && current_btn_) {
send_button(*current_btn_);
return;
}
}
/*
// TODO: This can happen when stopping an in-progress Tx.
if (return_code == ReplayThread::READ_ERROR)
show_error("Bad capture file.");
*/
stop();
}
void RemoteView::set_remote_path(fs::path&& path) {
// Unfortunately, have to keep these two in sync because
// settings doesn't know about fs::path.
remote_path_ = std::move(path);
settings_.remote_path = remote_path_.string();
}
void RemoteView::show_error(const std::string& msg) const {
nav_.display_modal("Error", msg);
}
} /* namespace ui */
+347 -21
View File
@@ -1,6 +1,5 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2018 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -21,42 +20,369 @@
*/
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_spectrum.hpp"
#include "ui_transmitter.hpp"
#include "transmitter_model.hpp"
#include "app_settings.hpp"
#include "baseband_api.hpp"
#include "bitmap.hpp"
#include "file.hpp"
#include "metadata_file.hpp"
#include "optional.hpp"
#include "radio_state.hpp"
#include "replay_thread.hpp"
#include <algorithm>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace ui {
/* Maps icon index to bitmap. */
class RemoteIcons {
public:
static constexpr const Bitmap* get(uint8_t index) {
if (index >= std::size(bitmaps_))
return bitmaps_[0];
return bitmaps_[index];
}
static constexpr size_t size() {
return std::size(bitmaps_);
}
private:
// NB: Icons need to be 16x16 or they won't fit corrently.
static constexpr std::array<const Bitmap*, 25> bitmaps_{
nullptr,
&bitmap_icon_fox,
&bitmap_icon_adsb,
&bitmap_icon_ais,
&bitmap_icon_aprs,
&bitmap_icon_btle,
&bitmap_icon_burger,
&bitmap_icon_camera,
&bitmap_icon_cwgen,
&bitmap_icon_dmr,
&bitmap_icon_file_image,
&bitmap_icon_lge,
&bitmap_icon_looking,
&bitmap_icon_memory,
&bitmap_icon_morse,
&bitmap_icon_nrf,
&bitmap_icon_notepad,
&bitmap_icon_rds,
&bitmap_icon_remote,
&bitmap_icon_setup,
&bitmap_icon_sleep,
&bitmap_icon_sonde,
&bitmap_icon_stealth,
&bitmap_icon_tetra,
&bitmap_icon_temperature};
};
// TODO: Use RGB colors instead?
/* Maps color index to color. */
class RemoteColors {
public:
static constexpr Color get(uint8_t index) {
if (index >= std::size(colors_))
return colors_[0];
return colors_[index];
}
static constexpr size_t size() {
return std::size(colors_);
}
private:
static constexpr std::array<Color, 21> colors_{
Color::black(), // 0
Color::white(), // 1
Color::darker_grey(), // 2
Color::dark_grey(), // 3
Color::grey(), // 4
Color::light_grey(), // 5
Color::red(), // 6
Color::orange(), // 7
Color::yellow(), // 8
Color::green(), // 9
Color::blue(), // 10
Color::cyan(), // 11
Color::magenta(), // 12
Color::dark_red(), // 13
Color::dark_orange(), // 14
Color::dark_yellow(), // 15
Color::dark_green(), // 16
Color::dark_blue(), // 17
Color::dark_cyan(), // 18
Color::dark_magenta(), // 19
Color::purple()}; // 20
};
/* Data model for a remote entry. */
struct RemoteEntryModel {
std::filesystem::path path{};
std::string name{};
uint8_t icon = 0;
uint8_t bg_color = 0;
uint8_t fg_color = 0;
capture_metadata metadata{};
// TODO: start/end position for trimming.
std::string to_string() const;
static Optional<RemoteEntryModel> parse(std::string_view line);
};
/* Data model for a remote. */
struct RemoteModel {
std::string name{};
std::vector<RemoteEntryModel> entries{};
bool delete_entry(const RemoteEntryModel* entry);
bool load(const std::filesystem::path& path);
bool save(const std::filesystem::path& path);
};
/* Button for the remote UI. */
class RemoteButton : public NewButton {
public:
std::function<void(RemoteButton&)> on_select2{};
std::function<void(RemoteButton&)> on_long_select{};
RemoteButton(Rect parent_rect, RemoteEntryModel* entry);
RemoteButton(const RemoteButton&) = delete;
RemoteButton& operator=(const RemoteButton&) = delete;
void on_focus() override;
void on_blur() override;
bool on_key(KeyEvent key) override;
void paint(Painter& painter) override;
RemoteEntryModel* entry();
void set_entry(RemoteEntryModel* entry);
protected:
Style paint_style() override;
private:
// Hide because it's not used.
using NewButton::on_select;
RemoteEntryModel* entry_;
};
/* Settings container for remote. */
struct RemoteSettings {
std::string remote_path{};
};
/* View for editing a remote entry button. */
class RemoteEntryEditView : public View {
public:
std::function<void(RemoteEntryModel&)> on_delete{};
RemoteEntryEditView(NavigationView& nav, RemoteEntryModel& entry);
std::string title() const override { return "Edit Button"; };
void focus() override;
private:
RemoteEntryModel& entry_;
void refresh_ui();
void load_path(std::filesystem::path&& path);
Labels labels{
{{2 * 8, 1 * 16}, "Name:", Color::light_grey()},
{{2 * 8, 2 * 16}, "Path:", Color::light_grey()},
{{2 * 8, 3 * 16}, "Freq:", Color::light_grey()},
{{17 * 8, 3 * 16}, "MHz", Color::light_grey()},
{{2 * 8, 4 * 16}, "Rate:", Color::light_grey()},
{{2 * 8, 5 * 16}, "Icon:", Color::light_grey()},
{{2 * 8, 6 * 16}, "FG Color:", Color::light_grey()},
{{2 * 8, 7 * 16}, "BG Color:", Color::light_grey()},
{{8 * 8, 9 * 16}, "Button preview", Color::light_grey()},
};
TextField field_name{{8 * 8, 1 * 16, 20 * 8, 1 * 16}, {}};
TextField field_path{{8 * 8, 2 * 16, 20 * 8, 1 * 16}, {}};
FrequencyField field_freq{{8 * 8, 3 * 16}};
Text text_rate{{8 * 8, 4 * 16, 20 * 8, 1 * 16}, {}};
NumberField field_icon_index{
{8 * 8, 5 * 16},
2,
{0, RemoteIcons::size() - 1},
/*step*/ 1,
/*fill*/ ' ',
/*loop*/ true};
NumberField field_fg_color_index{
{11 * 8, 6 * 16},
2,
{0, RemoteColors::size() - 1},
/*step*/ 1,
/*fill*/ ' ',
/*loop*/ true};
NumberField field_bg_color_index{
{11 * 8, 7 * 16},
2,
{0, RemoteColors::size() - 1},
/*step*/ 1,
/*fill*/ ' ',
/*loop*/ true};
RemoteButton button_preview{
{10 * 8, 11 * 16 - 8, 10 * 8, 50},
&entry_};
NewButton button_delete{
{2 * 8, 16 * 16, 4 * 8, 2 * 16},
{},
&bitmap_icon_trash,
Color::red()};
Button button_done{{11 * 8, 16 * 16, 8 * 8, 2 * 16}, "Done"};
};
/* App that allows for buttons to be bound to captures for playback. */
class RemoteView : public View {
public:
RemoteView(NavigationView& nav);
RemoteView(NavigationView& nav, std::filesystem::path path);
~RemoteView();
RemoteView(const RemoteView&) = delete;
RemoteView& operator=(const RemoteView&) = delete;
std::string title() const override { return "Remote"; };
void focus() override;
std::string title() const override { return "Custom remote"; };
private:
/*enum tx_modes {
IDLE = 0,
SINGLE,
SCAN
};
/* Creates the dynamic buttons. */
void create_buttons();
/* Resets all the pointers to null entries. */
void reset_buttons();
void refresh_ui();
tx_modes tx_mode = IDLE;
void add_button();
void edit_button(RemoteButton& btn);
void send_button(RemoteButton& btn);
void stop();
void new_remote();
void open_remote();
void init_remote();
bool load_remote(std::filesystem::path&& path);
void save_remote(bool show_errors = true);
void rename_remote(const std::string& new_name);
void handle_replay_thread_done(uint32_t return_code);
void set_needs_save(bool v = true) { needs_save_ = v; }
void set_remote_path(std::filesystem::path&& path);
bool is_sending() const { return replay_thread_ != nullptr; }
void show_error(const std::string& msg) const;
struct remote_layout_t {
Point position;
std::string text;
};
static constexpr Dim button_rows = 4;
static constexpr Dim button_cols = 3;
static constexpr uint8_t max_buttons = button_rows * button_cols;
static constexpr Dim button_area_height = 200;
static constexpr Dim button_width = screen_width / button_cols;
static constexpr Dim button_height = button_area_height / button_rows;
const std::array<remote_layout_t, 32> remote_layout { };*/
// This value is mysterious... why?
static constexpr uint32_t baseband_bandwidth = 2'500'000;
Labels labels{
{{1 * 8, 0}, "Work in progress...", Color::light_grey()}};
NavigationView& nav_;
RxRadioState radio_state_{};
Button button{
{60, 64, 120, 32},
"Exit"};
// Settings
RemoteSettings settings_{};
app_settings::SettingsManager app_settings_{
"tx_remote"sv,
app_settings::Mode::TX,
{
{"remote_path"sv, &settings_.remote_path},
}};
RemoteModel model_{};
bool needs_save_ = false;
std::string temp_buffer_{};
std::filesystem::path remote_path_{};
RemoteButton* current_btn_{};
const Point buttons_top_{0, 20};
std::vector<std::unique_ptr<RemoteButton>> buttons_{};
std::unique_ptr<ReplayThread> replay_thread_{};
bool ready_signal_{}; // Used to signal ReplayThread ready.
TextField field_title{
{0 * 8, 0 * 16 + 2, 30 * 8, 1 * 16},
{}};
TransmitterView2 tx_view{
{0 * 8, 17 * 16},
/*short_ui*/ true};
Checkbox check_loop{
{10 * 8, 17 * 16},
4,
"Loop",
/*small*/ true};
TextField field_filename{
{0 * 8, 18 * 16, 17 * 8, 1 * 16},
{}};
NewButton button_add{
{17 * 8 + 4, 17 * 16, 4 * 8, 2 * 16},
"",
&bitmap_icon_add,
Color::orange(),
/*vcenter*/ true};
NewButton button_new{
{22 * 8, 17 * 16, 4 * 8, 2 * 16},
"",
&bitmap_icon_new_file,
Color::dark_blue(),
/*vcenter*/ true};
NewButton button_open{
{26 * 8, 17 * 16, 4 * 8, 2 * 16},
"",
&bitmap_icon_load,
Color::dark_blue(),
/*vcenter*/ true};
spectrum::WaterfallView waterfall{};
MessageHandlerRegistration message_handler_replay_thread_error{
Message::ID::ReplayThreadDone,
[this](const Message* p) {
auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
handle_replay_thread_done(message.return_code);
}};
MessageHandlerRegistration message_handler_fifo_signal{
Message::ID::RequestSignal,
[this](const Message* p) {
auto message = static_cast<const RequestSignalMessage*>(p);
if (message->signal == RequestSignalMessage::Signal::FillRequest) {
ready_signal_ = true;
}
}};
};
} /* namespace ui */
+11 -5
View File
@@ -44,15 +44,21 @@ void WipeSDView::focus() {
dummy.focus();
if (!confirmed) {
nav_.push<ModalMessageView>("Warning !", "Wipe FAT of SD card?", YESCANCEL, [this](bool choice) {
if (choice)
confirmed = true;
});
nav_.push<ModalMessageView>(
"Warning !",
"Wipe FAT of SD card?",
YESNO,
[this](bool choice) {
if (choice)
confirmed = true;
else
nav_.pop(false); // Pop w/o update so the modal will pop off the app.
});
} else {
if (sdcGetInfo(&SDCD1, &block_device_info) == CH_SUCCESS) {
thread = chThdCreateFromHeap(NULL, 2048, NORMALPRIO, WipeSDView::static_fn, this);
} else {
nav_.pop(); // Just silently abort for now
nav_.pop(); // Just silently abort for now.
}
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ class WipeSDView : public View {
~WipeSDView();
void focus() override;
std::string title() const override { return "Wipe sdcard"; };
std::string title() const override { return "Wipe SD Card"; };
private:
NavigationView& nav_;
+124 -157
View File
@@ -23,7 +23,9 @@
#include "ui_search.hpp"
#include "baseband_api.hpp"
#include "binder.hpp"
#include "string_format.hpp"
#include "ui_freqman.hpp"
using namespace portapack;
@@ -47,8 +49,61 @@ void RecentEntriesTable<SearchRecentEntries>::draw(
painter.draw_string(target_rect.location(), style, to_string_short_freq(entry.frequency) + " " + entry.time + " " + str_duration);
}
void SearchView::focus() {
field_frequency_min.focus();
/* SearchView ********************************************/
SearchView::SearchView(
NavigationView& nav)
: nav_(nav) {
baseband::run_image(portapack::spi_flash::image_tag_wideband_spectrum);
add_children({&labels,
&field_frequency_min,
&field_frequency_max,
&field_lna,
&field_vga,
&field_threshold,
&text_mean,
&text_slices,
&text_rate,
&text_infos,
&vu_max,
&progress_timers,
&check_snap,
&options_snap,
&big_display,
&recent_entries_view});
baseband::set_spectrum(SEARCH_SLICE_WIDTH, 31);
recent_entries_view.set_parent_rect({0, 28 * 8, screen_width, 12 * 8});
recent_entries_view.on_select = [this, &nav](const SearchRecentEntry& entry) {
nav.push<FrequencySaveView>(entry.frequency);
};
text_mean.set_style(&Styles::grey);
text_slices.set_style(&Styles::grey);
text_rate.set_style(&Styles::grey);
progress_timers.set_style(&Styles::grey);
big_display.set_style(&Styles::grey);
field_frequency_min.set_step(100'000);
bind(field_frequency_min, settings_.freq_min, nav, [this](auto) {
on_range_changed();
});
field_frequency_max.set_step(100'000);
bind(field_frequency_max, settings_.freq_max, nav, [this](auto) {
on_range_changed();
});
bind(field_threshold, settings_.power_threshold);
bind(check_snap, settings_.snap_search);
bind(options_snap, settings_.snap_step);
progress_timers.set_max(DETECT_DELAY);
on_range_changed();
receiver_model.enable();
}
SearchView::~SearchView() {
@@ -56,6 +111,18 @@ SearchView::~SearchView() {
baseband::shutdown();
}
void SearchView::on_show() {
baseband::spectrum_streaming_start();
}
void SearchView::on_hide() {
baseband::spectrum_streaming_stop();
}
void SearchView::focus() {
field_frequency_min.focus();
}
void SearchView::do_detection() {
uint8_t power_max = 0;
int32_t bin_max = -1;
@@ -83,7 +150,7 @@ void SearchView::do_detection() {
if (power > overall_power_max)
overall_power_max = power;
if ((power >= mean_power + power_threshold) && (power > power_max)) {
if ((power >= mean_power + settings_.power_threshold) && (power > power_max)) {
power_max = power;
bin_max = slices[slice].max_index;
slice_max = slice;
@@ -104,7 +171,7 @@ void SearchView::do_detection() {
}
// Check range
if ((resolved_frequency >= f_min) && (resolved_frequency <= f_max)) {
if ((resolved_frequency >= settings_.freq_min) && (resolved_frequency <= settings_.freq_max)) {
duration = 0;
auto& entry = ::on_packet(recent, resolved_frequency);
@@ -159,16 +226,47 @@ void SearchView::do_detection() {
}
}
void SearchView::add_spectrum_pixel(Color color) {
// Is avoiding floats really necessary ?
bin_skip_acc += bin_skip_frac;
if (bin_skip_acc < 0x10000)
return;
void SearchView::do_timers() {
if (timing_div >= 60) {
// ~1Hz
bin_skip_acc -= 0x10000;
timing_div = 0;
if (pixel_index < 240)
spectrum_row[pixel_index++] = color;
// Update scan rate
text_rate.set(to_string_dec_uint(search_counter, 3));
search_counter = 0;
}
if (timing_div % 12 == 0) {
// ~5Hz
// Update power levels
text_mean.set(to_string_dec_uint(mean_power, 3));
vu_max.set_value(overall_power_max);
vu_max.set_mark(mean_power + settings_.power_threshold);
}
if (timing_div % 6 == 0) {
// ~10Hz
// Update timing indicator
if (locked) {
progress_timers.set_max(RELEASE_DELAY);
progress_timers.set_value(RELEASE_DELAY - release_timer);
} else {
progress_timers.set_max(DETECT_DELAY);
progress_timers.set_value(detect_timer);
}
// Increment timers
if (detect_timer < DETECT_DELAY) detect_timer++;
if (release_timer < RELEASE_DELAY) release_timer++;
if (locked) duration++;
}
timing_div++;
}
void SearchView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
@@ -221,22 +319,14 @@ void SearchView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
baseband::spectrum_streaming_start();
}
void SearchView::on_show() {
baseband::spectrum_streaming_start();
}
void SearchView::on_hide() {
baseband::spectrum_streaming_stop();
}
void SearchView::on_range_changed() {
rf::Frequency slices_span, center_frequency;
rf::Frequency slices_span;
rf::Frequency center_frequency;
int64_t offset;
size_t slice;
f_min = field_frequency_min.value();
f_max = field_frequency_max.value();
search_span = abs(f_max - f_min);
// TODO: enforce min < max?
search_span = abs(settings_.freq_max - settings_.freq_min);
if (search_span > SEARCH_SLICE_WIDTH) {
// ex: 100M~115M (15M span):
@@ -253,14 +343,14 @@ void SearchView::on_range_changed() {
// offset = 0 + 2.5/2 = 1.25M
offset = ((search_span - slices_span) / 2) + (SEARCH_SLICE_WIDTH / 2);
// slice_start = 100M + 1.25M = 101.25M
center_frequency = std::min(f_min, f_max) + offset;
center_frequency = std::min(settings_.freq_min, settings_.freq_max) + offset;
for (slice = 0; slice < slices_nb; slice++) {
slices[slice].center_frequency = center_frequency;
center_frequency += SEARCH_SLICE_WIDTH;
}
} else {
slices[0].center_frequency = (f_max + f_min) / 2;
slices[0].center_frequency = (settings_.freq_max + settings_.freq_min) / 2;
receiver_model.set_target_frequency(slices[0].center_frequency);
slices_nb = 1;
@@ -272,139 +362,16 @@ void SearchView::on_range_changed() {
slice_counter = 0;
}
void SearchView::on_lna_changed(int32_t v_db) {
receiver_model.set_lna(v_db);
}
void SearchView::add_spectrum_pixel(Color color) {
// Is avoiding floats really necessary?
bin_skip_acc += bin_skip_frac;
if (bin_skip_acc < 0x10000)
return;
void SearchView::on_vga_changed(int32_t v_db) {
receiver_model.set_vga(v_db);
}
bin_skip_acc -= 0x10000;
void SearchView::do_timers() {
if (timing_div >= 60) {
// ~1Hz
timing_div = 0;
// Update scan rate
text_rate.set(to_string_dec_uint(search_counter, 3));
search_counter = 0;
}
if (timing_div % 12 == 0) {
// ~5Hz
// Update power levels
text_mean.set(to_string_dec_uint(mean_power, 3));
vu_max.set_value(overall_power_max);
vu_max.set_mark(mean_power + power_threshold);
}
if (timing_div % 6 == 0) {
// ~10Hz
// Update timing indicator
if (locked) {
progress_timers.set_max(RELEASE_DELAY);
progress_timers.set_value(RELEASE_DELAY - release_timer);
} else {
progress_timers.set_max(DETECT_DELAY);
progress_timers.set_value(detect_timer);
}
// Increment timers
if (detect_timer < DETECT_DELAY) detect_timer++;
if (release_timer < RELEASE_DELAY) release_timer++;
if (locked) duration++;
}
timing_div++;
}
SearchView::SearchView(
NavigationView& nav)
: nav_(nav) {
baseband::run_image(portapack::spi_flash::image_tag_wideband_spectrum);
add_children({&labels,
&field_frequency_min,
&field_frequency_max,
&field_lna,
&field_vga,
&field_threshold,
&text_mean,
&text_slices,
&text_rate,
&text_infos,
&vu_max,
&progress_timers,
&check_snap,
&options_snap,
&big_display,
&recent_entries_view});
baseband::set_spectrum(SEARCH_SLICE_WIDTH, 31);
recent_entries_view.set_parent_rect({0, 28 * 8, 240, 12 * 8});
recent_entries_view.on_select = [this, &nav](const SearchRecentEntry& entry) {
nav.push<FrequencyKeypadView>(entry.frequency);
};
text_mean.set_style(&Styles::grey);
text_slices.set_style(&Styles::grey);
text_rate.set_style(&Styles::grey);
progress_timers.set_style(&Styles::grey);
big_display.set_style(&Styles::grey);
check_snap.set_value(true);
options_snap.set_selected_index(1); // 12.5kHz
field_threshold.set_value(80);
field_threshold.on_change = [this](int32_t value) {
power_threshold = value;
};
field_frequency_min.set_value(receiver_model.target_frequency() - 1000000);
field_frequency_min.set_step(100000);
field_frequency_min.on_change = [this](rf::Frequency) {
this->on_range_changed();
};
field_frequency_min.on_edit = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.target_frequency() - 1000000);
new_view->on_changed = [this](rf::Frequency f) {
this->field_frequency_min.set_value(f);
};
};
field_frequency_max.set_value(receiver_model.target_frequency() + 1000000);
field_frequency_max.set_step(100000);
field_frequency_max.on_change = [this](rf::Frequency) {
this->on_range_changed();
};
field_frequency_max.on_edit = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.target_frequency() + 1000000);
new_view->on_changed = [this](rf::Frequency f) {
this->field_frequency_max.set_value(f);
};
};
field_lna.set_value(receiver_model.lna());
field_lna.on_change = [this](int32_t v) {
this->on_lna_changed(v);
};
field_vga.set_value(receiver_model.vga());
field_vga.on_change = [this](int32_t v_db) {
this->on_vga_changed(v_db);
};
progress_timers.set_max(DETECT_DELAY);
on_range_changed();
receiver_model.enable();
if (pixel_index < screen_width)
spectrum_row[pixel_index++] = color;
}
} /* namespace ui */
+55 -31
View File
@@ -20,14 +20,13 @@
* Boston, MA 02110-1301, USA.
*/
#include "app_settings.hpp"
#include "receiver_model.hpp"
#include "recent_entries.hpp"
#include "radio_state.hpp"
#include "spectrum_color_lut.hpp"
#include "ui_receiver.hpp"
#include "ui_styles.hpp"
#include "recent_entries.hpp"
namespace ui {
@@ -95,6 +94,26 @@ class SearchView : public View {
SEARCH_SLICE_WIDTH /* sampling rate */,
ReceiverModel::Mode::SpectrumAnalysis};
// Settings
struct SearchSettings {
uint32_t power_threshold = 80;
rf::Frequency freq_min = 100'000'000;
rf::Frequency freq_max = 400'000'000;
bool snap_search = true;
uint32_t snap_step = 12'500;
};
SearchSettings settings_{};
app_settings::SettingsManager app_settings_{
"rx_search"sv,
app_settings::Mode::RX,
{
{"power_threshold"sv, &settings_.power_threshold},
{"freq_min"sv, &settings_.freq_min},
{"freq_max"sv, &settings_.freq_max},
{"snap_search"sv, &settings_.snap_search},
{"snap_step"sv, &settings_.snap_step},
}};
struct slice_t {
rf::Frequency center_frequency;
uint8_t max_power;
@@ -103,33 +122,36 @@ class SearchView : public View {
int16_t index;
} slices[32];
uint32_t bin_skip_acc{0}, bin_skip_frac{};
uint32_t pixel_index{0};
std::array<Color, 240> spectrum_row = {0};
ChannelSpectrumFIFO* fifo{nullptr};
rf::Frequency f_min{0}, f_max{0};
uint8_t detect_timer{0}, release_timer{0}, timing_div{0};
uint8_t overall_power_max{0};
uint32_t mean_power{0}, mean_acc{0};
uint32_t duration{0};
uint32_t power_threshold{80}; // Todo: Put this in persistent / settings
rf::Frequency slice_start{0};
uint8_t slices_nb{0};
uint8_t slice_counter{0};
int16_t last_bin{0};
uint32_t last_slice{0};
Coord last_tick_pos{0};
rf::Frequency search_span{0}, resolved_frequency{0};
uint16_t locked_bin{0};
uint8_t search_counter{0};
bool locked{false};
uint32_t bin_skip_acc = 0;
uint32_t bin_skip_frac = 0;
uint32_t pixel_index = 0;
std::array<Color, 240> spectrum_row{};
ChannelSpectrumFIFO* fifo = nullptr;
uint8_t detect_timer = 0;
uint8_t release_timer = 0;
uint8_t timing_div = 0;
uint8_t overall_power_max{0};
uint32_t mean_power = 0;
uint32_t mean_acc = 0;
uint32_t duration = 0;
rf::Frequency slice_start = 0;
uint8_t slices_nb = 0;
uint8_t slice_counter = 0;
int16_t last_bin = 0;
uint32_t last_slice = 0;
Coord last_tick_pos = 0;
rf::Frequency search_span = 0;
rf::Frequency resolved_frequency = 0;
uint16_t locked_bin = 0;
uint8_t search_counter = 0;
bool locked = false;
void do_detection();
void do_timers();
void on_channel_spectrum(const ChannelSpectrum& spectrum);
void on_range_changed();
void do_detection();
void on_lna_changed(int32_t v_db);
void on_vga_changed(int32_t v_db);
void do_timers();
void add_spectrum_pixel(Color color);
const RecentEntriesColumns columns{{{"Frequency", 9},
@@ -150,6 +172,7 @@ class SearchView : public View {
{1 * 8, 1 * 16}};
FrequencyField field_frequency_max{
{11 * 8, 1 * 16}};
LNAGainField field_lna{
{22 * 8, 1 * 16}};
VGAGainField field_vga{
@@ -191,10 +214,10 @@ class SearchView : public View {
{17 * 8, 15 * 8}, // Position
7, // Length
{ // Options
{"25kHz ", 25000},
{"12.5kHz", 12500},
{"8.33kHz", 8333},
{"2.5kHz", 2500},
{"25kHz ", 25'000},
{"12.5kHz", 12'500},
{"8.33kHz", 8'333},
{"2.5kHz", 2'500},
{"500Hz", 500}}};
BigFrequency big_display{
@@ -207,6 +230,7 @@ class SearchView : public View {
const auto message = *reinterpret_cast<const ChannelSpectrumConfigMessage*>(p);
this->fifo = message.fifo;
}};
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
+128 -107
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -125,8 +126,30 @@ SetRadioView::SetRadioView(
nav.pop();
};
add_children({
&label_source,
&value_source,
&value_source_frequency,
&check_clkout,
&field_clkout_freq,
&labels_clkout_khz,
&value_freq_step,
&labels_bias,
&check_bias,
&disable_external_tcxo, // TODO: always show?
&button_save,
&button_cancel,
});
const auto reference = clock_manager.get_reference();
if (reference.source == ClockManager::ReferenceSource::Xtal) {
add_children({
&labels_correction,
&field_ppm,
});
}
std::string source_name("---");
switch (reference.source) {
case ClockManager::ReferenceSource::Xtal:
@@ -141,36 +164,15 @@ SetRadioView::SetRadioView(
}
value_source.set(source_name);
value_source_frequency.set(to_string_dec_uint(reference.frequency / 1000000, 2) + "." + to_string_dec_uint((reference.frequency % 1000000) / 100, 4, '0') + " MHz");
value_source_frequency.set(
to_string_dec_uint(reference.frequency / 1000000, 2) + "." +
to_string_dec_uint((reference.frequency % 1000000) / 100, 4, '0') + " MHz");
// Make these Text controls look like Labels.
label_source.set_style(&Styles::light_grey);
value_source.set_style(&Styles::light_grey);
value_source_frequency.set_style(&Styles::light_grey);
add_children({
&label_source,
&value_source,
&value_source_frequency,
});
if (reference.source == ClockManager::ReferenceSource::Xtal) {
add_children({
&labels_correction,
&field_ppm,
});
}
add_children({&check_clkout,
&field_clkout_freq,
&labels_clkout_khz,
&value_freq_step,
&labels_bias,
&check_bias,
&button_save,
&button_cancel});
add_children({&disable_external_tcxo});
SetFrequencyCorrectionModel model{
static_cast<int8_t>(pmem::correction_ppb() / 1000), 0};
@@ -344,10 +346,13 @@ void SetUIView::focus() {
/* SetAppSettingsView ************************************/
SetAppSettingsView::SetAppSettingsView(NavigationView& nav) {
add_children({&checkbox_load_app_settings,
&checkbox_save_app_settings,
&button_save,
&button_cancel});
add_children({
&labels,
&checkbox_load_app_settings,
&checkbox_save_app_settings,
&button_save,
&button_cancel,
});
checkbox_load_app_settings.set_value(pmem::load_app_settings());
checkbox_save_app_settings.set_value(pmem::save_app_settings());
@@ -369,11 +374,14 @@ void SetAppSettingsView::focus() {
/* SetConverterSettingsView ******************************/
SetConverterSettingsView::SetConverterSettingsView(NavigationView& nav) {
add_children({&check_show_converter,
&check_converter,
&converter_mode,
&button_converter_freq,
&button_return});
add_children({
&labels,
&check_show_converter,
&check_converter,
&opt_converter_mode,
&field_converter_freq,
&button_return,
});
check_show_converter.set_value(!pmem::ui_hide_converter());
check_show_converter.on_select = [this](Checkbox&, bool v) {
@@ -383,7 +391,7 @@ SetConverterSettingsView::SetConverterSettingsView(NavigationView& nav) {
}
// Retune to take converter change in account.
receiver_model.set_target_frequency(receiver_model.target_frequency());
// Refresh status bar with/out converter
// Refresh status bar converter icon.
send_system_refresh();
};
@@ -394,27 +402,30 @@ SetConverterSettingsView::SetConverterSettingsView(NavigationView& nav) {
pmem::set_ui_hide_converter(false);
}
pmem::set_config_converter(v);
// Retune to take converter change in account
// Retune to take converter change in account.
receiver_model.set_target_frequency(receiver_model.target_frequency());
// Refresh status bar with/out converter
// Refresh status bar converter icon.
send_system_refresh();
};
converter_mode.set_by_value(pmem::config_updown_converter());
converter_mode.on_change = [this](size_t, OptionsField::value_t v) {
opt_converter_mode.set_by_value(pmem::config_updown_converter());
opt_converter_mode.on_change = [this](size_t, OptionsField::value_t v) {
pmem::set_config_updown_converter(v);
// Refresh status bar with icon up or down
// Refresh status bar with up or down icon.
send_system_refresh();
};
button_converter_freq.set_text(to_string_short_freq(pmem::config_converter_freq()) + "MHz");
button_converter_freq.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(pmem::config_converter_freq());
new_view->on_changed = [this, &button](rf::Frequency f) {
pmem::set_config_converter_freq(f);
// Retune to take converter change in account
receiver_model.set_target_frequency(receiver_model.target_frequency());
button_converter_freq.set_text("<" + to_string_short_freq(f) + " MHz>");
field_converter_freq.set_step(1'000'000);
field_converter_freq.set_value(pmem::config_converter_freq());
field_converter_freq.on_change = [this](rf::Frequency f) {
pmem::set_config_converter_freq(f);
// Retune to take converter change in account.
receiver_model.set_target_frequency(receiver_model.target_frequency());
};
field_converter_freq.on_edit = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(field_converter_freq.value());
new_view->on_changed = [this](rf::Frequency f) {
field_converter_freq.set_value(f);
};
};
@@ -430,46 +441,50 @@ void SetConverterSettingsView::focus() {
/* SetFrequencyCorrectionView ****************************/
SetFrequencyCorrectionView::SetFrequencyCorrectionView(NavigationView& nav) {
add_children({&text_freqCorrection_about,
&frequency_rx_correction_mode,
&frequency_tx_correction_mode,
&button_freq_rx_correction,
&button_freq_tx_correction,
&button_return});
add_children({
&labels,
&opt_rx_correction_mode,
&field_rx_correction,
&opt_tx_correction_mode,
&field_tx_correction,
&button_return,
});
frequency_rx_correction_mode.set_by_value(pmem::config_freq_rx_correction_updown());
frequency_rx_correction_mode.on_change = [this](size_t, OptionsField::value_t v) {
opt_rx_correction_mode.set_by_value(pmem::config_freq_rx_correction_updown());
opt_rx_correction_mode.on_change = [this](size_t, OptionsField::value_t v) {
pmem::set_freq_rx_correction_updown(v);
};
frequency_tx_correction_mode.set_by_value(pmem::config_freq_rx_correction_updown());
frequency_tx_correction_mode.on_change = [this](size_t, OptionsField::value_t v) {
opt_tx_correction_mode.set_by_value(pmem::config_freq_rx_correction_updown());
opt_tx_correction_mode.on_change = [this](size_t, OptionsField::value_t v) {
pmem::set_freq_tx_correction_updown(v);
};
button_freq_rx_correction.set_text(to_string_short_freq(pmem::config_freq_rx_correction()) + "MHz (Rx)");
button_freq_rx_correction.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(pmem::config_converter_freq());
new_view->on_changed = [this, &button](rf::Frequency f) {
if (f >= MAX_FREQ_CORRECTION)
f = MAX_FREQ_CORRECTION;
pmem::set_config_freq_rx_correction(f);
// Retune to take converter change in account
receiver_model.set_target_frequency(receiver_model.target_frequency());
button_freq_rx_correction.set_text("<" + to_string_short_freq(f) + " MHz>");
field_rx_correction.set_step(100'000);
field_rx_correction.set_value(pmem::config_freq_rx_correction());
field_rx_correction.on_change = [this](rf::Frequency f) {
pmem::set_config_freq_rx_correction(f);
// Retune to take converter change in account.
receiver_model.set_target_frequency(receiver_model.target_frequency());
};
field_rx_correction.on_edit = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(field_rx_correction.value());
new_view->on_changed = [this](rf::Frequency f) {
field_rx_correction.set_value(f);
};
};
button_freq_tx_correction.set_text(to_string_short_freq(pmem::config_freq_tx_correction()) + "MHz (Tx)");
button_freq_tx_correction.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(pmem::config_converter_freq());
new_view->on_changed = [this, &button](rf::Frequency f) {
if (f >= MAX_FREQ_CORRECTION)
f = MAX_FREQ_CORRECTION;
pmem::set_config_freq_tx_correction(f);
// Retune to take converter change in account
receiver_model.set_target_frequency(receiver_model.target_frequency());
button_freq_tx_correction.set_text("<" + to_string_short_freq(f) + " MHz>");
field_tx_correction.set_step(100'000);
field_tx_correction.set_value(pmem::config_freq_tx_correction());
field_tx_correction.on_change = [this](rf::Frequency f) {
pmem::set_config_freq_tx_correction(f);
// Retune to take converter change in account. NB: receiver_model.
receiver_model.set_target_frequency(receiver_model.target_frequency());
};
field_tx_correction.on_edit = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(field_tx_correction.value());
new_view->on_changed = [this](rf::Frequency f) {
field_tx_correction.set_value(f);
};
};
@@ -485,49 +500,52 @@ void SetFrequencyCorrectionView::focus() {
/* SetPersistentMemoryView *******************************/
SetPersistentMemoryView::SetPersistentMemoryView(NavigationView& nav) {
add_children({&text_pmem_about,
&text_pmem_informations,
&text_pmem_status,
&check_use_sdcard_for_pmem,
&button_save_mem_to_file,
&button_load_mem_from_file,
&button_load_mem_defaults,
&button_return});
add_children({
&labels,
&text_pmem_status,
&check_use_sdcard_for_pmem,
&button_save_mem_to_file,
&button_load_mem_from_file,
&button_load_mem_defaults,
&button_return,
});
text_pmem_status.set_style(&Styles::yellow);
check_use_sdcard_for_pmem.set_value(pmem::should_use_sdcard_for_pmem());
check_use_sdcard_for_pmem.on_select = [this](Checkbox&, bool v) {
File pmem_flag_file_handle;
if (v) {
if (fs::file_exists(PMEM_FILEFLAG)) {
text_pmem_status.set("pmem flag already present");
text_pmem_status.set("P.Mem flag file present.");
} else {
auto error = pmem_flag_file_handle.create(PMEM_FILEFLAG);
if (error)
text_pmem_status.set("!err. creating pmem flagfile!");
text_pmem_status.set("Error creating P.Mem File!");
else
text_pmem_status.set("pmem flag file created");
text_pmem_status.set("P.Mem flag file created.");
}
} else {
auto result = delete_file(PMEM_FILEFLAG);
if (result.code() != FR_OK)
text_pmem_status.set("!err. deleting pmem flagfile!");
text_pmem_status.set("Error deleting P.Mem flag!");
else
text_pmem_status.set("pmem flag file deleted");
text_pmem_status.set("P.Mem flag file deleted.");
}
};
button_save_mem_to_file.on_select = [&nav, this](Button&) {
if (!pmem::save_persistent_settings_to_file())
text_pmem_status.set("!problem saving settings!");
text_pmem_status.set("Error saving settings!");
else
text_pmem_status.set("settings saved");
text_pmem_status.set("Settings saved.");
};
button_load_mem_from_file.on_select = [&nav, this](Button&) {
if (!pmem::load_persistent_settings_from_file()) {
text_pmem_status.set("!problem loading settings!");
text_pmem_status.set("Error loading settings!");
} else {
text_pmem_status.set("settings loaded");
text_pmem_status.set("Settings loaded.");
// Refresh status bar with icon up or down
send_system_refresh();
}
@@ -536,7 +554,7 @@ SetPersistentMemoryView::SetPersistentMemoryView(NavigationView& nav) {
button_load_mem_defaults.on_select = [&nav, this](Button&) {
nav.push<ModalMessageView>(
"Warning!",
"This will reset the p.mem\nand set the default settings",
"This will reset the P.Mem\nto default settings.",
YESNO,
[this](bool choice) {
if (choice) {
@@ -582,9 +600,12 @@ void SetAudioView::focus() {
/* SetQRCodeView *****************************************/
SetQRCodeView::SetQRCodeView(NavigationView& nav) {
add_children({&checkbox_bigger_qr,
&button_save,
&button_cancel});
add_children({
&labels,
&checkbox_bigger_qr,
&button_save,
&button_cancel,
});
checkbox_bigger_qr.set_value(pmem::show_bigger_qr_code());
@@ -633,17 +654,17 @@ SettingsMenuView::SettingsMenuView(NavigationView& nav) {
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
{"App Settings", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetAppSettingsView>(); }},
{"Audio", ui::Color::dark_cyan(), &bitmap_icon_speaker, [&nav]() { nav.push<SetAudioView>(); }},
{"Calibration", ui::Color::dark_cyan(), &bitmap_icon_options_touch, [&nav]() { nav.push<TouchCalibrationView>(); }},
{"Converter", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetConverterSettingsView>(); }},
{"Date/Time", ui::Color::dark_cyan(), &bitmap_icon_options_datetime, [&nav]() { nav.push<SetDateTimeView>(); }},
{"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetEncoderDialView>(); }},
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetFrequencyCorrectionView>(); }},
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<SetPersistentMemoryView>(); }},
{"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push<SetQRCodeView>(); }},
{"Radio", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetRadioView>(); }},
{"User Interface", ui::Color::dark_cyan(), &bitmap_icon_options_ui, [&nav]() { nav.push<SetUIView>(); }},
{"Date/Time", ui::Color::dark_cyan(), &bitmap_icon_options_datetime, [&nav]() { nav.push<SetDateTimeView>(); }},
{"Calibration", ui::Color::dark_cyan(), &bitmap_icon_options_touch, [&nav]() { nav.push<TouchCalibrationView>(); }},
{"App Settings", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetAppSettingsView>(); }},
{"Converter", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetConverterSettingsView>(); }},
{"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
}
+113 -87
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -26,6 +27,7 @@
#include "ui_widget.hpp"
#include "ui_menu.hpp"
#include "ui_receiver.hpp"
#include "ui_navigation.hpp"
#include "bitmap.hpp"
#include "ff.h"
@@ -35,8 +37,6 @@
namespace ui {
#define MAX_FREQ_CORRECTION INT32_MAX
struct SetDateTimeModel {
uint16_t year;
uint8_t month;
@@ -56,25 +56,28 @@ class SetDateTimeView : public View {
private:
Labels labels{
{{6 * 8, 7 * 16}, "YYYY-MM-DD HH:MM:SS", Color::grey()},
{{10 * 8, 9 * 16}, "- - : :", Color::light_grey()}};
{{1 * 8, 1 * 16}, "Adjust the RTC clock date &", Color::light_grey()},
{{1 * 8, 2 * 16}, "time. If clock resets after", Color::light_grey()},
{{1 * 8, 3 * 16}, "reboot, coin batt. is dead. ", Color::light_grey()},
{{5 * 8, 8 * 16 - 2}, "YYYY-MM-DD HH:MM:SS", Color::grey()},
{{9 * 8, 9 * 16}, "- - : :", Color::light_grey()}};
NumberField field_year{
{6 * 8, 9 * 16},
{5 * 8, 9 * 16},
4,
{2015, 2099},
1,
'0',
};
NumberField field_month{
{11 * 8, 9 * 16},
{10 * 8, 9 * 16},
2,
{1, 12},
1,
'0',
};
NumberField field_day{
{14 * 8, 9 * 16},
{13 * 8, 9 * 16},
2,
{1, 31},
1,
@@ -82,21 +85,21 @@ class SetDateTimeView : public View {
};
NumberField field_hour{
{17 * 8, 9 * 16},
{16 * 8, 9 * 16},
2,
{0, 23},
1,
'0',
};
NumberField field_minute{
{20 * 8, 9 * 16},
{19 * 8, 9 * 16},
2,
{0, 59},
1,
'0',
};
NumberField field_second{
{23 * 8, 9 * 16},
{22 * 8, 9 * 16},
2,
{0, 59},
1,
@@ -131,22 +134,30 @@ class SetRadioView : public View {
uint8_t freq_step_khz = 3;
Text label_source{
{0, 1 * 16, 17 * 8, 16},
{1 * 8, 1 * 16, 17 * 8, 16},
"Reference Source:"};
Text value_source{
{(240 - 11 * 8), 1 * 16, 11 * 8, 16},
"---"};
{18 * 8, 1 * 16, 11 * 8, 16},
""};
Text value_source_frequency{
{(240 - 11 * 8), 2 * 16, 11 * 8, 16},
"---"};
{18 * 8, 2 * 16, 11 * 8, 16},
""};
Labels labels_correction{
{{2 * 8, 3 * 16}, "Frequency correction:", Color::light_grey()},
{{6 * 8, 4 * 16}, "PPM", Color::light_grey()},
};
NumberField field_ppm{
{2 * 8, 4 * 16},
3,
{-50, 50},
1,
'0',
};
Checkbox check_clkout{
{18, (6 * 16 - 4)},
13,
@@ -167,23 +178,15 @@ class SetRadioView : public View {
"| "};
Labels labels_bias{
{{24, 8 * 16}, "CAUTION: Ensure that all", Color::red()},
{{28, 9 * 16}, "devices attached to the", Color::red()},
{{8, 10 * 16}, "antenna connector can accept", Color::red()},
{{68, 11 * 16}, "a DC voltage!", Color::red()}};
NumberField field_ppm{
{2 * 8, 4 * 16},
3,
{-50, 50},
1,
'0',
};
{{4 * 8 + 4, 8 * 16}, "CAUTION: Ensure that all", Color::red()},
{{5 * 8 + 0, 9 * 16}, "devices attached to the", Color::red()},
{{6 * 8 + 0, 10 * 16}, "antenna connector can", Color::red()},
{{6 * 8 + 4, 11 * 16}, "accept a DC voltage!", Color::red()}};
Checkbox check_bias{
{18, 12 * 16},
5,
"Turn on bias voltage"};
"Enable DC bias voltage"};
Checkbox disable_external_tcxo{
{18, 14 * 16},
@@ -312,19 +315,25 @@ class SetAppSettingsView : public View {
SetAppSettingsView(NavigationView& nav);
void focus() override;
std::string title() const override { return "AppSettings"; };
std::string title() const override { return "App Settings"; };
private:
Labels labels{
{{1 * 8, 1 * 16}, "App settings are saved to", Color::light_grey()},
{{1 * 8, 2 * 16}, "the SD card in /SETTINGS.", Color::light_grey()},
{{1 * 8, 3 * 16}, "Radio settings may also be", Color::light_grey()},
{{1 * 8, 4 * 16}, "loaded or saved per app.", Color::light_grey()},
};
Checkbox checkbox_load_app_settings{
{3 * 8, 2 * 16},
{3 * 8, 6 * 16},
25,
"Load app settings"};
"Load radio settings"};
Checkbox checkbox_save_app_settings{
{3 * 8, 4 * 16},
{3 * 8, 8 * 16},
25,
"Save app settings"};
"Save radio settings"};
Button button_save{
{2 * 8, 16 * 16, 12 * 8, 32},
@@ -345,28 +354,34 @@ class SetConverterSettingsView : public View {
std::string title() const override { return "Converter"; };
private:
Labels labels{
{{1 * 8, 1 * 16}, "Options for working with", Color::light_grey()},
{{1 * 8, 2 * 16}, "up/down converter hardware", Color::light_grey()},
{{1 * 8, 3 * 16}, "like a Ham It Up.", Color::light_grey()},
{{2 * 8, 9 * 16 - 2}, "Conversion frequency:", Color::light_grey()},
{{18 * 8, 10 * 16}, "MHz", Color::light_grey()},
};
Checkbox check_show_converter{
{18, 4 * 16},
{2 * 8, 5 * 16},
19,
"show/hide converter"};
"Show converter icon"};
Checkbox check_converter{
{18, 6 * 16},
7,
"enable/disable converter"};
{2 * 8, 7 * 16},
16,
"Enable converter"};
OptionsField converter_mode{
{18, 8 * 16 + 4},
0,
OptionsField opt_converter_mode{
{5 * 8, 10 * 16},
3,
{
{" + ", 0}, // up converter
{" - ", 1} // down converter
{" - ", 1}, // down converter
}};
Button button_converter_freq{
{18 + 4 * 8, 8 * 16, 16 * 8, 24},
"",
};
FrequencyField field_converter_freq{
{8 * 8, 10 * 16}};
Button button_return{
{16 * 8, 16 * 16, 12 * 8, 32},
@@ -380,33 +395,36 @@ class SetFrequencyCorrectionView : public View {
void focus() override;
std::string title() const override { return "FreqCorrect"; };
std::string title() const override { return "Freq Correct"; };
private:
Text text_freqCorrection_about{
{0, 2 * 16, 240, 16},
"Set Frequency correction:"};
Labels labels{
{{1 * 8, 1 * 16}, "Frequency correction allows", Color::light_grey()},
{{1 * 8, 2 * 16}, "RX and TX frequencies to be", Color::light_grey()},
{{1 * 8, 3 * 16}, "adjusted for all apps.", Color::light_grey()},
{{2 * 8, 6 * 16}, "RX Adjustment Frequency", Color::light_grey()},
{{18 * 8, 7 * 16}, "MHz", Color::light_grey()},
{{2 * 8, 9 * 16}, "TX Adjustment Frequency", Color::light_grey()},
{{18 * 8, 10 * 16}, "MHz", Color::light_grey()},
};
OptionsField frequency_rx_correction_mode{
{18, 5 * 16 + 4},
0,
OptionsField opt_rx_correction_mode{
{5 * 8, 7 * 16},
3,
{{" + ", 0},
{" - ", 1}}};
OptionsField frequency_tx_correction_mode{
{18, 9 * 16 + 4},
0,
FrequencyField field_rx_correction{
{8 * 8, 7 * 16}};
OptionsField opt_tx_correction_mode{
{5 * 8, 10 * 16},
3,
{{" + ", 0},
{" - ", 1}}};
Button button_freq_rx_correction{
{18 + 4 * 8, 5 * 16, 20 * 8, 24},
"",
};
Button button_freq_tx_correction{
{18 + 4 * 8, 9 * 16, 20 * 8, 24},
"",
};
FrequencyField field_tx_correction{
{8 * 8, 10 * 16}};
Button button_return{
{16 * 8, 16 * 16, 12 * 8, 32},
@@ -424,11 +442,14 @@ class SetAudioView : public View {
private:
Labels labels{
{{2 * 8, 3 * 16}, "Tone key mix: %", Color::light_grey()},
{{1 * 8, 1 * 16}, "Controls the volume of the", Color::light_grey()},
{{1 * 8, 2 * 16}, "tone when transmitting in", Color::light_grey()},
{{1 * 8, 3 * 16}, "Soundboard or Mic apps.", Color::light_grey()},
{{2 * 8, 5 * 16}, "Tone key mix: %", Color::light_grey()},
};
NumberField field_tone_mix{
{16 * 8, 3 * 16},
{16 * 8, 5 * 16},
2,
{10, 99},
1,
@@ -453,8 +474,13 @@ class SetQRCodeView : public View {
std::string title() const override { return "QR Code"; };
private:
Labels labels{
{{1 * 8, 1 * 16}, "Change the size of the QR", Color::light_grey()},
{{1 * 8, 2 * 16}, "code shown in Radiosonde.", Color::light_grey()},
};
Checkbox checkbox_bigger_qr{
{3 * 8, 9 * 16},
{3 * 8, 4 * 16},
20,
"Show large QR code"};
@@ -480,11 +506,13 @@ class SetEncoderDialView : public View {
private:
Labels labels{
{{2 * 8, 3 * 16}, "Dial sensitivity:", Color::light_grey()},
{{1 * 8, 1 * 16}, "Adjusts how many steps to", Color::light_grey()},
{{1 * 8, 2 * 16}, "change the encoder value.", Color::light_grey()},
{{2 * 8, 4 * 16}, "Dial sensitivity:", Color::light_grey()},
};
OptionsField field_encoder_dial_sensitivity{
{20 * 8, 3 * 16},
{20 * 8, 4 * 16},
6,
{{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_NORMAL},
@@ -509,34 +537,32 @@ class SetPersistentMemoryView : public View {
std::string title() const override { return "P.Mem Mgmt"; };
private:
Text text_pmem_about{
{0, 1 * 16, 240, 16},
"Persistent Memory from/to SD"};
Text text_pmem_informations{
{0, 2 * 16, 240, 16},
"use: when no/dead coin bat."};
Labels labels{
{{1 * 8, 1 * 16}, "Save persistent memory on SD", Color::light_grey()},
{{1 * 8, 2 * 16}, "card. Needed when device has", Color::light_grey()},
{{1 * 8, 3 * 16}, "dead/missing coin battery.", Color::light_grey()},
};
Text text_pmem_status{
{0, 3 * 16, 240, 16},
{1 * 8, 4 * 16 + 8, 28 * 8, 16},
""};
Checkbox check_use_sdcard_for_pmem{
{18, 6 * 16},
19,
"use sdcard for p.mem"};
{2 * 8, 6 * 16},
21,
"Use SD card for P.Mem"};
Button button_save_mem_to_file{
{0, 8 * 16, 240, 32},
"save p.mem to sdcard"};
{1 * 8, 8 * 16, 28 * 8, 2 * 16},
"Save P.Mem to SD card"};
Button button_load_mem_from_file{
{0, 10 * 16 + 4, 240, 32},
"load p.mem from sdcard"};
{1 * 8, 10 * 16 + 2, 28 * 8, 2 * 16},
"Load P.Mem from SD Card"};
Button button_load_mem_defaults{
{0, 12 * 16 + 8, 240, 32},
"! reset p.mem, load defaults !"};
{1 * 8, 12 * 16 + 4, 28 * 8, 2 * 16},
"Reset P.Mem to defaults"};
Button button_return{
{16 * 8, 16 * 16, 12 * 8, 32},
+3 -3
View File
@@ -50,7 +50,7 @@ void SigGenView::update_config() {
}
void SigGenView::update_tone() {
baseband::set_siggen_tone(symfield_tone.value_dec_u32());
baseband::set_siggen_tone(symfield_tone.to_integer());
}
void SigGenView::start_tx() {
@@ -97,8 +97,8 @@ SigGenView::SigGenView(
field_stop.set_value(1);
symfield_tone.set_sym(1, 1); // Default: 1000 Hz
symfield_tone.on_change = [this]() {
symfield_tone.set_value(1000); // Default: 1000 Hz
symfield_tone.on_change = [this](SymField&) {
if (auto_update)
update_tone();
};
+2 -3
View File
@@ -92,9 +92,8 @@ class SigGenView : public View {
""};
SymField symfield_tone{
{13 * 8, 7 * 8},
5,
SymField::SYMFIELD_DEC};
{12 * 8, 7 * 8},
5};
Button button_update{
{5 * 8, 10 * 8, 8 * 8, 3 * 8},
+1 -1
View File
@@ -66,7 +66,7 @@ SondeView::SondeView(NavigationView& nav)
&button_see_qr,
&button_see_map});
if (!settings_.loaded())
if (!settings_.radio_loaded())
field_frequency.set_value(initial_target_frequency);
field_frequency.set_step(500); // euquiq: was 10000, but we are using this for fine-tunning
@@ -40,17 +40,13 @@ SpectrumInputImageView::SpectrumInputImageView(NavigationView& nav) {
auto open_view = nav.push<FileLoadView>(".bmp");
constexpr auto data_directory = u"SPECTRUM";
if (std::filesystem::is_directory(data_directory) == false) {
if (make_new_directory(data_directory).ok())
open_view->push_dir(data_directory);
} else
open_view->push_dir(data_directory);
ensure_directory(data_directory);
open_view->push_dir(data_directory);
open_view->on_changed = [this](std::filesystem::path new_file_path) {
this->file = new_file_path.string();
painted = false;
this->set_dirty();
this->on_input_avaliable();
};
};
+1 -1
View File
@@ -35,7 +35,7 @@ namespace ui {
void SSTVTXView::focus() {
if (file_error)
nav_.display_modal("No files", "No valid bitmaps\nin /sstv directory.", ABORT, nullptr);
nav_.display_modal("No files", "No valid bitmaps\nin /sstv directory.", ABORT);
else
options_bitmaps.focus();
}
+11 -26
View File
@@ -29,14 +29,6 @@
using namespace portapack;
namespace fs = std::filesystem;
namespace {
/*void log(const std::string& msg) {
LogFile log{};
log.append("LOGS/Notepad.txt");
log.write_entry(msg);
}*/
} // namespace
namespace ui {
/* TextViewer *******************************************************/
@@ -413,16 +405,9 @@ TextEditorView::TextEditorView(NavigationView& nav)
};
menu.on_open() = [this]() {
/*show_save_prompt([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]() {
@@ -530,16 +515,16 @@ void TextEditorView::hide_menu(bool hidden) {
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);
void TextEditorView::show_file_picker() {
auto open_view = nav_.push<FileLoadView>("");
open_view->on_changed = [this](std::filesystem::path path) {
// Can't update the UI focus while the FileLoadView is still up.
// Do this on a continuation instead of in on_changed.
nav_.set_on_pop([this, p = std::move(path)]() {
open_file(p);
hide_menu();
};
}
});
};
}
void TextEditorView::show_edit_line() {
+5 -8
View File
@@ -19,10 +19,6 @@
* Boston, MA 02110-1301, USA.
*/
/* TODO:
* - Busy indicator while reading files.
*/
#ifndef __UI_TEXT_EDITOR_H__
#define __UI_TEXT_EDITOR_H__
@@ -238,7 +234,7 @@ class TextEditorView : public View {
void refresh_ui();
void update_position();
void hide_menu(bool hidden = true);
void show_file_picker(bool immediate = true);
void show_file_picker();
void show_edit_line();
void show_save_prompt(std::function<void()> continuation);
@@ -252,8 +248,8 @@ class TextEditorView : public View {
bool has_temp_file_{false};
TextViewer viewer{
/* 272 = 320 - 16 (top bar) - 32 (bottom controls) */
{0, 0, 240, 272}};
/* 272 = screen_height - 16 (top bar) - 32 (bottom controls) */
{0, 0, screen_width, 272}};
TextEditorMenu menu{};
@@ -261,7 +257,8 @@ class TextEditorView : public View {
{26 * 8, 34 * 8, 4 * 8, 4 * 8},
{},
&bitmap_icon_controls,
Color::dark_grey()};
Color::dark_grey(),
/*vcenter*/ true};
Text text_position{
{0 * 8, 34 * 8, 26 * 8, 2 * 8},
+14 -11
View File
@@ -138,17 +138,20 @@ ViewWavView::ViewWavView(
reset_controls();
button_open.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".WAV");
open_view->on_changed = [this](std::filesystem::path file_path) {
if (!wav_reader->open(file_path)) {
nav_.display_modal("Error", "Couldn't open file.");
return;
}
if ((wav_reader->channels() != 1) || (wav_reader->bits_per_sample() != 16)) {
nav_.display_modal("Error", "Wrong format.\nWav viewer only accepts\n16-bit mono files.");
return;
}
load_wav(file_path);
field_pos_seconds.focus();
open_view->on_changed = [this, &nav](std::filesystem::path file_path) {
// Can't show new dialogs in an on_changed handler, so use continuation.
nav.set_on_pop([this, &nav, file_path]() {
if (!wav_reader->open(file_path)) {
nav_.display_modal("Error", "Couldn't open file.");
return;
}
if ((wav_reader->channels() != 1) || (wav_reader->bits_per_sample() != 16)) {
nav_.display_modal("Error", "Wrong format.\nWav viewer only accepts\n16-bit mono files.");
return;
}
load_wav(file_path);
field_pos_seconds.focus();
});
};
};
+34 -1
View File
@@ -189,7 +189,16 @@ void kill_afsk() {
send_message(&message);
}
void set_audiotx_config(const uint32_t divider, const float deviation_hz, const float audio_gain, uint8_t audio_shift_bits_s16, const uint32_t tone_key_delta, const bool am_enabled, const bool dsb_enabled, const bool usb_enabled, const bool lsb_enabled) {
void set_audiotx_config(
const uint32_t divider,
const float deviation_hz,
const float audio_gain,
uint8_t audio_shift_bits_s16,
const uint32_t tone_key_delta,
const bool am_enabled,
const bool dsb_enabled,
const bool usb_enabled,
const bool lsb_enabled) {
const AudioTXConfigMessage message{
divider,
deviation_hz,
@@ -320,6 +329,30 @@ void run_image(const spi_flash::image_tag_t image_tag) {
}
}
void run_prepared_image(const uint32_t m4_code) {
if (baseband_image_running) {
chDbgPanic("BBRunning");
}
creg::m4txevent::clear();
shared_memory.clear_baseband_ready();
m4_init_prepared(m4_code, false);
baseband_image_running = true;
creg::m4txevent::enable();
if constexpr (enforce_core_sync) {
// Wait up to 3 seconds for baseband to start handling events.
auto count = 3'000u;
while (!shared_memory.baseband_ready && --count)
chThdSleepMilliseconds(1);
if (count == 0)
chDbgPanic("Baseband Sync Fail");
}
}
void shutdown() {
if (!baseband_image_running) {
return;
+1
View File
@@ -89,6 +89,7 @@ void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bo
void request_beep();
void run_image(const portapack::spi_flash::image_tag_t image_tag);
void run_prepared_image(const uint32_t m4_code);
void shutdown();
void spectrum_streaming_start();
+117
View File
@@ -0,0 +1,117 @@
/*
* 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 __BINDER_H__
#define __BINDER_H__
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_widget.hpp"
namespace ui {
/* Default no-op binding callback. */
struct NoOp {
NoOp() {}
template <typename T>
void operator()(T) const {}
};
/* The gist of the bind functions is to bind a control
* to a value.
* bind(control, value)
*
* An optional callback can be specified which will be called
* when the control's value is changed.
* bind(control, value, [](auto new_val) { ... });
*
* Some controls need a reference to the NavigationView so
* edit dialogs (like text_prompt) can be launched.
* bind(control, value, nav, [](auto new_val) { ... });
*
* Be careful with lifetime of captured objects. Most things
* are captured by reference so the caller will need to ensure
* adequate lifetime of the referenced instances.
*/
template <typename T, typename Fn = NoOp>
void bind(Checkbox& check, T& value, Fn fn = Fn{}) {
check.set_value(value);
check.on_select = [&value, fn](Checkbox&, bool b) {
value = b;
fn(value);
};
}
template <typename T, typename Fn = NoOp>
void bind(NumberField& field, T& value, Fn fn = Fn{}) {
field.set_value(value);
field.on_change = [&value, fn](int32_t v) {
value = v;
fn(value);
};
}
template <typename T, typename Fn = NoOp>
void bind(OptionsField& field, T& value, Fn fn = Fn{}) {
field.set_by_value(static_cast<int32_t>(value));
field.on_change = [&value, fn](size_t, auto v) {
value = static_cast<T>(v);
fn(value);
};
}
template <typename T, typename Fn = NoOp>
void bind(FrequencyField& field, T& value, NavigationView& nav, Fn fn = Fn{}) {
field.set_value(value);
field.on_change = [&value, fn](rf::Frequency f) {
value = f;
fn(value);
};
field.on_edit = [&field, &value, &nav]() {
auto freq_view = nav.push<FrequencyKeypadView>(value);
freq_view->on_changed = [&field](rf::Frequency f) {
field.set_value(f);
};
};
}
template <typename T, typename Fn = NoOp>
void bind(TextField& field, T& value, NavigationView& nav, Fn fn = Fn{}) {
field.set_text(value);
field.on_change = [&value, fn](TextField& tf) {
value = tf.get_text();
fn(value);
};
// text_prompt needs a mutable working buffer.
// Capture a new string and make the lambda mutable so it can be modified.
field.on_select = [&nav, buf = std::string{}](TextField& tf) mutable {
buf = tf.get_text();
text_prompt(nav, buf, /*max_length*/ 255,
[&tf](std::string& str) {
tf.set_text(str);
});
};
}
} // namespace ui
#endif /*__BINDER_H__*/
+13
View File
@@ -59,6 +59,19 @@ void m4_init(const spi_flash::image_tag_t image_tag, const memory::region_t to,
chDbgPanic("NoImg");
}
void m4_init_prepared(const uint32_t m4_code, const bool full_reset) {
/* M4 core is assumed to be sleeping with interrupts off, so we can mess
* with its address space and RAM without concern.
*/
LPC_CREG->M4MEMMAP = m4_code;
/* Reset M4 core and optionally all peripherals */
LPC_RGU->RESET_CTRL[0] = (full_reset) ? (1 << 1) // PERIPH_RST
: (1 << 13); // M4_RST
return;
}
void m4_request_shutdown() {
baseband::shutdown();
}
+1
View File
@@ -28,6 +28,7 @@
#include "spi_image.hpp"
void m4_init(const portapack::spi_flash::image_tag_t image_tag, const portapack::memory::region_t to, const bool full_reset);
void m4_init_prepared(const uint32_t m4_code, const bool full_reset);
void m4_request_shutdown();
void m0_halt();
+82
View File
@@ -0,0 +1,82 @@
/*
* 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 "ui.hpp"
#include "ui_afsk_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::afsk_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<AFSKRxView>();
}
} // namespace ui::external_app::afsk_rx
extern "C" {
__attribute__((section(".external_app.app_afsk_rx.application_information"), used)) application_information_t _application_information_afsk_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::afsk_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "AFSK",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0xF8,
0x1F,
0x04,
0x20,
0x02,
0x40,
0xFF,
0xFF,
0xFF,
0xFF,
0xAB,
0xDF,
0xAB,
0xDF,
0xFF,
0xFF,
0xFF,
0xFF,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::RX,
/*.m4_app_tag = portapack::spi_flash::image_tag_afsk_rx */ {'P', 'A', 'F', 'R'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
@@ -32,20 +32,21 @@
using namespace portapack;
using namespace modems;
using namespace ui;
namespace ui::external_app::afsk_rx {
void AFSKLogger::log_raw_data(const std::string& data) {
log_file.write_entry(data);
}
namespace ui {
void AFSKRxView::focus() {
field_frequency.focus();
}
AFSKRxView::AFSKRxView(NavigationView& nav)
: nav_{nav} {
baseband::run_image(portapack::spi_flash::image_tag_afsk_rx);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&rssi,
&channel,
@@ -147,4 +148,4 @@ AFSKRxView::~AFSKRxView() {
baseband::shutdown();
}
} /* namespace ui */
} // namespace ui::external_app::afsk_rx
@@ -33,6 +33,10 @@
#include "log_file.hpp"
#include "utility.hpp"
using namespace ui;
namespace ui::external_app::afsk_rx {
class AFSKLogger {
public:
Optional<File::Error> append(const std::string& filename) {
@@ -45,8 +49,6 @@ class AFSKLogger {
LogFile log_file{};
};
namespace ui {
class AFSKRxView : public View {
public:
AFSKRxView(NavigationView& nav);
@@ -94,15 +96,15 @@ class AFSKRxView : public View {
false};
Text text_debug{
{0 * 8, 12 + 2 * 16, 240, 16},
{0 * 8, 12 + 2 * 16, screen_width, 16},
"DEBUG"};
Button button_modem_setup{
{240 - 12 * 8, 1 * 16, 96, 24},
{screen_width - 12 * 8, 1 * 16, 96, 24},
"Modem setup"};
Console console{
{0, 4 * 16, 240, 240}};
{0, 4 * 16, 240, screen_width}};
void on_data_afsk(const AFSKDataMessage& message);
@@ -116,6 +118,6 @@ class AFSKRxView : public View {
}};
};
} /* namespace ui */
} // namespace ui::external_app::afsk_rx
#endif /*__UI_AFSK_RX_H__*/
+15
View File
@@ -0,0 +1,15 @@
set(EXTCPPSRC
#pacman
external/pacman/main.cpp
external/pacman/ui_pacman.cpp
#afsk_rx
external/afsk_rx/main.cpp
external/afsk_rx/ui_afsk_rx.cpp
)
set(EXTAPPLIST
pacman
afsk_rx
)
+37
View File
@@ -0,0 +1,37 @@
/*
Copyright (C) 2023 Bernd Herzog
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.
*/
MEMORY
{
/* external apps: regions can't overlap so addresses are corrected after build */
ram_external_app_pacman (rwx) : org = 0xEEE90000, len = 40k
ram_external_app_afsk_rx (rwx) : org = 0xEEEA0000, len = 40k
}
SECTIONS
{
.external_app_pacman : ALIGN(16) SUBALIGN(16)
{
KEEP(*(.external_app.app_pacman.application_information));
*(*ui*external_app*pacman*);
} > ram_external_app_pacman
.external_app_afsk_rx : ALIGN(16) SUBALIGN(16)
{
KEEP(*(.external_app.app_afsk_rx.application_information));
*(*ui*external_app*afsk_rx*);
} > ram_external_app_afsk_rx
}
+66
View File
@@ -0,0 +1,66 @@
/******************************************************************************/
/* */
/* PACMAN GAME FOR ARDUINO DUE */
/* */
/******************************************************************************/
/* Copyright (c) 2014 Dr. NCX (mirracle.mxx@gmail.com) */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL */
/* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED */
/* WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR */
/* BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES */
/* OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, */
/* WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, */
/* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS */
/* SOFTWARE. */
/* */
/* MIT license, all text above must be included in any redistribution. */
// #include "ili9328.h"
typedef uint16_t ushort;
#define C16(_rr, _gg, _bb) ((ushort)(((_rr & 0xF8) << 8) | ((_gg & 0xFC) << 3) | ((_bb & 0xF8) >> 3)))
uint16_t _paletteW[] =
{
C16(0, 0, 0),
C16(255, 0, 0), // 1 red
C16(222, 151, 81), // 2 brown
C16(255, 0, 255), // 3 pink
C16(0, 0, 0),
C16(0, 255, 255), // 5 cyan
C16(71, 84, 255), // 6 mid blue
C16(255, 184, 81), // 7 lt brown
C16(0, 0, 0),
C16(255, 255, 0), // 9 yellow
C16(0, 0, 0),
C16(33, 33, 255), // 11 blue
C16(0, 255, 0), // 12 green
C16(71, 84, 174), // 13 aqua
C16(255, 184, 174), // 14 lt pink
C16(222, 222, 255), // 15 whiteish
};
void drawIndexedmap(uint8_t* indexmap, int16_t x, uint16_t y) {
ui::Painter painter;
byte i = 0;
word color = (word)_paletteW[indexmap[0]];
for (byte tmpY = 0; tmpY < 8; tmpY++) {
byte width = 1;
for (byte tmpX = 0; tmpX < 8; tmpX++) {
word next_color = (word)_paletteW[indexmap[++i]];
if ((color != next_color && width >= 1) || tmpX == 7) {
painter.draw_hline({x + tmpX - width + 1, y + tmpY}, width, ui::Color(color));
color = next_color;
width = 0;
}
width++;
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
/*
* 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 "ui.hpp"
#include "ui_pacman.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::pacman {
void initialize_app(ui::NavigationView& nav) {
nav.push<PacmanView>();
}
} // namespace ui::external_app::pacman
extern "C" {
__attribute__((section(".external_app.app_pacman.application_information"), used)) application_information_t _application_information_pacman = {
/*.memory_location = */ (uint8_t*)0x00000000, // will be filled at compile time
/*.externalAppEntry = */ ui::external_app::pacman::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Pac-Man",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0xC0,
0x07,
0xE0,
0x0F,
0xF0,
0x1F,
0xF8,
0x07,
0xF8,
0x01,
0x78,
0x00,
0xF8,
0x01,
0xF8,
0x07,
0xF0,
0x1F,
0xE0,
0x0F,
0xC0,
0x07,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::UTILITIES,
/*.m4_app_tag = portapack::spi_flash::image_tag_noop */ {'\0', '\0', '\0', '\0'}, // optional
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
#include "ui_pacman.hpp"
#include "irq_controls.hpp"
namespace ui::external_app::pacman {
#pragma GCC diagnostic push
// external code, so ignore warnings
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wreturn-type"
#pragma GCC diagnostic ignored "-Weffc++"
#include "playfield.hpp"
#pragma GCC diagnostic pop
Playfield _game;
PacmanView::PacmanView(NavigationView& nav)
: nav_(nav) {
add_children({&dummy});
}
void PacmanView::focus() {
dummy.focus();
}
void PacmanView::paint(Painter& painter) {
(void)painter;
if (!initialized) {
initialized = true;
_game.Init();
}
auto switches_debounced = get_switches_state().to_ulong();
but_RIGHT = (switches_debounced & 0x01) == 0x01;
but_LEFT = (switches_debounced & 0x02) == 0x02;
but_DOWN = (switches_debounced & 0x04) == 0x04;
but_UP = (switches_debounced & 0x08) == 0x08;
but_A = (switches_debounced & 0x10) == 0x10;
_game.Step();
}
void PacmanView::frame_sync() {
set_dirty();
}
} // namespace ui::external_app::pacman
+59
View File
@@ -0,0 +1,59 @@
/*
* 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.
*/
#ifndef __UI_PACMAN_H__
#define __UI_PACMAN_H__
#include "ui_navigation.hpp"
#include "event_m0.hpp"
#include "message.hpp"
namespace ui::external_app::pacman {
class PacmanView : public View {
public:
PacmanView(NavigationView& nav);
void focus() override;
std::string title() const override { return "Pac-Man"; };
void paint(Painter& painter) override;
void frame_sync();
private:
bool initialized = false;
NavigationView& nav_;
Button dummy{
{240, 0, 0, 0},
""};
MessageHandlerRegistration message_handler_sample{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->frame_sync();
}};
};
} // namespace ui::external_app::pacman
#endif /*__UI_PACMAN_H__*/
+1 -2
View File
@@ -292,8 +292,7 @@ std::filesystem::filesystem_error rename_file(
std::filesystem::filesystem_error copy_file(
const std::filesystem::path& file_path,
const std::filesystem::path& dest_path) {
// 512 seems to be the largest block size FatFS likes.
constexpr size_t buffer_size = 512;
constexpr size_t buffer_size = std::filesystem::max_file_block_size;
uint8_t buffer[buffer_size];
File src;
File dst;
+4
View File
@@ -173,6 +173,9 @@ uint8_t capture_file_sample_size(const path& filename);
using file_status = BYTE;
/* The largest block that can be read/written to a file. */
constexpr uint16_t max_file_block_size = 512;
static_assert(sizeof(path::value_type) == 2, "sizeof(std::filesystem::path::value_type) != 2");
static_assert(sizeof(path::value_type) == sizeof(TCHAR), "FatFs TCHAR size != std::filesystem::path::value_type");
@@ -298,6 +301,7 @@ static_assert(sizeof(FIL::err) == 1, "FatFs FIL::err size not expected.");
#define FR_BAD_SEEK (0x102)
#define FR_UNEXPECTED (0x103)
/* NOTE: sizeof(File) == 556 bytes because of the FIL's buf member. */
class File {
public:
using Size = uint64_t;
+195
View File
@@ -0,0 +1,195 @@
/*
* 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 "iq_trim.hpp"
#include <memory>
#include "string_format.hpp"
namespace fs = std::filesystem;
namespace iq {
/* Trimming helpers based on the sample type (complex8 or complex16). */
template <typename T>
uint32_t power(T value) {
auto real = value.real();
auto imag = value.imag();
return (real * real) + (imag * imag);
}
/* Collects capture file metadata and sample power buckets. */
template <typename T>
Optional<CaptureInfo> profile_capture(
const std::filesystem::path& path,
PowerBuckets& buckets,
uint8_t samples_per_bucket) {
File f;
auto error = f.open(path);
if (error)
return {};
CaptureInfo info{
.file_size = f.size(),
.sample_count = f.size() / sizeof(T),
.sample_size = sizeof(T),
.max_power = 0};
auto profile_samples = buckets.size * samples_per_bucket;
auto sample_interval = info.sample_count / profile_samples;
uint32_t bucket_width = std::max(1ULL, info.sample_count / buckets.size);
uint64_t sample_index = 0;
T value{};
while (true) {
f.seek(sample_index * info.sample_size);
auto result = f.read(&value, info.sample_size);
if (!result) return {}; // Read failed.
if (*result != info.sample_size)
break; // EOF
auto mag_squared = power(value);
if (mag_squared > info.max_power)
info.max_power = mag_squared;
auto bucket_index = sample_index / bucket_width;
buckets.add(bucket_index, mag_squared);
sample_index += sample_interval;
}
return info;
}
Optional<CaptureInfo> profile_capture(
const fs::path& path,
PowerBuckets& buckets,
uint8_t samples_per_bucket) {
auto sample_size = fs::capture_file_sample_size(path);
switch (sample_size) {
case sizeof(complex16_t):
return profile_capture<complex16_t>(path, buckets, samples_per_bucket);
case sizeof(complex8_t):
return profile_capture<complex8_t>(path, buckets, samples_per_bucket);
default:
return {};
};
}
TrimRange compute_trim_range(
CaptureInfo info,
const PowerBuckets& buckets,
uint8_t cutoff_percent) {
bool has_start = false;
uint8_t start_bucket = 0;
uint8_t end_bucket = 0;
uint32_t power_cutoff = cutoff_percent * static_cast<uint64_t>(info.max_power) / 100;
for (size_t i = 0; i < buckets.size; ++i) {
auto power = buckets.p[i].power;
if (power > power_cutoff) {
if (has_start)
end_bucket = i;
else {
start_bucket = i;
end_bucket = i;
has_start = true;
}
}
}
// End should be the first bucket after the last with signal.
// This makes the math downstream simpler. NB: may be > buckets.size.
end_bucket++;
auto samples_per_bucket = info.sample_count / buckets.size;
return {
start_bucket * samples_per_bucket,
end_bucket * samples_per_bucket,
info.sample_size};
}
bool trim_capture_with_range(
const fs::path& path,
TrimRange range,
const std::function<void(uint8_t)>& on_progress) {
constexpr size_t buffer_size = std::filesystem::max_file_block_size;
uint8_t buffer[buffer_size];
auto temp_path = path + u"-tmp";
// end_sample is the first sample to _not_ include.
auto start_byte = range.start_sample * range.sample_size;
auto end_byte = (range.end_sample * range.sample_size);
auto length = end_byte - start_byte;
// 'File' is 556 bytes! Heap alloc to avoid overflowing the stack.
auto src = std::make_unique<File>();
auto dst = std::make_unique<File>();
auto error = src->open(path);
if (error) return false;
error = dst->create(temp_path);
if (error) return false;
src->seek(start_byte);
auto processed = 0UL;
auto next_report = 0UL;
auto report_interval = length / 20UL;
while (true) {
auto result = src->read(buffer, buffer_size);
if (result.is_error()) return false;
auto remaining = length - processed;
auto to_write = std::min(remaining, *result);
result = dst->write(buffer, to_write);
if (result.is_error()) return false;
processed += *result;
if (*result < buffer_size || processed >= length)
break;
if (processed >= next_report) {
on_progress(100 * processed / length);
next_report += report_interval;
}
}
// Close files before renaming/deleting.
src.reset();
dst.reset();
// Delete original and overwrite with temp file.
delete_file(path);
rename_file(temp_path, path);
return true;
}
} // namespace iq
+93
View File
@@ -0,0 +1,93 @@
/*
* 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 __IQ_TRIM_H__
#define __IQ_TRIM_H__
#include "complex.hpp"
#include "file.hpp"
#include "optional.hpp"
#include <functional>
#include <limits>
namespace iq {
/* Information about a capture. */
struct CaptureInfo {
uint64_t file_size;
uint64_t sample_count;
uint8_t sample_size;
uint32_t max_power;
};
/* Holds sample average power by bucket. */
struct PowerBuckets {
struct Bucket {
uint32_t power = 0;
uint8_t count = 0;
};
Bucket* p = nullptr;
const size_t size = 0;
/* Add the power to the bucket average at index. */
void add(size_t index, uint32_t power) {
if (index < size) {
auto& b = p[index];
auto avg = static_cast<uint64_t>(b.power) * b.count;
b.count++;
b.power = (power + avg) / b.count;
}
}
};
/* Data needed to trim a capture by sample range.
* end_sample is the sample *after* the last to keep. */
struct TrimRange {
uint64_t start_sample;
uint64_t end_sample;
uint8_t sample_size;
};
/* Collects capture file metadata and samples power buckets. */
Optional<CaptureInfo> profile_capture(
const std::filesystem::path& path,
PowerBuckets& buckets,
uint8_t samples_per_bucket = 10);
/* Computes the trimming range given profiling info.
* Cutoff percent is a number 1-100. */
TrimRange compute_trim_range(
CaptureInfo info,
const PowerBuckets& buckets,
uint8_t cutoff_percent);
/* Trims the capture file with the specified range. */
bool trim_capture_with_range(
const std::filesystem::path& path,
TrimRange range,
const std::function<void(uint8_t)>& on_progress);
} // namespace iq
#endif /*__IQ_TRIM_H__*/
+46 -19
View File
@@ -21,6 +21,8 @@
#include "string_format.hpp"
using namespace std::literals;
/* This takes a pointer to the end of a buffer
* and fills it backwards towards the front.
* The return value 'q' is a pointer to the start.
@@ -230,29 +232,31 @@ std::string to_string_time_ms(const uint32_t ms) {
return final_str;
}
static void to_string_hex_internal(char* p, const uint64_t n, const int32_t l) {
const uint32_t d = n & 0xf;
p[l] = (d > 9) ? (d + 55) : (d + 48);
if (l > 0) {
to_string_hex_internal(p, n >> 4, l - 1);
}
static char* to_string_hex_internal(char* ptr, uint64_t value, uint8_t length) {
if (length == 0)
return ptr;
*(--ptr) = uint_to_char(value & 0xF, 16);
return to_string_hex_internal(ptr, value >> 4, length - 1);
}
std::string to_string_hex(const uint64_t n, int32_t l) {
char p[32];
std::string to_string_hex(uint64_t value, int32_t length) {
constexpr uint8_t buffer_length = 33;
char buffer[buffer_length];
l = std::min<int32_t>(l, 31);
to_string_hex_internal(p, n, l - 1);
p[l] = 0;
return p;
char* ptr = &buffer[buffer_length - 1];
*ptr = '\0';
length = std::min<uint8_t>(buffer_length - 1, length);
return to_string_hex_internal(ptr, value, length);
}
std::string to_string_hex_array(uint8_t* const array, const int32_t l) {
std::string str_return = "";
uint8_t bytes;
std::string to_string_hex_array(uint8_t* array, int32_t length) {
std::string str_return;
str_return.reserve(length);
for (bytes = 0; bytes < l; bytes++)
str_return += to_string_hex(array[bytes], 2);
for (uint8_t i = 0; i < length; i++)
str_return += to_string_hex(array[i], 2);
return str_return;
}
@@ -307,10 +311,10 @@ std::string to_string_file_size(uint32_t file_size) {
return to_string_dec_uint(file_size) + suffix[suffix_index];
}
std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precision) {
std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision) {
const uint32_t powers_of_ten[5] = {1, 10, 100, 1000, 10000};
std::string string{""};
uint32_t prefix_index = base_nano;
uint32_t prefix_index = base_unit;
double integer_part;
double fractional_part;
@@ -364,3 +368,26 @@ std::string trimr(std::string_view str) {
std::string truncate(std::string_view str, size_t length) {
return std::string{str.length() <= length ? str : str.substr(0, length)};
}
uint8_t char_to_uint(char c, uint8_t radix) {
uint8_t v = 0;
if (c >= '0' && c <= '9')
v = c - '0';
else if (c >= 'A' && c <= 'F')
v = c - 'A' + 10; // A is dec: 10
else if (c >= 'a' && c <= 'f')
v = c - 'a' + 10; // A is dec: 10
return v < radix ? v : 0;
}
char uint_to_char(uint8_t val, uint8_t radix) {
if (val >= radix)
return 0;
if (val < 10)
return '0' + val;
else
return 'A' + val - 10; // A is dec: 10
}
+19 -4
View File
@@ -50,14 +50,19 @@ char* to_string_dec_uint(uint64_t n, StringFormatBuffer& buffer, size_t& length)
std::string to_string_dec_int(int64_t n);
std::string to_string_dec_uint(uint64_t n);
// TODO: Allow l=0 to not fill/justify? Already using this way in ui_spectrum.hpp...
std::string to_string_bin(const uint32_t n, const uint8_t l = 0);
std::string to_string_dec_uint(const uint32_t n, const int32_t l, const char fill = ' ');
std::string to_string_dec_int(const int32_t n, const int32_t l, const char fill = 0);
std::string to_string_decimal(float decimal, int8_t precision);
std::string to_string_hex(const uint64_t n, const int32_t l = 0);
std::string to_string_hex_array(uint8_t* const array, const int32_t l = 0);
std::string to_string_hex(uint64_t n, int32_t length);
std::string to_string_hex_array(uint8_t* array, int32_t length);
/* Helper to select length based on type size. */
template <typename T>
std::string to_string_hex(T n) {
return to_string_hex(n, sizeof(T) * 2); // Two digits/byte.
}
std::string to_string_freq(const uint64_t f);
std::string to_string_short_freq(const uint64_t f);
@@ -71,11 +76,21 @@ std::string to_string_FAT_timestamp(const FATTimestamp& timestamp);
// Gets a human readable file size string.
std::string to_string_file_size(uint32_t file_size);
std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precision);
/* Scales 'n' to be a value less than 1000. 'base_unit' is the index of the unit from
* 'unit_prefix' that 'n' is in initially. 3 is the index of the '1s' unit. */
std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision);
double get_decimals(double num, int16_t mult, bool round = false);
std::string trim(std::string_view str); // Remove whitespace at ends.
std::string trimr(std::string_view str); // Remove trailing spaces
std::string truncate(std::string_view, size_t length);
/* Gets the int value for a character given the radix.
* e.g. '5' => 5, 'D' => 13. Out of bounds => 0. */
uint8_t char_to_uint(char c, uint8_t radix = 10);
/* Gets the int value for a character given the radix.
* e.g. 5 => '5', 13 => 'D'. Out of bounds => '\0'. */
char uint_to_char(uint8_t val, uint8_t radix = 10);
#endif /*__STRING_FORMAT_H__*/
+2 -1
View File
@@ -82,7 +82,8 @@ class AlphanumView : public TextEntryView {
{192, 214, screen_width / 5, 38},
{},
&bitmap_icon_shift,
Color::dark_grey()};
Color::dark_grey(),
/*vcenter*/ true};
Labels labels{
{{1 * 8, 33 * 8}, "Raw:", Color::light_grey()},
+6
View File
@@ -116,6 +116,12 @@ void BtnGridView::add_items(std::initializer_list<GridItem> new_items) {
update_items();
}
void BtnGridView::add_item(GridItem new_item) {
menu_items.push_back(new_item);
update_items();
}
void BtnGridView::update_items() {
size_t i = 0;
+1
View File
@@ -54,6 +54,7 @@ class BtnGridView : public View {
~BtnGridView();
void add_items(std::initializer_list<GridItem> new_items);
void add_item(GridItem new_item);
void set_max_rows(int rows);
int rows();
void clear();
+1 -1
View File
@@ -413,7 +413,7 @@ void GeoMapView::focus() {
geopos.focus();
if (!map_opened)
nav_.display_modal("No map", "No world_map.bin file in\n/ADSB/ directory", ABORT, nullptr);
nav_.display_modal("No map", "No world_map.bin file in\n/ADSB/ directory", ABORT);
}
void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t altitude) {
+1 -2
View File
@@ -273,9 +273,8 @@ FrequencyKeypadView::FrequencyKeypadView(
};
button_close.on_select = [this, &nav](Button&) {
if (on_changed) {
if (on_changed)
on_changed(this->value());
}
nav.pop();
};
-8
View File
@@ -41,20 +41,12 @@ void text_prompt(
uint32_t cursor_pos,
size_t max_length,
std::function<void(std::string&)> on_done) {
// if (persistent_memory::ui_config_textentry() == 0) {
auto te_view = nav.push<AlphanumView>(str, max_length);
te_view->set_cursor(cursor_pos);
te_view->on_changed = [on_done](std::string& value) {
if (on_done)
on_done(value);
};
/*} else {
auto te_view = nav.push<HandWriteView>(str, max_length);
te_view->on_changed = [on_done](std::string * value) {
if (on_done)
on_done(value);
};
}*/
}
/* TextEntryView ***********************************************************/
+68 -82
View File
@@ -32,8 +32,25 @@
using namespace portapack;
#define POWER_THRESHOLD_HIGH 47
#define POWER_THRESHOLD_MED 38
#define POWER_THRESHOLD_LOW 17
namespace ui {
/* Gets a style indicating total TX gain level. */
static const Style* get_style_for_gain(uint8_t tot_gain) {
if (tot_gain > POWER_THRESHOLD_HIGH) return &Styles::red;
if (tot_gain > POWER_THRESHOLD_MED)
return &Styles::orange;
if (tot_gain > POWER_THRESHOLD_LOW)
return &Styles::yellow;
return nullptr; // Uses default.
}
/* TransmitterView *******************************************************/
void TransmitterView::paint(Painter& painter) {
@@ -73,21 +90,13 @@ void TransmitterView::on_tx_amp_changed(bool rf_amp) {
}
void TransmitterView::update_gainlevel_styles() {
const Style* new_style_ptr = NULL;
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
auto style = get_style_for_gain(tot_gain);
if (tot_gain > POWER_THRESHOLD_HIGH) {
new_style_ptr = &style_power_high;
} else if (tot_gain > POWER_THRESHOLD_MED) {
new_style_ptr = &style_power_med;
} else if (tot_gain > POWER_THRESHOLD_LOW) {
new_style_ptr = &style_power_low;
}
field_gain.set_style(new_style_ptr);
text_gain.set_style(new_style_ptr);
field_amp.set_style(new_style_ptr);
text_amp.set_style(new_style_ptr);
field_gain.set_style(style);
text_gain.set_style(style);
field_amp.set_style(style);
text_amp.set_style(style);
}
void TransmitterView::set_transmitting(const bool transmitting) {
@@ -186,89 +195,66 @@ TransmitterView::TransmitterView(
}
TransmitterView::~TransmitterView() {
// TODO: Does this make sense? Seems wrong to have
// what's basically a widget control the radio.
audio::output::stop();
transmitter_model.disable();
baseband::shutdown();
}
/* TransmitterView2 *******************************************************/
/* Simpler transmitter view that only renders TX Gain and Amp.
* There are two modes, NORMAL_UI and SHORT_UI. SHORT_UI abbreviates control labels. */
void TransmitterView2::paint(Painter&) {
// All widgets paint themselves. Don't let base paint.
}
void TransmitterView2::on_tx_gain_changed(int32_t tx_gain) {
transmitter_model.set_tx_gain(tx_gain);
update_gainlevel_styles();
}
TransmitterView2::TransmitterView2(Point pos, bool short_ui) {
// There are two modes, short and !short
// Short: "G:XX A:YY"
// !Short: "Gain:XX Amp:YY"
Dim width = short_ui ? (9 * 8) : (14 * 8);
set_parent_rect({pos, {width, 16}});
add_children({
&text_labels,
&field_gain,
&field_amp,
});
// Set up controls depending UI mode.
text_labels.set(short_ui ? "G: A:" : "Gain: Amp:");
text_labels.set_parent_rect(
short_ui
? Rect{0 * 8, 0 * 16, 7 * 8, 1 * 16}
: Rect{0 * 8, 0 * 16, 12 * 8, 1 * 16});
field_gain.set_parent_rect(
short_ui
? Rect{2 * 8, 0 * 16, 2 * 8, 1 * 16}
: Rect{5 * 8, 0 * 16, 2 * 8, 1 * 16});
field_amp.set_parent_rect(
short_ui
? Rect{7 * 8, 0 * 16, 2 * 8, 1 * 16}
: Rect{12 * 8, 0 * 16, 2 * 8, 1 * 16});
field_gain.set_value(transmitter_model.tx_gain());
field_gain.on_change = [this](uint32_t tx_gain) {
transmitter_model.set_tx_gain(tx_gain);
update_gainlevel_styles();
};
field_amp.set_value(transmitter_model.rf_amp() ? 14 : 0);
field_amp.on_change = [this](uint32_t rf_amp) {
transmitter_model.set_rf_amp(rf_amp > 0);
update_gainlevel_styles();
};
void TransmitterView2::on_tx_amp_changed(bool rf_amp) {
transmitter_model.set_rf_amp(rf_amp);
update_gainlevel_styles();
}
void TransmitterView2::update_gainlevel_styles() {
const Style* new_style_ptr = NULL;
int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0);
auto style = get_style_for_gain(tot_gain);
if (tot_gain > POWER_THRESHOLD_HIGH) {
new_style_ptr = &style_power_high;
} else if (tot_gain > POWER_THRESHOLD_MED) {
new_style_ptr = &style_power_med;
} else if (tot_gain > POWER_THRESHOLD_LOW) {
new_style_ptr = &style_power_low;
}
field_gain.set_style(new_style_ptr);
text_gain_amp.set_style(new_style_ptr);
field_amp.set_style(new_style_ptr);
field_gain_short_UI.set_style(new_style_ptr);
text_gain_amp_short_UI.set_style(new_style_ptr);
field_amp_short_UI.set_style(new_style_ptr);
}
void TransmitterView2::on_show() {
field_gain.set_value(transmitter_model.tx_gain());
field_amp.set_value(transmitter_model.rf_amp() ? 14 : 0);
field_gain_short_UI.set_value(transmitter_model.tx_gain());
field_amp_short_UI.set_value(transmitter_model.rf_amp() ? 14 : 0);
update_gainlevel_styles();
}
TransmitterView2::TransmitterView2(const Coord x, const Coord y, bool short_UI) {
set_parent_rect({x, y, 20 * 8, 1 * 8});
add_children({
&(short_UI ? text_gain_amp_short_UI : text_gain_amp),
&(short_UI ? field_gain_short_UI : field_gain),
&(short_UI ? field_amp_short_UI : field_amp),
});
if (short_UI) {
field_gain_short_UI.on_change = [this](uint32_t tx_gain) {
on_tx_gain_changed(tx_gain);
};
field_amp_short_UI.on_change = [this](uint32_t rf_amp) {
on_tx_amp_changed((bool)rf_amp);
};
} else {
field_gain.on_change = [this](uint32_t tx_gain) {
on_tx_gain_changed(tx_gain);
};
field_amp.on_change = [this](uint32_t rf_amp) {
on_tx_amp_changed((bool)rf_amp);
};
}
}
TransmitterView2::~TransmitterView2() {
audio::output::stop();
transmitter_model.disable();
baseband::shutdown();
text_labels.set_style(style);
field_gain.set_style(style);
field_amp.set_style(style);
}
} /* namespace ui */
+12 -47
View File
@@ -26,9 +26,9 @@
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_painter.hpp"
#include "ui_receiver.hpp"
#include "ui_styles.hpp"
#include "ui_widget.hpp"
#include "ui_receiver.hpp"
#include "rf_path.hpp"
@@ -37,10 +37,6 @@
#include <algorithm>
#include <functional>
#define POWER_THRESHOLD_HIGH 47
#define POWER_THRESHOLD_MED 38
#define POWER_THRESHOLD_LOW 17
namespace ui {
class TXGainField : public NumberField {
@@ -74,11 +70,8 @@ class TransmitterView : public View {
private:
const Style& style_start = Styles::green;
const Style style_stop = Styles::red;
const Style style_locked = Styles::dark_grey;
const Style style_power_low = Styles::yellow;
const Style style_power_med = Styles::orange;
const Style style_power_high = Styles::red;
const Style& style_stop = Styles::red;
const Style& style_locked = Styles::dark_grey;
bool lock_{false};
bool transmitting_{false};
@@ -134,60 +127,32 @@ class TransmitterView : public View {
void update_gainlevel_styles(void);
};
/* Simpler transmitter view that only renders TX Gain and Amp.
* When short_UI is set it abbreviates control labels. */
class TransmitterView2 : public View {
public:
TransmitterView2(const Coord x, const Coord y, bool short_UI);
~TransmitterView2();
void on_show() override;
void paint(Painter& painter) override;
TransmitterView2(Point pos, bool short_ui);
private:
const Style& style_power_low = Styles::yellow;
const Style& style_power_med = Styles::orange;
const Style& style_power_high = Styles::red;
Text text_gain_amp{
{0, 3 * 8, 5 * 8, 1 * 16},
"Gain: Amp:"};
Text text_labels{
{}, // Set in ctor.
{}};
NumberField field_gain{
{5 * 8, 3 * 8},
{}, // Set in ctor.
2,
{max2837::tx::gain_db_range.minimum, max2837::tx::gain_db_range.maximum},
max2837::tx::gain_db_step,
' '};
NumberField field_amp{
{12 * 8, 3 * 8},
{}, // Set in ctor.
2,
{0, 14},
14,
' '};
Text text_gain_amp_short_UI{
{0, (3 * 8), 5 * 8, 1 * 16},
"Gain A:"};
NumberField field_gain_short_UI{
{(4 * 8) + 2, 3 * 8},
2,
{max2837::tx::gain_db_range.minimum, max2837::tx::gain_db_range.maximum},
max2837::tx::gain_db_step,
' '};
NumberField field_amp_short_UI{
{(9 * 8) - 2, 3 * 8},
2,
{0, 14},
14,
' '};
void on_tx_gain_changed(int32_t tx_gain);
void on_tx_amp_changed(bool rf_amp);
void update_gainlevel_styles(void);
void update_gainlevel_styles();
};
} /* namespace ui */
@@ -0,0 +1,139 @@
#include "ui_external_items_menu_loader.hpp"
#include "sd_card.hpp"
namespace ui {
/* static */ std::vector<DynamicBitmap<16, 16>> ExternalItemsMenuLoader::bitmaps;
/* static */ std::vector<GridItem> ExternalItemsMenuLoader::load_external_items(app_location_t app_location, NavigationView& nav) {
bitmaps.clear();
std::vector<GridItem> external_apps;
if (sd_card::status() != sd_card::Status::Mounted)
return external_apps;
for (const auto& entry : std::filesystem::directory_iterator(u"APPS", u"*.ppma")) {
auto filePath = u"/APPS/" + entry.path();
File app;
auto openError = app.open(filePath);
if (openError)
continue;
application_information_t application_information = {};
auto readResult = app.read(&application_information, sizeof(application_information_t));
if (!readResult)
continue;
if (application_information.menu_location != app_location)
continue;
if (application_information.header_version != CURRENT_HEADER_VERSION)
continue;
bool versionMatches = VERSION_MD5 == application_information.app_version;
GridItem gridItem = {};
gridItem.text = reinterpret_cast<char*>(&application_information.app_name[0]);
if (versionMatches) {
gridItem.color = Color((uint16_t)application_information.icon_color);
auto dyn_bmp = DynamicBitmap<16, 16>{application_information.bitmap_data};
gridItem.bitmap = dyn_bmp.bitmap();
bitmaps.push_back(std::move(dyn_bmp));
gridItem.on_select = [&nav, app_location, filePath]() {
run_external_app(nav, filePath);
};
} else {
gridItem.color = Color::light_grey();
gridItem.bitmap = &bitmap_sd_card_error;
gridItem.on_select = [&nav]() {
nav.display_modal("Error", "The .ppma file in your APPS\nfolder is outdated. Please\nupdate your SD Card content.");
};
}
external_apps.push_back(gridItem);
}
return external_apps;
}
/* static */ void ExternalItemsMenuLoader::run_external_app(ui::NavigationView& nav, std::filesystem::path filePath) {
File app;
auto openError = app.open(filePath);
if (openError)
chDbgPanic("file gone");
application_information_t application_information = {};
auto readResult = app.read(&application_information, sizeof(application_information_t));
if (!readResult)
chDbgPanic("no data");
app.seek(0);
if (application_information.m4_app_offset != 0) {
// copy application image
for (size_t file_read_index = 0; file_read_index < application_information.m4_app_offset; file_read_index += std::filesystem::max_file_block_size) {
auto bytes_to_read = std::filesystem::max_file_block_size;
if (file_read_index + std::filesystem::max_file_block_size > application_information.m4_app_offset)
bytes_to_read = application_information.m4_app_offset - file_read_index;
if (bytes_to_read == 0)
break;
readResult = app.read(&application_information.memory_location[file_read_index], bytes_to_read);
if (!readResult)
chDbgPanic("read error");
if (readResult.value() < std::filesystem::max_file_block_size)
break;
}
// copy baseband image
for (size_t file_read_index = application_information.m4_app_offset;; file_read_index += readResult.value()) {
size_t bytes_to_read = std::filesystem::max_file_block_size;
// not aligned
if ((file_read_index % std::filesystem::max_file_block_size) != 0)
bytes_to_read = std::filesystem::max_file_block_size - (file_read_index % std::filesystem::max_file_block_size);
if (bytes_to_read == 0)
break;
auto target_memory = reinterpret_cast<void*>(portapack::memory::map::m4_code.base() + file_read_index - application_information.m4_app_offset);
readResult = app.read(target_memory, bytes_to_read);
if (!readResult)
chDbgPanic("read error #2");
if (readResult.value() != bytes_to_read)
break;
}
} else {
// copy application image
for (size_t file_read_index = 0; file_read_index < 80 * std::filesystem::max_file_block_size; file_read_index += std::filesystem::max_file_block_size) {
auto bytes_to_read = std::filesystem::max_file_block_size;
readResult = app.read(&application_information.memory_location[file_read_index], bytes_to_read);
if (!readResult)
chDbgPanic("read error #3");
if (readResult.value() < std::filesystem::max_file_block_size)
break;
}
}
application_information.externalAppEntry(nav);
}
} // namespace ui
@@ -0,0 +1,66 @@
/*
* 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.
*/
#ifndef __EXTERNAL_ITEMS_MENU_LOADER_H__
#define __EXTERNAL_ITEMS_MENU_LOADER_H__
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
#include "file.hpp"
namespace ui {
template <size_t Width, size_t Height>
class DynamicBitmap {
public:
static constexpr size_t buffer_size = Width * Height / (sizeof(uint8_t) * 8); // one bit per pixel
DynamicBitmap(const uint8_t data[buffer_size])
: _buffer(buffer_size, 0),
_bitmap{new Bitmap{{Width, Height}, &_buffer[0]}} {
memcpy(&_buffer[0], data, buffer_size);
}
const Bitmap* bitmap() { return _bitmap.get(); }
private:
// Allocating both members so the class is movable without invalidation.
std::vector<uint8_t> _buffer;
std::unique_ptr<Bitmap> _bitmap;
};
class ExternalItemsMenuLoader {
public:
static std::vector<GridItem> load_external_items(app_location_t, NavigationView&);
ExternalItemsMenuLoader() = delete;
private:
static std::vector<DynamicBitmap<16, 16>> bitmaps;
static void run_external_app(ui::NavigationView&, std::filesystem::path);
};
} // namespace ui
#endif /*__EXTERNAL_ITEMS_MENU_LOADER_H__*/
+136 -159
View File
@@ -24,60 +24,62 @@
// #include "modules.h"
#include "portapack.hpp"
#include "event_m0.hpp"
#include "bmp_splash.hpp"
#include "bmp_modal_warning.hpp"
#include "bmp_splash.hpp"
#include "event_m0.hpp"
#include "portapack_persistent_memory.hpp"
#include "portapack_shared_memory.hpp"
#include "portapack.hpp"
#include "ui_about_simple.hpp"
#include "ui_adsb_rx.hpp"
#include "ui_adsb_tx.hpp"
#include "ui_afsk_rx.hpp"
#include "ui_aprs_rx.hpp"
#include "ui_btle_rx.hpp"
#include "ui_nrf_rx.hpp"
#include "ui_aprs_tx.hpp"
#include "ui_bht_tx.hpp"
#include "ui_btle_rx.hpp"
#include "ui_coasterp.hpp"
#include "ui_debug.hpp"
#include "ui_encoders.hpp"
#include "ui_fileman.hpp"
#include "ui_flash_utility.hpp"
#include "ui_font_fixed_8x16.hpp"
#include "ui_freqman.hpp"
#include "ui_iq_trim.hpp"
#include "ui_jammer.hpp"
// #include "ui_keyfob.hpp"
#include "ui_lcr.hpp"
#include "ui_level.hpp"
#include "ui_looking_glass_app.hpp"
#include "ui_mictx.hpp"
#include "ui_morse.hpp"
#include "ui_nrf_rx.hpp"
// #include "ui_numbers.hpp"
// #include "ui_nuoptix.hpp"
// #include "ui_playdead.hpp"
#include "ui_playlist.hpp"
#include "ui_pocsag_tx.hpp"
#include "ui_rds.hpp"
#include "ui_recon.hpp"
#include "ui_remote.hpp"
#include "ui_scanner.hpp"
#include "ui_search.hpp"
#include "ui_recon.hpp"
#include "ui_level.hpp"
#include "ui_sd_over_usb.hpp"
#include "ui_sd_wipe.hpp"
#include "ui_search.hpp"
#include "ui_settings.hpp"
#include "ui_siggen.hpp"
#include "ui_sonde.hpp"
#include "ui_spectrum_painter.hpp"
#include "ui_ss_viewer.hpp"
#include "ui_sstvtx.hpp"
#include "ui_styles.hpp"
// #include "ui_test.hpp"
#include "ui_text_editor.hpp"
#include "ui_tone_search.hpp"
#include "ui_touchtunes.hpp"
#include "ui_playlist.hpp"
#include "ui_view_wav.hpp"
#include "ui_whipcalc.hpp"
#include "ui_flash_utility.hpp"
#include "ui_sd_over_usb.hpp"
#include "ui_spectrum_painter.hpp"
#include "ui_ss_viewer.hpp"
#include "ui_external_items_menu_loader.hpp"
// #include "acars_app.hpp"
#include "ais_app.hpp"
@@ -85,16 +87,16 @@
#include "analog_tv_app.hpp"
#include "capture_app.hpp"
#include "ert_app.hpp"
#include "gps_sim_app.hpp"
#include "lge_app.hpp"
#include "pocsag_app.hpp"
#include "replay_app.hpp"
#include "gps_sim_app.hpp"
#include "soundboard_app.hpp"
#include "tpms_app.hpp"
#include "core_control.hpp"
#include "ui_looking_glass_app.hpp"
#include "file.hpp"
#include "file_reader.hpp"
#include "png_writer.hpp"
using portapack::receiver_model;
@@ -302,17 +304,18 @@ void SystemStatusView::on_converter() {
void SystemStatusView::on_bias_tee() {
if (!portapack::get_antenna_bias()) {
nav_.display_modal("Bias voltage",
"Enable DC voltage on\nantenna connector?",
YESNO,
[this](bool v) {
if (v) {
portapack::set_antenna_bias(true);
receiver_model.set_antenna_bias();
transmitter_model.set_antenna_bias();
refresh();
}
});
nav_.display_modal(
"Bias voltage",
"Enable DC voltage on\nantenna connector?",
YESNO,
[this](bool v) {
if (v) {
portapack::set_antenna_bias(true);
receiver_model.set_antenna_bias();
transmitter_model.set_antenna_bias();
refresh();
}
});
} else {
portapack::set_antenna_bias(false);
receiver_model.set_antenna_bias();
@@ -404,15 +407,21 @@ View* NavigationView::push_view(std::unique_ptr<View> new_view) {
return p;
}
void NavigationView::pop() {
pop(true);
}
void NavigationView::pop(bool trigger_update) {
// Don't pop off the NavView.
if (view_stack.size() <= 1)
return;
void NavigationView::pop_modal() {
// Pop modal view + underlying app view.
// TODO: this shouldn't be necessary.
pop(false);
pop(true);
auto on_pop = view_stack.back().on_pop;
free_view();
view_stack.pop_back();
// NB: These are executed _after_ the view has been
// destroyed. The old view MUST NOT be referenced in
// these callbacks or it will cause crashes.
if (trigger_update) update_view();
if (on_pop) on_pop();
}
void NavigationView::display_modal(
@@ -424,49 +433,33 @@ void NavigationView::display_modal(
void NavigationView::display_modal(
const std::string& title,
const std::string& message,
const modal_t type,
const std::function<void(bool)> on_choice) {
/* If a modal view is already visible, don't display another */
if (!modal_view) {
modal_view = push<ModalMessageView>(title, message, type, on_choice);
}
}
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();
}
modal_t type,
std::function<void(bool)> on_choice) {
push<ModalMessageView>(title, message, type, on_choice);
}
void NavigationView::free_view() {
// The focus_manager holds a raw pointer to the currently focused Widget.
// It then tries to call blur() on that instance when the focus is changed.
// This causes crashes if focused_widget has been deleted (as is the case
// when a view is popped). Calling blur() here resets the focus_manager's
// focus_widget pointer so focus can be called safely.
this->blur();
remove_child(view());
}
void NavigationView::update_view() {
const auto new_view = view_stack.back().view.get();
const auto& top = view_stack.back();
auto top_view = top.view.get();
add_child(new_view);
new_view->set_parent_rect({{0, 0}, size()});
add_child(top_view);
top_view->set_parent_rect({{0, 0}, size()});
focus();
set_dirty();
if (on_view_changed) {
on_view_changed(*new_view);
}
if (on_view_changed)
on_view_changed(*top_view);
}
Widget* NavigationView::view() const {
@@ -474,9 +467,8 @@ Widget* NavigationView::view() const {
}
void NavigationView::focus() {
if (view()) {
if (view())
view()->focus();
}
}
bool NavigationView::set_on_pop(std::function<void()> on_pop) {
@@ -498,30 +490,31 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) {
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
// {"ACARS", Color::yellow(), &bitmap_icon_adsb, [&nav](){ nav.push<ACARSAppView>(); }},
{"ADS-B", Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBRxView>(); }},
//{ "ACARS", Color::yellow(), &bitmap_icon_adsb, [&nav](){ nav.push<ACARSAppView>(); }},
{"AIS Boats", Color::green(), &bitmap_icon_ais, [&nav]() { nav.push<AISAppView>(); }},
{"AFSK", Color::yellow(), &bitmap_icon_modem, [&nav]() { nav.push<AFSKRxView>(); }},
{"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push<BTLERxView>(); }},
{"NRF", Color::yellow(), &bitmap_icon_nrf, [&nav]() { nav.push<NRFRxView>(); }},
{"Audio", Color::green(), &bitmap_icon_speaker, [&nav]() { nav.push<AnalogAudioView>(); }},
{"Analog TV", Color::yellow(), &bitmap_icon_sstv, [&nav]() { nav.push<AnalogTvView>(); }},
{"APRS", Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSRXView>(); }},
{"Audio", Color::green(), &bitmap_icon_speaker, [&nav]() { nav.push<AnalogAudioView>(); }},
{"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push<BTLERxView>(); }},
{"ERT Meter", Color::green(), &bitmap_icon_ert, [&nav]() { nav.push<ERTAppView>(); }},
{"Level", Color::green(), &bitmap_icon_options_radio, [&nav]() { nav.push<LevelView>(); }},
{"NRF", Color::yellow(), &bitmap_icon_nrf, [&nav]() { nav.push<NRFRxView>(); }},
{"POCSAG", Color::green(), &bitmap_icon_pocsag, [&nav]() { nav.push<POCSAGAppView>(); }},
{"Radiosnde", Color::green(), &bitmap_icon_sonde, [&nav]() { nav.push<SondeView>(); }},
{"TPMS Cars", Color::green(), &bitmap_icon_tpms, [&nav]() { nav.push<TPMSAppView>(); }},
{"Recon", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<ReconView>(); }},
{"Level", Color::green(), &bitmap_icon_options_radio, [&nav]() { nav.push<LevelView>(); }},
{"APRS", Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSRXView>(); }}
/*
{ "DMR", Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push<NotImplementedView>(); } },
{ "SIGFOX", Color::dark_grey(), &bitmap_icon_fox, [&nav](){ nav.push<NotImplementedView>(); } }, // SIGFRXView
{ "LoRa", Color::dark_grey(), &bitmap_icon_lora, [&nav](){ nav.push<NotImplementedView>(); } },
{ "SSTV", Color::dark_grey(), &bitmap_icon_sstv, [&nav](){ nav.push<NotImplementedView>(); } },
{ "TETRA", Color::dark_grey(), &bitmap_icon_tetra, [&nav](){ nav.push<NotImplementedView>(); } },*/
{"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push<SearchView>(); }},
{"TPMS Cars", Color::green(), &bitmap_icon_tpms, [&nav]() { nav.push<TPMSAppView>(); }},
// {"DMR", Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push<NotImplementedView>(); }},
// {"SIGFOX", Color::dark_grey(), &bitmap_icon_fox, [&nav](){ nav.push<NotImplementedView>(); }},
// {"LoRa", Color::dark_grey(), &bitmap_icon_lora, [&nav](){ nav.push<NotImplementedView>(); }},
// {"SSTV", Color::dark_grey(), &bitmap_icon_sstv, [&nav](){ nav.push<NotImplementedView>(); }},
// {"TETRA", Color::dark_grey(), &bitmap_icon_tetra, [&nav](){ nav.push<NotImplementedView>(); }},
});
// set_highlighted(0); // Default selection is "Audio"
for (auto const& gridItem : ExternalItemsMenuLoader::load_external_items(app_location_t::RX, nav)) {
add_item(gridItem);
};
}
/* TransmittersMenuView **************************************************/
@@ -531,27 +524,29 @@ TransmittersMenuView::TransmittersMenuView(NavigationView& nav) {
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
{"ADS-B [S]", ui::Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBTxView>(); }},
{"ADS-B", ui::Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBTxView>(); }},
{"APRS", ui::Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSTXView>(); }},
{"BHT Xy/EP", ui::Color::green(), &bitmap_icon_bht, [&nav]() { nav.push<BHTView>(); }},
{"BurgerPgr", ui::Color::yellow(), &bitmap_icon_burger, [&nav]() { nav.push<CoasterPagerView>(); }},
{"GPS Sim", ui::Color::green(), &bitmap_icon_gps_sim, [&nav]() { nav.push<GpsSimAppView>(); }},
{"Jammer", ui::Color::green(), &bitmap_icon_jammer, [&nav]() { nav.push<JammerView>(); }},
//{ "Key fob", ui::Color::orange(), &bitmap_icon_keyfob, [&nav](){ nav.push<KeyfobView>(); } },
{"LGE tool", ui::Color::yellow(), &bitmap_icon_lge, [&nav]() { nav.push<LGEView>(); }},
// { "Key fob", ui::Color::orange(), &bitmap_icon_keyfob, [&nav](){ nav.push<KeyfobView>(); }},
{"LGE", ui::Color::yellow(), &bitmap_icon_lge, [&nav]() { nav.push<LGEView>(); }},
{"Morse", ui::Color::green(), &bitmap_icon_morse, [&nav]() { nav.push<MorseView>(); }},
{"BurgerPgr", ui::Color::yellow(), &bitmap_icon_burger, [&nav]() { nav.push<CoasterPagerView>(); }},
//{ "Nuoptix DTMF", ui::Color::green(), &bitmap_icon_nuoptix, [&nav](){ nav.push<NuoptixView>(); } },
// { "Nuoptix DTMF", ui::Color::green(), &bitmap_icon_nuoptix, [&nav](){ nav.push<NuoptixView>(); }},
{"OOK", ui::Color::yellow(), &bitmap_icon_remote, [&nav]() { nav.push<EncodersView>(); }},
{"POCSAG", ui::Color::green(), &bitmap_icon_pocsag, [&nav]() { nav.push<POCSAGTXView>(); }},
{"RDS", ui::Color::green(), &bitmap_icon_rds, [&nav]() { nav.push<RDSView>(); }},
{"Soundbrd", ui::Color::green(), &bitmap_icon_soundboard, [&nav]() { nav.push<SoundBoardView>(); }},
{"S.Painter", ui::Color::orange(), &bitmap_icon_paint, [&nav]() { nav.push<SpectrumPainterView>(); }},
{"SSTV", ui::Color::green(), &bitmap_icon_sstv, [&nav]() { nav.push<SSTVTXView>(); }},
{"TEDI/LCR", ui::Color::yellow(), &bitmap_icon_lcr, [&nav]() { nav.push<LCRView>(); }},
{"TouchTune", ui::Color::green(), &bitmap_icon_touchtunes, [&nav]() { nav.push<TouchTunesView>(); }},
//{"Playlist", ui::Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<PlaylistView>(); }},
{"S.Painter", ui::Color::orange(), &bitmap_icon_paint, [&nav]() { nav.push<SpectrumPainterView>(); }},
//{ "Remote", ui::Color::dark_grey(), &bitmap_icon_remote, [&nav](){ nav.push<RemoteView>(); } },
});
for (auto const& gridItem : ExternalItemsMenuLoader::load_external_items(app_location_t::TX, nav)) {
add_item(gridItem);
};
}
/* UtilitiesMenuView *****************************************************/
@@ -561,41 +556,51 @@ UtilitiesMenuView::UtilitiesMenuView(NavigationView& nav) {
add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
}
add_items({
//{ "Test app", Color::dark_grey(), nullptr, [&nav](){ nav.push<TestView>(); } },
{"Freq. manager", Color::green(), &bitmap_icon_freqman, [&nav]() { nav.push<FrequencyManagerView>(); }},
{"File manager", Color::green(), &bitmap_icon_dir, [&nav]() { nav.push<FileManagerView>(); }},
{"Antenna Length", Color::green(), &bitmap_icon_tools_antenna, [&nav]() { nav.push<WhipCalcView>(); }},
{"File Manager", Color::green(), &bitmap_icon_dir, [&nav]() { nav.push<FileManagerView>(); }},
{"Freq. Manager", Color::green(), &bitmap_icon_freqman, [&nav]() { nav.push<FrequencyManagerView>(); }},
{"Notepad", Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<TextEditorView>(); }},
{"Signal gen", Color::green(), &bitmap_icon_cwgen, [&nav]() { nav.push<SigGenView>(); }},
//{ "Tone search", Color::dark_grey(), nullptr, [&nav](){ nav.push<ToneSearchView>(); } },
{"Wav viewer", Color::yellow(), &bitmap_icon_soundboard, [&nav]() { nav.push<ViewWavView>(); }},
{"Antenna length", Color::green(), &bitmap_icon_tools_antenna, [&nav]() { nav.push<WhipCalcView>(); }},
{"IQ Trim", Color::orange(), &bitmap_icon_cut, [&nav]() { nav.push<IQTrimView>(); }},
{"SD Over USB", Color::yellow(), &bitmap_icon_hackrf, [&nav]() { nav.push<SdOverUsbView>(); }},
{"Signal Gen", Color::green(), &bitmap_icon_cwgen, [&nav]() { nav.push<SigGenView>(); }},
// {"Test App", Color::dark_grey(), nullptr, [&nav](){ nav.push<TestView>(); }},
// {"Tone Search", Color::dark_grey(), nullptr, [&nav](){ nav.push<ToneSearchView>(); }},
{"Wav View", Color::yellow(), &bitmap_icon_soundboard, [&nav]() { nav.push<ViewWavView>(); }},
{"Wipe SD card", Color::red(), &bitmap_icon_tools_wipesd, [&nav]() { nav.push<WipeSDView>(); }},
// Dangerous apps.
{"Flash Utility", Color::red(), &bitmap_icon_temperature, [&nav]() { nav.push<FlashUtilityView>(); }},
{"SD over USB", Color::yellow(), &bitmap_icon_hackrf, [&nav]() { nav.push<SdOverUsbView>(); }},
{"Wipe SD card", Color::red(), &bitmap_icon_tools_wipesd, [&nav]() { nav.push<WipeSDView>(); }},
});
for (auto const& gridItem : ExternalItemsMenuLoader::load_external_items(app_location_t::UTILITIES, nav)) {
add_item(gridItem);
};
set_max_rows(2); // allow wider buttons
}
/* SystemMenuView ********************************************************/
void SystemMenuView::hackrf_mode(NavigationView& nav) {
nav.push<ModalMessageView>("HackRF mode", " This mode enables HackRF\n functionality. To return,\n press the reset button.\n\n Switch to HackRF mode?", YESNO,
[this](bool choice) {
if (choice) {
EventDispatcher::request_stop();
}
});
nav.push<ModalMessageView>(
"HackRF mode",
" This mode enables HackRF\n functionality. To return,\n press the reset button.\n\n Switch to HackRF mode?",
YESNO,
[this](bool choice) {
if (choice) {
EventDispatcher::request_stop();
}
});
}
SystemMenuView::SystemMenuView(NavigationView& nav) {
add_items({
//{ "Play dead", Color::red(), &bitmap_icon_playdead, [&nav](){ nav.push<PlayDeadView>(); } },
// {"Play dead", Color::red(), &bitmap_icon_playdead, [&nav]() { nav.push<PlayDeadView>(); }},
{"Receive", Color::cyan(), &bitmap_icon_receivers, [&nav]() { nav.push<ReceiversMenuView>(); }},
{"Transmit", Color::cyan(), &bitmap_icon_transmit, [&nav]() { nav.push<TransmittersMenuView>(); }},
{"Capture", Color::red(), &bitmap_icon_capture, [&nav]() { nav.push<CaptureAppView>(); }},
{"Replay", Color::green(), &bitmap_icon_replay, [&nav]() { nav.push<PlaylistView>(); }},
{"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push<SearchView>(); }},
{"Remote", ui::Color::green(), &bitmap_icon_remote, [&nav]() { nav.push<RemoteView>(); }},
{"Scanner", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<ScannerView>(); }},
{"Microphone", Color::green(), &bitmap_icon_microphone, [&nav]() { nav.push<MicTXView>(); }},
{"Looking Glass", Color::green(), &bitmap_icon_looking, [&nav]() { nav.push<GlassView>(); }},
@@ -603,11 +608,11 @@ SystemMenuView::SystemMenuView(NavigationView& nav) {
{"Settings", Color::cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SettingsMenuView>(); }},
{"Debug", Color::light_grey(), &bitmap_icon_debug, [&nav]() { nav.push<DebugMenuView>(); }},
{"HackRF", Color::cyan(), &bitmap_icon_hackrf, [this, &nav]() { hackrf_mode(nav); }},
//{ "About", Color::cyan(), nullptr, [&nav](){ nav.push<AboutView>(); } }
// {"About", Color::cyan(), nullptr, [&nav]() { nav.push<AboutView>(); }},
});
set_max_rows(2); // allow wider buttons
set_arrow_enabled(false);
// set_highlighted(1); // Startup selection
}
/* SystemView ************************************************************/
@@ -623,19 +628,22 @@ SystemView::SystemView(
constexpr Dim info_view_height = 16;
add_child(&status_view);
status_view.set_parent_rect({{0, 0},
{parent_rect.width(), status_view_height}});
status_view.set_parent_rect(
{{0, 0},
{parent_rect.width(), status_view_height}});
status_view.on_back = [this]() {
this->navigation_view.pop();
};
add_child(&navigation_view);
navigation_view.set_parent_rect({{0, status_view_height},
{parent_rect.width(), static_cast<Dim>(parent_rect.height() - status_view_height)}});
navigation_view.set_parent_rect(
{{0, status_view_height},
{parent_rect.width(), static_cast<Dim>(parent_rect.height() - status_view_height)}});
add_child(&info_view);
info_view.set_parent_rect({{0, 19 * 16},
{parent_rect.width(), info_view_height}});
info_view.set_parent_rect(
{{0, 19 * 16},
{parent_rect.width(), info_view_height}});
navigation_view.on_view_changed = [this](const View& new_view) {
if (!this->navigation_view.is_top()) {
@@ -651,14 +659,6 @@ SystemView::SystemView(
this->status_view.set_dirty();
};
// pmem::set_playdead_sequence(0x8D1);
// Initial view
/*if ((pmem::playing_dead() == 0x5920C1DF) || // Enable code
(pmem::ui_config() & 16)) { // Login option
navigation_view.push<PlayDeadView>();
} else {*/
navigation_view.push<SystemMenuView>();
if (pmem::config_splash()) {
@@ -667,10 +667,6 @@ SystemView::SystemView(
status_view.set_back_enabled(false);
status_view.set_title_image_enabled(true);
status_view.set_dirty();
// else
// navigation_view.push<SystemMenuView>();
//}
}
Context& SystemView::context() const {
@@ -758,21 +754,19 @@ ModalMessageView::ModalMessageView(
NavigationView& nav,
const std::string& title,
const std::string& message,
const modal_t type,
const std::function<void(bool)> on_choice)
modal_t type,
std::function<void(bool)> on_choice)
: title_{title},
message_{message},
type_{type},
on_choice_{on_choice} {
if (type == INFO) {
add_child(&button_ok);
button_ok.on_select = [this, &nav](Button&) {
if (on_choice_)
on_choice_(true); // Assumes handler will pop.
else
nav.pop();
if (on_choice_) on_choice_(true);
nav.pop();
};
} else if (type == YESNO) {
add_children({&button_yes,
&button_no});
@@ -785,50 +779,33 @@ ModalMessageView::ModalMessageView(
if (on_choice_) on_choice_(false);
nav.pop();
};
} else if (type == YESCANCEL) {
add_children({&button_yes,
&button_no});
button_yes.on_select = [this, &nav](Button&) {
if (on_choice_) on_choice_(true);
nav.pop();
};
button_no.on_select = [this, &nav](Button&) {
// if (on_choice_) on_choice_(false);
nav.pop_modal();
};
} else { // ABORT
add_child(&button_ok);
button_ok.on_select = [this, &nav](Button&) {
if (on_choice_) on_choice_(true);
nav.pop_modal();
nav.pop(false); // Pop the modal.
nav.pop(); // Pop the underlying view.
};
}
}
void ModalMessageView::paint(Painter& painter) {
size_t pos, i = 0, start = 0;
portapack::display.drawBMP({100, 48}, modal_warning_bmp, false);
// Terrible...
while ((pos = message_.find("\n", start)) != std::string::npos) {
// Break lines.
auto lines = split_string(message_, '\n');
for (size_t i = 0; i < lines.size(); ++i) {
painter.draw_string(
{1 * 8, (Coord)(120 + (i * 16))},
style(),
message_.substr(start, pos - start));
i++;
start = pos + 1;
lines[i]);
}
painter.draw_string(
{1 * 8, (Coord)(120 + (i * 16))},
style(),
message_.substr(start, pos));
}
void ModalMessageView::focus() {
if ((type_ == YESNO) || (type_ == YESCANCEL)) {
if ((type_ == YESNO)) {
button_yes.focus();
} else {
button_ok.focus();
+9 -19
View File
@@ -51,7 +51,6 @@ namespace ui {
enum modal_t {
INFO = 0,
YESNO,
YESCANCEL,
ABORT
};
@@ -73,15 +72,6 @@ class NavigationView : public View {
return reinterpret_cast<T*>(push_view(std::unique_ptr<View>(new T(*this, std::forward<Args>(args)...))));
}
// Pushes a new view under the current on the stack so the current view returns into this new one.
template <class T, class... Args>
T* push_under_current(Args&&... args) {
auto new_view = std::unique_ptr<View>(new T(*this, std::forward<Args>(args)...));
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>
T* replace(Args&&... args) {
pop();
@@ -89,13 +79,16 @@ class NavigationView : public View {
}
void push(View* v);
View* push_view(std::unique_ptr<View> new_view);
void replace(View* v);
void pop();
void pop_modal();
void pop(bool trigger_update = true);
void display_modal(const std::string& title, const std::string& message);
void display_modal(const std::string& title, const std::string& message, const modal_t type, const std::function<void(bool)> on_choice = nullptr);
void display_modal(
const std::string& title,
const std::string& message,
modal_t type,
std::function<void(bool)> on_choice = nullptr);
void focus() override;
@@ -110,14 +103,11 @@ class NavigationView : public View {
};
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);
};
/* Holds widgets and grows dynamically toward the left.
@@ -362,8 +352,8 @@ class ModalMessageView : public View {
NavigationView& nav,
const std::string& title,
const std::string& message,
const modal_t type,
const std::function<void(bool)> on_choice);
modal_t type,
std::function<void(bool)> on_choice);
void paint(Painter& painter) override;
void focus() override;

Some files were not shown because too many files have changed in this diff Show More