mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-16 03:22:59 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| abb42e8df4 | |||
| 7fdb1af69d | |||
| 78713cc2af | |||
| 951890eaff | |||
| fb00bfac3f | |||
| a6a1483083 | |||
| ef03f020ce | |||
| 070eb4003e | |||
| fca373d936 | |||
| 537cf2e79b |
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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_????.*",
|
||||
|
||||
@@ -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,35 +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.to_integer();
|
||||
settings_.filter_mode = opt_filter_mode.selected_index_value();
|
||||
settings_.filter_address = field_filter_address.to_integer();
|
||||
|
||||
nav.pop();
|
||||
};
|
||||
@@ -90,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,
|
||||
@@ -109,14 +103,16 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
&widget_baud,
|
||||
&widget_bits,
|
||||
&widget_frames,
|
||||
&button_ignore_last,
|
||||
&button_filter_last,
|
||||
&button_config,
|
||||
&console});
|
||||
|
||||
// No app settings, use fallbacks from pmem.
|
||||
if (!app_settings_.loaded()) {
|
||||
settings_.address_to_ignore = pmem::pocsag_ignore_address();
|
||||
settings_.enable_ignore = settings_.address_to_ignore > 0;
|
||||
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);
|
||||
@@ -129,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&) {
|
||||
@@ -155,15 +153,48 @@ POCSAGAppView::~POCSAGAppView() {
|
||||
baseband::shutdown();
|
||||
|
||||
// Save pmem settings.
|
||||
pmem::set_pocsag_ignore_address(settings_.address_to_ignore);
|
||||
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) {
|
||||
@@ -176,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,13 +170,15 @@ 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}}};
|
||||
|
||||
SymField field_ignore{
|
||||
{7 * 8, 13 * 16 + 8},
|
||||
SymField field_filter_address{
|
||||
{15 * 8, 13 * 16},
|
||||
7,
|
||||
SymField::Type::Dec,
|
||||
true /*explicit_edit*/};
|
||||
@@ -194,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; };
|
||||
|
||||
@@ -210,14 +215,14 @@ 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);
|
||||
@@ -273,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},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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__*/
|
||||
@@ -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},
|
||||
|
||||
@@ -39,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();
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -654,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>(); }},
|
||||
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetFrequencyCorrectionView>(); }},
|
||||
{"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push<SetQRCodeView>(); }},
|
||||
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<SetPersistentMemoryView>(); }},
|
||||
{"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetEncoderDialView>(); }},
|
||||
});
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -329,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__*/
|
||||
@@ -311,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;
|
||||
|
||||
|
||||
@@ -76,7 +76,9 @@ 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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -36,6 +36,7 @@ using namespace portapack;
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -163,6 +164,7 @@ void RecordView::start() {
|
||||
|
||||
text_record_filename.set("");
|
||||
text_record_dropped.set("");
|
||||
trim_path = {};
|
||||
|
||||
if (sampling_rate == 0) {
|
||||
return;
|
||||
@@ -217,7 +219,8 @@ void RecordView::start() {
|
||||
}
|
||||
|
||||
auto p = std::make_unique<FileConvertWriter>();
|
||||
auto create_error = p->create(base_path.replace_extension((file_type == FileType::RawS8) ? u".C8" : u".C16"));
|
||||
trim_path = base_path.replace_extension((file_type == FileType::RawS8) ? u".C8" : u".C16");
|
||||
auto create_error = p->create(trim_path);
|
||||
if (create_error.is_valid()) {
|
||||
handle_error(create_error.value());
|
||||
} else {
|
||||
@@ -257,6 +260,7 @@ void RecordView::stop() {
|
||||
if (is_active()) {
|
||||
capture_thread.reset();
|
||||
button_record.set_bitmap(&bitmap_record);
|
||||
trim_capture();
|
||||
}
|
||||
|
||||
update_status_display();
|
||||
@@ -299,6 +303,34 @@ void RecordView::update_status_display() {
|
||||
}
|
||||
}
|
||||
|
||||
void RecordView::trim_capture() {
|
||||
using bucket_t = iq::PowerBuckets::Bucket;
|
||||
|
||||
if (file_type != FileType::WAV && auto_trim && !trim_path.empty()) {
|
||||
// Need to heap alloc the buckets in this case. The large static buffer overflows the stack.
|
||||
std::vector<bucket_t> buckets(size_t(255), bucket_t{});
|
||||
;
|
||||
iq::PowerBuckets power_buckets{
|
||||
.p = &buckets[0],
|
||||
.size = buckets.size()};
|
||||
|
||||
trim_ui.show_reading();
|
||||
auto info = iq::profile_capture(trim_path, power_buckets);
|
||||
|
||||
if (info) {
|
||||
// 7% - decent trimming without being too aggressive.
|
||||
auto trim_range = iq::compute_trim_range(*info, power_buckets, 7);
|
||||
|
||||
trim_ui.show_trimming();
|
||||
iq::trim_capture_with_range(trim_path, trim_range, trim_ui.get_callback());
|
||||
}
|
||||
|
||||
trim_ui.clear();
|
||||
}
|
||||
|
||||
trim_path = {};
|
||||
}
|
||||
|
||||
void RecordView::handle_capture_thread_done(const File::Error error) {
|
||||
stop();
|
||||
if (error.code()) {
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
#ifndef __UI_RECORD_VIEW_H__
|
||||
#define __UI_RECORD_VIEW_H__
|
||||
|
||||
#include "ui_iq_trim.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
#include "capture_thread.hpp"
|
||||
#include "signal.hpp"
|
||||
|
||||
#include "bitmap.hpp"
|
||||
#include "capture_thread.hpp"
|
||||
#include "iq_trim.hpp"
|
||||
#include "signal.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
@@ -63,6 +64,7 @@ class RecordView : public View {
|
||||
uint32_t set_sampling_rate(uint32_t new_sampling_rate);
|
||||
|
||||
void set_file_type(const FileType v) { file_type = v; }
|
||||
void set_auto_trim(bool v) { auto_trim = v; }
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
@@ -78,6 +80,7 @@ class RecordView : public View {
|
||||
|
||||
void on_tick_second();
|
||||
void update_status_display();
|
||||
void trim_capture();
|
||||
|
||||
void handle_capture_thread_done(const File::Error error);
|
||||
void handle_error(const File::Error error);
|
||||
@@ -98,6 +101,10 @@ class RecordView : public View {
|
||||
uint32_t sampling_rate{0};
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
bool auto_trim = false;
|
||||
std::filesystem::path trim_path{};
|
||||
TrimProgressUI trim_ui{};
|
||||
|
||||
Rectangle rect_background{
|
||||
Color::black()};
|
||||
|
||||
|
||||
@@ -265,6 +265,7 @@ include_directories(. ${INCDIR})
|
||||
#######################################################################
|
||||
|
||||
set(BASEBAND_IMAGES)
|
||||
set(BASEBAND_EXTERNAL_IMAGES)
|
||||
|
||||
macro(DeclareTargets chunk_tag name)
|
||||
project("baseband_${name}")
|
||||
@@ -279,18 +280,38 @@ macro(DeclareTargets chunk_tag name)
|
||||
target_link_libraries(${PROJECT_NAME}.elf ${LIBS})
|
||||
target_link_libraries(${PROJECT_NAME}.elf -Wl,-Map=${PROJECT_NAME}.map)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${PROJECT_NAME}.bin ${PROJECT_NAME}.img
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin
|
||||
COMMAND ${LZ4} -f -9 ${PROJECT_NAME}.bin ${PROJECT_NAME}.lz4
|
||||
COMMAND ${MAKE_IMAGE_CHUNK} ${PROJECT_NAME}.lz4 ${chunk_tag} ${PROJECT_NAME}.img
|
||||
DEPENDS ${PROJECT_NAME}.elf ${MAKE_IMAGE_CHUNK}
|
||||
VERBATIM
|
||||
)
|
||||
if(add_to_firmware)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${chunk_tag}.bin ${PROJECT_NAME}.img
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${chunk_tag}.bin
|
||||
COMMAND ${LZ4} -f -9 ${chunk_tag}.bin ${PROJECT_NAME}.lz4
|
||||
COMMAND ${MAKE_IMAGE_CHUNK} ${PROJECT_NAME}.lz4 ${chunk_tag} ${PROJECT_NAME}.img
|
||||
DEPENDS ${PROJECT_NAME}.elf ${MAKE_IMAGE_CHUNK}
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
set(BASEBAND_IMAGES ${BASEBAND_IMAGES} ${PROJECT_NAME}.img)
|
||||
|
||||
else()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${chunk_tag}.bin
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${chunk_tag}.bin
|
||||
DEPENDS ${PROJECT_NAME}.elf
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
set(BASEBAND_EXTERNAL_IMAGES ${BASEBAND_EXTERNAL_IMAGES} ${chunk_tag}.bin)
|
||||
|
||||
endif()
|
||||
|
||||
|
||||
|
||||
set(BASEBAND_IMAGES ${BASEBAND_IMAGES} ${PROJECT_NAME}.img)
|
||||
endmacro()
|
||||
|
||||
set(add_to_firmware TRUE)
|
||||
|
||||
### ACARS RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
@@ -312,20 +333,13 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PADT adsbtx)
|
||||
|
||||
### AFSK
|
||||
### AFSK TX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_afsk.cpp
|
||||
)
|
||||
DeclareTargets(PAFT afsktx)
|
||||
|
||||
### AFSK RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_afskrx.cpp
|
||||
)
|
||||
DeclareTargets(PAFR afskrx)
|
||||
|
||||
### APRS RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
@@ -437,12 +451,12 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(POOK ook)
|
||||
|
||||
### POCSAG RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_pocsag.cpp
|
||||
)
|
||||
DeclareTargets(PPOC pocsag)
|
||||
#### POCSAG RX
|
||||
#
|
||||
#set(MODE_CPPSRC
|
||||
# proc_pocsag.cpp
|
||||
#)
|
||||
#DeclareTargets(PPOC pocsag)
|
||||
|
||||
### POCSAG2 RX
|
||||
|
||||
@@ -493,13 +507,6 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PSTX sstvtx)
|
||||
|
||||
### Test
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_test.cpp
|
||||
)
|
||||
DeclareTargets(PTST test)
|
||||
|
||||
### Tones
|
||||
|
||||
set(MODE_CPPSRC
|
||||
@@ -586,6 +593,24 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PUSB sd_over_usb)
|
||||
|
||||
### Place external app and disabled images below so they don't get added to the firmware
|
||||
set(add_to_firmware FALSE)
|
||||
|
||||
### AFSK RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_afskrx.cpp
|
||||
)
|
||||
DeclareTargets(PAFR afskrx)
|
||||
|
||||
### Test
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_test.cpp
|
||||
)
|
||||
DeclareTargets(PTST test)
|
||||
|
||||
|
||||
### HackRF "factory" firmware
|
||||
|
||||
add_custom_command(
|
||||
@@ -616,7 +641,7 @@ project(baseband)
|
||||
add_custom_command(
|
||||
OUTPUT ${PROJECT_NAME}.img
|
||||
COMMAND cat ${BASEBAND_IMAGES} > ${PROJECT_NAME}.img
|
||||
DEPENDS ${BASEBAND_IMAGES}
|
||||
DEPENDS ${BASEBAND_IMAGES} ${BASEBAND_EXTERNAL_IMAGES}
|
||||
DEPENDS hackrf.img terminator.img
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
@@ -33,6 +33,9 @@ __ram_end__ = __ram_start__ + __ram_size__;
|
||||
|
||||
ENTRY(ResetHandler)
|
||||
|
||||
/* external apps */
|
||||
INCLUDE external.ld
|
||||
|
||||
SECTIONS
|
||||
{
|
||||
. = 0;
|
||||
@@ -142,7 +145,7 @@ SECTIONS
|
||||
*(COMMON)
|
||||
. = ALIGN(4);
|
||||
PROVIDE(_bss_end = .);
|
||||
} > ram
|
||||
} > ram
|
||||
}
|
||||
|
||||
PROVIDE(end = .);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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_APPS_H__
|
||||
#define __EXTERNAL_APPS_H__
|
||||
|
||||
#include "ch.h"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "spi_image.hpp"
|
||||
|
||||
#define CURRENT_HEADER_VERSION 0x00000001
|
||||
|
||||
typedef void (*externalAppEntry_t)(ui::NavigationView& nav);
|
||||
|
||||
enum app_location_t : uint32_t {
|
||||
UTILITIES = 0,
|
||||
RX,
|
||||
TX
|
||||
};
|
||||
|
||||
struct application_information_t {
|
||||
uint8_t* memory_location;
|
||||
externalAppEntry_t externalAppEntry;
|
||||
uint32_t header_version;
|
||||
uint32_t app_version;
|
||||
|
||||
uint8_t app_name[16];
|
||||
uint8_t bitmap_data[32];
|
||||
uint32_t icon_color;
|
||||
app_location_t menu_location;
|
||||
|
||||
portapack::spi_flash::image_tag_t m4_app_tag;
|
||||
uint32_t m4_app_offset;
|
||||
};
|
||||
|
||||
#endif /*__EXTERNAL_APPS_H__*/
|
||||
@@ -405,13 +405,12 @@ void ILI9341::drawBMP(const ui::Point p, const uint8_t* bitmap, const bool trans
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Draw BMP from SD card.
|
||||
Currently supported formats:
|
||||
16bpp ARGB, RGB565
|
||||
24bpp RGB
|
||||
32bpp ARGB
|
||||
*/
|
||||
/* Draw BMP from SD card.
|
||||
* Currently supported formats:
|
||||
* 16bpp ARGB, RGB565
|
||||
* 24bpp RGB
|
||||
* 32bpp ARGB
|
||||
*/
|
||||
bool ILI9341::drawBMP2(const ui::Point p, const std::filesystem::path& file) {
|
||||
File bmpimage;
|
||||
size_t file_pos = 0;
|
||||
|
||||
@@ -90,14 +90,52 @@ class ILI9341 {
|
||||
const ui::Color foreground,
|
||||
const ui::Color background);
|
||||
|
||||
/*** Scrolling ***
|
||||
* Scrolling support is implemented in the ILI9341 driver. Basically a region
|
||||
* of the screen is set up to act as a circular buffer. The VSA (vertical scroll
|
||||
* address) is the line that defines the "start" of the circular buffer. In our
|
||||
* case, the driver is set up for "bottom-up" scrolling. In this mode, drawing
|
||||
* starts at the bottom of the scroll region and draws the buffer upward.
|
||||
* However, the whole display's address space is inverted (the screen is actually
|
||||
* upside down in the PortaPack) so this bottom-up drawing appears to be top-down.
|
||||
*
|
||||
* What this means is that the line pointed to by VSA will be drawn at the top
|
||||
* of the scroll region and the line at VSA - 1 (wrapped) will be the bottom.
|
||||
* Consider the following screen buffers and VSA pointers.
|
||||
*
|
||||
* Buffer: Display: Buffer: Display:
|
||||
* VSA > A A A C NB: VSA points to the "Top"
|
||||
* B B B D of the rendered output.
|
||||
* C C VSA > C A
|
||||
* D D D B
|
||||
*/
|
||||
|
||||
/* Sets the top and bottom lines of the scrolling region. */
|
||||
void scroll_set_area(const ui::Coord top_y, const ui::Coord bottom_y);
|
||||
ui::Coord scroll_set_position(const ui::Coord position);
|
||||
ui::Coord scroll(const int32_t delta);
|
||||
ui::Coord scroll_area_y(const ui::Coord y) const;
|
||||
void scroll_disable();
|
||||
|
||||
constexpr ui::Dim width() const { return 240; }
|
||||
constexpr ui::Dim height() const { return 320; }
|
||||
/* Sets VSA. This an offset from top_y not a screen coordinate. */
|
||||
ui::Coord scroll_set_position(const ui::Coord position);
|
||||
|
||||
/* Relative adjustment to VSA. Positive value will cause output
|
||||
* to scoll "down", negative values will cause the output to scroll "up". */
|
||||
ui::Coord scroll(const int32_t delta);
|
||||
|
||||
/* Gets a screen coordinate from a scroll region offset. Values are wrapped
|
||||
* so the offset is wrapped within the bounds of the scroll region. The
|
||||
* specified offset is applied to VSA and then offset by the scroll region
|
||||
* top to produce a screen coordinate.
|
||||
* Consider the following screen buffers and VSA pointer. Note the offsets.
|
||||
*
|
||||
* VSA > A = +0 A = +2
|
||||
* B = +1 B = +3 NB: As before, VSA is always the "top"
|
||||
* C = +2 VSA > C = +0 and line at VSA - 1 will be the
|
||||
* D = +3 D = +1 "bottom" or the max 'y' offset.
|
||||
*/
|
||||
ui::Coord scroll_area_y(const ui::Coord y) const;
|
||||
|
||||
constexpr ui::Dim width() const { return ui::screen_width; }
|
||||
constexpr ui::Dim height() const { return ui::screen_height; }
|
||||
constexpr ui::Rect screen_rect() const { return {0, 0, width(), height()}; }
|
||||
|
||||
void draw_pixels(const ui::Rect r, const ui::Color* const colors, const size_t count);
|
||||
|
||||
@@ -97,6 +97,14 @@ struct Color {
|
||||
return (v ^ 0xffff);
|
||||
}
|
||||
|
||||
/* Converts a 32-bit color into a 16-bit color.
|
||||
* High byte is ignored. */
|
||||
static constexpr Color RGB(uint32_t rgb) {
|
||||
return {static_cast<uint8_t>((rgb >> 16) & 0xff),
|
||||
static_cast<uint8_t>((rgb >> 8) & 0xff),
|
||||
static_cast<uint8_t>(rgb & 0xff)};
|
||||
}
|
||||
|
||||
static constexpr Color black() {
|
||||
return {0, 0, 0};
|
||||
}
|
||||
|
||||
@@ -43,18 +43,12 @@ void FocusManager::set_focus_widget(Widget* const new_focus_widget) {
|
||||
}
|
||||
|
||||
if (new_focus_widget) {
|
||||
// if( !new_focus_widget->visible() ) {
|
||||
// if( new_focus_widget->hidden() ) {
|
||||
// // New widget is not visible. Do nothing.
|
||||
// // TODO: Should this be a debug assertion?
|
||||
// return;
|
||||
// }
|
||||
if (!new_focus_widget->focusable()) {
|
||||
if (!new_focus_widget->focusable())
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Blur old widget.
|
||||
// NB: This will crash if the focus_widget is a destroyed instance.
|
||||
if (focus_widget()) {
|
||||
focus_widget()->on_blur();
|
||||
focus_widget()->set_dirty();
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <cstddef>
|
||||
#include <algorithm>
|
||||
|
||||
#include "irq_controls.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
@@ -134,7 +135,6 @@ void Widget::focus() {
|
||||
}
|
||||
|
||||
void Widget::on_focus() {
|
||||
// set_dirty();
|
||||
}
|
||||
|
||||
void Widget::blur() {
|
||||
@@ -142,7 +142,6 @@ void Widget::blur() {
|
||||
}
|
||||
|
||||
void Widget::on_blur() {
|
||||
// set_dirty();
|
||||
}
|
||||
|
||||
bool Widget::focusable() const {
|
||||
@@ -494,7 +493,9 @@ void BigFrequency::paint(Painter& painter) {
|
||||
const auto rect = screen_rect();
|
||||
|
||||
// Erase
|
||||
painter.fill_rectangle({{0, rect.location().y()}, {240, 52}}, ui::Color::black());
|
||||
painter.fill_rectangle(
|
||||
{{0, rect.location().y()}, {screen_width, 52}},
|
||||
ui::Color::black());
|
||||
|
||||
// Prepare digits
|
||||
if (!_frequency) {
|
||||
@@ -612,10 +613,10 @@ void Console::write(std::string message) {
|
||||
if (!hidden() && visible()) {
|
||||
const Style& s = style();
|
||||
const Font& font = s.font;
|
||||
const auto rect = screen_rect();
|
||||
auto rect = screen_rect();
|
||||
ui::Color pen_color = s.foreground;
|
||||
|
||||
for (const auto c : message) {
|
||||
for (auto c : message) {
|
||||
if (escape) {
|
||||
if (c < std::size(term_colors))
|
||||
pen_color = term_colors[(uint8_t)c];
|
||||
@@ -628,12 +629,13 @@ void Console::write(std::string message) {
|
||||
} else if (c == '\x1B') {
|
||||
escape = true;
|
||||
} else {
|
||||
const auto glyph = font.glyph(c);
|
||||
const auto advance = glyph.advance();
|
||||
if ((pos.x() + advance.x()) > rect.width()) {
|
||||
auto glyph = font.glyph(c);
|
||||
auto advance = glyph.advance();
|
||||
// Would drawing next character be off the end? Newline.
|
||||
if ((pos.x() + advance.x()) > rect.width())
|
||||
crlf();
|
||||
}
|
||||
const Point pos_glyph{
|
||||
|
||||
Point pos_glyph{
|
||||
rect.left() + pos.x(),
|
||||
display.scroll_area_y(pos.y())};
|
||||
display.draw_glyph(pos_glyph, glyph, pen_color, s.background);
|
||||
@@ -649,7 +651,6 @@ void Console::write(std::string message) {
|
||||
|
||||
void Console::writeln(std::string message) {
|
||||
write(message + "\n");
|
||||
// crlf();
|
||||
}
|
||||
|
||||
void Console::paint(Painter&) {
|
||||
@@ -659,15 +660,23 @@ void Console::paint(Painter&) {
|
||||
void Console::on_show() {
|
||||
enable_scrolling(true);
|
||||
clear();
|
||||
// visible = true;
|
||||
}
|
||||
|
||||
bool Console::scrolling_enabled = false;
|
||||
|
||||
void Console::enable_scrolling(bool enable) {
|
||||
if (enable) {
|
||||
const auto screen_r = screen_rect();
|
||||
display.scroll_set_area(screen_r.top(), screen_r.bottom());
|
||||
auto sr = screen_rect();
|
||||
auto line_height = style().font.line_height();
|
||||
|
||||
// Count full lines that can fit in console's rectangle.
|
||||
auto max_lines = sr.height() / line_height; // NB: int division to floor.
|
||||
|
||||
// The scroll area must be a multiple of the line_height
|
||||
// or some lines will end up vertically truncated.
|
||||
scroll_height = max_lines * line_height;
|
||||
|
||||
display.scroll_set_area(sr.top(), sr.top() + scroll_height);
|
||||
display.scroll_set_position(0);
|
||||
scrolling_enabled = true;
|
||||
} else {
|
||||
@@ -678,28 +687,37 @@ void Console::enable_scrolling(bool enable) {
|
||||
|
||||
void Console::on_hide() {
|
||||
/* TODO: Clear region to eliminate brief flash of content at un-shifted
|
||||
* position?
|
||||
*/
|
||||
* position? */
|
||||
enable_scrolling(false);
|
||||
// visible = false;
|
||||
}
|
||||
|
||||
void Console::crlf() {
|
||||
if (hidden() || !visible()) return;
|
||||
|
||||
const Style& s = style();
|
||||
const auto sr = screen_rect();
|
||||
const auto line_height = s.font.line_height();
|
||||
pos = {0, pos.y() + line_height};
|
||||
const int32_t y_excess = pos.y() + line_height - sr.height();
|
||||
if (y_excess > 0) {
|
||||
if (!scrolling_enabled) {
|
||||
enable_scrolling(true);
|
||||
}
|
||||
display.scroll(-y_excess);
|
||||
pos = {pos.x(), pos.y() - y_excess};
|
||||
const auto& s = style();
|
||||
auto sr = screen_rect();
|
||||
auto line_height = s.font.line_height();
|
||||
|
||||
const Rect dirty{sr.left(), display.scroll_area_y(pos.y()), sr.width(), line_height};
|
||||
// Advance to the next line (\n) position and "carriage return" x to 0.
|
||||
pos = {0, pos.y() + line_height};
|
||||
|
||||
if (pos.y() >= scroll_height) {
|
||||
// Line is past off the "bottom", need to scroll.
|
||||
if (!scrolling_enabled)
|
||||
enable_scrolling(true);
|
||||
|
||||
// See the notes in lcd_ili9341.hpp about how scrolling works.
|
||||
// The gist is that VSA will be moved to scroll the "top" off the
|
||||
// screen. The drawing code uses 'scroll_area_y' to get the actual
|
||||
// screen coordinate based on VSA. The "bottom" line is *always*
|
||||
// at 'VSA + ((max_lines - 1) * line_height)' and so is constant.
|
||||
pos = {0, scroll_height - line_height};
|
||||
|
||||
// Scroll off the "top" line.
|
||||
display.scroll(-line_height);
|
||||
|
||||
// Clear the new line at the "bottom".
|
||||
Rect dirty{sr.left(), display.scroll_area_y(pos.y()), sr.width(), line_height};
|
||||
display.fill_rectangle(dirty, s.background);
|
||||
}
|
||||
}
|
||||
@@ -1120,9 +1138,9 @@ NewButton::NewButton(
|
||||
Color color,
|
||||
bool vertical_center)
|
||||
: Widget{parent_rect},
|
||||
color_{color},
|
||||
text_{text},
|
||||
bitmap_{bitmap},
|
||||
color_{color},
|
||||
vertical_center_{vertical_center} {
|
||||
set_focusable(true);
|
||||
}
|
||||
@@ -1163,18 +1181,8 @@ void NewButton::paint(Painter& painter) {
|
||||
if (!bitmap_ && text_.empty())
|
||||
return;
|
||||
|
||||
Color bg, fg;
|
||||
const auto r = screen_rect();
|
||||
|
||||
if (has_focus() || highlighted()) {
|
||||
bg = style().foreground;
|
||||
fg = Color::black();
|
||||
} else {
|
||||
bg = Color::grey();
|
||||
fg = style().foreground;
|
||||
}
|
||||
|
||||
const Style paint_style = {style().font, bg, fg};
|
||||
const Style style = paint_style();
|
||||
|
||||
painter.draw_rectangle({r.location(), {r.width(), 1}}, Color::light_grey());
|
||||
painter.draw_rectangle({r.left(), r.top() + r.height() - 1, r.width(), 1}, Color::dark_grey());
|
||||
@@ -1182,7 +1190,7 @@ void NewButton::paint(Painter& painter) {
|
||||
|
||||
painter.fill_rectangle(
|
||||
{r.left(), r.top() + 1, r.width() - 1, r.height() - 2},
|
||||
paint_style.background);
|
||||
style.background);
|
||||
|
||||
int y = r.top();
|
||||
if (bitmap_) {
|
||||
@@ -1194,18 +1202,32 @@ void NewButton::paint(Painter& painter) {
|
||||
bmp_pos,
|
||||
*bitmap_,
|
||||
color_,
|
||||
bg);
|
||||
style.background);
|
||||
}
|
||||
|
||||
if (!text_.empty()) {
|
||||
const auto label_r = paint_style.font.size_of(text_);
|
||||
const auto label_r = style.font.size_of(text_);
|
||||
painter.draw_string(
|
||||
{r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
|
||||
paint_style,
|
||||
style,
|
||||
text_);
|
||||
}
|
||||
}
|
||||
|
||||
Style NewButton::paint_style() {
|
||||
MutableStyle s{style()};
|
||||
|
||||
if (has_focus() || highlighted()) {
|
||||
s.background = style().foreground;
|
||||
s.foreground = Color::black();
|
||||
} else {
|
||||
s.background = Color::grey();
|
||||
s.foreground = style().foreground;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
void NewButton::on_focus() {
|
||||
if (on_highlight)
|
||||
on_highlight(*this);
|
||||
@@ -1709,9 +1731,15 @@ bool TextEdit::on_key(const KeyEvent key) {
|
||||
cursor_pos_--;
|
||||
else if (key == KeyEvent::Right && cursor_pos_ < text_.length())
|
||||
cursor_pos_++;
|
||||
else if (key == KeyEvent::Select)
|
||||
insert_mode_ = !insert_mode_;
|
||||
else
|
||||
else if (key == KeyEvent::Select) {
|
||||
if (key_is_long_pressed(key)) {
|
||||
// Delete text to the cursor.
|
||||
text_ = text_.substr(cursor_pos_);
|
||||
set_cursor(0);
|
||||
} else {
|
||||
insert_mode_ = !insert_mode_;
|
||||
}
|
||||
} else
|
||||
return false;
|
||||
|
||||
set_dirty();
|
||||
@@ -1739,6 +1767,19 @@ bool TextEdit::on_touch(const TouchEvent event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void TextEdit::on_focus() {
|
||||
// Enable long press on "Select".
|
||||
SwitchesState config;
|
||||
config[toUType(Switch::Sel)] = true;
|
||||
set_switches_long_press_config(config);
|
||||
}
|
||||
|
||||
void TextEdit::on_blur() {
|
||||
// Reset long press.
|
||||
SwitchesState config{};
|
||||
set_switches_long_press_config(config);
|
||||
}
|
||||
|
||||
/* TextField *************************************************************/
|
||||
|
||||
TextField::TextField(Rect parent_rect, std::string text)
|
||||
@@ -1964,6 +2005,9 @@ void SymField::set_value(uint64_t value) {
|
||||
value_[i] = uint_to_char(temp, radix);
|
||||
v /= radix;
|
||||
}
|
||||
|
||||
if (on_change)
|
||||
on_change(*this);
|
||||
}
|
||||
|
||||
void SymField::set_value(std::string_view value) {
|
||||
|
||||
@@ -332,8 +332,8 @@ class Console : public Widget {
|
||||
void on_hide() override;
|
||||
|
||||
private:
|
||||
// bool visible = false;
|
||||
Point pos{0, 0};
|
||||
Dim scroll_height = 0;
|
||||
std::string buffer{};
|
||||
static bool scrolling_enabled;
|
||||
|
||||
@@ -479,10 +479,13 @@ class NewButton : public Widget {
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
protected:
|
||||
virtual Style paint_style();
|
||||
Color color_;
|
||||
|
||||
private:
|
||||
std::string text_;
|
||||
const Bitmap* bitmap_;
|
||||
Color color_;
|
||||
bool vertical_center_{false};
|
||||
};
|
||||
|
||||
@@ -686,6 +689,9 @@ class TextEdit : public Widget {
|
||||
bool on_encoder(const EncoderEvent delta) override;
|
||||
bool on_touch(const TouchEvent event) override;
|
||||
|
||||
void on_focus() override;
|
||||
void on_blur() override;
|
||||
|
||||
protected:
|
||||
std::string& text_;
|
||||
size_t max_length_;
|
||||
|
||||
@@ -213,3 +213,25 @@ static constexpr uint32_t gcd_top(const uint32_t u, const uint32_t v) {
|
||||
uint32_t gcd(const uint32_t u, const uint32_t v) {
|
||||
return gcd_top(u, v);
|
||||
}
|
||||
|
||||
std::string join(char c, std::initializer_list<std::string_view> strings) {
|
||||
std::string result;
|
||||
size_t total_size = strings.size();
|
||||
|
||||
for (auto s : strings)
|
||||
total_size += s.size();
|
||||
|
||||
result.reserve(total_size);
|
||||
bool first = true;
|
||||
|
||||
for (auto s : strings) {
|
||||
if (!first)
|
||||
result += c;
|
||||
else
|
||||
first = false;
|
||||
|
||||
result += s;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
#include <complex>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
#define LOCATE_IN_RAM __attribute__((section(".ramtext")))
|
||||
@@ -213,4 +215,6 @@ struct range_t {
|
||||
}
|
||||
};
|
||||
|
||||
std::string join(char c, std::initializer_list<std::string_view> strings);
|
||||
|
||||
#endif /*__UTILITY_H__*/
|
||||
|
||||
@@ -47,6 +47,7 @@ add_executable(application_test EXCLUDE_FROM_ALL
|
||||
|
||||
${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp
|
||||
${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp
|
||||
${PROJECT_SOURCE_DIR}/../../common/utility.cpp
|
||||
|
||||
# Dependencies
|
||||
${PROJECT_SOURCE_DIR}/../../application/file.cpp
|
||||
|
||||
@@ -89,4 +89,11 @@ TEST_CASE("to_byte_array returns correct size and values.") {
|
||||
CHECK_EQ(arr4[1], 0xCD);
|
||||
CHECK_EQ(arr4[2], 0x12);
|
||||
CHECK_EQ(arr4[3], 0x34);
|
||||
}
|
||||
|
||||
TEST_CASE("join will join strings") {
|
||||
CHECK_EQ(join(',', {}), "");
|
||||
CHECK_EQ(join(',', {"a"}), "a");
|
||||
CHECK_EQ(join('-', {"a", "b"}), "a-b");
|
||||
CHECK_EQ(join(',', {"a", "b", "c"}), "a,b,c");
|
||||
}
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
# This script will copy compiled external apps to the Portapack.
|
||||
|
||||
try=1
|
||||
while [ $try -le 180 ]
|
||||
do
|
||||
if ls /dev/disk/by-id/*Portapack*part1 1> /dev/null 2>&1; then
|
||||
disk=$(ls /dev/disk/by-id/*Portapack*part1)
|
||||
part=$(readlink -f $disk)
|
||||
mountpoint=$(findmnt -f -o TARGET --noheadings $part)
|
||||
if [[ ! -z "$mountpoint" ]]; then
|
||||
|
||||
echo "Copying external applications to" $mountpoint
|
||||
mkdir -p $mountpoint/APPS
|
||||
cp application/*.ppma $mountpoint/APPS
|
||||
|
||||
echo "Unmounting" $mountpoint
|
||||
umount $mountpoint
|
||||
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$(( $try %5 ))" -eq "1" ]; then
|
||||
echo "Please start SD over USB app ..."
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
try=$(( $try + 1 ))
|
||||
done
|
||||
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
import struct
|
||||
import subprocess
|
||||
|
||||
usage_message = """
|
||||
PortaPack external app image creator
|
||||
This script is used in the build process and should never be run manually.
|
||||
See firmware/application/CMakeLists.txt > COMMAND ${EXPORT_EXTERNAL_APP_IMAGES}
|
||||
|
||||
Usage: <command> <project source dir> <binary dir> <cmake objcopy path> <list of external image prefixes>
|
||||
"""
|
||||
|
||||
maximum_application_size = 40*1024
|
||||
|
||||
if len(sys.argv) < 4:
|
||||
print(usage_message)
|
||||
sys.exit(-1)
|
||||
|
||||
def read_image(path):
|
||||
f = open(path, 'rb')
|
||||
data = f.read()
|
||||
f.close()
|
||||
return data
|
||||
|
||||
def write_image(data, path):
|
||||
f = open(path, 'wb')
|
||||
f.write(data)
|
||||
f.close()
|
||||
|
||||
def patch_image(image_data, search_address, replace_address):
|
||||
if (len(image_data) % 4) != 0:
|
||||
print("file size not divideable by 4")
|
||||
sys.exit(-1)
|
||||
|
||||
external_application_image = bytearray()
|
||||
|
||||
for x in range(int(len(image_data)/4)):
|
||||
snippet = image_data[x*4:4*(x+1)]
|
||||
val = int.from_bytes(snippet, byteorder='little')
|
||||
|
||||
# in firmware/application/external/external.ld the origin is set to something like search_address=0xEEE90000
|
||||
# if the value is above the search_address and inside a 40kb window (maximum size of an app) we replace it
|
||||
# with replace_address=(0x1008000 + m4 size) where the app will actually be located. The reason we do this instead just
|
||||
# using the right address in external.ld is gcc does not permit to use the same memory range multiple times.
|
||||
if val > search_address and (val - search_address) < maximum_application_size:
|
||||
relative_address = val - search_address
|
||||
new_address = replace_address + relative_address
|
||||
|
||||
new_snippet = new_address.to_bytes(4, byteorder='little')
|
||||
external_application_image += new_snippet
|
||||
else:
|
||||
external_application_image += snippet
|
||||
|
||||
return external_application_image
|
||||
|
||||
project_source_dir = sys.argv[1] #/portapack-mayhem/firmware/application
|
||||
binary_dir = sys.argv[2] #/portapack-mayhem/build/firmware/application
|
||||
cmake_objcopy = sys.argv[3]
|
||||
|
||||
memory_location_header_position = 0
|
||||
externalAppEntry_header_position = 4
|
||||
m4_app_tag_header_position = 72
|
||||
m4_app_offset_header_position = 76
|
||||
|
||||
for external_image_prefix in sys.argv[4:]:
|
||||
|
||||
# COMMAND ${CMAKE_OBJCOPY} -v -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}_ext_pacman.bin --only-section=.external_app_pacman
|
||||
himg = "{}/external_app_{}.himg".format(binary_dir, external_image_prefix)
|
||||
subprocess.run([cmake_objcopy, "-v", "-O", "binary", "{}/application.elf".format(binary_dir), himg, "--only-section=.external_app_{}".format(external_image_prefix)])
|
||||
|
||||
external_application_image = read_image(himg)
|
||||
|
||||
#m4 image @ 0x44
|
||||
chunk_data = external_application_image[m4_app_tag_header_position:m4_app_tag_header_position+4]
|
||||
|
||||
# skip m4 if not set
|
||||
if (chunk_data[0] == 0 and chunk_data[1] == 0 and chunk_data[2] == 0 and chunk_data[3] == 0):
|
||||
replace_address = 0x10080000
|
||||
search_address = int.from_bytes(external_application_image[externalAppEntry_header_position:externalAppEntry_header_position+4], byteorder='little') & 0xFFFF0000
|
||||
external_application_image = patch_image(external_application_image, search_address, replace_address)
|
||||
external_application_image[memory_location_header_position:memory_location_header_position+4] = replace_address.to_bytes(4, byteorder='little')
|
||||
|
||||
write_image(external_application_image, "{}/{}.ppma".format(binary_dir, external_image_prefix))
|
||||
continue
|
||||
|
||||
chunk_tag = chunk_data.decode("utf-8")
|
||||
m4_image = read_image("{}/../baseband/{}.bin".format(binary_dir, chunk_tag))
|
||||
app_image_len = len(external_application_image)
|
||||
external_application_image += m4_image
|
||||
|
||||
if (len(m4_image) % 4) != 0:
|
||||
print("m4 file size not divideable by 4")
|
||||
sys.exit(-1)
|
||||
|
||||
replace_address = 0x10080000 + len(m4_image)
|
||||
search_address = int.from_bytes(external_application_image[externalAppEntry_header_position:externalAppEntry_header_position+4], byteorder='little') & 0xFFFF0000
|
||||
external_application_image = patch_image(external_application_image, search_address, replace_address)
|
||||
|
||||
external_application_image[memory_location_header_position:memory_location_header_position+4] = replace_address.to_bytes(4, byteorder='little')
|
||||
external_application_image[m4_app_offset_header_position:m4_app_offset_header_position+4] = app_image_len.to_bytes(4, byteorder='little')
|
||||
|
||||
if (len(external_application_image) > maximum_application_size) != 0:
|
||||
print("application {} can not exceed 40kb: {} bytes used".format(external_image_prefix, len(external_application_image)))
|
||||
sys.exit(-1)
|
||||
|
||||
# write .ppma (portapack mayhem application)
|
||||
write_image(external_application_image, "{}/{}.ppma".format(binary_dir, external_image_prefix))
|
||||
Reference in New Issue
Block a user