mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-14 10:39:30 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| abb42e8df4 | |||
| 7fdb1af69d | |||
| 78713cc2af | |||
| 951890eaff | |||
| fb00bfac3f | |||
| a6a1483083 | |||
| ef03f020ce | |||
| 070eb4003e | |||
| fca373d936 | |||
| 537cf2e79b | |||
| af424aa5f8 | |||
| 70e0f2913f |
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_????.*",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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},
|
||||
""};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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.");
|
||||
};
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
@@ -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__*/
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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__*/
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
}
|
||||
Vendored
+5
-4
@@ -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
|
||||
Vendored
+8
-6
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2462
File diff suppressed because it is too large
Load Diff
+2858
File diff suppressed because it is too large
Load Diff
+2342
File diff suppressed because it is too large
Load Diff
+82
@@ -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
|
||||
};
|
||||
}
|
||||
+1276
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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__*/
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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__*/
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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__*/
|
||||
|
||||
@@ -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()},
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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 ***********************************************************/
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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__*/
|
||||
@@ -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();
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user