Compare commits

...

74 Commits

Author SHA1 Message Date
gullradriel b306a0d490 Revert "Update Map view labels to match marine navigation standards (#2951)" (#2955)
This reverts commit c6130c6e8d.
2026-01-31 21:15:12 +01:00
Herbenderbler c6130c6e8d Update Map view labels to match marine navigation standards (#2951)
Replace Alt: with CoG: and Spd: with SoG: in the Map view to better align with standard marine navigation terminology and the AIS Rx screen.

- Alt (Altitude) → CoG (Course over Ground)
- Spd (Speed) → SoG (Speed over Ground)

Rationale: AIS is primarily used for marine navigation where altitude is typically zero (sea level), while Course over Ground is essential navigation information. This change provides consistency with the AIS Rx screen which already displays CoG and SoG.

Modified files:
- firmware/application/ui/ui_geomap.hpp
- firmware/standalone/common/ui/ui_geomap.hpp
2026-01-31 20:53:58 +01:00
Herbenderbler 97f2ca42f1 Fix aviation 8.33kHz step size to reduce frequency drift (#2950)
Update aviation radio step size from 8330 Hz to 8333 Hz in freqman_steps
and freqman_steps_short arrays. This better represents the actual 8⅓ kHz
(25000/3 Hz) aviation channel spacing and significantly reduces frequency
drift during dial tuning.
With this fix, stepping 24 times from 123.000 MHz now correctly reaches
123.200 MHz instead of 123.199 MHz.
2026-01-31 19:35:14 +01:00
Pezsma e2b642ae25 New Morse TX app (#2948)
* morse tx
* global button repeat function improvement
2026-01-30 17:18:13 +01:00
gullradriel 106d9b1207 updated submodule (#2947) 2026-01-30 10:14:08 +01:00
gullradriel c8ae419e0b updated submodule (#2946) 2026-01-30 00:21:36 +01:00
Totoo 2319dcd0d6 Aprs tx gps added, and save config added, path added (#2937) 2026-01-25 10:01:33 +01:00
lifegame1lu111 44d9ce00cd Holtek HT6P20B support for SubGhzD (#2941) 2026-01-25 09:41:06 +01:00
Pezsma 5cc5f8faa6 Morserx added ssb modes (#2934)
* USB receive good
* LSB receive good
* all mode receive good, log is dead
* frequency measurement good, decode dead
* total killed
* szutyok, gerjed
* frequenty meas only.
* ui ok
* Add frequency measurement handling and update Morse processing logic
* fix ssb speed
2026-01-22 18:49:21 +01:00
gullradriel 3a0ca480b0 conform to clang spec (#2925)
* conform to clang spec
* proper find call
* Update format-code.sh
2026-01-22 15:26:31 +01:00
Totoo 7a2b432bfa Dyn flash size (#2933) 2026-01-22 15:24:42 +01:00
zxkmm 86d7b345d0 waterfall color designer app (#2625) 2026-01-19 21:46:25 +01:00
Pezsma 877ede5f86 Added morse receiver app. (#2923)
* Adder morse receiver app. Works with cw and fm mode. Adaptive speed learning. Logging.
2026-01-16 09:57:45 +01:00
gullradriel 19eb1c40ab fix NumberField wrapping when steps is not '1' and minimum can be negative (#2920) 2026-01-11 22:49:18 +01:00
gullradriel 9c03a18893 Looking glass marker no encoder (#2919)
* enable repeat mode for select button too
* added marker + and marker - buttons, removed non effective iqcalc tuning
2026-01-10 20:19:10 +01:00
gullradriel cd504eb4a7 Udpdate hackrf submodule (#2906)
* add missing parameter to usb endpoint init
* make firmware compile with latest hackrf
* updated submodule
* format code
2026-01-10 16:19:53 +01:00
Totoo dc976c7f38 make sd over usb external (#2918) 2026-01-10 15:10:23 +01:00
Totoo 266d90a600 Updated subcar processors (#2913) 2026-01-10 14:47:33 +01:00
Totoo 2884bb9ab7 make siggen ext (#2917) 2026-01-10 21:25:41 +08:00
Totoo 133b2d2065 fix for #2903 (#2914) 2026-01-10 20:25:30 +08:00
Totoo 2f2671f76c Some tweaks to support some usecases without encoder, also maximize the NewButton's text size (#2915) 2026-01-10 20:21:57 +08:00
Totoo f5c2c78733 fix looking glass marker on portarf (#2912) 2026-01-09 20:32:31 +00:00
Harin Lee e4ab7227fc Fix typos of 'transceiver' (#2910) 2026-01-08 10:34:05 +01:00
Harin Lee 93799bde6d New approach to support RX-only mode more cleanly (#2908)
* Add enabled checks in ReceiverModel and TransmitterModel

* Move TX limit logic to TransmitterModel from radio API

* Add TX disable functionality and UI
2026-01-07 12:29:56 +08:00
RML 3d979a613a remove github pages CNAME (#2905) 2026-01-04 09:25:45 +01:00
Harin Lee ee97b51eb3 Add some features to support RX-only mode (#2904)
* Remove duplicate LED control code
* Add TX limit settings and UI
* Fix App Manager to handle non-application menu items
2026-01-03 22:10:34 +01:00
gullradriel 70ac1a03a1 Make stable release (#2895)
* updated submodule
* updated versions
2025-12-21 11:39:47 +01:00
Totoo 2261e53981 fix bit counts (#2894) 2025-12-21 11:38:35 +01:00
Totoo f86d3e51f1 Subcar (#2893) 2025-12-21 11:21:55 +01:00
StarVore Labs f71f19e719 Addition of SSTV RX application (#2888) 2025-12-17 13:14:19 +01:00
RocketGod c53adfc765 Refactor FLEX RX UI to use console for messages (#2890) 2025-12-14 10:08:39 -08:00
gullradriel 7e8ad83537 Update clang format to 18 (#2891)
* move to clang-format-18
* clang-format-18 indendation
2025-12-14 11:26:21 +01:00
RocketGod 472571b1ed Refactor FLEX RX UI to add color cycling and message log (#2885)
Replaces the console with a scrollable menu view for displaying FLEX messages, adds support for cycling text colors, and implements message logging with line wrapping and persistence. Also refactors frequency handling for persistence and updates UI element layout and initialization.
2025-12-12 19:40:15 +01:00
Tim Elfelt 106e56abc3 FLEX pager app (#2883)
* Add FLEX pager support

- Introduced a new FLEX configuration function in baseband_api.
- Added FLEX application view and associated UI elements.
- Implemented FLEX processing logic in proc_flex, including demodulation and message handling.
- Updated CMakeLists to include new FLEX source files and headers.
- Enhanced message system to support FLEX-specific messages and statistics.

This commit lays the groundwork for FLEX pager functionality, allowing for the reception and processing of FLEX messages.

* Fixed baseband and moved app to external with some other fixes.

* Format code

---------

Co-authored-by: RocketGod <57732082+RocketGod-git@users.noreply.github.com>
2025-12-12 17:43:51 +01:00
zxkmm 4129d57c09 fix dead link (#2882)
* fix dead link

* submodule
2025-12-06 07:34:10 -08:00
Totoo caac5e1041 Sonde + map (#2879)
* Update geomap view position on GPS data reception

* Refactor timestamp and temperature/humidity display formatting in SondeView

* Implement battery voltage reading for Meteomodem M20

* Enhance GPS data validation and update OSM zoom handling

- Introduced a new method to validate GPS data in the Packet structure.
- Updated the SondeView to use the new GPS validation method.
- Modified GeoMap to improve handling of OSM zoom levels and ensure consistent usage of real zoom values.
- Adjusted battery voltage calculation for Meteomodem M20 to ensure correct scaling.

* Add serial number extraction for Meteomodem M20 support

* Add support for Meteomodem M20 temperature and humidity readings

* Update log file naming to include timestamp in SondeView

* Add pressure reading support for Meteomodem M20 and update UI

* Update SondeView UI layout and enhance Meteomodem M20 packet handling

* Add vertical speed calculation and display to SondeView

* Fix set_fsk function parameter type for samplesPerSymbol
2025-11-26 19:13:21 +01:00
Totoo 6b02ba6e5d Faster osm (#2874)
Much faster OSM map handler
2025-11-21 09:30:01 +01:00
Totoo c01597baf2 Pocsag manual baud option (#2870)
* baseband part of manual baud control of pocsag

* added manual pocsag baud to ui, to fix #2546

* fix adult toys settings filename
2025-11-18 14:05:27 +01:00
Totoo 43a5163b77 prevent sonde pos to change to 0 when the frame is invalid. fixes #2862 (#2869) 2025-11-18 19:07:40 +08:00
jLynx c46cc431c9 Revert "resolve conflicts merge" (#2860)
This reverts commit 344aa0c741.
2025-11-08 10:21:33 +13:00
jLynx a4d23768c1 Update version.txt (#2858) 2025-11-08 10:13:35 +13:00
gullradriel 344aa0c741 resolve conflicts merge 2025-11-07 21:36:41 +01:00
gullradriel fd7637187a updated version number (#2855)
Co-authored-by: gullradriel <gullradriel@no-mail.com>
2025-11-07 20:52:28 +01:00
Totoo 674b865ec0 Refactor OnTouchEvent functions to return boolean values for better event handling (#2854) 2025-11-05 20:53:23 +01:00
Totoo 00c9c6b40e Fix shift key position (#2850) 2025-10-31 18:25:23 +01:00
RocketGod 113cbc42c9 Add sound effects (#2849)
* Add sound effects

Adding sound effects and volume adjuster in start menu. Sounds are a little clicky but I tinkered a long time and couldn't solve that. Maybe someone else wants to tinker a bit with me.

* Format code
2025-10-30 07:38:45 +01:00
RocketGod 454b8776b6 Refactor gfxEQView UI layout for consistency (#2848)
* Refactor gfxEQView UI layout for consistency

Updated control and graph positioning in gfxEQView to use a standard layout approach, aligning with other applications. Screen dimensions are now used for dynamic placement, and hardcoded values have been replaced for improved maintainability and consistency.

Basically, centered for smaller and larger screens with other visual improvements.

* Fixed screen to Macros - looks good now

Better methods for dynamic screen size
2025-10-30 07:33:38 +01:00
Matej Sochan efdf043d62 fix typo: wps -> wpm in morse_tx (#2847) 2025-10-28 20:55:21 +01:00
gullradriel 0f9d9e589d Revert "Additional debounce control parameters for rotary encoder settings (f…" (#2846)
This reverts commit bf18851b6b.
2025-10-28 22:34:14 +13:00
Totoo 3b57672dda sht4x support,, typo fix (#2843) 2025-10-28 10:14:11 +08:00
Totoo b739dd883c Free up some fw space (#2844) 2025-10-28 08:10:09 +08:00
rmckay bf18851b6b Additional debounce control parameters for rotary encoder settings (for bad/noisy encoders) (#2841)
* Added debounce control options for rotary encoder settings

* ran format-code.sh to adjust whitespace

---------

Co-authored-by: Robert McKay <robert.mckay@ubermorgen.land>
2025-10-27 23:56:27 +01:00
Pezsma 81afec9ea4 Added Adult toy controller external app (#2840) 2025-10-23 13:09:48 +02:00
Totoo 60a5222058 Pacman update (#2837) 2025-10-21 07:37:26 +02:00
Totoo 7041d507ca gracefully handle i2c error (#2838) 2025-10-21 07:37:14 +02:00
Totoo d3373734e9 battleship resizes based on screen size (#2836) 2025-10-18 17:00:50 +02:00
Totoo 200831ad67 fix some bugs due to framesync not fired on stealth mode (#2834) 2025-10-18 13:13:51 +02:00
gullradriel dafe3b7641 update hackrf submodule (#2832) 2025-10-17 19:28:18 +02:00
gullradriel 63634407b6 fix ctrl click link (#2833) 2025-10-17 17:09:57 +00:00
Totoo 9f06ccf9ae Support some space chars in serial commands (fs) (#2828) 2025-10-17 17:05:27 +02:00
Totoo 6782e12a15 Fix morse tx cw+loop (#2827) 2025-10-17 17:03:34 +02:00
Totoo cbbd8103d3 Touch for MenuView and Freqlist (#2829)
* freqman add touch support. solves #1919

* menuview touch support
2025-10-17 21:59:35 +08:00
Pezsma e88ab44f49 Modify Morse code table (#2825)
Fixed morse code table
2025-10-17 10:32:01 +02:00
Pezsma 545dead1ca Added new game: Morse trainer (#2820)
* Added new game: Morse trainer

* code format error fix, and ui alignment
2025-10-16 13:01:28 +02:00
Totoo e3b2a65b39 Oregon fixes (#2822) 2025-10-16 13:00:40 +02:00
Totoo 2f4d222403 New memory address for i2c dev loaded apps (#2817)
* New memory address for i2c dev loaded apps

* better download handling on error

* new hackrf version (added PortapackRf support)

* lock i2cdev while loading apps
2025-10-14 09:45:52 +02:00
Totoo 54d99945f2 fix touch calib on prf (#2816) 2025-10-14 09:03:28 +08:00
RocketGod 65a3c797c6 Blackjack - Dynamic screen for PortaRF (#2814)
Blackjack - Dynamic screen for PortaRF
2025-10-10 22:48:26 +02:00
Totoo 6a81a981aa Fix standalont touch_release event (#2813) 2025-10-10 22:47:17 +02:00
RocketGod 193ac73b34 Doom UI improvements for dynamic screen (#2812)
* Doom dynamic screen and other improvements

Doom dynamic screen and other improvements

* Res divider fix for big screens

* Format code
2025-10-09 22:54:54 +02:00
RocketGod 477d035dce Dino Game Dynamic Screen for PortaRF plus sprite animation improvements (#2811)
* Dino Game Dynamic Screen for PortaRF plus sprite animation improvements

Dino Game Dynamic Screen for PortaRF plus sprite animation improvements

* Format code per usual
2025-10-09 22:31:26 +02:00
RocketGod 1d8f53b49e Space invaders UI improvements for all device screens including new PortaRF (#2810)
* Dynamic screen for PortaRF

Dynamic screen size for new PortaRF device.

* Format code

* Space Invaders improvements for all devices including new PortaRF

Space Invaders improvements for all devices including new PortaRF
2025-10-09 22:30:30 +02:00
Totoo db324a5225 PortaRF battery fix (#2809) 2025-10-09 22:29:53 +02:00
Totoo b2331bf818 clarify battery settings (#2805) 2025-10-06 10:29:17 +02:00
248 changed files with 15000 additions and 1437 deletions
+6 -7
View File
@@ -1,14 +1,13 @@
---
BasedOnStyle: Chromium
IndentWidth: '4'
SortIncludes: 'false'
AllowAllArgumentsOnNextLine: 'true'
IndentWidth: 4
SortIncludes: false
AllowAllArgumentsOnNextLine: true
ColumnLimit: 0
Cpp11BracedListStyle: 'true'
AllowShortLoopsOnASingleLine: 'true'
AllowShortIfStatementsOnASingleLine: 'true'
Cpp11BracedListStyle: true
AllowShortLoopsOnASingleLine: true
AllowShortIfStatementsOnASingleLine: true
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: 1
...
+1
View File
@@ -19,5 +19,6 @@ jobs:
- name: clang-format Check
uses: jidicula/clang-format-action@v4.11.0
with:
clang-format-version: '18'
check-path: ${{ matrix.path }}
fallback-style: Chromium
+1 -1
View File
@@ -1 +1 @@
v2.1.0
v2.3.1
+1 -1
View File
@@ -1 +1 @@
v2.2.0
v2.3.2
+1
View File
@@ -83,3 +83,4 @@ venv/
# generated bitmap arr file
# TODO: generate bitmap during build, since we use python during build anyway, lemme know if this is a bad idea @zxkmm
/firmware/tools/bitmap.hpp
firmware/flashsize.h
+21
View File
@@ -27,6 +27,27 @@ set(CMAKE_COLOR_MAKEFILE ON)
add_compile_options(-fdiagnostics-color=always)
#Flash size related options
#This is the size of the flash memory we SUPPORT. Don't worry, the internl flash app has harder limits based on device type.
if(NOT DEFINED FLASH_MB_SIZE)
set(FLASH_MB_SIZE 2) # Default to 2MB if not provided. For PortaRf this should be 2MB passed to cmake with -DFLASH_MB_SIZE=2
endif()
message("Configuring for FLASH_MB_SIZE=${FLASH_MB_SIZE}MB")
#This is the flash memory size we build. This can be less, so the FW size will be less, so can be flashed with older utils.
if(NOT DEFINED FLASH_MB_LIMIT_SIZE)
set(FLASH_MB_LIMIT_SIZE 1) #TODO: should be ${FLASH_MB_SIZE} remove once we got a stable build for all devices we support with bigger than 1 mb flash
endif()
message("Configuring for FLASH_MB_LIMIT_SIZE=${FLASH_MB_LIMIT_SIZE}MB")
if(FLASH_MB_SIZE LESS FLASH_MB_LIMIT_SIZE)
message(FATAL_ERROR "Error: The supported flash size is lower than the generated flash file. Build halted.")
endif()
math(EXPR FLASH_BYTES_SIZE "${FLASH_MB_SIZE} * 1024 * 1024")
math(EXPR FLASH_BYTES_LIMIT_SIZE "${FLASH_MB_LIMIT_SIZE} * 1024 * 1024")
#generate h files
configure_file(flashsize.h.in ${CMAKE_CURRENT_SOURCE_DIR}/firmware/flashsize.h)
option(USE_CCACHE "Enable ccache, please keep an eye on this because sometimes it's not quite stable and generated unusable binary" OFF)
project(portapack-h1)
-1
View File
@@ -1 +0,0 @@
portapack.ried.cl
+1 -1
View File
@@ -56,7 +56,7 @@ add_subdirectory(test)
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
add_custom_command(
OUTPUT ${FIRMWARE_FILENAME}
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME}
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} ${FLASH_BYTES_LIMIT_SIZE}
DEPENDS baseband application ${MAKE_SPI_IMAGE}
${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin
VERBATIM
+3 -4
View File
@@ -192,7 +192,6 @@ set(CPPSRC
core_control.cpp
database.cpp
gradient.cpp
rfm69.cpp
event_m0.cpp
file_reader.cpp
file.cpp
@@ -261,7 +260,6 @@ set(CPPSRC
ui/ui_receiver.cpp
ui/ui_rssi.cpp
ui/ui_freqlist.cpp
ui/ui_tv.cpp
ui/ui_spectrum.cpp
ui/ui_tabview.cpp
ui/ui_textentry.cpp
@@ -298,10 +296,8 @@ set(CPPSRC
apps/ui_rds.cpp
apps/ui_recon_settings.cpp
apps/ui_recon.cpp
apps/ui_sd_over_usb.cpp
apps/ui_search.cpp
apps/ui_settings.cpp
apps/ui_siggen.cpp
apps/ui_sonde.cpp
apps/ui_ss_viewer.cpp
apps/ui_standalone_view.cpp
@@ -471,6 +467,9 @@ add_custom_command(
)
add_executable(${PROJECT_NAME}.elf ${CSRC} ${CPPSRC} ${ASMSRC})
target_link_options(${PROJECT_NAME}.elf PRIVATE
"LINKER:--defsym,LD_FLASH_SIZE=${FLASH_BYTES_LIMIT_SIZE}"
)
set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT})
add_definitions(${DEFS})
include_directories(. ${INCDIR})
+8
View File
@@ -68,6 +68,9 @@ void BoundSetting::parse(std::string_view value) {
as<bool>() = (parsed != 0);
break;
}
case SettingType::Float:
as<float>() = atof(value.data());
break;
};
}
@@ -101,6 +104,11 @@ void BoundSetting::write(File& file) const {
case SettingType::Bool:
file.write(as<bool>() ? "1" : "0", 1);
break;
case SettingType::Float: {
auto float_str = to_string_decimal(as<float>(), 6);
file.write(float_str.c_str(), float_str.length());
}
}
file.write("\r\n", 2);
+4
View File
@@ -52,6 +52,7 @@ class BoundSetting {
U8,
String,
Bool,
Float
};
public:
@@ -73,6 +74,9 @@ class BoundSetting {
BoundSetting(std::string_view name, bool* target)
: name_{name}, target_{target}, type_{SettingType::Bool} {}
BoundSetting(std::string_view name, float* target)
: name_{name}, target_{target}, type_{SettingType::Float} {}
std::string_view name() const { return name_; }
void parse(std::string_view value);
void write(File& file) const;
+1 -1
View File
@@ -159,7 +159,7 @@ class AISAppView : public View {
~AISAppView();
void set_parent_rect(const Rect new_parent_rect) override;
void paint(Painter&) override{};
void paint(Painter&) override {};
void focus() override;
+1 -1
View File
@@ -59,7 +59,7 @@ class BLECommView : public View {
~BLECommView();
void set_parent_rect(const Rect new_parent_rect) override;
void paint(Painter&) override{};
void paint(Painter&) override {};
void focus() override;
+1 -1
View File
@@ -209,7 +209,7 @@ class BLERxView : public View {
~BLERxView();
void set_parent_rect(const Rect new_parent_rect) override;
void paint(Painter&) override{};
void paint(Painter&) override {};
void focus() override;
+1 -1
View File
@@ -105,7 +105,7 @@ class BLETxView : public View {
~BLETxView();
void set_parent_rect(const Rect new_parent_rect) override;
void paint(Painter&) override{};
void paint(Painter&) override {};
void focus() override;
+5 -2
View File
@@ -56,6 +56,7 @@ POCSAGSettingsView::POCSAGSettingsView(
: settings_{settings} {
add_children(
{&labels,
&opt_baud_rate,
&check_log,
&check_log_raw,
&check_small_font,
@@ -65,6 +66,7 @@ POCSAGSettingsView::POCSAGSettingsView(
&field_filter_address,
&button_save});
opt_baud_rate.set_by_value(settings_.baud_rate);
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);
@@ -81,7 +83,7 @@ POCSAGSettingsView::POCSAGSettingsView(
settings_.hide_addr_only = check_hide_addr_only.value();
settings_.filter_mode = opt_filter_mode.selected_index_value();
settings_.filter_address = field_filter_address.to_integer();
settings_.baud_rate = opt_baud_rate.selected_index_value();
nav.pop();
};
}
@@ -142,7 +144,7 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
audio::output::start();
receiver_model.enable();
baseband::set_pocsag();
baseband::set_pocsag((int8_t)settings_.baud_rate);
}
void POCSAGAppView::focus() {
@@ -182,6 +184,7 @@ void POCSAGAppView::refresh_ui() {
btn_text = "Filter Last";
break;
}
baseband::set_pocsag((int8_t)settings_.baud_rate);
button_filter_last.set_text(btn_text);
}
+11
View File
@@ -126,6 +126,7 @@ struct POCSAGSettings {
bool hide_bad_data = false;
bool hide_addr_only = false;
uint8_t filter_mode = false;
int32_t baud_rate = -1;
uint32_t filter_address = 0;
};
@@ -139,7 +140,16 @@ class POCSAGSettingsView : public View {
private:
POCSAGSettings& settings_;
OptionsField opt_baud_rate{
{8 * 8, 0 * 16},
4,
{{"Auto", -1},
{" 512", 0},
{"1200", 1},
{"2400", 2}}};
Labels labels{
{{2 * 8, 0 * 16}, "Baud:", Theme::getInstance()->fg_light->foreground},
{{2 * 8, 12 * 16}, "Filter Mode:", Theme::getInstance()->fg_light->foreground},
{{2 * 8, 13 * 16}, "Filter Addr:", Theme::getInstance()->fg_light->foreground},
};
@@ -221,6 +231,7 @@ class POCSAGAppView : public View {
{"filter_address"sv, &settings_.filter_address},
{"hide_bad_data"sv, &settings_.hide_bad_data},
{"hide_addr_only"sv, &settings_.hide_addr_only},
{"baud_rate"sv, &settings_.baud_rate},
}};
void refresh_ui();
@@ -10,7 +10,7 @@ namespace ui {
constexpr std::string_view mayhem_information_list[] = {
"#****** Mayhem Community ******",
" ",
" https://discord.mayhem.app",
" https://discord.hackrf.app",
" ",
"#**** List of contributors ****",
" ",
+124 -1
View File
@@ -29,6 +29,7 @@
#include "baseband_api.hpp"
#include "portapack_shared_memory.hpp"
#include "portapack_persistent_memory.hpp"
#include "ui_geomap.hpp"
#include <cstring>
#include <stdio.h>
@@ -49,10 +50,24 @@ APRSTXView::~APRSTXView() {
void APRSTXView::start_tx() {
// TODO: Clean up this API to take string_views to avoid allocations.
std::string new_payload = payload;
std::string gps = text_gps_coord.get();
std::string token = "?GPS?";
size_t pos = 0;
while ((pos = new_payload.find(token, pos)) != std::string::npos) {
new_payload.replace(pos, token.size(), gps);
pos += gps.size();
}
text_payload.set(new_payload);
std::string path = text_path.get();
make_aprs_frame(
sym_source.to_string().c_str(), num_ssid_source.value(),
sym_dest.to_string().c_str(), num_ssid_dest.value(),
payload);
new_payload, path);
// uint8_t * bb_data_ptr = shared_memory.bb_data.data;
// text_payload.set(to_string_hex_array(bb_data_ptr + 56, 15));
@@ -77,6 +92,61 @@ void APRSTXView::on_tx_progress(const uint32_t progress, const bool done) {
}
}
void APRSTXView::process_coordinates(float lat_, float lon_) {
std::string s;
char ns = lat_ >= 0 ? 'N' : 'S';
float lat = (lat_ >= 0) ? lat_ : -lat_;
uint32_t lat_d = (uint32_t)lat;
uint32_t lat_m = (uint32_t)((lat - lat_d) * 6000.0f + 0.5f);
if (lat_m >= 6000) {
lat_m = 0;
lat_d++;
}
s += to_string_dec_uint(lat_d, 2, '0');
s += to_string_dec_uint(lat_m / 100, 2, '0');
s += ".";
s += to_string_dec_uint(lat_m % 100, 2, '0');
s += ns;
s += "/";
char ew = lon_ >= 0 ? 'E' : 'W';
float lon = (lon_ >= 0) ? lon_ : -lon_;
uint32_t lon_d = (uint32_t)lon;
uint32_t lon_m = (uint32_t)((lon - lon_d) * 6000.0f + 0.5f);
if (lon_m >= 6000) {
lon_m = 0;
lon_d++;
}
s += to_string_dec_uint(lon_d, 3, '0');
s += to_string_dec_uint(lon_m / 100, 2, '0');
s += ".";
s += to_string_dec_uint(lon_m % 100, 2, '0');
s += ew;
text_gps_coord.set(s);
}
void APRSTXView::on_gps(const GPSPosDataMessage* message) {
if (manual_gps_mode) {
return; // no more update once set manually
}
if (message->lat < -90.0 || message->lat > 90.0 ||
message->lon < -180.0 || message->lon > 180.0) {
return;
}
if (message->lat == 0.0f && message->lon == 0.0f) {
return;
}
last_lat = message->lat;
last_lon = message->lon;
process_coordinates(message->lat, message->lon);
}
APRSTXView::APRSTXView(NavigationView& nav) {
baseband::run_image(portapack::spi_flash::image_tag_afsk);
@@ -87,8 +157,33 @@ APRSTXView::APRSTXView(NavigationView& nav) {
&num_ssid_dest,
&text_payload,
&button_set,
&text_gps_coord,
&button_mangps,
&gps_is_manual,
&text_path,
&button_setpath,
&tx_view});
sym_source.set_value(symsrc);
num_ssid_source.set_value(ssidsrc);
sym_dest.set_value(symdst);
num_ssid_dest.set_value(ssiddst);
text_payload.set(payload);
text_path.set(path_cache);
sym_source.on_change = [this](SymField&) {
symsrc = sym_source.to_string();
};
num_ssid_source.on_change = [this](int32_t v) {
ssidsrc = v;
};
sym_dest.on_change = [this](SymField&) {
symdst = sym_dest.to_string();
};
num_ssid_dest.on_change = [this](int32_t v) {
ssiddst = v;
};
button_set.on_select = [this, &nav](Button&) {
text_prompt(
nav,
@@ -99,6 +194,17 @@ APRSTXView::APRSTXView(NavigationView& nav) {
text_payload.set(s);
});
};
button_setpath.on_select = [this, &nav](Button&) {
text_prompt(
nav,
path_cache,
60,
ENTER_KEYBOARD_MODE_ALPHA,
[this](std::string& s) {
text_path.set(s);
path_cache = s;
});
};
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
@@ -116,6 +222,23 @@ APRSTXView::APRSTXView(NavigationView& nav) {
tx_view.set_transmitting(false);
transmitter_model.disable();
};
button_mangps.on_select = [this, &nav](Button&) {
nav.push<GeoMapView>(
0,
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::HIDDEN,
last_lat,
last_lon,
[this](int32_t, float lat, float lon, int32_t) {
last_lat = lat;
last_lon = lon;
manual_gps_mode = true;
gps_is_manual.set("MANUAL");
process_coordinates(lat, lon);
});
};
// process_coordinates(last_lat, last_lon); //don't load last, so won't confuse users
}
} /* namespace ui */
+62 -12
View File
@@ -50,59 +50,109 @@ class APRSTXView : public View {
1750000 /* bandwidth */,
AFSK_TX_SAMPLERATE /* sampling rate */
};
app_settings::SettingsManager settings_{
"tx_aprs", app_settings::Mode::TX};
std::string symsrc = "";
std::string symdst = "";
std::string payload{""};
std::string path_cache = "";
int32_t ssidsrc = 0;
int32_t ssiddst = 0;
bool manual_gps_mode = false;
float last_lat = 0.0f;
float last_lon = 0.0f;
app_settings::SettingsManager settings_{
"tx_aprs",
app_settings::Mode::TX,
{{"symsrc"sv, &symsrc},
{"symdst"sv, &symdst},
{"ssidsrc"sv, &ssidsrc},
{"ssiddst"sv, &ssiddst},
{"payload"sv, &payload},
{"last_lat"sv, &last_lat},
{"last_lon"sv, &last_lon},
{"path"sv, &path_cache}}};
void start_tx();
void generate_frame();
void generate_frame_pos();
void on_tx_progress(const uint32_t progress, const bool done);
void on_gps(const GPSPosDataMessage* message);
void process_coordinates(float lat, float lon);
Labels labels{
{{UI_POS_X(0), 1 * 16}, "Source: SSID:", Theme::getInstance()->fg_light->foreground}, // 6 alphanum + SSID
{{UI_POS_X(0), 2 * 16}, " Dest.: SSID:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 4 * 16}, "Info field:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(0)}, "Source: SSID:", Theme::getInstance()->fg_light->foreground}, // 6 alphanum + SSID
{{UI_POS_X(0), UI_POS_Y(1)}, " Dest.: SSID:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(2)}, "Path: (ex.: WIDE1-1,WIDE2-1)", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(6)}, "Info field:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(10)}, "GPS:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_CENTER(22), UI_POS_Y(14)}, "Use ?GPS? in the info", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_CENTER(17), UI_POS_Y(15)}, "as a placeholder.", Theme::getInstance()->fg_light->foreground},
};
Text text_path{
{UI_POS_X(0), UI_POS_Y(3), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)},
"WIDE1-1",
};
Text gps_is_manual{
{UI_POS_X(6), UI_POS_Y(10), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"",
};
SymField sym_source{
{7 * 8, 1 * 16},
{UI_POS_X(7), UI_POS_Y(0)},
6,
SymField::Type::Alpha};
NumberField num_ssid_source{
{19 * 8, 1 * 16},
{UI_POS_X(19), UI_POS_Y(0)},
2,
{0, 15},
1,
' '};
SymField sym_dest{
{7 * 8, 2 * 16},
{UI_POS_X(7), UI_POS_Y(1)},
6,
SymField::Type::Alpha};
NumberField num_ssid_dest{
{19 * 8, 2 * 16},
{UI_POS_X(19), UI_POS_Y(1)},
2,
{0, 15},
1,
' '};
Text text_payload{
{UI_POS_X(0), 5 * 16, screen_width, 16},
"-"};
{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)},
""};
Button button_set{
{UI_POS_X(0), 6 * 16, 80, 32},
{UI_POS_X(0), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)},
"Set"};
Text text_gps_coord{
{UI_POS_X(0), UI_POS_Y(11), UI_POS_WIDTH(20), UI_POS_HEIGHT(1)},
"-"};
Button button_mangps{
{UI_POS_X(0), UI_POS_Y(12), UI_POS_WIDTH(14), UI_POS_HEIGHT(2)},
"Manual GPS"};
Button button_setpath{
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(14), UI_POS_HEIGHT(2)},
"Set Path"};
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
5000,
0 // disable setting bandwith, since APRS used fixed 10k bandwidth
};
MessageHandlerRegistration message_handler_gps{
Message::ID::GPSPosData,
[this](Message* const p) {
const auto message = static_cast<const GPSPosDataMessage*>(p);
this->on_gps(message);
}};
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
+1 -1
View File
@@ -98,7 +98,7 @@ class BattinfoView : public View {
"Volt"};
Button button_exit{
{UI_POS_X_CENTER(12), 17 * 16, UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Back"};
static msg_t static_fn(void* arg);
Thread* thread{nullptr};
@@ -45,10 +45,15 @@ bool valid_firmware_file(std::filesystem::path::string_type path) {
// test read of the whole file just to validate checksum (baseband flash code will re-read when flashing)
auto result = firmware_file.open(path.c_str());
if (!result.is_valid()) {
uint64_t file_size = firmware_file.size();
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK && file_size > 1 * 1024 * 1024) {
// Portapack firmware files must not be larger than 1MB
return false;
}
// May need to add a check to portarf and portarf pro too.
checksum = 0;
for (uint32_t offset = 0; offset < FLASH_ROM_SIZE; offset += sizeof(read_buffer)) {
for (uint64_t offset = 0; offset < FLASH_ROM_SIZE && offset < file_size; offset += sizeof(read_buffer)) {
auto readResult = firmware_file.read(&read_buffer, sizeof(read_buffer));
if ((!readResult) || (readResult.value() != sizeof(read_buffer))) {
// File was smaller than 1MB:
// If version is such that the file SHOULD have been 1MB, call it a checksum error (otherwise say it's OK).
@@ -32,7 +32,9 @@
#include "untar.hpp"
#include <cstdint>
#define FLASH_ROM_SIZE 1048576
#include "../../flashsize.h"
#define FLASH_ROM_SIZE (FLASH_SIZE_MB * 1024 * 1024)
#define FLASH_STARTING_ADDRESS 0x00000000
#define FLASH_EXPECTED_CHECKSUM 0x00000000
#define FLASH_CHECKSUM_ERROR 0xFFFFFFFF
+1 -1
View File
@@ -65,7 +65,7 @@ class FreqManBaseView : public View {
OptionsField options_category{
{3 * 8, 2},
20 /* length */,
19 /* length */,
{}};
FreqManUIList freqlist_view{
+1 -1
View File
@@ -171,7 +171,7 @@ void IQTrimView::update_range_controls(iq::TrimRange trim_range) {
}
void IQTrimView::profile_capture() {
power_buckets_ = {};
power_buckets_.clear();
iq::PowerBuckets buckets{
.p = power_buckets_.data(),
.size = power_buckets_.size()};
@@ -69,7 +69,7 @@ void GlassView::manage_beep_audio() {
}
}
void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power) {
void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint16_t bin, uint8_t& max_power) {
if (mode == LOOKING_GLASS_SINGLEPASS) {
// <20MHz spectrum mode
if (bin < 120) {
@@ -91,7 +91,7 @@ void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint
}
}
rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) {
rf::Frequency GlassView::get_freq_from_bin_pos(uint16_t pos) {
rf::Frequency freq_at_pos = 0;
if (mode == LOOKING_GLASS_SINGLEPASS) {
// starting from the middle, minus 8 ignored bin on each side. Since pos is [-120,120] after the (pos - 120), it's divided by screen_width(240)/2 => 120
@@ -101,7 +101,14 @@ rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) {
return freq_at_pos;
}
void GlassView::on_marker_change() {
void GlassView::on_marker_change(int8_t delta) {
if ((marker_pixel_index + delta) < 0)
marker_pixel_index = marker_pixel_index + delta + screen_width;
else if ((marker_pixel_index + delta) > screen_width)
marker_pixel_index = marker_pixel_index + delta - screen_width;
else
marker_pixel_index = marker_pixel_index + delta;
marker = get_freq_from_bin_pos(marker_pixel_index);
field_marker.set_text(to_string_short_freq(marker));
plot_marker(marker_pixel_index); // Refresh marker on screen
@@ -200,7 +207,7 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
baseband::spectrum_streaming_stop();
// Convert bins of this spectrum slice into a representative max_power and when enough, into pixels
// we actually need screen_width (240) of those bins
for (uint8_t bin = 0; bin < bin_length; bin++) {
for (uint16_t bin = 0; bin < bin_length; bin++) {
get_max_power(spectrum, bin, max_power);
if (max_power > range_max_power)
range_max_power = max_power;
@@ -208,7 +215,7 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
if (bin == 119) {
uint8_t next_max_power = 0;
get_max_power(spectrum, bin + 1, next_max_power);
for (uint8_t it = 0; it < ignore_dc; it++) {
for (uint16_t it = 0; it < ignore_dc; it++) {
uint8_t med_max_power = (max_power + next_max_power) / 2; // due to the way process_bins works we have to keep resetting the color
if (process_bins(&med_max_power) == true)
return; // new line signaled, return
@@ -285,7 +292,7 @@ void GlassView::on_range_changed() {
max_power = 0;
bins_hz_size = 0;
on_marker_change();
on_marker_change(0);
update_range_field();
// set the sample rate and bandwidth
@@ -298,8 +305,8 @@ void GlassView::on_range_changed() {
receiver_model.set_target_frequency(f_center); // tune rx for this slice
}
void GlassView::plot_marker(uint8_t pos) {
uint8_t shift_y = 0;
void GlassView::plot_marker(uint16_t pos) {
uint16_t shift_y = 0;
if (live_frequency_view > 0) // plot one line down when in live view
{
shift_y = 16;
@@ -381,9 +388,10 @@ GlassView::GlassView(
&button_beep_squelch,
&field_marker,
&field_trigger,
&button_marker_minus,
&button_marker_plus,
&button_jump,
&button_rst,
&field_rx_iq_phase_cal,
&freq_stats});
load_presets(); // Load available presets from TXT files (or default).
@@ -491,13 +499,7 @@ GlassView::GlassView(
range_presets.set_selected_index(preset_index);
field_marker.on_encoder_change = [this](TextField&, EncoderEvent delta) {
if ((marker_pixel_index + delta) < 0)
marker_pixel_index = marker_pixel_index + delta + screen_width;
else if ((marker_pixel_index + delta) > screen_width)
marker_pixel_index = marker_pixel_index + delta - screen_width;
else
marker_pixel_index = marker_pixel_index + delta;
on_marker_change();
on_marker_change(delta);
};
field_marker.on_select = [this](TextField&) {
@@ -516,6 +518,14 @@ GlassView::GlassView(
update_range_field();
};
button_marker_minus.on_select = [this](Button&) {
on_marker_change(-1);
};
button_marker_plus.on_select = [this](Button&) {
on_marker_change(1);
};
button_jump.on_select = [this](Button&) {
// Launch Audio with peak frequency.
launch_audio(max_freq_hold);
@@ -525,13 +535,6 @@ GlassView::GlassView(
reset_live_view();
};
field_rx_iq_phase_cal.set_range(0, hackrf_r9 ? 63 : 31); // max2839 has 6 bits [0..63], max2837 has 5 bits [0..31]
field_rx_iq_phase_cal.set_value(get_spec_iq_phase_calibration_value()); // using accessor function of AnalogAudioView to read iq_phase_calibration_value from rx_audio.ini
field_rx_iq_phase_cal.on_change = [this](int32_t v) {
set_spec_iq_phase_calibration_value(v); // using accessor function of AnalogAudioView to write inside SPEC submenu, register value to max283x and save it to rx_audio.ini
};
set_spec_iq_phase_calibration_value(get_spec_iq_phase_calibration_value()); // initialize iq_phase_calibration in radio
display.scroll_set_area(109, screen_height - 1); // Restart scroll on the correct coordinates
// trigger:
@@ -581,15 +584,6 @@ void GlassView::on_freqchg(int64_t freq) {
on_range_changed();
}
uint8_t GlassView::get_spec_iq_phase_calibration_value() { // define accessor functions inside AnalogAudioView to read & write real iq_phase_calibration_value
return iq_phase_calibration_value;
}
void GlassView::set_spec_iq_phase_calibration_value(uint8_t cal_value) { // define accessor functions
iq_phase_calibration_value = cal_value;
radio::set_rx_max283x_iq_phase_calibration(iq_phase_calibration_value);
}
void GlassView::load_presets() {
File presets_file;
auto error = presets_file.open(looking_glass_dir / u"PRESETS.TXT");
@@ -66,9 +66,6 @@ class GlassView : public View {
void on_hide() override;
void focus() override;
uint8_t get_spec_iq_phase_calibration_value();
void set_spec_iq_phase_calibration_value(uint8_t cal_value);
private:
NavigationView& nav_;
Gradient gradient{};
@@ -80,11 +77,10 @@ class GlassView : public View {
uint8_t filter_index = 0; // OFF
uint8_t trigger = 32;
uint8_t mode = LOOKING_GLASS_FASTSCAN;
uint8_t live_frequency_view = 0; // Spectrum
uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4
uint8_t iq_phase_calibration_value{15}; // initial default RX IQ phase calibration value , used for both max2837 & max2839
int32_t beep_squelch = 20; // range from -100 to +20, >=20 disabled
bool beep_enabled = false; // activate on bip button click
uint8_t live_frequency_view = 0; // Spectrum
uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4
int32_t beep_squelch = 20; // range from -100 to +20, >=20 disabled
bool beep_enabled = false; // activate on bip button click
app_settings::SettingsManager settings_{
"rx_glass"sv,
app_settings::Mode::RX,
@@ -97,7 +93,6 @@ class GlassView : public View {
{"scan_mode"sv, &mode},
{"freq_view"sv, &live_frequency_view},
{"freq_integrate"sv, &live_frequency_integrate},
{"iq_phase_calibration"sv, &iq_phase_calibration_value}, // we are saving and restoring that CAL from Settings.
{"beep_squelch"sv, &beep_squelch},
{"beep_enabled"sv, &beep_enabled},
}};
@@ -116,9 +111,9 @@ class GlassView : public View {
void update_min(int32_t v);
void update_max(int32_t v);
void update_range_field();
void get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power);
rf::Frequency get_freq_from_bin_pos(uint8_t pos);
void on_marker_change();
void get_max_power(const ChannelSpectrum& spectrum, uint16_t bin, uint8_t& max_power);
rf::Frequency get_freq_from_bin_pos(uint16_t pos);
void on_marker_change(int8_t delta);
void retune();
bool process_bins(uint8_t* powerlevel);
void on_channel_spectrum(const ChannelSpectrum& spectrum);
@@ -128,7 +123,7 @@ class GlassView : public View {
void on_range_changed();
void reset_live_view();
void add_spectrum_pixel(uint8_t power);
void plot_marker(uint8_t pos);
void plot_marker(uint16_t pos);
void load_presets();
void populate_presets();
void launch_audio(rf::Frequency center_freq);
@@ -138,7 +133,7 @@ class GlassView : public View {
rf::Frequency f_center_ini{0};
rf::Frequency marker{0};
uint8_t marker_pixel_index{0};
uint16_t marker_pixel_index{0};
rf::Frequency marker_pixel_step{0};
// Size of one spectrum bin in Hz.
@@ -173,8 +168,7 @@ class GlassView : public View {
{{0, UI_POS_Y(0)}, "MIN: MAX: LNA VGA ", Theme::getInstance()->fg_light->foreground},
{{0, 1 * 16}, "RANGE: FILTER: AMP:", Theme::getInstance()->fg_light->foreground},
{{0, 2 * 16}, "P:", Theme::getInstance()->fg_light->foreground},
{{0, 3 * 16}, "MARKER: MHz RXIQCAL", Theme::getInstance()->fg_light->foreground},
//{{0, 4 * 16}, "RES: STEPS:", Theme::getInstance()->fg_light->foreground}};
{{0, 3 * 16}, "MARKER( / ): MHz", Theme::getInstance()->fg_light->foreground},
{{0, 4 * 16}, "RES: VOL:", Theme::getInstance()->fg_light->foreground}};
NumberField field_frequency_min{
@@ -223,16 +217,16 @@ class GlassView : public View {
""};
TextField field_marker{
{7 * 8, 3 * 16, 9 * 8, 16},
{12 * 8, 3 * 16, 9 * 8, 16},
""};
NumberField field_rx_iq_phase_cal{
{28 * 8, 3 * 16},
2,
{0, 63}, // 5 or 6 bits IQ CAL phase adjustment (range updated later)
1,
' ',
};
Button button_marker_minus{
{7 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
"-"};
Button button_marker_plus{
{9 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
"+"};
NumberField field_trigger{
{4 * 8, 4 * 16},
+2 -2
View File
@@ -1086,8 +1086,8 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
if (stepper < 0) stepper++;
if (stepper > 0) stepper--;
} // if( recon || stepper != 0 || index_stepper != 0 )
} // if (frequency_list.size() > 0 )
} /* on_statistics_updates */
} // if (frequency_list.size() > 0 )
} /* on_statistics_updates */
}
handle_retune();
recon_redraw();
+34
View File
@@ -309,6 +309,39 @@ SetFrequencyCorrectionModel SetRadioView::form_collect() {
};
}
/* SetTXLimitView ************************************/
SetTXLimitView::SetTXLimitView(NavigationView& nav) {
add_children({
&labels,
&tx_gain_max_db,
&tx_disable_switch,
&tx_amp_disable_switch,
&button_save,
&button_cancel,
});
tx_disable_switch.set_value(pmem::config_tx_disabled());
tx_amp_disable_switch.set_value(pmem::config_tx_amp_disabled());
tx_gain_max_db.set_value(pmem::config_tx_gain_max_db());
button_save.on_select = [&nav, this](Button&) {
pmem::set_config_tx_disabled(tx_disable_switch.value());
pmem::set_config_tx_amp_disabled(tx_amp_disable_switch.value());
pmem::set_config_tx_gain_max_db(tx_gain_max_db.value());
send_system_refresh();
nav.pop();
};
button_cancel.on_select = [&nav, this](Button&) {
nav.pop();
};
}
void SetTXLimitView::focus() {
button_save.focus();
}
/* SetUIView *********************************************/
SetUIView::SetUIView(NavigationView& nav) {
@@ -1107,6 +1140,7 @@ void SettingsMenuView::on_populate() {
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetFrequencyCorrectionView>(); }},
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [this]() { nav_.push<SetPersistentMemoryView>(); }},
{"Radio", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetRadioView>(); }},
{"TX Limit", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetTXLimitView>(); }},
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [this]() { nav_.push<SetSDCardView>(); }},
{"User Interface", ui::Color::dark_cyan(), &bitmap_icon_options_ui, [this]() { nav_.push<SetUIView>(); }},
{"Display", ui::Color::dark_cyan(), &bitmap_icon_brightness, [this]() { nav_.push<SetDisplayView>(); }},
+46 -1
View File
@@ -264,6 +264,51 @@ class SetRadioView : public View {
SetFrequencyCorrectionModel form_collect();
};
class SetTXLimitView : public View {
public:
SetTXLimitView(NavigationView& nav);
void focus() override;
std::string title() const override { return "TX Limit"; };
private:
Labels labels{
{{1 * 8, 1 * 16}, "Limits RF TX Gain", Theme::getInstance()->fg_light->foreground},
{{1 * 8, 2 * 16}, "(This may affect", Theme::getInstance()->fg_light->foreground},
{{1 * 8, 3 * 16}, "all applications.)", Theme::getInstance()->fg_light->foreground},
{{2 * 8, 12 * 16}, "TX Max Gain:", Theme::getInstance()->fg_light->foreground},
};
Checkbox tx_disable_switch{
{1 * 8, 6 * 16},
23,
"Disable TX"};
Checkbox tx_amp_disable_switch{
{1 * 8, 8 * 16},
23,
"Disable TX Amp"};
NumberField tx_gain_max_db{
{20 * 8, 12 * 16},
6,
{0, 47},
1,
' ',
};
Button button_save{
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Save"};
Button button_cancel{
{UI_POS_X_CENTER(16) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Cancel",
};
};
using portapack::persistent_memory::backlight_timeout_t;
class SetUIView : public View {
@@ -1008,7 +1053,7 @@ class SetBatteryView : public View {
"Save"};
Button button_reset{
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(7), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y(13), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Reset",
};
};
+34 -12
View File
@@ -30,11 +30,10 @@
#include "portapack.hpp"
#include <cstring>
#include <stdio.h>
#include "rtc_time.hpp"
using namespace portapack;
namespace pmem = portapack::persistent_memory;
#include "string_format.hpp"
#include "complex.hpp"
void SondeLogger::on_packet(const sonde::Packet& packet) {
@@ -65,6 +64,8 @@ SondeView::SondeView(NavigationView& nav)
&text_frame,
&text_temp,
&text_humid,
&text_press,
&text_vspeed,
&geopos,
&button_see_qr,
&button_see_map});
@@ -107,7 +108,7 @@ SondeView::SondeView(NavigationView& nav)
logger = std::make_unique<SondeLogger>();
if (logger)
logger->append(logs_dir / u"SONDE.TXT");
logger->append(logs_dir / u"SONDE_" + to_string_timestamp(rtc_time::now()) + u".TXT");
if (pmem::beep_on_packets()) {
audio::set_rate(audio::Rate::Hz_24000);
@@ -155,7 +156,7 @@ void SondeView::on_packet(const sonde::Packet& packet) {
sonde_id = packet.serial_number(); // used also as tag on the geomap
text_serial.set(sonde_id);
text_timestamp.set(to_string_timestamp(packet.received_at()));
text_timestamp.set(to_string_datetime(packet.received_at(), TimeFormat::YMDHMS));
text_voltage.set(unit_auto_scale(packet.battery_voltage(), 2, 2) + "V");
@@ -163,21 +164,42 @@ void SondeView::on_packet(const sonde::Packet& packet) {
temp_humid_info = packet.get_temp_humid();
if (temp_humid_info.humid != 0) {
double decimals = abs(get_decimals(temp_humid_info.humid, 10, true));
text_humid.set(to_string_dec_int((int)temp_humid_info.humid) + "." + to_string_dec_uint(decimals, 1) + "%");
text_humid.set(to_string_decimal(temp_humid_info.humid, 1) + "%");
}
if (temp_humid_info.temp != 0) {
double decimals = abs(get_decimals(temp_humid_info.temp, 10, true));
text_temp.set(to_string_dec_int((int)temp_humid_info.temp) + "." + to_string_dec_uint(decimals, 1) + STR_DEGREES_C);
text_temp.set(to_string_decimal(temp_humid_info.temp, 1) + STR_DEGREES_C);
}
if (packet.get_pressure() != 0) {
text_press.set(to_string_decimal(packet.get_pressure(), 1) + " hPa");
}
gps_info = packet.get_GPS_data();
geopos.set_altitude(gps_info.alt);
geopos.set_lat(gps_info.lat);
geopos.set_lon(gps_info.lon);
if (last_timestamp_update_ != 0 && last_altitude_ != 0) {
// calculate speeds
float vspeed = 0;
time_t currpackettime = rtc_time::rtcToUnixUTC(packet.received_at());
int32_t time_diff = (currpackettime - last_timestamp_update_);
if (time_diff >= 10) { // update only every 10 seconds
vspeed = (static_cast<int>(gps_info.alt) - static_cast<int>(last_altitude_)) / (float)time_diff;
last_timestamp_update_ = currpackettime;
last_altitude_ = gps_info.alt;
text_vspeed.set(to_string_decimal(vspeed, 1) + " m/s");
}
} else { // save first valid packet time + altitude
last_timestamp_update_ = rtc_time::rtcToUnixUTC(packet.received_at());
last_altitude_ = geopos.altitude();
}
if (gps_info.is_valid()) { // only update when valid, to prevent flashing
geopos.set_altitude(gps_info.alt);
geopos.set_lat(gps_info.lat);
geopos.set_lon(gps_info.lon);
if (geomap_view_) {
geomap_view_->update_position(gps_info.lat, gps_info.lon, 400, gps_info.alt, 0);
}
}
if (logger && logging) {
logger->on_packet(packet);
}
+36 -24
View File
@@ -30,6 +30,7 @@
#include "ui_rssi.hpp"
#include "ui_qrcode.hpp"
#include "ui_geomap.hpp"
#include "string_format.hpp"
#include "event_m0.hpp"
@@ -94,32 +95,33 @@ class SondeView : public View {
// AudioOutput audio_output { };
Labels labels{
{{4 * 8, 2 * 16}, "Type:", Theme::getInstance()->fg_light->foreground},
{{6 * 8, 3 * 16}, "ID:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 4 * 16}, "DateTime:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(4), UI_POS_Y(2)}, "Type:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(6), UI_POS_Y(3)}, "ID:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(4)}, "DateTime:", Theme::getInstance()->fg_light->foreground},
{{3 * 8, 5 * 16}, "Vbatt:", Theme::getInstance()->fg_light->foreground},
{{3 * 8, 6 * 16}, "Frame:", Theme::getInstance()->fg_light->foreground},
{{4 * 8, 7 * 16}, "Temp:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 8 * 16}, "Humidity:", Theme::getInstance()->fg_light->foreground}};
{{UI_POS_X(3), UI_POS_Y(5)}, "Vbatt:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(3), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(4), UI_POS_Y(7)}, "Temp:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(8)}, "Humidity:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(9)}, "Pressure:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(10)}, "VSpeed:", Theme::getInstance()->fg_light->foreground}};
RxFrequencyField field_frequency{
{UI_POS_X(0), 0 * 8},
{UI_POS_X(0), UI_POS_Y(0)},
nav_};
RFAmpField field_rf_amp{
{13 * 8, UI_POS_Y(0)}};
{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{
{15 * 8, UI_POS_Y(0)}};
{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{
{18 * 8, UI_POS_Y(0)}};
{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
{UI_POS_X(21), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(24), 4},
};
AudioVolumeField field_volume{
@@ -136,47 +138,57 @@ class SondeView : public View {
"CRC"};
Text text_signature{
{9 * 8, 2 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(2), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)},
"..."};
Text text_serial{
{9 * 8, 3 * 16, 11 * 8, 16},
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)},
"..."};
Text text_timestamp{
{9 * 8, 4 * 16, 11 * 8, 16},
{UI_POS_X(9), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(9), UI_POS_HEIGHT(1)},
"..."};
Text text_voltage{
{9 * 8, 5 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_frame{
{9 * 8, 6 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(6), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_temp{
{9 * 8, 7 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(7), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_humid{
{9 * 8, 8 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_press{
{UI_POS_X(9), UI_POS_Y(9), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_vspeed{
{UI_POS_X(9), UI_POS_Y(10), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
GeoPos geopos{
{0, 12 * 16},
{UI_POS_X(0), UI_POS_Y(12)},
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::HIDDEN};
Button button_see_qr{
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16},
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
"See QR"};
Button button_see_map{
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16},
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
"See on map"};
GeoMapView* geomap_view_{nullptr};
time_t last_timestamp_update_{0};
uint32_t last_altitude_{0};
MessageHandlerRegistration message_handler_packet{
Message::ID::SondePacket,
@@ -285,7 +285,7 @@ bool StandaloneView::on_encoder(const EncoderEvent event) {
bool StandaloneView::on_touch(const TouchEvent event) {
if (get_application_information()->header_version > 1) {
get_application_information()->OnTouchEvent(event.point.x(), event.point.y(), (uint32_t)event.type);
return get_application_information()->OnTouchEvent(event.point.x(), event.point.y(), (uint32_t)event.type);
}
return false;
}
+10 -2
View File
@@ -242,6 +242,8 @@ const char* SubGhzDView::getSensorTypeName(FPROTO_SUBGHZD_SENSOR type) {
return "GangQi";
case FPS_MARANTEC24:
return "Marantec24";
case FPS_HOLTEKHT6P20B:
return "Holtek HT6P20B";
case FPS_Invalid:
default:
return "Unknown";
@@ -339,7 +341,7 @@ void SubGhzDRecentEntryDetailView::parseProtocol() {
if (entry_.sensorType == FPS_CAMEATOMO) {
entry_.data ^= 0xFFFFFFFFFFFFFFFF;
entry_.data <<= 4;
uint8_t pack[8] = {};
uint8_t pack[8];
pack[0] = (entry_.data >> 56);
pack[1] = ((entry_.data >> 48) & 0xFF);
pack[2] = ((entry_.data >> 40) & 0xFF);
@@ -756,5 +758,11 @@ void SubGhzDRecentEntryDetailView::parseProtocol() {
btn = entry_.data & 0xf;
return;
}
if (entry_.sensorType == FPS_HOLTEKHT6P20B) {
serial = entry_.data >> 8;
btn = (entry_.data >> 4) & 0xF;
return;
}
}
} // namespace ui
} // namespace ui
+1 -1
View File
@@ -612,7 +612,7 @@ void TextEditorView::open_file(const fs::path& path) {
viewer.clear_file();
delete_temp_file(path_);
path_ = {};
path_ = "";
file_dirty_ = false;
has_temp_file_ = false;
auto result = FileWrapper::open(
@@ -81,13 +81,13 @@ class TouchCalibrationView : public View {
Color::black()};
Image image_calibrate_1{
{240 - 32 - 16, (320 - 16) / 2 - 16, 32, 32},
{screen_width - 32 - 16, (screen_height - 16) / 2 - 16, 32, 32},
&bitmap_target_calibrate,
Color::white(),
Color::black()};
Image image_calibrate_2{
{240 / 2 - 16, (320 - 16) - 32 - 16, 32, 32},
{screen_width / 2 - 16, (screen_height - 16) - 32 - 16, 32, 32},
&bitmap_target_calibrate,
Color::white(),
Color::black()};
@@ -99,13 +99,13 @@ class TouchCalibrationView : public View {
Color::black()};
Image image_verify_1{
{240 - 32 - 16, (320 - 16) / 2 - 16, 32, 32},
{screen_width - 32 - 16, (screen_height - 16) / 2 - 16, 32, 32},
&bitmap_target_verify,
Color::white(),
Color::black()};
Image image_verify_2{
{240 / 2 - 16, (320 - 16) - 32 - 16, 32, 32},
{screen_width / 2 - 16, (screen_height - 16) - 32 - 16, 32, 32},
&bitmap_target_verify,
Color::white(),
Color::black()};
@@ -131,11 +131,11 @@ class TouchCalibrationView : public View {
"Calibration failed. Retry?"};
Button button_cancel{
{40, 200, 64, 24},
{40, 200, UI_POS_WIDTH(8), 24},
"Cancel"};
Button button_ok{
{136, 200, 64, 24},
{136, 200, UI_POS_WIDTH(8), 24},
"OK"};
void on_frame_sync();
@@ -197,6 +197,10 @@ WeatherView::~WeatherView() {
baseband::shutdown();
}
uint16_t WeatherView::bcd_decode_short(uint32_t data) {
return (data & 0xF) * 10 + ((data >> 4) & 0xF);
}
const char* WeatherView::getWeatherSensorTypeName(FPROTO_WEATHER_SENSOR type) {
switch (type) {
case FPW_NexusTH:
@@ -225,6 +229,8 @@ const char* WeatherView::getWeatherSensorTypeName(FPROTO_WEATHER_SENSOR type) {
return "LaCrosse TX141THBv2";
case FPW_OREGON2:
return "Oregon2";
case FPW_OREGON2B:
return "Oregon2B";
case FPW_OREGON3:
return "Oregon3";
case FPW_OREGONv1:
@@ -299,7 +305,9 @@ WeatherRecentEntry WeatherView::process_data(const WeatherDataMessage* data) {
WeatherRecentEntry ret = {};
ret.sensorType = data->sensorType;
int16_t i16 = 0;
int32_t i32 = 0;
uint16_t u16 = 0;
uint64_t u64 = 0;
uint8_t u8 = 0;
uint8_t channel_3021[] = {3, 0, 2, 1};
@@ -442,12 +450,34 @@ WeatherRecentEntry WeatherView::process_data(const WeatherDataMessage* data) {
ret.humidity = (data->decode_data >> 8) & 0xFF;
break;
case FPW_OREGON2:
i16 = ((data->decode_data >> 4) & 0xF) * 10 + (((data->decode_data >> 4) >> 4) & 0xF);
i16 *= 10;
i16 += (data->decode_data >> 12) & 0xF;
if (data->decode_data & 0xF) i16 = -i16;
ret.temp = (float)i16 / 10.0;
// todo fix missing parts.
i32 = bcd_decode_short(data->decode_data >> 4);
i32 *= 10;
i32 += (data->decode_data >> 12) & 0xf;
if (data->decode_data & 0xF) i32 = -i32;
ret.temp = (float)i32 / 10.0;
ret.battery_low = ((data->decode_data >> 32) & 0x4) ? 1 : 0;
u8 = (data->decode_data >> 44) & 0xF;
ret.channel = 1;
while (u8 > 1) {
ret.channel++;
u8 >>= 1;
}
break;
case FPW_OREGON2B:
ret.humidity = bcd_decode_short(data->decode_data);
u64 = data->decode_data >> 8;
i32 = bcd_decode_short(u64 >> 4);
i32 *= 10;
i32 += (u64 >> 12) & 0xf;
if (u64 & 0xF) i32 = -i32;
ret.temp = (float)i32 / 10.0;
ret.battery_low = ((data->decode_data >> 32) & 0x4) ? 1 : 0;
u8 = (data->decode_data >> 44) & 0xF;
ret.channel = 1;
while (u8 > 1) {
ret.channel++;
u8 >>= 1;
}
break;
case FPW_OREGON3:
// todo check
@@ -114,6 +114,7 @@ class WeatherView : public View {
private:
void on_tick_second();
uint16_t bcd_decode_short(uint32_t data);
void on_data(const WeatherDataMessage* data);
WeatherRecentEntry process_data(const WeatherDataMessage* data);
NavigationView& nav_;
+35 -2
View File
@@ -147,6 +147,19 @@ void set_sstv_data(const uint8_t vis_code, const uint32_t pixel_duration) {
send_message(&message);
}
void set_sstvrx_data(const uint8_t code) {
const SSTVRXConfigureMessage message{
code};
send_message(&message);
}
void set_sstvrx_phase_slant(const int16_t phase, const int16_t slant) {
const SSTVRXPhaseSlantMessage message{
phase,
slant};
send_message(&message);
}
void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word) {
const AFSKRxConfigureMessage message{
baudrate,
@@ -288,8 +301,8 @@ void set_fsk_data(const uint32_t stream_length, const uint32_t samples_per_bit,
send_message(&message);
}
void set_pocsag() {
const POCSAGConfigureMessage message{};
void set_pocsag(int8_t baud_config) {
const POCSAGConfigureMessage message{baud_config};
send_message(&message);
}
@@ -328,6 +341,11 @@ void set_noaaapt_config() {
send_message(&message);
}
void set_flex_config() {
const FlexConfigureMessage message{};
send_message(&message);
}
void set_siggen_tone(const uint32_t tone) {
const SigGenToneMessage message{
TONES_F2D(tone, TONES_SAMPLERATE)};
@@ -350,6 +368,21 @@ void set_subghzd_config(uint8_t modulation = 0, uint32_t sampling_rate = 0) {
send_message(&message);
}
void set_moreserx_config(uint8_t mode) {
const MorseRXConfigureMessage message{mode};
send_message(&message);
}
void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta) {
const MorseTXConfigureMessage message{mode, tone, fm_delta};
send_message(&message);
}
void set_morsetx_key(bool key_down) {
const MorseTXkeyMessage message{key_down};
send_message(&message);
}
static bool baseband_image_running = false;
void run_image(const spi_flash::image_tag_t image_tag) {
+8 -2
View File
@@ -74,13 +74,15 @@ void set_tone(const uint32_t index, const uint32_t delta, const uint32_t duratio
void set_tones_config(const uint32_t bw, const uint32_t pre_silence, const uint16_t tone_count, const bool dual_tone, const bool audio_out);
void kill_tone();
void set_sstv_data(const uint8_t vis_code, const uint32_t pixel_duration);
void set_sstvrx_data(const uint8_t code);
void set_sstvrx_phase_slant(const int16_t phase, const int16_t slant);
void set_audiotx_config(const uint32_t divider, const float deviation_hz, const float audio_gain, uint8_t audio_shift_bits_s16, uint8_t bits_per_sample, const uint32_t tone_key_delta, const bool am_enabled, const bool dsb_enabled, const bool usb_enabled, const bool lsb_enabled);
void set_fifo_data(const int8_t* data);
void set_pitch_rssi(int32_t avg, bool enabled);
void set_afsk_data(const uint32_t afsk_samples_per_bit, const uint32_t afsk_phase_inc_mark, const uint32_t afsk_phase_inc_space, const uint8_t afsk_repeat, const uint32_t afsk_bw, const uint8_t symbol_count);
void kill_afsk();
void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word);
void set_fsk(const uint32_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes);
void set_fsk(const uint8_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes);
void set_aprs(const uint32_t baudrate);
void set_btlerx(uint8_t channel_number);
@@ -91,7 +93,7 @@ void set_nrf(const uint32_t baudrate, const uint32_t word_length, const uint32_t
void set_ook_data(const uint32_t stream_length, const uint32_t samples_per_bit, const uint8_t repeat, const uint32_t pause_symbols, const uint8_t de_bruijn_length = 0);
void kill_ook();
void set_fsk_data(const uint32_t stream_length, const uint32_t samples_per_bit, const uint32_t shift, const uint32_t progress_notice);
void set_pocsag();
void set_pocsag(int8_t baud_config = -1);
void set_adsb();
void set_jammer(const bool run, const jammer::JammerType type, const uint32_t speed);
void set_rds_data(const uint16_t message_length);
@@ -100,8 +102,12 @@ void set_siggen_tone(const uint32_t tone);
void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t duration);
void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw);
void set_subghzd_config(uint8_t modulation, uint32_t sampling_rate);
void set_moreserx_config(uint8_t mode);
void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta);
void set_morsetx_key(bool key_down);
void set_wefax_config(uint8_t lpm, uint8_t ioc);
void set_noaaapt_config();
void set_flex_config();
void request_roger_beep();
void request_rssi_beep();
+41 -3
View File
@@ -415,6 +415,44 @@ static constexpr Bitmap bitmap_icon_adsb{
{16, 16},
bitmap_icon_adsb_data};
static constexpr uint8_t bitmap_icon_adulttoys_data[] = {
0x00,
0x00,
0x30,
0x0C,
0x78,
0x1E,
0x7C,
0x3E,
0xFE,
0x7F,
0xFE,
0x7F,
0xFE,
0x7F,
0xFE,
0x7F,
0xFC,
0x3F,
0xFC,
0x3F,
0xF8,
0x1F,
0xF0,
0x0F,
0xE0,
0x07,
0xC0,
0x03,
0x80,
0x01,
0x00,
0x00,
};
static constexpr Bitmap bitmap_icon_adulttoys{
{16, 16},
bitmap_icon_adulttoys_data};
static constexpr uint8_t bitmap_icon_ais_data[] = {
0x00,
0x01,
@@ -4259,7 +4297,7 @@ static constexpr Bitmap bitmap_icon_tpms{
{16, 16},
bitmap_icon_tpms_data};
static constexpr uint8_t bitmap_icon_tranceivers_data[] = {
static constexpr uint8_t bitmap_icon_transceivers_data[] = {
0x80,
0x01,
0xC0,
@@ -4293,9 +4331,9 @@ static constexpr uint8_t bitmap_icon_tranceivers_data[] = {
0xFF,
0xFF,
};
static constexpr Bitmap bitmap_icon_tranceivers{
static constexpr Bitmap bitmap_icon_transceivers{
{16, 16},
bitmap_icon_tranceivers_data};
bitmap_icon_transceivers_data};
static constexpr uint8_t bitmap_icon_transmit_data[] = {
0x80,
+6 -12
View File
@@ -244,8 +244,7 @@ static void portapack_tcxo_enable() {
/* Delay >10ms at 96MHz clock speed for reference oscillator to start. */
/* Delay an additional 1ms (arbitrary) for the clock generator to detect a signal. */
volatile uint32_t delay = 240000 + 24000;
while (delay--)
;
while (delay--);
}
static void portapack_tcxo_disable() {
@@ -329,8 +328,7 @@ void ClockManager::init_clock_generator() {
: (ref_pll == ClockControl::MultiSynthSource::PLLB)
? 0x40
: 0x20;
while ((clock_generator.device_status() & device_status_mask) != 0)
;
while ((clock_generator.device_status() & device_status_mask) != 0);
clock_generator.set_clock_control(
clock_generator_output_mcu_clkin,
@@ -377,8 +375,7 @@ ClockManager::Reference ClockManager::choose_reference() {
if (hackrf_r9) {
gpio_r9_clkin_en.write(1);
volatile uint32_t delay = 240000 + 24000;
while (delay--)
;
while (delay--);
}
const auto detected_reference = detect_reference_source();
@@ -513,8 +510,7 @@ void ClockManager::start_frequency_monitor_measurement(const cgu::CLK_SEL clk_se
void ClockManager::wait_For_frequency_monitor_measurement_done() {
// FREQ_MON mechanism fails to finish if there's no clock present on selected input?!
while (LPC_CGU->FREQ_MON.MEAS == 1)
;
while (LPC_CGU->FREQ_MON.MEAS == 1);
}
uint32_t ClockManager::get_frequency_monitor_measurement_in_hertz() {
@@ -559,8 +555,7 @@ void ClockManager::start_audio_pll() {
});
cgu::pll0audio::power_up();
while (!cgu::pll0audio::is_locked())
;
while (!cgu::pll0audio::is_locked());
cgu::pll0audio::clock_enable();
set_base_audio_clock_divider(1);
@@ -577,8 +572,7 @@ void ClockManager::set_base_audio_clock_divider(const size_t divisor) {
void ClockManager::stop_audio_pll() {
cgu::pll0audio::clock_disable();
cgu::pll0audio::power_down();
while (cgu::pll0audio::is_locked())
;
while (cgu::pll0audio::is_locked());
}
void ClockManager::enable_clock_output(bool enable) {
+3 -6
View File
@@ -139,13 +139,11 @@ void runtime_error(uint8_t source) {
led.off();
// wait for DFU button release if pressed, so we don't immediately jump into stack dump
while (swizzled_switches() & (1 << (int)Switch::Dfu))
;
while (swizzled_switches() & (1 << (int)Switch::Dfu));
while (true) {
volatile size_t n = 1000000U;
while (n--)
;
while (n--);
led.toggle();
// Stack dump will cover entire screen, so wait for DFU button press to attempt it
@@ -225,8 +223,7 @@ void draw_stack_dump() {
// Out of room on the screen or end of stack - allow Up/Down paging.
// First wait for button release from previous press.
// NOTE: can't call swizzle_switches() with interrupted enabled!
while (swizzled_switches() & ((1 << (int)Switch::Right) | (1 << (int)Switch::Left) | (1 << (int)Switch::Down) | (1 << (int)Switch::Up) | (1 << (int)Switch::Sel) | (1 << (int)Switch::Dfu)))
;
while (swizzled_switches() & ((1 << (int)Switch::Right) | (1 << (int)Switch::Left) | (1 << (int)Switch::Down) | (1 << (int)Switch::Up) | (1 << (int)Switch::Sel) | (1 << (int)Switch::Dfu)));
painter.draw_string({border, portapack::display.height() - border - 8}, *Theme::getInstance()->bg_darkest_small, "Use UP/DOWN key");
+1 -2
View File
@@ -41,8 +41,7 @@ extern uint32_t __process_stack_end__;
inline uint32_t get_free_stack_space() {
uint32_t* p;
for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++)
;
for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++);
auto stack_space_left = p - &__process_stack_base__;
return stack_space_left;
+13 -16
View File
@@ -124,12 +124,12 @@ void EventDispatcher::request_stop() {
void EventDispatcher::set_display_sleep(const bool sleep) {
// TODO: Distribute display sleep message more broadly, shut down data generation
// on baseband side, since all that data is being discarded during sleep.
// on baseband side, since all that data is being discarded during sleep. -- DON'T TODO it, sincethe stealth mode want to send with screen off!
if (sleep) {
portapack::backlight()->off();
portapack::display.sleep();
portapack::display.sleep(false); // when called the hw_sleep = true, the irq wont fire, so the EVT_MASK_LCD_FRAME_SYNC won't set.
} else {
portapack::display.wake();
portapack::display.wake(true); // not important, command not affect if already hw waken up
// Don't turn on backlight here.
// Let frame sync handler turn on backlight after repaint.
}
@@ -180,12 +180,10 @@ void EventDispatcher::dispatch(const eventmask_t events) {
/*if( events & EVT_MASK_LCD_FRAME_SYNC ) {
blink_timer();
}*/
if (events & EVT_MASK_LCD_FRAME_SYNC) {
handle_lcd_frame_sync(!EventDispatcher::display_sleep);
}
if (!EventDispatcher::display_sleep) {
if (events & EVT_MASK_LCD_FRAME_SYNC) {
handle_lcd_frame_sync();
}
if (events & EVT_MASK_ENCODER) {
handle_encoder();
}
@@ -328,17 +326,16 @@ ui::Widget* EventDispatcher::getFocusedWidget() {
return context.focus_manager().focus_widget();
}
void EventDispatcher::handle_lcd_frame_sync() {
void EventDispatcher::handle_lcd_frame_sync(bool screen_on) {
bool waiting_for_frame = this->waiting_for_frame;
DisplayFrameSyncMessage message;
DisplayFrameSyncMessage message; // send framesync msg all the time, bc some apps relay on it
message_map.send(&message);
static_cast<ui::SystemView*>(top_widget)->paint_overlay();
painter.paint_widget_tree(top_widget);
portapack::backlight()->on();
if (screen_on) { // only draw when screen is on
static_cast<ui::SystemView*>(top_widget)->paint_overlay();
painter.paint_widget_tree(top_widget);
portapack::backlight()->on();
}
if (waiting_for_frame)
this->waiting_for_frame = false;
}
+1 -1
View File
@@ -133,7 +133,7 @@ class EventDispatcher {
void on_keyboard_event(ui::KeyboardEvent event);
// void blink_timer();
void handle_lcd_frame_sync();
void handle_lcd_frame_sync(bool screen_on);
void handle_switches();
void handle_encoder();
void handle_touch();
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2025 Pezsma
*
* 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_adult_toys_controller.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::adult_toys_controller {
void initialize_app(::ui::NavigationView& nav) {
nav.push<AdultToysView>();
}
} // namespace ui::external_app::adult_toys_controller
extern "C" {
__attribute__((section(".external_app.app_adult_toys_controller.application_information"), used)) application_information_t _application_information_adult_toys_controller = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::adult_toys_controller::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Adult Toys",
/*.bitmap_data = */ {
0x00,
0x00,
0x30,
0x0C,
0x78,
0x1E,
0x7C,
0x3E,
0xFE,
0x7F,
0xFE,
0x7F,
0xFE,
0x7F,
0xFE,
0x7F,
0xFC,
0x3F,
0xFC,
0x3F,
0xF8,
0x1F,
0xF0,
0x0F,
0xE0,
0x07,
0xC0,
0x03,
0x80,
0x01,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_btle_tx */ {'P', 'B', 'T', 'T'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
@@ -0,0 +1,373 @@
#include "ui_adult_toys_controller.hpp"
#include "ui_modemsetup.hpp"
#include "modems.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "portapack_persistent_memory.hpp"
#include "ui_navigation.hpp"
using namespace portapack;
using namespace modems;
using namespace ui;
namespace ui::external_app::adult_toys_controller {
void AdultToysView::focus() {
button_on.focus();
}
AdultToysView::AdultToysView(NavigationView& nav)
: nav_{nav} {
add_children({
&button_on,
&btn_adult,
&btn_child,
&btn_mode_decrease,
&btn_mode_increase,
&btn_pcknum_inc,
&btn_pcknum_dec,
&chk_mode_rnd,
&chk_mode_play,
&chk_mode_stop,
&chk_mode_ncnt,
&chk_mode_infinite,
&field_frequency,
&txt_selected_mode,
&options_target,
&labels,
&message_num,
&tx_view,
&screen_message_1,
&screen_message_2,
&screen_message_3,
&screen_message_4,
&screen_message_5,
&screen_message_6,
&screen_message_7,
&screen_message_8,
&screen_message_9,
&screen_message_10,
});
btn_adult.on_select = [this](Button&) {
start_screen = true;
hidden_program(!start_screen);
};
btn_child.on_select = [this](Button&) {
nav_.pop();
};
button_on.on_select = [this](Button&) {
if (play_running || stop_running) {
if (button_onstate) {
button_onstate = false;
stop();
button_on.set_text(LanguageHelper::currentMessages[LANG_START]);
} else {
button_onstate = true;
printCurrentModes();
start();
button_on.set_text(LanguageHelper::currentMessages[LANG_STOP]);
}
} // if play_running || stop_running
};
btn_mode_decrease.on_select = [this](Button&) {
if (toy_packet > 0) {
toy_packet--;
printCurrentModes();
}
};
btn_mode_increase.on_select = [this](Button&) {
if (stop_running || play_running) {
if (play_running && (toy_packet < max_plays - 1)) {
toy_packet++;
} else {
if (stop_running && toy_packet < max_stops - 1) {
toy_packet++;
}
}
printCurrentModes();
}
};
btn_pcknum_inc.on_select = [this](Button&) {
if (!button_onstate) {
chk_mode_ncnt.set_value(true);
if (n_message_set < n_message_max) {
n_message_set++;
} else {
n_message_set = 0;
}
message_num.set_value(n_message_set);
n_message_current = n_message_set;
}
};
btn_pcknum_dec.on_select = [this](Button&) {
if (!button_onstate) {
chk_mode_ncnt.set_value(true);
if (n_message_set > 0) {
n_message_set--;
} else {
n_message_set = n_message_max;
}
message_num.set_value(n_message_set);
n_message_current = n_message_set;
}
};
message_num.on_change = [this](int32_t v) {
if (!button_onstate) {
chk_mode_ncnt.set_value(true);
n_message_set = static_cast<uint16_t>(v);
n_message_current = n_message_set;
} else {
if (!button_onstate) message_num.set_value(n_message_current);
}
};
chk_mode_rnd.on_select = [this](Checkbox&, bool v) {
random_running = v;
};
chk_mode_play.on_select = [this](Checkbox&, bool v) {
play_running = v;
if (play_running && stop_running) {
toy_packet = 0;
stop_running = false;
chk_mode_stop.set_value(stop_running);
}
if (play_running) printCurrentModes();
};
chk_mode_stop.on_select = [this](Checkbox&, bool v) {
stop_running = v;
if (play_running && stop_running) {
toy_packet = 0;
play_running = false;
chk_mode_play.set_value(play_running);
}
if (stop_running) printCurrentModes();
};
chk_mode_ncnt.on_select = [this](Checkbox&, bool v) {
n_message_running = v;
if (n_message_running && inf_message_running) {
inf_message_running = false;
chk_mode_infinite.set_value(inf_message_running);
}
};
chk_mode_infinite.on_select = [this](Checkbox&, bool v) {
inf_message_running = v;
if (n_message_running && inf_message_running) {
n_message_running = false;
chk_mode_ncnt.set_value(n_message_running);
}
};
options_target.on_change = [this](size_t, int32_t i) {
target = (uint8_t)i;
};
chk_mode_infinite.set_value(true);
chk_mode_play.set_value(true);
}
void AdultToysView::hidden_program(bool hide) {
options_target.hidden(hide);
labels.hidden(hide);
button_on.hidden(hide);
btn_mode_decrease.hidden(hide);
btn_mode_increase.hidden(hide);
btn_pcknum_inc.hidden(hide);
btn_pcknum_dec.hidden(hide);
chk_mode_rnd.hidden(hide);
chk_mode_play.hidden(hide);
chk_mode_stop.hidden(hide);
chk_mode_ncnt.hidden(hide);
chk_mode_infinite.hidden(hide);
field_frequency.hidden(hide);
txt_selected_mode.hidden(hide);
message_num.hidden(hide);
tx_view.hidden(hide);
btn_adult.hidden(!hide);
btn_child.hidden(!hide);
screen_message_1.hidden(!hide);
screen_message_2.hidden(!hide);
screen_message_3.hidden(!hide);
screen_message_4.hidden(!hide);
screen_message_5.hidden(!hide);
screen_message_6.hidden(!hide);
screen_message_7.hidden(!hide);
screen_message_8.hidden(!hide);
screen_message_9.hidden(!hide);
screen_message_10.hidden(!hide);
set_dirty();
if (hide)
btn_child.focus();
else
button_on.focus();
}
void AdultToysView::on_show() {
hidden_program(!start_screen);
}
void AdultToysView::createPacket(bool regenerate) {
if (regenerate) randomizeMac();
randomChn();
if (target == 1 || 1) { // now only lovespouse supported, in the future, need to separee them
uint32_t mode = play_running ? plays[toy_packet].value : stops[toy_packet].value;
const uint8_t size = 22;
uint8_t packet[size];
uint8_t i = 0;
// 1. AD Flags
packet[i++] = 2;
packet[i++] = 0x01;
packet[i++] = 0x1A;
// 2. Manufacturer Specific
packet[i++] = 14;
packet[i++] = 0xFF;
packet[i++] = 0xFF;
packet[i++] = 0x00;
packet[i++] = 0x6D;
packet[i++] = 0xB6;
packet[i++] = 0x43;
packet[i++] = 0xCE;
packet[i++] = 0x97;
packet[i++] = 0xFE;
packet[i++] = 0x42;
packet[i++] = 0x7C;
packet[i++] = (mode >> 0x10) & 0xFF;
packet[i++] = (mode >> 0x8) & 0xFF;
packet[i++] = (mode >> 0x0) & 0xFF;
// 3. Service UUID List
packet[i++] = 3;
packet[i++] = 0x03;
packet[i++] = 0x8F;
packet[i++] = 0xAE;
std::string res = to_string_hex_array(packet, i);
memset(advertisementData, 0, sizeof(advertisementData));
std::copy(res.begin(), res.end(), advertisementData);
}
}
AdultToysView::~AdultToysView() {
stop();
}
void AdultToysView::stop() {
transmitter_model.disable();
baseband::shutdown();
}
void AdultToysView::start() {
baseband::run_image(portapack::spi_flash::image_tag_btle_tx);
transmitter_model.enable();
on_tx_progress(true);
}
void AdultToysView::reset() {
stop();
start();
}
uint8_t AdultToysView::randomize(uint8_t max) {
return static_cast<uint8_t>(rand() % max);
}
void AdultToysView::printCurrentModes() {
uint8_t max_val = play_running ? max_plays : max_stops;
auto name = play_running ? plays[toy_packet].name : stops[toy_packet].name;
std::string current = to_string_dec_uint(toy_packet + 1);
std::string max_val_str = to_string_dec_uint(max_val);
std::string result_str = current;
result_str += "/";
result_str += max_val_str;
result_str += " ";
result_str += name;
txt_selected_mode.set(result_str.c_str());
}
uint64_t AdultToysView::get_freq_by_channel_number(uint8_t channel_number) {
uint64_t freq_hz;
switch (channel_number) {
case 37:
freq_hz = 2'402'000'000ull;
break;
case 38:
freq_hz = 2'426'000'000ull;
break;
case 39:
freq_hz = 2'480'000'000ull;
break;
case 0 ... 10:
freq_hz = 2'404'000'000ull + channel_number * 2'000'000ull;
break;
case 11 ... 36:
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
break;
default:
freq_hz = UINT64_MAX;
}
return freq_hz;
}
void AdultToysView::randomizeMac() {
const char hexDigits[] = "0123456789ABCDEF";
// Generate 12 random hexadecimal characters
for (int i = 0; i < 12; ++i) {
int randomIndex = rand() % 16;
mac[i] = hexDigits[randomIndex];
}
mac[12] = '\0'; // Null-terminate the string
}
void AdultToysView::randomChn() {
channel_number = 37 + std::rand() % (39 - 37 + 1);
field_frequency.set_value(get_freq_by_channel_number(channel_number));
}
void AdultToysView::on_tx_progress(const bool done) {
if (!done) return;
if (!button_onstate) return;
bool regenerate = (counter++ % 600) == 0;
if (random_running && regenerate) {
toy_packet = randomize(play_running ? max_plays : max_stops);
}
createPacket(regenerate);
printCurrentModes();
if (n_message_running) {
if (n_message_current > 0) {
n_message_current--;
message_num.set_value(n_message_current);
} else {
stop();
message_num.set_value(n_message_set);
button_on.set_text(LanguageHelper::currentMessages[LANG_START]);
button_onstate = false;
n_message_current = n_message_set;
return;
}
} // if n_message
baseband::set_btletx(channel_number, mac, advertisementData, pduType);
}
} // namespace ui::external_app::adult_toys_controller
@@ -0,0 +1,245 @@
/*
* Copyright (C) 2025 Pezsma
*
* 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.
*/
// Code from https://github.com/Next-Flip/Momentum-Apps/blob/89d493daa1ea0cb86906fa0042c41dd4edc22a6a/ble_spam/protocols/lovespouse.c#L93
// Thanks for the work of the original creators!
#ifndef __UI_ADULT_TOYS_H__
#define __UI_ADULT_TOYS_H__
#include "ui.hpp"
#include "ui_language.hpp"
#include "ui_navigation.hpp"
#include "ui_transmitter.hpp"
#include "ui_freq_field.hpp"
#include "ui_record_view.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "log_file.hpp"
#include "utility.hpp"
using namespace ui;
namespace ui::external_app::adult_toys_controller {
enum PKT_TYPE {
PKT_TYPE_INVALID_TYPE,
PKT_TYPE_RAW,
PKT_TYPE_DISCOVERY,
PKT_TYPE_IBEACON,
PKT_TYPE_ADV_IND,
PKT_TYPE_ADV_DIRECT_IND,
PKT_TYPE_ADV_NONCONN_IND,
PKT_TYPE_ADV_SCAN_IND,
PKT_TYPE_SCAN_REQ,
PKT_TYPE_SCAN_RSP,
PKT_TYPE_CONNECT_REQ,
PKT_TYPE_LL_DATA,
PKT_TYPE_LL_CONNECTION_UPDATE_REQ,
PKT_TYPE_LL_CHANNEL_MAP_REQ,
PKT_TYPE_LL_TERMINATE_IND,
PKT_TYPE_LL_ENC_REQ,
PKT_TYPE_LL_ENC_RSP,
PKT_TYPE_LL_START_ENC_REQ,
PKT_TYPE_LL_START_ENC_RSP,
PKT_TYPE_LL_UNKNOWN_RSP,
PKT_TYPE_LL_FEATURE_REQ,
PKT_TYPE_LL_FEATURE_RSP,
PKT_TYPE_LL_PAUSE_ENC_REQ,
PKT_TYPE_LL_PAUSE_ENC_RSP,
PKT_TYPE_LL_VERSION_IND,
PKT_TYPE_LL_REJECT_IND,
PKT_TYPE_NUM_PKT_TYPE
};
class AdultToysView : public ui::View {
public:
AdultToysView(NavigationView& nav);
~AdultToysView();
void focus() override;
void on_show() override;
std::string title() const override {
return "Adult Toys";
};
struct LovespouseMode {
uint32_t value;
std::string name;
};
private:
NavigationView& nav_;
TxRadioState radio_state_{
2'402'000'000 /* frequency */,
4'000'000 /* bandwidth */,
4'000'000 /* sampling rate */
};
TxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
nav_};
TransmitterView2 tx_view{
{UI_POS_X(11), UI_POS_Y(0)},
/*short_ui*/ true};
app_settings::SettingsManager settings_{
"tx_adult_toys", app_settings::Mode::TX};
OptionsField options_target{
{UI_POS_X(6), UI_POS_Y(1)},
11,
{{"LoveSpouse", 1}}};
Button btn_adult{{UI_POS_X(0), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(10), UI_POS_HEIGHT(3)}, "adult"};
Button btn_child{{UI_POS_X_RIGHT(10), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(10), UI_POS_HEIGHT(3)}, "child"};
Button button_on{
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
LanguageHelper::currentMessages[LANG_START]};
Button btn_mode_decrease{
{UI_POS_X(0), UI_POS_Y(13), UI_POS_WIDTH(3), UI_POS_HEIGHT(2)},
"<-"};
Button btn_mode_increase{
{UI_POS_X_RIGHT(3), UI_POS_Y(13), UI_POS_WIDTH(3), UI_POS_HEIGHT(2)},
"->"};
Text txt_selected_mode{{UI_POS_X_CENTER(22), UI_POS_Y(13.5), UI_POS_WIDTH(22), UI_POS_HEIGHT(1)}, ""};
Button btn_pcknum_inc{
{UI_POS_X(7), UI_POS_Y(9), UI_POS_WIDTH(2), UI_POS_HEIGHT(1.5)},
"+"};
Button btn_pcknum_dec{
{UI_POS_X(0), UI_POS_Y(9), UI_POS_WIDTH(2), UI_POS_HEIGHT(1.5)},
"-"};
NumberField message_num{{UI_POS_X(3), UI_POS_Y(9.2)}, 3, {0, 999}, 1, ' ', true};
Checkbox chk_mode_rnd{{UI_POS_X_RIGHT(15), UI_POS_Y(3)}, 12, "Random Msg.", false};
Checkbox chk_mode_play{{UI_POS_X_RIGHT(15), UI_POS_Y(5)}, 10, "Play Mode", false};
Checkbox chk_mode_stop{{UI_POS_X_RIGHT(15), UI_POS_Y(7)}, 10, "Stop Mode", false};
Checkbox chk_mode_ncnt{{UI_POS_X_RIGHT(15), UI_POS_Y(9)}, 7, "N Msg.", false};
Checkbox chk_mode_infinite{{UI_POS_X_RIGHT(15), UI_POS_Y(11)}, 10, "Inf. Msg.", false};
Text screen_message_1{{0, 0, UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "The application is intended"};
Text screen_message_2{{0, 25, UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "for controlling Love Spouse"};
Text screen_message_3{{0, 50, UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "auxiliary tools/devices."};
Text screen_message_4{{0, 75, UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "It is only for use by adults."};
Text screen_message_5{{0, 100, UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "Are you an adult?"};
Text screen_message_6{{0, 140, UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "To use the application, "};
Text screen_message_7{{0, 165, UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "I assume full responsibility"};
Text screen_message_8{{0, 190, UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "for the \"I do not assume"};
Text screen_message_9{{0, 215, UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "responsibility\"."};
Text screen_message_10{{UI_POS_X_RIGHT(16), 230, UI_POS_WIDTH(16), UI_POS_HEIGHT(1)}, "(The developer)"};
Labels labels{
{{UI_POS_X(0), UI_POS_Y(1)}, "Type:", Theme::getInstance()->fg_light->foreground}};
bool button_onstate{false};
bool play_running{false};
bool stop_running{false};
bool random_running{false};
bool n_message_running{false};
bool inf_message_running{false};
bool start_screen{false};
uint8_t target = 1; // lovespouse
char mac[13] = "010203040506";
uint8_t channel_number = 37;
char advertisementData[63] = {"03032CFE06162CFED5A59E020AB4\0"};
PKT_TYPE pduType = {PKT_TYPE_DISCOVERY};
uint8_t toy_packet = 0;
uint16_t n_message_max = 999;
uint16_t n_message_set = 0;
uint16_t n_message_current = 0;
static const size_t max_plays = 27;
static const size_t max_stops = 3;
uint16_t counter = 599;
void createPacket(bool regenerate);
void start();
void stop();
void reset();
void printCurrentModes();
void on_tx_progress(const bool done);
void hidden_program(bool hide);
void randomizeMac();
void randomChn();
uint8_t randomize(uint8_t max);
uint64_t get_freq_by_channel_number(uint8_t channel_number);
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
this->on_tx_progress(message.done);
}};
LovespouseMode plays[max_plays] = {
{0xE49C6C, "Classic 1"},
{0xE7075E, "Classic 2"},
{0xE68E4F, "Classic 3"},
{0xE1313B, "Classic 4"},
{0xE0B82A, "Classic 5"},
{0xE32318, "Classic 6"},
{0xE2AA09, "Classic 7"},
{0xED5DF1, "Classic 8"},
{0xECD4E0, "Classic 9"},
{0xD41F5D, "Independent 1-1"},
{0xD7846F, "Independent 1-2"},
{0xD60D7E, "Independent 1-3"},
{0xD1B20A, "Independent 1-4"},
{0xD0B31B, "Independent 1-5"},
{0xD3A029, "Independent 1-6"},
{0xD22938, "Independent 1-7"},
{0xDDDEC0, "Independent 1-8"},
{0xDC57D1, "Independent 1-9"},
{0xA4982E, "Independent 2-1"},
{0xA7031C, "Independent 2-2"},
{0xA68A0D, "Independent 2-3"},
{0xA13579, "Independent 2-4"},
{0xA0BC68, "Independent 2-5"},
{0xA3275A, "Independent 2-6"},
{0xA2AE4B, "Independent 2-7"},
{0xAD59B3, "Independent 2-8"},
{0xACD0A2, "Independent 2-9"},
};
LovespouseMode stops[max_stops] = {
{0xE5157D, "Classic Stop"},
{0xD5964C, "Independent 1 Stop"},
{0xA5113F, "Independent 2 Stop"},
};
};
}; // namespace ui::external_app::adult_toys_controller
#endif // ui_adult_toys_controller_hpp
@@ -34,7 +34,7 @@ using namespace portapack;
#include <cmath>
#include <array>
namespace ui {
namespace ui::external_app::analogtv {
namespace tv {
/* TimeScopeView******************************************************/
@@ -235,4 +235,4 @@ void TVWidget::on_audio_spectrum() {
}
} /* namespace tv */
} /* namespace ui */
} // namespace ui::external_app::analogtv
@@ -33,7 +33,7 @@
#include <cstdint>
#include <cstddef>
namespace ui {
namespace ui::external_app::analogtv {
namespace tv {
class TimeScopeView : public View {
@@ -167,6 +167,6 @@ class TVWidget : public View {
};
} /* namespace tv */
} /* namespace ui */
} // namespace ui::external_app::analogtv
#endif /*__UI_TV_H__*/
+38 -25
View File
@@ -62,19 +62,24 @@ AppManagerView::AppManagerView(NavigationView& nav)
auto app_name = get_app_info(menu_view.highlighted_index(), true);
auto app_id = get_app_info(menu_view.highlighted_index(), false);
info += "Hidden:";
if (!app_name.empty()) {
info += "Hidden:";
if (is_blacklisted(app_name)) {
info += "Yes ";
} else {
info += "No ";
if (is_blacklisted(app_name)) {
info += "Yes ";
} else {
info += "No ";
}
}
info += "Autostart:";
if (is_autostart_app(app_id)) {
info += "Yes";
} else {
info += "No";
if (!app_id.empty()) {
info += "Autostart:";
if (is_autostart_app(app_id)) {
info += "Yes";
} else {
info += "No";
}
}
if (info.empty()) {
@@ -82,6 +87,8 @@ AppManagerView::AppManagerView(NavigationView& nav)
}
text_app_info.set(info);
button_hide_unhide.set_focusable(!app_name.empty());
button_set_cancel_autostart.set_focusable(!app_id.empty());
};
button_hide_unhide.on_select = [this](Button&) {
@@ -120,27 +127,23 @@ void AppManagerView::refresh_list() {
};
for (auto& app : NavigationView::appList) {
if (app.id == nullptr) continue;
app_list_index++;
menu_view.add_item({app.displayName + std::string(is_autostart_app(app.id) ? padding(app.displayName) : ""),
menu_view.add_item({app.displayName + std::string((app.id != nullptr && is_autostart_app(app.id)) ? padding(app.displayName) : ""),
app.iconColor,
app.icon,
[this, app_id = std::string(app.id)](KeyEvent) {
[this, app_id = std::string(app.id != nullptr ? app.id : "")](KeyEvent) {
button_hide_unhide.focus();
}});
}
ExternalItemsMenuLoader::load_all_external_items_callback([this, &index, &padding](ui::AppInfoConsole& app) {
if (app.appCallName == nullptr) return;
app_list_index++;
menu_view.add_item({app.appFriendlyName + std::string(is_autostart_app(app.appCallName) ? padding(app.appFriendlyName) : ""),
menu_view.add_item({app.appFriendlyName + std::string((app.appCallName != nullptr && is_autostart_app(app.appCallName)) ? padding(app.appFriendlyName) : ""),
ui::Color::light_grey(),
&bitmap_icon_sdcard,
[this, app_id = std::string(app.appCallName)](KeyEvent) {
[this, app_id = std::string(app.appCallName != nullptr ? app.appCallName : "")](KeyEvent) {
button_hide_unhide.focus();
}});
});
@@ -156,19 +159,29 @@ void AppManagerView::hide_app() {
std::vector<std::string> blacklist;
get_blacklist(blacklist);
blacklist.push_back(get_app_info(menu_view.highlighted_index(), true));
auto app_name = get_app_info(menu_view.highlighted_index(), true);
if (app_name.empty()) return;
blacklist.push_back(app_name);
write_blacklist(blacklist);
}
void AppManagerView::unhide_app() {
std::vector<std::string> blacklist;
get_blacklist(blacklist);
blacklist.erase(std::remove(blacklist.begin(), blacklist.end(), get_app_info(menu_view.highlighted_index(), true)), blacklist.end());
auto app_name = get_app_info(menu_view.highlighted_index(), true);
if (app_name.empty()) return;
blacklist.erase(std::remove(blacklist.begin(), blacklist.end(), app_name), blacklist.end());
write_blacklist(blacklist);
}
void AppManagerView::hide_unhide_app() {
if (is_blacklisted(get_app_info(menu_view.highlighted_index(), true)))
auto app_name = get_app_info(menu_view.highlighted_index(), true);
if (app_name.empty()) return;
if (is_blacklisted(app_name))
unhide_app();
else
hide_app();
@@ -218,6 +231,7 @@ void AppManagerView::set_auto_start() {
if (app_index >= app_list_index) return;
auto id_aka_app_call_name = get_app_info(app_index, false);
if (id_aka_app_call_name.empty()) return;
autostart_app = id_aka_app_call_name;
@@ -234,6 +248,7 @@ void AppManagerView::set_unset_autostart_app() {
if (app_index >= app_list_index) return;
auto id_aka_friendly_name = get_app_info(app_index, false);
if (id_aka_friendly_name.empty()) return;
if (is_autostart_app(id_aka_friendly_name))
unset_auto_start();
@@ -254,17 +269,15 @@ std::string AppManagerView::get_app_info(uint16_t index, bool is_display_name) {
std::string result;
for (auto& app : NavigationView::appList) {
if (app.id == nullptr) continue;
if (current_index == index) {
return is_display_name ? std::string(app.displayName) : std::string(app.id);
return is_display_name ? std::string(app.displayName != nullptr ? app.displayName : "") : std::string(app.id != nullptr ? app.id : "");
}
current_index++;
}
ExternalItemsMenuLoader::load_all_external_items_callback([&current_index, &index, &result, &is_display_name](ui::AppInfoConsole& app) {
if (app.appCallName == nullptr) return;
if (current_index == index) {
result = is_display_name ? app.appFriendlyName : app.appCallName;
result = is_display_name ? (app.appFriendlyName != nullptr ? app.appFriendlyName : "") : (app.appCallName != nullptr ? app.appCallName : "");
}
current_index++;
});
+75 -127
View File
@@ -24,25 +24,25 @@ constexpr uint32_t BLUE_TEAM_ADDRESS = BATTLESHIP_BASE_ADDRESS + 2;
BattleshipView::BattleshipView(NavigationView& nav)
: nav_{nav} {
CELL_SIZE = screen_width / GRID_SIZE;
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
add_children({&text_title, &text_subtitle,
&rect_radio_settings, &label_radio, &button_frequency,
//&rect_radio_settings, &rect_audio_settings, &rect_team_selection,
&label_radio, &button_frequency,
&label_rf_amp, &checkbox_rf_amp,
&label_lna, &field_lna,
&label_vga, &field_vga,
&label_tx_gain, &field_tx_gain,
&rect_audio_settings, &label_audio,
&label_audio,
&checkbox_sound, &label_volume, &field_volume,
&rect_team_selection, &label_team,
&label_team,
&button_red_team, &button_blue_team,
&rssi, &text_status, &text_score,
&rssi,
&button_rotate, &button_place, &button_fire, &button_menu});
// Hide in-game elements
rssi.hidden(true);
text_status.hidden(true);
text_score.hidden(true);
button_rotate.hidden(true);
button_place.hidden(true);
button_fire.hidden(true);
@@ -151,8 +151,8 @@ BattleshipView::BattleshipView(NavigationView& nav)
field_tx_gain.set_parent_rect({185, 118, 32, 16});
checkbox_sound.set_parent_rect({17, 187, 80, 24});
field_volume.set_parent_rect({165, 187, 32, 16});
button_red_team.set_parent_rect({25, 242, 85, 45});
button_blue_team.set_parent_rect({130, 242, 85, 45});
// button_red_team.set_parent_rect({25, 242, 85, 45});
// button_blue_team.set_parent_rect({130, 242, 85, 45});
// Make menu elements focusable
button_frequency.set_focusable(true);
@@ -191,11 +191,42 @@ void BattleshipView::init_game() {
enemy_grid[y][x] = CellState::EMPTY;
}
}
setup_ships();
update_score();
}
void BattleshipView::show_hide_menu(bool menu_vis) {
text_title.hidden(!menu_vis);
text_subtitle.hidden(!menu_vis);
// rect_radio_settings.hidden(!menu_vis); rect_audio_settings.hidden(!menu_vis); rect_team_selection.hidden(!menu_vis);
label_radio.hidden(!menu_vis);
button_frequency.hidden(!menu_vis);
label_rf_amp.hidden(!menu_vis);
checkbox_rf_amp.hidden(!menu_vis);
label_lna.hidden(!menu_vis);
field_lna.hidden(!menu_vis);
label_vga.hidden(!menu_vis);
field_vga.hidden(!menu_vis);
label_tx_gain.hidden(!menu_vis);
field_tx_gain.hidden(!menu_vis);
label_audio.hidden(!menu_vis);
checkbox_sound.hidden(!menu_vis);
label_volume.hidden(!menu_vis);
field_volume.hidden(!menu_vis);
label_team.hidden(!menu_vis);
button_red_team.hidden(!menu_vis);
button_blue_team.hidden(!menu_vis);
rssi.hidden(menu_vis);
button_rotate.hidden(menu_vis);
button_place.hidden(menu_vis);
button_menu.hidden(menu_vis);
// button_rotate.set_focusable(false); //no need, since can't focus on hidden
// button_place.set_focusable(false);
// button_menu.set_focusable(false);
set_dirty();
}
void BattleshipView::reset_game() {
transmitter_model.disable();
receiver_model.disable();
@@ -221,50 +252,7 @@ void BattleshipView::reset_game() {
update_score();
// Toggle visibility
bool menu_vis = true;
bool game_vis = false;
text_title.hidden(!menu_vis);
text_subtitle.hidden(!menu_vis);
rect_radio_settings.hidden(!menu_vis);
label_radio.hidden(!menu_vis);
button_frequency.hidden(!menu_vis);
label_rf_amp.hidden(!menu_vis);
checkbox_rf_amp.hidden(!menu_vis);
label_lna.hidden(!menu_vis);
field_lna.hidden(!menu_vis);
label_vga.hidden(!menu_vis);
field_vga.hidden(!menu_vis);
label_tx_gain.hidden(!menu_vis);
field_tx_gain.hidden(!menu_vis);
rect_audio_settings.hidden(!menu_vis);
label_audio.hidden(!menu_vis);
checkbox_sound.hidden(!menu_vis);
label_volume.hidden(!menu_vis);
field_volume.hidden(!menu_vis);
rect_team_selection.hidden(!menu_vis);
label_team.hidden(!menu_vis);
button_red_team.hidden(!menu_vis);
button_blue_team.hidden(!menu_vis);
rssi.hidden(!game_vis);
text_status.hidden(!game_vis);
text_score.hidden(!game_vis);
button_rotate.hidden(!game_vis);
button_place.hidden(!game_vis);
button_fire.hidden(!game_vis);
button_menu.hidden(!game_vis);
// Restore focusability
button_frequency.set_focusable(menu_vis);
checkbox_rf_amp.set_focusable(menu_vis);
field_lna.set_focusable(menu_vis);
field_vga.set_focusable(menu_vis);
field_tx_gain.set_focusable(menu_vis);
checkbox_sound.set_focusable(menu_vis);
field_volume.set_focusable(menu_vis);
button_red_team.set_focusable(menu_vis);
button_blue_team.set_focusable(menu_vis);
show_hide_menu(true);
button_frequency.focus();
set_dirty();
@@ -283,45 +271,10 @@ void BattleshipView::start_team(bool red) {
game_state = GameState::PLACING_SHIPS;
// Toggle visibility
bool menu_vis = false;
bool game_vis = true;
text_title.hidden(!menu_vis);
text_subtitle.hidden(!menu_vis);
rect_radio_settings.hidden(!menu_vis);
label_radio.hidden(!menu_vis);
button_frequency.hidden(!menu_vis);
label_rf_amp.hidden(!menu_vis);
checkbox_rf_amp.hidden(!menu_vis);
label_lna.hidden(!menu_vis);
field_lna.hidden(!menu_vis);
label_vga.hidden(!menu_vis);
field_vga.hidden(!menu_vis);
label_tx_gain.hidden(!menu_vis);
field_tx_gain.hidden(!menu_vis);
rect_audio_settings.hidden(!menu_vis);
label_audio.hidden(!menu_vis);
checkbox_sound.hidden(!menu_vis);
label_volume.hidden(!menu_vis);
field_volume.hidden(!menu_vis);
rect_team_selection.hidden(!menu_vis);
label_team.hidden(!menu_vis);
button_red_team.hidden(!menu_vis);
button_blue_team.hidden(!menu_vis);
rssi.hidden(!game_vis);
text_status.hidden(true);
text_score.hidden(true);
button_rotate.hidden(false);
button_place.hidden(false);
button_menu.hidden(false);
show_hide_menu(false);
current_status = "Place carrier (5)";
button_rotate.set_focusable(false);
button_place.set_focusable(false);
button_menu.set_focusable(false);
focus();
is_transmitting = true;
configure_radio_rx();
@@ -382,7 +335,7 @@ void BattleshipView::configure_radio_rx() {
}
void BattleshipView::paint(Painter& painter) {
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
painter.fill_rectangle({0, 0, UI_POS_MAXWIDTH, UI_POS_MAXHEIGHT}, Color::black());
if (game_state == GameState::MENU) {
draw_menu_screen(painter);
@@ -426,24 +379,21 @@ void BattleshipView::paint(Painter& painter) {
}
Color team_color = is_red_team ? Color::red() : Color::blue();
painter.fill_rectangle({0, 5, 240, 16}, team_color);
painter.fill_rectangle({0, 5, screen_width, 16}, team_color);
auto style_white = Style{
.font = ui::font::fixed_8x16,
.background = team_color,
.foreground = Color::white()};
painter.draw_string({85, 5}, style_white, is_red_team ? "RED TEAM" : "BLUE TEAM");
painter.draw_string({UI_POS_X_CENTER(9), 5}, style_white, is_red_team ? "RED TEAM" : "BLUE TEAM");
auto style_status = Style{
.font = ui::font::fixed_8x16,
.background = Color::black(),
.foreground = Color::white()};
painter.fill_rectangle({0, 21, 240, 16}, Color::black());
painter.draw_string({10, 21}, style_status, current_status);
if (game_state != GameState::MENU) {
painter.draw_string({170, 21}, style_status, current_score);
}
painter.fill_rectangle({0, 21, screen_width, 16}, Color::black());
painter.draw_string({5, 21}, style_status, current_status);
painter.draw_string({170, 21}, style_status, current_score);
if (game_state == GameState::PLACING_SHIPS) {
draw_grid(painter, GRID_OFFSET_X, GRID_OFFSET_Y + 5, my_grid, true);
@@ -452,35 +402,35 @@ void BattleshipView::paint(Painter& painter) {
}
} else if (game_state == GameState::MY_TURN) {
draw_grid(painter, GRID_OFFSET_X, GRID_OFFSET_Y + 5, enemy_grid, false, true);
painter.draw_string({10, GRID_OFFSET_Y + GRID_SIZE * CELL_SIZE + 10}, style_status,
"Enemy ships: " + to_string_dec_uint(enemy_ships_remaining));
painter.draw_string({0, GRID_OFFSET_Y + GRID_SIZE * CELL_SIZE + 10}, style_status,
"Enemy: " + to_string_dec_uint(enemy_ships_remaining));
} else if (game_state == GameState::OPPONENT_TURN || game_state == GameState::WAITING_FOR_OPPONENT) {
draw_grid(painter, GRID_OFFSET_X, GRID_OFFSET_Y + 5, my_grid, true);
painter.draw_string({10, GRID_OFFSET_Y + GRID_SIZE * CELL_SIZE + 10}, style_status,
"Your ships: " + to_string_dec_uint(ships_remaining));
painter.draw_string({0, GRID_OFFSET_Y + GRID_SIZE * CELL_SIZE + 10}, style_status,
"You: " + to_string_dec_uint(ships_remaining));
} else if (game_state == GameState::GAME_OVER) {
painter.draw_string({50, 150}, style_status, "Game Over!");
painter.draw_string({UI_POS_X_CENTER(11), 150}, style_status, "Game Over!");
painter.draw_string({30, 170}, style_status, current_status);
}
}
void BattleshipView::draw_menu_screen(Painter& painter) {
painter.draw_hline({12, 38}, 216, Color::dark_cyan());
painter.draw_hline({12, 20 + 16}, screen_width - 24, Color::dark_cyan());
painter.fill_rectangle({13, 41, 214, 116}, Color::dark_grey());
painter.draw_hline({12, 40}, 216, Color::cyan());
painter.draw_hline({12, 157}, 216, Color::cyan());
painter.fill_rectangle({13, 59, screen_width - 26, 116}, Color::dark_grey());
painter.draw_hline({12, 58}, screen_width - 24, Color::cyan());
painter.draw_hline({12, 157}, screen_width - 24, Color::cyan());
painter.fill_rectangle({13, 165, 214, 43}, Color::dark_grey());
painter.draw_hline({12, 164}, 216, Color::cyan());
painter.draw_hline({12, 208}, 216, Color::cyan());
painter.fill_rectangle({13, 165, screen_width - 24, 57}, Color::dark_grey());
painter.draw_hline({12, 164}, screen_width - 24, Color::cyan());
painter.draw_hline({12, 223}, screen_width - 24, Color::cyan());
painter.fill_rectangle({13, 218, 214, 73}, Color::dark_grey());
painter.draw_hline({12, 217}, 216, Color::cyan());
painter.draw_hline({12, 291}, 216, Color::cyan());
painter.fill_rectangle({13, 232, screen_width - 26, 67}, Color::dark_grey());
painter.draw_hline({12, 232}, screen_width - 24, Color::cyan());
painter.draw_hline({12, 300}, screen_width - 24, Color::cyan());
// Radio status indicator
Point indicator_pos{220, 53};
Point indicator_pos{UI_POS_MAXWIDTH - 20, 59};
if (is_transmitting) {
painter.fill_rectangle({indicator_pos, {6, 6}}, Color::red());
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
@@ -490,7 +440,7 @@ void BattleshipView::draw_menu_screen(Painter& painter) {
}
}
void BattleshipView::draw_grid(Painter& painter, uint8_t grid_x, uint8_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid) {
void BattleshipView::draw_grid(Painter& painter, uint16_t grid_x, uint16_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid) {
painter.fill_rectangle({grid_x, grid_y, GRID_SIZE * CELL_SIZE, GRID_SIZE * CELL_SIZE},
Color::dark_blue());
@@ -501,26 +451,24 @@ void BattleshipView::draw_grid(Painter& painter, uint8_t grid_x, uint8_t grid_y,
GRID_SIZE * CELL_SIZE, Color::grey());
}
for (uint8_t y = 0; y < GRID_SIZE; y++) {
for (uint8_t x = 0; x < GRID_SIZE; x++) {
draw_cell(painter, grid_x + x * CELL_SIZE + 1, grid_y + y * CELL_SIZE + 1,
grid[y][x], show_ships, is_offense_grid,
is_cursor_at(x, y, is_offense_grid));
for (uint16_t y = 0; y < GRID_SIZE; y++) {
for (uint16_t x = 0; x < GRID_SIZE; x++) {
draw_cell(painter, grid_x + x * CELL_SIZE + 1, grid_y + y * CELL_SIZE + 1, grid[y][x], show_ships, is_offense_grid, is_cursor_at(x, y, is_offense_grid));
}
}
}
void BattleshipView::draw_cell(Painter& painter, uint8_t cell_x, uint8_t cell_y, CellState state, bool show_ships, bool is_offense_grid, bool is_cursor) {
void BattleshipView::draw_cell(Painter& painter, uint16_t cell_x, uint16_t cell_y, CellState state, bool show_ships, bool is_offense_grid, bool is_cursor) {
Color cell_color = Color::dark_blue();
bool should_fill = false;
if (game_state == GameState::PLACING_SHIPS && !is_offense_grid && current_ship_index < 5) {
uint8_t ship_size = my_ships[current_ship_index].size();
for (uint8_t i = 0; i < ship_size; i++) {
uint8_t preview_x = placing_horizontal ? cursor_x + i : cursor_x;
uint8_t preview_y = placing_horizontal ? cursor_y : cursor_y + i;
uint8_t grid_x = (cell_x - 1) / CELL_SIZE;
uint8_t grid_y = (cell_y - GRID_OFFSET_Y - 6) / CELL_SIZE;
uint16_t preview_x = placing_horizontal ? cursor_x + i : cursor_x;
uint16_t preview_y = placing_horizontal ? cursor_y : cursor_y + i;
uint16_t grid_x = (cell_x - 1) / CELL_SIZE;
uint16_t grid_y = (cell_y - GRID_OFFSET_Y - 6) / CELL_SIZE;
if (grid_x == preview_x && grid_y == preview_y && preview_x < GRID_SIZE && preview_y < GRID_SIZE) {
return;
}
@@ -597,8 +545,8 @@ void BattleshipView::draw_ship_preview(Painter& painter) {
uint8_t y = placing_horizontal ? cursor_y : cursor_y + i;
if (x < GRID_SIZE && y < GRID_SIZE) {
uint8_t cell_x = GRID_OFFSET_X + x * CELL_SIZE + 1;
uint8_t cell_y = GRID_OFFSET_Y + 5 + y * CELL_SIZE + 1;
uint16_t cell_x = GRID_OFFSET_X + x * CELL_SIZE + 1;
uint16_t cell_y = GRID_OFFSET_Y + 5 + y * CELL_SIZE + 1;
Color preview_color = can_place ? Color::green() : Color::red();
+26 -25
View File
@@ -23,12 +23,9 @@
#include <array>
namespace ui::external_app::battleship {
#define GRID_SIZE 10
constexpr uint8_t GRID_SIZE = 10;
constexpr uint8_t CELL_SIZE = 24;
constexpr uint8_t GRID_OFFSET_X = 0;
constexpr uint8_t GRID_OFFSET_Y = 32;
namespace ui::external_app::battleship {
enum class ShipType : uint8_t {
CARRIER = 5,
@@ -103,6 +100,10 @@ class BattleshipView : public View {
private:
NavigationView& nav_;
uint16_t CELL_SIZE = 24;
uint16_t GRID_OFFSET_X = 0;
uint16_t GRID_OFFSET_Y = 32;
RxRadioState rx_radio_state_{
433920000 /* frequency */,
1750000 /* bandwidth */,
@@ -163,11 +164,11 @@ class BattleshipView : public View {
uint32_t last_address{0};
// UI Elements - Menu/Settings Screen
Text text_title{{60, 2, 120, 24}, "BATTLESHIP"};
Text text_subtitle{{40, 20, 160, 16}, "Naval Combat Game"};
Text text_title{{UI_POS_X_CENTER(11), 2, UI_POS_WIDTH(11), 16}, "BATTLESHIP"};
Text text_subtitle{{UI_POS_X_CENTER(18), 24, UI_POS_WIDTH(18), 16}, "Naval Combat Game"};
Rectangle rect_radio_settings{{12, 40, 216, 118}, Color::dark_grey()};
Text label_radio{{17, 45, 100, 16}, "RADIO SETUP"};
// Rectangle rect_radio_settings{{12, 40, 216, 118}, Color::dark_grey()};
Text label_radio{{UI_POS_X_CENTER(12), 45, UI_POS_WIDTH(12), 16}, "RADIO SETUP"};
ButtonWithEncoder button_frequency{{17, 65, 11 * 8, 20}, ""};
// Radio controls
@@ -183,25 +184,24 @@ class BattleshipView : public View {
Text label_tx_gain{{155, 118, 25, 16}, "TX:"};
NumberField field_tx_gain{{185, 118}, 2, {0, 47}, 8, ' '};
Rectangle rect_audio_settings{{12, 164, 216, 45}, Color::dark_grey()};
Text label_audio{{17, 169, 80, 16}, "AUDIO"};
Checkbox checkbox_sound{{17, 187}, 8, "Sound On", true};
// Rectangle rect_audio_settings{{12, 164, 216, 45}, Color::dark_grey()};
Text label_audio{{UI_POS_X_CENTER(6), 155, UI_POS_WIDTH(6), 16}, "AUDIO"};
Checkbox checkbox_sound{{17, 177}, 8, "Sound On", true};
Text label_volume{{110, 187, 50, 16}, "Volume:"};
AudioVolumeField field_volume{{165, 187}};
AudioVolumeField field_volume{{165, 177}};
Rectangle rect_team_selection{{12, 217, 216, 75}, Color::dark_grey()};
Text label_team{{17, 222, 110, 16}, "SELECT TEAM"};
Button button_red_team{{25, 242, 85, 45}, "RED\nTEAM"};
Button button_blue_team{{130, 242, 85, 45}, "BLUE\nTEAM"};
// Rectangle rect_team_selection{{12, 217, 216, 75}, Color::dark_grey()};
Text label_team{{17, 220, 8 * 12, 16}, "SELECT TEAM"};
Button button_red_team{{UI_POS_X_CENTER(10) - UI_POS_WIDTH(7), 237, UI_POS_WIDTH(10), 45}, "RED\nTEAM"};
Button button_blue_team{{UI_POS_X_CENTER(10) + UI_POS_WIDTH(7), 237, UI_POS_WIDTH(10), 45}, "BLUE\nTEAM"};
// In-game UI elements
RSSI rssi{{21 * 8, 0, 6 * 8, 4}};
Text text_status{{10, 16, 220, 16}, ""};
RSSI rssi{{UI_POS_X_RIGHT(6), 0, 6 * 8, 4}};
Text text_score{{170, 16, 60, 16}, ""};
Button button_rotate{{10, 265, 65, 32}, "Rotate"};
Button button_place{{82, 265, 65, 32}, "Place"};
Button button_fire{{82, 265, 65, 32}, "Fire!"};
Button button_menu{{155, 265, 65, 32}, "Menu"};
Button button_rotate{{5, UI_POS_Y_BOTTOM(3), 65, 32}, "Rotate"};
Button button_place{{UI_POS_X_CENTER(6), UI_POS_Y_BOTTOM(3), 65, 32}, "Place"};
Button button_fire{{UI_POS_X_RIGHT(17), UI_POS_Y_BOTTOM(3), 65, 32}, "Fire!"};
Button button_menu{{UI_POS_X_RIGHT(8), UI_POS_Y_BOTTOM(3), 65, 32}, "Menu"};
// Methods
void init_game();
@@ -209,8 +209,8 @@ class BattleshipView : public View {
void start_team(bool red);
void setup_ships();
void draw_menu_screen(Painter& painter);
void draw_grid(Painter& painter, uint8_t grid_x, uint8_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid = false);
void draw_cell(Painter& painter, uint8_t cell_x, uint8_t cell_y, CellState state, bool show_ships, bool is_offense_grid, bool is_cursor);
void draw_grid(Painter& painter, uint16_t grid_x, uint16_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid = false);
void draw_cell(Painter& painter, uint16_t cell_x, uint16_t cell_y, CellState state, bool show_ships, bool is_offense_grid, bool is_cursor);
void draw_ship_preview(Painter& painter);
void place_ship();
bool can_place_ship(uint8_t x, uint8_t y, uint8_t size, bool horizontal);
@@ -218,6 +218,7 @@ class BattleshipView : public View {
void process_shot(uint8_t x, uint8_t y);
void update_score();
bool is_cursor_at(uint8_t x, uint8_t y, bool is_offense_grid);
void show_hide_menu(bool menu_vis);
void send_message(const GameMessage& msg);
void process_message(const std::string& message);
-1
View File
@@ -21,7 +21,6 @@
*/
#include "bht.hpp"
#include "portapack_persistent_memory.hpp"
namespace ui::external_app::bht_tx {
+1 -1
View File
@@ -207,7 +207,7 @@ class BHTView : public View {
"Scan"};
Checkbox checkbox_flashing{
{16 * 8, 25 * 8},
{UI_POS_X(16), 25 * 8},
8,
"Flashing"};
NumberField field_speed{
+146 -132
View File
@@ -41,6 +41,19 @@ void game_timer_check() {
BlackjackView::BlackjackView(NavigationView& nav)
: nav_{nav}, game_timer{} {
// Initialize screen dimensions dynamically
screen_w = ui::screen_width;
screen_h = ui::screen_height;
// Scale card size based on screen
if (screen_w <= 240) {
card_width = 50;
card_height = 65;
} else if (screen_w >= 400) {
card_width = 70;
card_height = 90;
}
add_children({&dummy});
current_instance = this;
game_timer.attach(&game_timer_check, 1.0 / 60.0);
@@ -70,7 +83,7 @@ void BlackjackView::frame_sync() {
}
void BlackjackView::clear_screen() {
painter.fill_rectangle({0, 0, screen_width, screen_height}, Color::black());
painter.fill_rectangle({0, 0, screen_w, screen_h}, Color::black());
}
void BlackjackView::init_deck() {
@@ -267,22 +280,24 @@ void BlackjackView::draw_menu_static() {
painter.draw_string({UI_POS_X_CENTER(10), 20}, style_title, "BLACKJACK");
// Draw rules
painter.draw_string({UI_POS_X_CENTER(12), 55}, style_rules, "-- RULES --");
painter.draw_string({UI_POS_X_CENTER(16), 75}, style_text, "Get close to 21");
painter.draw_string({UI_POS_X_CENTER(19), 90}, style_text, "without going over");
painter.draw_string({UI_POS_X_CENTER(18), 110}, style_text, "Dealer hits on 16");
painter.draw_string({UI_POS_X_CENTER(19), 125}, style_text, "Dealer stays on 17");
painter.draw_string({UI_POS_X_CENTER(19), 145}, style_text, "Blackjack pays 1:1");
// Draw rules - adjust Y positions for smaller screens
int rules_y = (screen_h > 300) ? 55 : 45;
painter.draw_string({UI_POS_X_CENTER(12), rules_y}, style_rules, "-- RULES --");
painter.draw_string({UI_POS_X_CENTER(16), rules_y + 20}, style_text, "Get close to 21");
painter.draw_string({UI_POS_X_CENTER(19), rules_y + 35}, style_text, "without going over");
painter.draw_string({UI_POS_X_CENTER(18), rules_y + 55}, style_text, "Dealer hits on 16");
painter.draw_string({UI_POS_X_CENTER(19), rules_y + 70}, style_text, "Dealer stays on 17");
painter.draw_string({UI_POS_X_CENTER(19), rules_y + 90}, style_text, "Blackjack pays 1:1");
// Controls
painter.draw_string({UI_POS_X_CENTER(15), 175}, style_rules, "-- CONTROLS --");
painter.draw_string({UI_POS_X_CENTER(18), 195}, style_text, "SELECT: Start/Hit");
painter.draw_string({UI_POS_X_CENTER(12), 210}, style_text, "LEFT: Stats");
painter.draw_string({UI_POS_X_CENTER(17), 225}, style_text, "RIGHT: Exit/Stay");
int controls_y = (screen_h > 300) ? 175 : 155;
painter.draw_string({UI_POS_X_CENTER(15), controls_y}, style_rules, "-- CONTROLS --");
painter.draw_string({UI_POS_X_CENTER(18), controls_y + 20}, style_text, "SELECT: Start/Hit");
painter.draw_string({UI_POS_X_CENTER(12), controls_y + 35}, style_text, "LEFT: Stats");
painter.draw_string({UI_POS_X_CENTER(17), controls_y + 50}, style_text, "RIGHT: Exit/Stay");
// Draw high score
painter.draw_string({UI_POS_X_CENTER(17), 250}, style_text, "High Score: $" + std::to_string(high_score));
// Draw high score at bottom
painter.draw_string({UI_POS_X_CENTER(17), screen_h - 70}, style_text, "High Score: $" + std::to_string(high_score));
}
void BlackjackView::draw_menu() {
@@ -293,10 +308,10 @@ void BlackjackView::draw_menu() {
if (blink_state) {
auto style = *ui::Theme::getInstance()->fg_yellow;
painter.draw_string({UI_POS_X_CENTER(17), 280}, style, "* PRESS SELECT *");
painter.draw_string({UI_POS_X_CENTER(17), screen_h - 40}, style, "* PRESS SELECT *");
} else {
// Clear just the text area
painter.fill_rectangle({UI_POS_X_CENTER(17), 278, 130, 20}, Color::black());
painter.fill_rectangle({UI_POS_X_CENTER(17), screen_h - 42, 130, 20}, Color::black());
}
}
}
@@ -310,27 +325,29 @@ void BlackjackView::draw_stats() {
painter.draw_string({UI_POS_X_CENTER(11), 30}, style_title, "STATISTICS");
painter.draw_string({30, 80}, style_text, "Wins:");
int stats_x = (screen_w > 300) ? 50 : 30;
painter.draw_string({stats_x, 80}, style_text, "Wins:");
painter.draw_string({UI_POS_X_CENTER(5), 80}, style_value, std::to_string(wins));
painter.draw_string({30, 100}, style_text, "Losses:");
painter.draw_string({stats_x, 100}, style_text, "Losses:");
painter.draw_string({UI_POS_X_CENTER(5), 100}, style_value, std::to_string(losses));
// Win percentage
uint32_t total = wins + losses;
if (total > 0) {
uint32_t win_pct = (wins * 100) / total;
painter.draw_string({30, 120}, style_text, "Win %:");
painter.draw_string({stats_x, 120}, style_text, "Win %:");
painter.draw_string({UI_POS_X_CENTER(5), 120}, style_value, std::to_string(win_pct) + "%");
}
painter.draw_string({30, 160}, style_text, "High Score:");
painter.draw_string({stats_x, 160}, style_text, "High Score:");
painter.draw_string({UI_POS_X_CENTER(5), 160}, style_value, "$" + std::to_string(high_score));
painter.draw_string({30, 180}, style_text, "Cash:");
painter.draw_string({stats_x, 180}, style_text, "Cash:");
painter.draw_string({UI_POS_X_CENTER(5), 180}, style_value, "$" + std::to_string(cash));
painter.draw_string({40, 250}, style_text, "SELECT: Back");
painter.draw_string({UI_POS_X_CENTER(12), screen_h - 70}, style_text, "SELECT: Back");
}
void BlackjackView::draw_betting() {
@@ -343,7 +360,7 @@ void BlackjackView::draw_betting() {
auto style_title = *ui::Theme::getInstance()->fg_green;
auto style_text = *ui::Theme::getInstance()->fg_light;
painter.draw_string({70, 40}, style_title, "PLACE BET");
painter.draw_string({UI_POS_X_CENTER(10), 40}, style_title, "PLACE BET");
painter.draw_string({30, 80}, style_text, "Cash: $" + std::to_string(cash));
painter.draw_string({30, 140}, style_text, "ENCODER: +/- $10");
@@ -373,7 +390,7 @@ void BlackjackView::draw_game() {
auto style = *ui::Theme::getInstance()->fg_green;
painter.draw_string({10, 10}, style, "Cash: $" + std::to_string(cash));
painter.draw_string({140, 10}, style, "Bet: $" + std::to_string(bet));
painter.draw_string({screen_w - 80, 10}, style, "Bet: $" + std::to_string(bet));
// Draw dealer hand with value next to label
auto style_value = *ui::Theme::getInstance()->fg_yellow;
@@ -385,16 +402,18 @@ void BlackjackView::draw_game() {
draw_hand(10, 65, dealer_cards, dealer_card_count, true);
// Draw player hand with value next to label
painter.draw_string({10, 165}, *ui::Theme::getInstance()->fg_light, "You:");
int player_y = (screen_h > 300) ? 165 : 145;
painter.draw_string({10, player_y}, *ui::Theme::getInstance()->fg_light, "You:");
int player_value = calculate_hand_value(player_cards, player_card_count);
painter.draw_string({50, 165}, style_value, "(" + std::to_string(player_value) + ")");
draw_hand(10, 185, player_cards, player_card_count, false);
painter.draw_string({50, player_y}, style_value, "(" + std::to_string(player_value) + ")");
draw_hand(10, player_y + 20, player_cards, player_card_count, false);
// Draw controls or result
int controls_y = screen_h - 50;
if (game_state == GameState::PLAYING) {
auto style_text = *ui::Theme::getInstance()->fg_light;
painter.draw_string({30, 290}, style_text, "SELECT: Hit");
painter.draw_string({130, 290}, style_text, "RIGHT: Stay");
painter.draw_string({30, controls_y}, style_text, "SELECT: Hit");
painter.draw_string({screen_w - 100, controls_y}, style_text, "RIGHT: Stay");
} else if (game_state == GameState::GAME_OVER) {
const Style* style_result = ui::Theme::getInstance()->fg_yellow;
std::string result;
@@ -416,14 +435,14 @@ void BlackjackView::draw_game() {
}
// Draw result
painter.draw_string({UI_POS_X_CENTER(result.length()), 270}, *style_result, result);
painter.draw_string({UI_POS_X_CENTER(result.length()), controls_y - 20}, *style_result, result);
// Draw compact bet selector in top right area
// Draw compact bet selector
auto style_bet = *ui::Theme::getInstance()->fg_cyan;
painter.draw_string({140, 25}, style_bet, "Next: $" + std::to_string(bet));
painter.draw_string({screen_w - 100, 25}, style_bet, "Next: $" + std::to_string(bet));
// Show controls
painter.draw_string({10, 290}, *ui::Theme::getInstance()->fg_light, "SELECT: Deal ENC: +/-");
painter.draw_string({10, controls_y}, *ui::Theme::getInstance()->fg_light, "SELECT: Deal ENC: +/-");
}
last_player_count = player_card_count;
@@ -432,109 +451,106 @@ void BlackjackView::draw_game() {
last_bet = bet;
} else if (game_state == GameState::GAME_OVER && bet != last_bet) {
// Only update the bet display when it changes
painter.fill_rectangle({140, 25, 90, 16}, Color::black());
painter.draw_string({140, 25}, *ui::Theme::getInstance()->fg_cyan, "Next: $" + std::to_string(bet));
painter.fill_rectangle({screen_w - 100, 25, 90, 16}, Color::black());
painter.draw_string({screen_w - 100, 25}, *ui::Theme::getInstance()->fg_cyan, "Next: $" + std::to_string(bet));
last_bet = bet;
}
}
void BlackjackView::draw_suit_symbol(int x, int y, uint8_t suit, Color color, bool large) {
if (large) {
// Large suit symbols for center of card
// Scale large symbols based on card size
int scale = card_width / 60;
switch (suit) {
case 0: // Spades
// Top curve
painter.fill_rectangle({x + 9, y + 4, 2, 2}, color);
painter.fill_rectangle({x + 8, y + 5, 4, 2}, color);
painter.fill_rectangle({x + 7, y + 6, 6, 2}, color);
painter.fill_rectangle({x + 6, y + 7, 8, 2}, color);
painter.fill_rectangle({x + 5, y + 8, 10, 2}, color);
painter.fill_rectangle({x + 4, y + 9, 12, 2}, color);
painter.fill_rectangle({x + 3, y + 10, 14, 2}, color);
painter.fill_rectangle({x + 2, y + 11, 16, 2}, color);
painter.fill_rectangle({x + 1, y + 12, 18, 2}, color);
painter.fill_rectangle({x + 0, y + 13, 20, 3}, color);
painter.fill_rectangle({x + 1, y + 16, 18, 2}, color);
painter.fill_rectangle({x + 2, y + 17, 16, 1}, color);
painter.fill_rectangle({x + 3, y + 18, 14, 1}, color);
// Draw using scaled rectangles
painter.fill_rectangle({x + 9 * scale, y + 4 * scale, 2 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 8 * scale, y + 5 * scale, 4 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 7 * scale, y + 6 * scale, 6 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 6 * scale, y + 7 * scale, 8 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 5 * scale, y + 8 * scale, 10 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 4 * scale, y + 9 * scale, 12 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 3 * scale, y + 10 * scale, 14 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 2 * scale, y + 11 * scale, 16 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 1 * scale, y + 12 * scale, 18 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 0 * scale, y + 13 * scale, 20 * scale, 3 * scale}, color);
painter.fill_rectangle({x + 1 * scale, y + 16 * scale, 18 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 2 * scale, y + 17 * scale, 16 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 3 * scale, y + 18 * scale, 14 * scale, 1 * scale}, color);
// Stem
painter.fill_rectangle({x + 9, y + 19, 2, 4}, color);
painter.fill_rectangle({x + 8, y + 22, 4, 1}, color);
painter.fill_rectangle({x + 7, y + 23, 6, 1}, color);
painter.fill_rectangle({x + 6, y + 24, 8, 1}, color);
painter.fill_rectangle({x + 9 * scale, y + 19 * scale, 2 * scale, 4 * scale}, color);
painter.fill_rectangle({x + 8 * scale, y + 22 * scale, 4 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 7 * scale, y + 23 * scale, 6 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 6 * scale, y + 24 * scale, 8 * scale, 1 * scale}, color);
break;
case 1: // Hearts
// Left bump
painter.fill_rectangle({x + 3, y + 5, 4, 2}, color);
painter.fill_rectangle({x + 2, y + 6, 6, 2}, color);
painter.fill_rectangle({x + 1, y + 7, 8, 3}, color);
painter.fill_rectangle({x + 0, y + 9, 9, 3}, color);
// Right bump
painter.fill_rectangle({x + 13, y + 5, 4, 2}, color);
painter.fill_rectangle({x + 12, y + 6, 6, 2}, color);
painter.fill_rectangle({x + 11, y + 7, 8, 3}, color);
painter.fill_rectangle({x + 11, y + 9, 9, 3}, color);
// Body
painter.fill_rectangle({x + 1, y + 11, 18, 3}, color);
painter.fill_rectangle({x + 2, y + 14, 16, 2}, color);
painter.fill_rectangle({x + 3, y + 16, 14, 2}, color);
painter.fill_rectangle({x + 4, y + 18, 12, 1}, color);
painter.fill_rectangle({x + 5, y + 19, 10, 1}, color);
painter.fill_rectangle({x + 6, y + 20, 8, 1}, color);
painter.fill_rectangle({x + 7, y + 21, 6, 1}, color);
painter.fill_rectangle({x + 8, y + 22, 4, 1}, color);
painter.fill_rectangle({x + 9, y + 23, 2, 1}, color);
// Draw using scaled rectangles
painter.fill_rectangle({x + 3 * scale, y + 5 * scale, 4 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 2 * scale, y + 6 * scale, 6 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 1 * scale, y + 7 * scale, 8 * scale, 3 * scale}, color);
painter.fill_rectangle({x + 0 * scale, y + 9 * scale, 9 * scale, 3 * scale}, color);
painter.fill_rectangle({x + 13 * scale, y + 5 * scale, 4 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 12 * scale, y + 6 * scale, 6 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 11 * scale, y + 7 * scale, 8 * scale, 3 * scale}, color);
painter.fill_rectangle({x + 11 * scale, y + 9 * scale, 9 * scale, 3 * scale}, color);
painter.fill_rectangle({x + 1 * scale, y + 11 * scale, 18 * scale, 3 * scale}, color);
painter.fill_rectangle({x + 2 * scale, y + 14 * scale, 16 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 3 * scale, y + 16 * scale, 14 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 4 * scale, y + 18 * scale, 12 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 5 * scale, y + 19 * scale, 10 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 6 * scale, y + 20 * scale, 8 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 7 * scale, y + 21 * scale, 6 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 8 * scale, y + 22 * scale, 4 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 9 * scale, y + 23 * scale, 2 * scale, 1 * scale}, color);
break;
case 2: // Diamonds
painter.fill_rectangle({x + 10, y + 3, 1, 1}, color);
painter.fill_rectangle({x + 9, y + 4, 3, 1}, color);
painter.fill_rectangle({x + 8, y + 5, 5, 1}, color);
painter.fill_rectangle({x + 7, y + 6, 7, 1}, color);
painter.fill_rectangle({x + 6, y + 7, 9, 1}, color);
painter.fill_rectangle({x + 5, y + 8, 11, 1}, color);
painter.fill_rectangle({x + 4, y + 9, 13, 1}, color);
painter.fill_rectangle({x + 3, y + 10, 15, 1}, color);
painter.fill_rectangle({x + 2, y + 11, 17, 1}, color);
painter.fill_rectangle({x + 1, y + 12, 19, 1}, color);
painter.fill_rectangle({x + 0, y + 13, 21, 1}, color);
painter.fill_rectangle({x + 1, y + 14, 19, 1}, color);
painter.fill_rectangle({x + 2, y + 15, 17, 1}, color);
painter.fill_rectangle({x + 3, y + 16, 15, 1}, color);
painter.fill_rectangle({x + 4, y + 17, 13, 1}, color);
painter.fill_rectangle({x + 5, y + 18, 11, 1}, color);
painter.fill_rectangle({x + 6, y + 19, 9, 1}, color);
painter.fill_rectangle({x + 7, y + 20, 7, 1}, color);
painter.fill_rectangle({x + 8, y + 21, 5, 1}, color);
painter.fill_rectangle({x + 9, y + 22, 3, 1}, color);
painter.fill_rectangle({x + 10, y + 23, 1, 1}, color);
// Draw using scaled rectangles
painter.fill_rectangle({x + 10 * scale, y + 3 * scale, 1 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 9 * scale, y + 4 * scale, 3 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 8 * scale, y + 5 * scale, 5 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 7 * scale, y + 6 * scale, 7 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 6 * scale, y + 7 * scale, 9 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 5 * scale, y + 8 * scale, 11 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 4 * scale, y + 9 * scale, 13 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 3 * scale, y + 10 * scale, 15 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 2 * scale, y + 11 * scale, 17 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 1 * scale, y + 12 * scale, 19 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 0 * scale, y + 13 * scale, 21 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 1 * scale, y + 14 * scale, 19 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 2 * scale, y + 15 * scale, 17 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 3 * scale, y + 16 * scale, 15 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 4 * scale, y + 17 * scale, 13 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 5 * scale, y + 18 * scale, 11 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 6 * scale, y + 19 * scale, 9 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 7 * scale, y + 20 * scale, 7 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 8 * scale, y + 21 * scale, 5 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 9 * scale, y + 22 * scale, 3 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 10 * scale, y + 23 * scale, 1 * scale, 1 * scale}, color);
break;
case 3: // Clubs
// Center circle
painter.fill_rectangle({x + 8, y + 8, 5, 1}, color);
painter.fill_rectangle({x + 7, y + 9, 7, 3}, color);
painter.fill_rectangle({x + 8, y + 12, 5, 1}, color);
// Left circle
painter.fill_rectangle({x + 3, y + 11, 4, 1}, color);
painter.fill_rectangle({x + 2, y + 12, 6, 3}, color);
painter.fill_rectangle({x + 3, y + 15, 4, 1}, color);
// Right circle
painter.fill_rectangle({x + 14, y + 11, 4, 1}, color);
painter.fill_rectangle({x + 13, y + 12, 6, 3}, color);
painter.fill_rectangle({x + 14, y + 15, 4, 1}, color);
// Connect circles
painter.fill_rectangle({x + 6, y + 13, 9, 2}, color);
// Stem
painter.fill_rectangle({x + 9, y + 16, 3, 4}, color);
painter.fill_rectangle({x + 8, y + 19, 5, 1}, color);
painter.fill_rectangle({x + 7, y + 20, 7, 1}, color);
painter.fill_rectangle({x + 6, y + 21, 9, 1}, color);
// Draw using scaled rectangles
painter.fill_rectangle({x + 8 * scale, y + 8 * scale, 5 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 7 * scale, y + 9 * scale, 7 * scale, 3 * scale}, color);
painter.fill_rectangle({x + 8 * scale, y + 12 * scale, 5 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 3 * scale, y + 11 * scale, 4 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 2 * scale, y + 12 * scale, 6 * scale, 3 * scale}, color);
painter.fill_rectangle({x + 3 * scale, y + 15 * scale, 4 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 14 * scale, y + 11 * scale, 4 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 13 * scale, y + 12 * scale, 6 * scale, 3 * scale}, color);
painter.fill_rectangle({x + 14 * scale, y + 15 * scale, 4 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 6 * scale, y + 13 * scale, 9 * scale, 2 * scale}, color);
painter.fill_rectangle({x + 9 * scale, y + 16 * scale, 3 * scale, 4 * scale}, color);
painter.fill_rectangle({x + 8 * scale, y + 19 * scale, 5 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 7 * scale, y + 20 * scale, 7 * scale, 1 * scale}, color);
painter.fill_rectangle({x + 6 * scale, y + 21 * scale, 9 * scale, 1 * scale}, color);
break;
}
} else {
// Small suit symbols
// Small suit symbols remain the same
switch (suit) {
case 0: // Spades - small
painter.fill_rectangle({x + 2, y + 1, 3, 3}, color);
@@ -569,18 +585,15 @@ void BlackjackView::draw_suit_symbol(int x, int y, uint8_t suit, Color color, bo
}
void BlackjackView::draw_card(int x, int y, uint8_t card, bool hidden) {
const int width = 60;
const int height = 80;
// Draw card background
painter.fill_rectangle({x, y, width, height}, Color::white());
painter.draw_rectangle({x, y, width, height}, Color::black());
painter.draw_rectangle({x + 1, y + 1, width - 2, height - 2}, Color::grey()); // Inner border
painter.fill_rectangle({x, y, card_width, card_height}, Color::white());
painter.draw_rectangle({x, y, card_width, card_height}, Color::black());
painter.draw_rectangle({x + 1, y + 1, card_width - 2, card_height - 2}, Color::grey());
if (hidden) {
// Draw card back pattern - diagonal lines
for (int i = 4; i < width - 4; i += 6) {
for (int j = 4; j < height - 4; j += 6) {
for (int i = 4; i < card_width - 4; i += 6) {
for (int j = 4; j < card_height - 4; j += 6) {
painter.fill_rectangle({x + i, y + j, 3, 3}, Color::blue());
painter.fill_rectangle({x + i + 3, y + j + 3, 3, 3}, Color::red());
}
@@ -606,22 +619,23 @@ void BlackjackView::draw_card(int x, int y, uint8_t card, bool hidden) {
draw_suit_symbol(suit_x, y + 4, suit, suit_color, false);
// Draw value in bottom-right corner
int bottom_x = (value_str == "10") ? x + width - 24 : x + width - 16;
painter.draw_string({bottom_x, y + height - 18}, card_style, value_str);
int bottom_x = (value_str == "10") ? x + card_width - 24 : x + card_width - 16;
painter.draw_string({bottom_x, y + card_height - 18}, card_style, value_str);
// Draw small suit symbol in bottom-right
draw_suit_symbol(x + width - 10, y + height - 16, suit, suit_color, false);
draw_suit_symbol(x + card_width - 10, y + card_height - 16, suit, suit_color, false);
// Draw large suit symbol in center
draw_suit_symbol(x + 20, y + 28, suit, suit_color, true);
int center_x = x + (card_width - 20) / 2;
int center_y = y + (card_height - 25) / 2;
draw_suit_symbol(center_x, center_y, suit, suit_color, true);
}
}
void BlackjackView::draw_hand(int x, int y, uint8_t* cards, uint8_t count, bool is_dealer) {
// Calculate total width needed
const int card_width = 60;
const int overlap = 40; // Amount of overlap when cards need to fit
const int max_width = screen_width - 10 - x; // Available width on screen
const int overlap = card_width * 2 / 3; // Amount of overlap when cards need to fit
const int max_width = screen_w - 10 - x; // Available width on screen
int spacing;
if (count == 1) {
@@ -651,7 +665,7 @@ bool BlackjackView::on_key(const KeyEvent key) {
switch (game_state) {
case GameState::MENU:
if (cash < 10) {
cash = 100; // Reset if broke - maybe should provide https://gamblersanonymous.org/ link if they also lost their wife and house
cash = 100; // Reset if broke
wins = 0;
losses = 0;
}
@@ -724,4 +738,4 @@ bool BlackjackView::on_encoder(const EncoderEvent delta) {
return false;
}
} // namespace ui::external_app::blackjack
} // namespace ui::external_app::blackjack
+8 -2
View File
@@ -77,6 +77,12 @@ class BlackjackView : public View {
NavigationView& nav_;
bool initialized = false;
// Screen dimensions (set dynamically)
int screen_w = 0;
int screen_h = 0;
int card_width = 60;
int card_height = 80;
// Game variables
static constexpr uint8_t MAX_CARDS_IN_HAND = 11; // Maximum possible cards before bust
uint8_t deck[52];
@@ -141,7 +147,7 @@ class BlackjackView : public View {
{"highscore"sv, &high_score}}};
Button dummy{
{screen_width, 0, 0, 0},
{0, 0, 0, 0},
""};
MessageHandlerRegistration message_handler_frame_sync{
@@ -153,4 +159,4 @@ class BlackjackView : public View {
} // namespace ui::external_app::blackjack
#endif /* __UI_BLACKJACK_H__ */
#endif /* __UI_BLACKJACK_H__ */
+12 -12
View File
@@ -20,13 +20,13 @@ void initialize_app(ui::NavigationView& nav) {
extern "C" {
__attribute__((section(".external_app.app_breakout.application_information"), used)) application_information_t _application_information_breakout = {
(uint8_t*)0x00000000,
ui::external_app::breakout::initialize_app,
CURRENT_HEADER_VERSION,
VERSION_MD5,
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::breakout::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
"Breakout",
{
/*.app_name = */ "Breakout",
/*.bitmap_data = */ {
0x00,
0x00,
0x7F,
@@ -60,11 +60,11 @@ __attribute__((section(".external_app.app_breakout.application_information"), us
0xF7,
0xF7,
},
ui::Color::green().v,
app_location_t::GAMES,
-1,
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::GAMES,
/*.desired_menu_position = */ -1,
{0, 0, 0, 0},
0x00000000,
/*.m4_app_tag = */ {'P', 'A', 'B', 'P'}, // This is the audio beep baseband tag!
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
} // namespace ui::external_app::breakout
}
+279 -10
View File
@@ -7,6 +7,12 @@
*/
#include "ui_breakout.hpp"
#include "baseband_api.hpp"
#include "audio.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
using namespace portapack;
namespace ui::external_app::breakout {
@@ -92,6 +98,37 @@ void BreakoutView::init_game() {
game_state = STATE_MENU;
menu_blink_state = true;
menu_blink_counter = 0;
// Play startup sound
if (sound_enabled) {
baseband::request_audio_beep(262, 24000, 150); // C4 -
chThdSleepMilliseconds(200);
baseband::request_audio_beep(262, 24000, 150); // C4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(392, 24000, 150); // G4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(392, 24000, 150); // G4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(440, 24000, 150); // A4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(440, 24000, 150); // A4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(392, 24000, 300); // G4
chThdSleepMilliseconds(350);
baseband::request_audio_beep(349, 24000, 150); // F4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(349, 24000, 150); // F4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(330, 24000, 150); // E4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(330, 24000, 150); // E4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(294, 24000, 150); // D4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(294, 24000, 150); // D4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(262, 24000, 400); // C4
}
}
void BreakoutView::init_level() {
@@ -126,6 +163,34 @@ void BreakoutView::draw_screen() {
draw_level();
}
void BreakoutView::draw_menu_volume_bar() {
// Clear the entire volume area first to handle bar shrinking
painter.fill_rectangle({40, 259, 220, 40}, Color::black());
// Draw volume label
auto style = *ui::Theme::getInstance()->fg_light;
painter.draw_string({40, 260}, style, "Volume:");
// Draw volume bar background
painter.draw_rectangle({100, 259, 102, 10}, Color::grey());
// Draw volume level
int bar_width = (volume_level * 100) / 99;
if (bar_width > 0) {
painter.fill_rectangle({101, 260, bar_width, 8}, Color::green());
}
// Draw percentage
painter.draw_string({210, 260}, style, std::to_string(volume_level) + "%");
// Draw sound status
if (sound_enabled) {
painter.draw_string({UI_POS_X_CENTER(11), 280}, *ui::Theme::getInstance()->fg_green, "Sound: ON");
} else {
painter.draw_string({UI_POS_X_CENTER(11), 280}, *ui::Theme::getInstance()->fg_red, "Sound: OFF");
}
}
void BreakoutView::draw_borders() {
rect(0, GAME_AREA_TOP - 1, SCREEN_WIDTH, GAME_AREA_TOP, COLOR_BORDER);
}
@@ -166,6 +231,22 @@ void BreakoutView::draw_level() {
painter.draw_string({80, 30}, style, "Level: " + std::to_string(level));
}
void BreakoutView::adjust_volume(int delta) {
volume_level += delta;
if (volume_level < 0) volume_level = 0;
if (volume_level > 99) volume_level = 99;
// Update the actual audio volume using decibel conversion
// Map 0-99 to roughly -60dB to 0dB range
int db = (volume_level - 99) * 60 / 99; // Maps 0->-60dB, 99->0dB
audio::headphone::set_volume(volume_t::decibel(db));
// If we're in the menu, redraw the volume bar
if (game_state == STATE_MENU) {
draw_menu_volume_bar();
}
}
void BreakoutView::move_paddle_left() {
if (paddle_x > 0) {
fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_BACKGROUND);
@@ -231,19 +312,23 @@ void BreakoutView::update_game() {
if (ball_x < 0) {
ball_x = 0;
ball_dx = -ball_dx;
play_wall_hit();
} else if (ball_x > SCREEN_WIDTH - BALL_SIZE) {
ball_x = SCREEN_WIDTH - BALL_SIZE;
ball_dx = -ball_dx;
play_wall_hit();
}
if (ball_y < GAME_AREA_TOP) {
ball_y = GAME_AREA_TOP;
ball_dy = -ball_dy;
play_wall_hit();
}
// Bottom edge - lose life
if (ball_y > GAME_AREA_BOTTOM) {
lives--;
play_death_sound(); // Play death sound
draw_lives();
if (lives <= 0) {
handle_game_over();
@@ -262,6 +347,8 @@ void BreakoutView::update_game() {
ball_y = PADDLE_Y - BALL_SIZE;
ball_dy = -ball_dy;
play_paddle_hit();
// Add some angle based on hit position
float hit_position = (ball_x + (BALL_SIZE / 2)) - paddle_x;
float angle = (hit_position / PADDLE_WIDTH) - 0.5f;
@@ -301,6 +388,8 @@ bool BreakoutView::check_brick_collision(int row, int col) {
score += (5 - row) * 10;
draw_score();
play_brick_hit();
ball_dy = -ball_dy;
return true;
}
@@ -314,6 +403,8 @@ bool BreakoutView::check_level_complete() {
void BreakoutView::next_level() {
level++;
play_level_complete();
chThdSleepMilliseconds(500); // Give time for sound to play
init_level();
draw_screen();
}
@@ -322,9 +413,11 @@ void BreakoutView::handle_game_over() {
game_state = STATE_GAME_OVER;
gameover_blink_state = true;
gameover_blink_counter = 0;
play_game_over();
}
void BreakoutView::show_menu() {
// Only draw the static parts once
if (menu_blink_counter == 0) {
cls();
background(COLOR_BACKGROUND);
@@ -338,23 +431,30 @@ void BreakoutView::show_menu() {
painter.draw_string({UI_POS_X_CENTER(20), 130}, style_cyan, "ROTARY: MOVE PADDLE");
painter.draw_string({UI_POS_X_CENTER(21), 160}, style_cyan, "SELECT: START/LAUNCH");
painter.draw_string({UI_POS_X_CENTER(25), 190}, style_blue, "========================");
// Volume adjustment instructions
painter.draw_string({UI_POS_X_CENTER(24), 220}, style_cyan, "Press UP/DOWN for Volume");
// Draw volume control at bottom
draw_menu_volume_bar();
}
auto style_red = *ui::Theme::getInstance()->fg_red;
// Only flash the "PRESS SELECT" text
if (++menu_blink_counter >= 30) {
menu_blink_counter = 0;
menu_blink_counter = 1; // Keep it at 1 so we don't redraw everything
menu_blink_state = !menu_blink_state;
painter.fill_rectangle({UI_POS_X_CENTER(17), 228, 128, 20}, Color::black());
auto style_red = *ui::Theme::getInstance()->fg_red;
painter.fill_rectangle({UI_POS_X_CENTER(17), 238, 128, 20}, Color::black());
if (menu_blink_state) {
painter.draw_string({UI_POS_X_CENTER(17), 230}, style_red, "* PRESS SELECT *");
painter.draw_string({UI_POS_X_CENTER(17), 240}, style_red, "* PRESS SELECT *");
}
}
}
void BreakoutView::show_game_over() {
// Only draw the static parts once
if (gameover_blink_counter == 0) {
cls();
background(COLOR_BACKGROUND);
@@ -366,12 +466,12 @@ void BreakoutView::show_game_over() {
painter.draw_string({UI_POS_X_CENTER(11), 150}, style_yellow, "SCORE: " + std::to_string(score));
}
auto style_green = *ui::Theme::getInstance()->fg_green;
// Only flash the "PRESS SELECT" text
if (++gameover_blink_counter >= 30) {
gameover_blink_counter = 0;
gameover_blink_counter = 1; // Keep it at 1 so we don't redraw everything
gameover_blink_state = !gameover_blink_state;
auto style_green = *ui::Theme::getInstance()->fg_green;
painter.fill_rectangle({UI_POS_X_CENTER(13), 198, 96, 20}, Color::black());
if (gameover_blink_state) {
@@ -385,17 +485,169 @@ void BreakoutView::reset_game() {
score = 0;
lives = 3;
game_state = STATE_PLAYING;
dummy.focus();
init_level();
draw_screen();
}
// Sound effect implementations with timing control
void BreakoutView::play_paddle_hit() {
if (sound_enabled) {
uint32_t current_time = chTimeNow();
if (current_time - last_sound_time >= min_sound_interval) {
baseband::request_beep_stop(); // Stop any previous sound
chThdSleepMilliseconds(5); // Small delay
baseband::request_audio_beep(440, 24000, 40); // 440Hz A4, 40ms
last_sound_time = current_time;
}
}
}
void BreakoutView::play_brick_hit() {
if (sound_enabled) {
uint32_t current_time = chTimeNow();
if (current_time - last_sound_time >= min_sound_interval) {
baseband::request_beep_stop(); // Stop any previous sound
chThdSleepMilliseconds(5); // Small delay
// Higher pitch that varies with remaining bricks
uint32_t freq = 880 + ((25 - brick_count) * 20); // Gets higher as bricks are destroyed
baseband::request_audio_beep(freq, 24000, 30);
last_sound_time = current_time;
}
}
}
void BreakoutView::play_wall_hit() {
if (sound_enabled) {
uint32_t current_time = chTimeNow();
if (current_time - last_sound_time >= min_sound_interval) {
baseband::request_beep_stop(); // Stop any previous sound
chThdSleepMilliseconds(5); // Small delay
baseband::request_audio_beep(220, 24000, 120); // 220Hz A3, 120ms
last_sound_time = current_time;
}
}
}
void BreakoutView::play_death_sound() {
if (sound_enabled) {
baseband::request_audio_beep(493, 24000, 150); // B4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(466, 24000, 150); // Bb4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(440, 24000, 150); // A4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(415, 24000, 150); // Ab4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(392, 24000, 150); // G4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(369, 24000, 150); // Gb4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(349, 24000, 150); // F4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(329, 24000, 150); // E4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(293, 24000, 150); // D4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(261, 24000, 150); // C4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(196, 24000, 200); // G3
chThdSleepMilliseconds(230);
baseband::request_audio_beep(130, 24000, 300); // C3
}
}
void BreakoutView::play_game_over() {
if (sound_enabled) {
// First phrase
baseband::request_audio_beep(392, 24000, 300); // G4
chThdSleepMilliseconds(350);
baseband::request_audio_beep(392, 24000, 150); // G4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(392, 24000, 150); // G4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(311, 24000, 500); // Eb4 - long
chThdSleepMilliseconds(600);
// Second phrase
baseband::request_audio_beep(349, 24000, 300); // F4
chThdSleepMilliseconds(350);
baseband::request_audio_beep(349, 24000, 150); // F4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(349, 24000, 150); // F4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(294, 24000, 500); // D4 - long
chThdSleepMilliseconds(600);
// Final phrase - slower and more dramatic
baseband::request_audio_beep(262, 24000, 200); // C4
chThdSleepMilliseconds(250);
baseband::request_audio_beep(247, 24000, 200); // B3
chThdSleepMilliseconds(250);
baseband::request_audio_beep(233, 24000, 200); // Bb3
chThdSleepMilliseconds(250);
baseband::request_audio_beep(220, 24000, 200); // A3
chThdSleepMilliseconds(250);
baseband::request_audio_beep(196, 24000, 200); // G3
chThdSleepMilliseconds(250);
baseband::request_audio_beep(175, 24000, 200); // F3
chThdSleepMilliseconds(250);
baseband::request_audio_beep(131, 24000, 600); // C3
}
}
void BreakoutView::play_level_complete() {
if (sound_enabled) {
baseband::request_beep_stop(); // Stop any previous sound
chThdSleepMilliseconds(10);
baseband::request_audio_beep(1047, 24000, 300); // 1047Hz C6, 300ms
}
}
BreakoutView::BreakoutView(NavigationView& nav)
: nav_{nav}, bricks{} { // Add bricks{} here
: nav_{nav}, bricks{} {
// Initialize audio system for beeps FIRST
baseband::run_prepared_image(portapack::memory::map::m4_code.base()); // Load the audio beep baseband
add_children({&dummy});
// Initialize audio
audio::set_rate(audio::Rate::Hz_24000);
audio::output::start();
// Set initial volume to 80% and ensure sound is ON
volume_level = 80;
sound_enabled = true; // Make sure this is explicitly set to ON
// Set the actual volume
int db = (volume_level - 99) * 60 / 99; // Maps to roughly -12dB for 80%
audio::headphone::set_volume(volume_t::decibel(db));
attach(1.0 / 60.0);
}
BreakoutView::~BreakoutView() {
// Clean shutdown
baseband::request_beep_stop();
receiver_model.disable();
baseband::shutdown();
audio::output::stop();
}
void BreakoutView::on_show() {
// Nothing needed here, initialization done in constructor
}
void BreakoutView::on_hide() {
// Additional cleanup
baseband::request_beep_stop();
audio::output::stop();
}
void BreakoutView::focus() {
dummy.focus();
}
void BreakoutView::paint(Painter& painter) {
@@ -433,16 +685,33 @@ bool BreakoutView::on_key(const KeyEvent key) {
} else if (game_state == STATE_PLAYING && ball_attached) {
launch_ball();
} else if (game_state == STATE_GAME_OVER) {
reset_game();
// Return to menu after game over
game_state = STATE_MENU;
menu_blink_counter = 0;
init_game();
}
} else if (key == KeyEvent::Left && game_state == STATE_PLAYING) {
move_paddle_left();
} else if (key == KeyEvent::Right && game_state == STATE_PLAYING) {
move_paddle_right();
} else if (key == KeyEvent::Up && game_state == STATE_MENU) {
// Only adjust volume in menu
adjust_volume(5);
if (!sound_enabled) sound_enabled = true;
set_dirty();
} else if (key == KeyEvent::Down && game_state == STATE_MENU) {
// Only adjust volume in menu
adjust_volume(-5);
set_dirty();
}
set_dirty();
return true;
}
bool BreakoutView::on_touch(const TouchEvent event) {
(void)event; // Intentionally unused - touch disabled to avoid nav bar issues
return true;
}
} // namespace ui::external_app::breakout
+21 -1
View File
@@ -11,6 +11,7 @@
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "event_m0.hpp"
#include "message.hpp"
#include "irq_controls.hpp"
@@ -56,15 +57,18 @@ enum {
class BreakoutView : public View {
public:
BreakoutView(NavigationView& nav);
~BreakoutView();
void on_show() override;
void on_hide() override;
std::string title() const override { return "Breakout"; }
void focus() override { dummy.focus(); }
void focus() override;
void paint(Painter& painter) override;
void frame_sync();
bool on_encoder(const EncoderEvent event) override;
bool on_key(KeyEvent key) override;
bool on_touch(const TouchEvent event) override;
void cls();
void background(int color);
@@ -81,6 +85,7 @@ class BreakoutView : public View {
void draw_lives();
void draw_level();
void draw_borders();
void draw_menu_volume_bar();
void move_paddle_left();
void move_paddle_right();
void launch_ball();
@@ -96,6 +101,15 @@ class BreakoutView : public View {
void check_game_timer();
void attach(double delay_sec);
void detach();
void adjust_volume(int delta);
// Sound effect methods
void play_paddle_hit();
void play_brick_hit();
void play_wall_hit();
void play_death_sound();
void play_game_over();
void play_level_complete();
private:
const Color pp_colors[9] = {
@@ -147,6 +161,12 @@ class BreakoutView : public View {
double game_update_timeout = 0;
uint32_t game_update_counter = 0;
// Audio control
bool sound_enabled = true;
int volume_level = 80; // 0-99
uint32_t last_sound_time = 0;
uint32_t min_sound_interval = 20; // Minimum ms between sounds
Button dummy{
{screen_width, 0, 0, 0},
""};
+123 -77
View File
@@ -43,7 +43,7 @@ const Color pp_colors[] = {
// Drawing functions
void cls() {
painter.fill_rectangle({0, 0, portapack::display.width(), portapack::display.height()}, Color::black());
painter.fill_rectangle({0, 0, ui::screen_width, ui::screen_height}, Color::black());
}
void fillrect(int x1, int y1, int x2, int y2, int color) {
@@ -100,6 +100,15 @@ DinoGameView::DinoGameView(NavigationView& nav)
: nav_{nav}, bird_info{}, game_timer{} {
add_children({&dummy, &button_difficulty});
current_instance = this;
// Initialize dimensions first
init_dimensions();
// Now reposition button with proper centering
int button_y = SCREEN_HEIGHT - 100;
int button_x = (SCREEN_WIDTH - 100) / 2;
button_difficulty.set_parent_rect({button_x, button_y, 100, 20});
game_timer.attach(&game_timer_check, 1.0 / 60.0);
button_difficulty.on_select = [this](Button&) {
@@ -126,6 +135,33 @@ void DinoGameView::frame_sync() {
set_dirty();
}
void DinoGameView::init_dimensions() {
SCREEN_WIDTH = ui::screen_width;
SCREEN_HEIGHT = ui::screen_height;
// Scale game area based on screen size
GAME_AREA_HEIGHT = (SCREEN_HEIGHT * 160) / 320; // Scale proportionally
GAME_AREA_TOP = SCREEN_HEIGHT / 4;
// Calculate positions
DINO_Y = GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - DINO_HEIGHT;
DINO_DUCK_Y = GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - DINO_DUCK_HEIGHT;
BIRD_Y_UP = GAME_AREA_TOP + 20;
BIRD_Y_DOWN = GAME_AREA_TOP + (GAME_AREA_HEIGHT * 60) / 160;
// Scale jump height
JUMP_MAX_HEIGHT = (GAME_AREA_HEIGHT * 70) / 160;
// Scale distances based on screen width
MIN_OBSTACLE_DISTANCE = (SCREEN_WIDTH * 300) / 240;
MAX_OBSTACLE_DISTANCE = (SCREEN_WIDTH * 600) / 240;
// Scale horizontal position
DINO_X = SCREEN_WIDTH / 8;
last_dino_y = DINO_Y;
}
void DinoGameView::init_game() {
game_state = GameState::MENU;
menu_initialized = false;
@@ -142,7 +178,7 @@ void DinoGameView::new_game() {
duck_timer = 0;
displayedGameOver = false;
bird_info.inGame = false;
bird_info.x_offset = 320;
bird_info.x_offset = SCREEN_WIDTH;
bird_info.y_offset = 0;
bird_info.y_velocity = 0;
jumping = false;
@@ -181,9 +217,9 @@ void DinoGameView::show_menu() {
painter.draw_string({UI_POS_X_CENTER(10), 60}, style_title, "DINO GAME");
// Draw instructions
painter.draw_string({UI_POS_X_CENTER(19), 130}, style, "SELECT: Jump/Start");
painter.draw_string({UI_POS_X_CENTER(11), 150}, style, "DOWN: Duck");
painter.draw_string({UI_POS_X_CENTER(17), 170}, style, "Avoid obstacles!");
painter.draw_string({UI_POS_X_CENTER(19), 120}, style, "SELECT: Jump/Start");
painter.draw_string({UI_POS_X_CENTER(11), 140}, style, "DOWN: Duck");
painter.draw_string({UI_POS_X_CENTER(17), 160}, style, "Avoid obstacles!");
// Draw high score
draw_high_score();
@@ -194,14 +230,21 @@ void DinoGameView::show_menu() {
// Show difficulty button
button_difficulty.hidden(false);
// Animate the menu dino
bool menu_run_frame = (blink_counter / 15) % 2;
// Animate the menu dino - use frame counter for smooth animation
bool menu_run_frame = (blink_counter / 10) % 2;
// Clear previous dino position
fillrect(103, 90, 103 + DINO_WIDTH, 90 + DINO_HEIGHT, Black);
int menu_dino_x = UI_POS_X_CENTER(DINO_WIDTH);
int menu_dino_y = 90;
// Clear previous dino position - clear a bit extra to handle any artifacts
static bool last_menu_frame = false;
if (last_menu_frame != menu_run_frame || blink_counter == 0) {
fillrect(menu_dino_x - 5, menu_dino_y - 5, menu_dino_x + DINO_WIDTH + 5, menu_dino_y + DINO_HEIGHT + 5, Black);
last_menu_frame = menu_run_frame;
}
// Draw animated dino
draw_dino_at(103, 90, false, menu_run_frame);
draw_dino_at(menu_dino_x, menu_dino_y, false, menu_run_frame);
// Blinking start prompt
auto style_prompt = *ui::Theme::getInstance()->fg_light;
@@ -209,9 +252,10 @@ void DinoGameView::show_menu() {
blink_counter = 0;
blink_state = !blink_state;
painter.fill_rectangle({UI_POS_X_CENTER(17), 258, 130, 20}, Color::black());
int prompt_y = SCREEN_HEIGHT - 60;
painter.fill_rectangle({UI_POS_X_CENTER(17), prompt_y - 2, 130, 20}, Color::black());
if (blink_state) {
painter.draw_string({UI_POS_X_CENTER(17), 260}, style_prompt, "* PRESS SELECT *");
painter.draw_string({UI_POS_X_CENTER(17), prompt_y}, style_prompt, "* PRESS SELECT *");
}
}
}
@@ -220,33 +264,30 @@ void DinoGameView::show_game_over() {
if (!displayedGameOver) {
displayedGameOver = true;
// Clear the last normal dino position
if (last_ducking) {
fillrect(DINO_X, last_dino_y, DINO_X + DINO_DUCK_WIDTH, last_dino_y + DINO_DUCK_HEIGHT, Black);
} else {
fillrect(DINO_X, last_dino_y, DINO_X + DINO_WIDTH, last_dino_y + DINO_HEIGHT, Black);
}
// Clear the entire play area to ensure clean display
fillrect(0, GAME_AREA_TOP, SCREEN_WIDTH, GAME_AREA_HEIGHT + 20, Black);
// Draw the game over dino sprite
draw_dino_sprite(DINO_X, DINO_Y, dino_gameover);
// Draw the game over dino sprite in the center
int gameover_dino_x = UI_POS_X_CENTER(DINO_WIDTH);
int gameover_dino_y = GAME_AREA_TOP + 40;
draw_dino_sprite(gameover_dino_x, gameover_dino_y, dino_gameover);
auto style = *ui::Theme::getInstance()->fg_light;
auto style_score = *ui::Theme::getInstance()->fg_medium;
// Game over text
painter.draw_string({UI_POS_X_CENTER(10), 70}, style, "GAME OVER");
painter.draw_string({UI_POS_X_CENTER(10), GAME_AREA_TOP + 90}, style, "GAME OVER");
// Show final score
std::string score_text = "SCORE: " + score_to_string(score);
int score_x = (screen_width - score_text.length() * 8) / 2;
painter.draw_string({score_x, 90}, style_score, score_text);
painter.draw_string({UI_POS_X_CENTER(score_text.length()), GAME_AREA_TOP + 110}, style_score, score_text);
painter.draw_string({UI_POS_X_CENTER(16), 110}, style, "SELECT TO RETRY");
painter.draw_string({UI_POS_X_CENTER(16), GAME_AREA_TOP + 130}, style, "SELECT TO RETRY");
// Update high score
if (score > highScore) {
highScore = score;
painter.draw_string({UI_POS_X_CENTER(16), 130}, style, "NEW HIGH SCORE!");
painter.draw_string({UI_POS_X_CENTER(16), GAME_AREA_TOP + 150}, style, "NEW HIGH SCORE!");
}
}
}
@@ -263,7 +304,7 @@ void DinoGameView::game_loop() {
// Update ground animation
ground_offset = (ground_offset + GAME_SPEED_BASE + speed_modifier) % 20;
// Clear only the game area (not the whole screen to reduce flicker)
// Draw ground
draw_ground();
// Update and draw obstacles
@@ -295,38 +336,38 @@ void DinoGameView::game_loop() {
// Update run animation
if (get_steps() % (10 - speed_modifier) == 0) runstate = !runstate;
// Draw dino with minimal redraw
// Draw dino with proper clearing
int current_dino_y = jumping ? (DINO_Y - jumpHeight) : (ducking ? DINO_DUCK_Y : DINO_Y);
// Clear old dino position more precisely
// Only clear and redraw if something changed
if (current_dino_y != last_dino_y || runstate != last_runstate || ducking != last_ducking) {
// Clear based on last state
// Clear old position with extra margin to prevent artifacts
if (last_ducking) {
fillrect(DINO_X, last_dino_y, DINO_X + DINO_DUCK_WIDTH, last_dino_y + DINO_DUCK_HEIGHT, Black);
fillrect(DINO_X - 2, last_dino_y - 2, DINO_X + DINO_DUCK_WIDTH + 2, last_dino_y + DINO_DUCK_HEIGHT + 2, Black);
} else {
fillrect(DINO_X, last_dino_y, DINO_X + DINO_WIDTH, last_dino_y + DINO_HEIGHT, Black);
fillrect(DINO_X - 2, last_dino_y - 2, DINO_X + DINO_WIDTH + 2, last_dino_y + DINO_HEIGHT + 2, Black);
}
}
// Draw dino at new position
if (jumping) {
draw_dino_sprite(DINO_X, current_dino_y, dino_default);
} else if (ducking) {
if (runstate)
draw_dino_sprite(DINO_X, current_dino_y, dino_ducking_leftstep);
else
draw_dino_sprite(DINO_X, current_dino_y, dino_ducking_rightstep);
} else {
if (runstate)
draw_dino_sprite(DINO_X, current_dino_y, dino_leftstep);
else
draw_dino_sprite(DINO_X, current_dino_y, dino_rightstep);
}
// Draw dino at new position
if (jumping) {
draw_dino_sprite(DINO_X, current_dino_y, dino_default);
} else if (ducking) {
if (runstate)
draw_dino_sprite(DINO_X, current_dino_y, dino_ducking_leftstep);
else
draw_dino_sprite(DINO_X, current_dino_y, dino_ducking_rightstep);
} else {
if (runstate)
draw_dino_sprite(DINO_X, current_dino_y, dino_leftstep);
else
draw_dino_sprite(DINO_X, current_dino_y, dino_rightstep);
}
// Update last state
last_dino_y = current_dino_y;
last_ducking = ducking;
last_runstate = runstate;
// Update last state
last_dino_y = current_dino_y;
last_ducking = ducking;
last_runstate = runstate;
}
// Check collisions
check_collision();
@@ -346,14 +387,14 @@ void DinoGameView::draw_ground() {
int ground_y = GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT;
// Clear ground area
fillrect(0, ground_y, 320, ground_y + GROUND_HEIGHT, Black);
fillrect(0, ground_y, SCREEN_WIDTH, ground_y + GROUND_HEIGHT, Black);
// Draw ground line
painter.draw_hline({0, ground_y}, 320, Color::white());
painter.draw_hline({0, ground_y + 1}, 320, Color::dark_grey());
painter.draw_hline({0, ground_y}, SCREEN_WIDTH, Color::white());
painter.draw_hline({0, ground_y + 1}, SCREEN_WIDTH, Color::dark_grey());
// Draw ground texture
for (int x = -ground_offset; x < 320; x += 20) {
for (int x = -ground_offset; x < SCREEN_WIDTH; x += 20) {
painter.draw_hline({x, ground_y + 3}, 10, Color::dark_grey());
painter.draw_hline({x + 5, ground_y + 5}, 5, Color::dark_grey());
}
@@ -364,7 +405,7 @@ void DinoGameView::update_obstacles() {
for (int i = 0; i < MAX_OBSTACLES; i++) {
if (obstacles[i].active) {
// Clear old position
if (obstacles[i].last_x != obstacles[i].x && obstacles[i].last_x < 320) {
if (obstacles[i].last_x != obstacles[i].x && obstacles[i].last_x < SCREEN_WIDTH) {
clear_obstacle_area(obstacles[i].last_x, obstacles[i].width + 10, obstacles[i].height + 10);
}
@@ -394,8 +435,8 @@ void DinoGameView::update_obstacles() {
for (int i = 0; i < MAX_OBSTACLES; i++) {
if (!obstacles[i].active) {
obstacles[i].active = true;
obstacles[i].x = 320;
obstacles[i].last_x = 320;
obstacles[i].x = SCREEN_WIDTH;
obstacles[i].last_x = SCREEN_WIDTH;
obstacles[i].type = rand() % 4;
// Set obstacle dimensions based on type
@@ -495,7 +536,7 @@ void DinoGameView::manage_bird() {
// Only spawn bird if no obstacles are too close
bool obstacle_nearby = false;
for (int i = 0; i < MAX_OBSTACLES; i++) {
if (obstacles[i].active && obstacles[i].x > 200) {
if (obstacles[i].active && obstacles[i].x > SCREEN_WIDTH * 0.8) {
obstacle_nearby = true;
break;
}
@@ -504,7 +545,7 @@ void DinoGameView::manage_bird() {
// Randomly spawn bird if no nearby obstacles
if (!obstacle_nearby && rand() % 400 == 0 && get_steps() > 100) {
bird_info.inGame = true;
bird_info.x_offset = 320;
bird_info.x_offset = SCREEN_WIDTH;
bird_info.y_offset = 0;
bird_info.y_velocity = easy_mode ? 0 : (rand() % 3) - 1; // No vertical movement in easy mode
bird_info.y_position = BirdPosition::DOWN; // Always spawn at standing height in easy mode
@@ -514,16 +555,16 @@ void DinoGameView::manage_bird() {
int base_y = (bird_info.y_position == BirdPosition::UP) ? BIRD_Y_UP : BIRD_Y_DOWN;
int current_y = base_y + bird_info.y_offset;
// Clear previous bird position
if (last_bird_x < 320 && last_bird_x >= -BIRD_WIDTH) {
int clear_start_x = last_bird_x;
int clear_end_x = last_bird_x + BIRD_WIDTH;
// Clear previous bird position with extra margin
if (last_bird_x < SCREEN_WIDTH && last_bird_x >= -BIRD_WIDTH) {
int clear_start_x = last_bird_x - 2;
int clear_end_x = last_bird_x + BIRD_WIDTH + 2;
if (clear_start_x < 0) clear_start_x = 0;
if (clear_end_x > 320) clear_end_x = 320;
if (clear_end_x > SCREEN_WIDTH) clear_end_x = SCREEN_WIDTH;
if (clear_end_x > clear_start_x) {
fillrect(clear_start_x, last_bird_y, clear_end_x, last_bird_y + BIRD_HEIGHT, Black);
fillrect(clear_start_x, last_bird_y - 2, clear_end_x, last_bird_y + BIRD_HEIGHT + 2, Black);
}
}
@@ -549,7 +590,6 @@ void DinoGameView::manage_bird() {
}
} else {
// In easy mode, keep bird at perfect ducking height
// Bird should hit standing dino but miss ducking dino
current_y = DINO_Y - 10; // Position bird just above ducking height
}
@@ -557,7 +597,7 @@ void DinoGameView::manage_bird() {
if (get_steps() % 15 == 0) bird_info.flop = !bird_info.flop;
// Draw bird only if any part is visible
if (bird_info.x_offset > -BIRD_WIDTH && bird_info.x_offset < 320) {
if (bird_info.x_offset > -BIRD_WIDTH && bird_info.x_offset < SCREEN_WIDTH) {
if (bird_info.flop) {
draw_bird_sprite(bird_info.x_offset, current_y, pterodactyl_upflop);
} else {
@@ -590,6 +630,7 @@ void DinoGameView::draw_dino_sprite(int x, int y, const uint16_t* sprite) {
width = DINO_WIDTH;
}
// Draw sprite efficiently with run-length optimization
for (int dy = 0; dy < height; dy++) {
int run_start = -1;
uint16_t run_color = 0;
@@ -623,16 +664,21 @@ void DinoGameView::draw_dino_sprite(int x, int y, const uint16_t* sprite) {
}
void DinoGameView::draw_bird_sprite(int x, int y, const uint16_t* sprite) {
// Draw sprite efficiently with clipping
for (int dy = 0; dy < BIRD_HEIGHT; dy++) {
int run_start = -1;
uint16_t run_color = 0;
for (int dx = 0; dx < BIRD_WIDTH; dx++) {
// Skip pixels that would be off-screen
if (x + dx < 0 || x + dx >= 320) {
if (x + dx < 0 || x + dx >= SCREEN_WIDTH) {
// End any current run
if (run_start != -1) {
painter.fill_rectangle({x + run_start, y + dy, dx - run_start, 1}, Color(run_color));
if (run_start != -1 && x + run_start < SCREEN_WIDTH) {
int run_width = dx - run_start;
if (x + run_start + run_width > SCREEN_WIDTH) {
run_width = SCREEN_WIDTH - (x + run_start);
}
painter.fill_rectangle({x + run_start, y + dy, run_width, 1}, Color(run_color));
run_start = -1;
}
continue;
@@ -660,10 +706,10 @@ void DinoGameView::draw_bird_sprite(int x, int y, const uint16_t* sprite) {
}
}
if (run_start != -1 && x + run_start < 320) {
if (run_start != -1 && x + run_start < SCREEN_WIDTH) {
int width = BIRD_WIDTH - run_start;
if (x + run_start + width > 320) {
width = 320 - (x + run_start);
if (x + run_start + width > SCREEN_WIDTH) {
width = SCREEN_WIDTH - (x + run_start);
}
if (width > 0) {
painter.fill_rectangle({x + run_start, y + dy, width, 1}, Color(run_color));
@@ -683,7 +729,7 @@ void DinoGameView::draw_dino_at(int x, int y, bool is_ducking, bool run_frame) {
if (run_frame) {
draw_dino_sprite(x, y, dino_leftstep);
} else {
draw_dino_sprite(x, y, dino_default);
draw_dino_sprite(x, y, dino_rightstep);
}
}
}
@@ -743,8 +789,8 @@ void DinoGameView::draw_current_score() {
void DinoGameView::draw_high_score() {
auto style = *ui::Theme::getInstance()->fg_light;
painter.fill_rectangle({UI_POS_X_RIGHT(9), 28, UI_POS_WIDTH(9), 18}, Color::black());
painter.draw_string({UI_POS_X_RIGHT(9), 30}, style, "HI " + score_to_string(highScore));
painter.fill_rectangle({UI_POS_X_RIGHT(90), 28, 90, 18}, Color::black());
painter.draw_string({UI_POS_X_RIGHT(90), 30}, style, "HI " + score_to_string(highScore));
}
void DinoGameView::jump() {
@@ -805,4 +851,4 @@ bool DinoGameView::on_encoder(const EncoderEvent delta) {
return true;
}
} // namespace ui::external_app::dinogame
} // namespace ui::external_app::dinogame
+32 -27
View File
@@ -41,29 +41,6 @@ enum {
Black,
};
// Game constants
#define DINO_WIDTH 34
#define DINO_HEIGHT 36
#define DINO_DUCK_WIDTH 45
#define DINO_DUCK_HEIGHT 22
#define BIRD_WIDTH 34
#define BIRD_HEIGHT 27
#define GROUND_HEIGHT 10
#define GAME_AREA_TOP 78
#define GAME_AREA_HEIGHT 160
#define DINO_X 30
#define DINO_Y (GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - DINO_HEIGHT)
#define DINO_DUCK_Y (GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - DINO_DUCK_HEIGHT)
#define BIRD_Y_UP (GAME_AREA_TOP + 20)
#define BIRD_Y_DOWN (GAME_AREA_TOP + 60)
#define JUMP_MAX_HEIGHT 70
#define JUMP_SPEED 3
#define GAME_SPEED_BASE 3
#define SPRITE_COLOR 0x528A
#define TRANSPARENT_COLOR 0xFFFF
#define MIN_OBSTACLE_DISTANCE 300
#define MAX_OBSTACLE_DISTANCE 600
// Game states
enum class GameState {
MENU,
@@ -159,6 +136,33 @@ class DinoGameView : public View {
bool initialized = false;
NavigationView& nav_;
// Dynamic screen dimensions
int SCREEN_WIDTH = 240;
int SCREEN_HEIGHT = 320;
int DINO_WIDTH = 34;
int DINO_HEIGHT = 36;
int DINO_DUCK_WIDTH = 45;
int DINO_DUCK_HEIGHT = 22;
int BIRD_WIDTH = 34;
int BIRD_HEIGHT = 27;
int GROUND_HEIGHT = 10;
int GAME_AREA_TOP = 78;
int GAME_AREA_HEIGHT = 160;
int DINO_X = 30;
int DINO_Y = 0; // Will be calculated
int DINO_DUCK_Y = 0; // Will be calculated
int BIRD_Y_UP = 0; // Will be calculated
int BIRD_Y_DOWN = 0; // Will be calculated
int JUMP_MAX_HEIGHT = 70;
int JUMP_SPEED = 3;
int GAME_SPEED_BASE = 3;
int MIN_OBSTACLE_DISTANCE = 300;
int MAX_OBSTACLE_DISTANCE = 600;
// Sprite constants
static constexpr uint16_t SPRITE_COLOR = 0x528A;
static constexpr uint16_t TRANSPARENT_COLOR = 0xFFFF;
// Game variables
static constexpr uint8_t MAX_OBSTACLES = 1;
SimpleObstacle obstacles[MAX_OBSTACLES];
@@ -185,7 +189,7 @@ class DinoGameView : public View {
int32_t obstacle_spawn_timer = 0;
// Position tracking for minimal redraw
int16_t last_dino_y = DINO_Y;
int16_t last_dino_y = 0;
int16_t last_bird_x = -1;
int16_t last_bird_y = -1;
uint8_t duck_timer = 0;
@@ -202,6 +206,7 @@ class DinoGameView : public View {
Ticker game_timer;
// Private methods
void init_dimensions();
void init_game();
void new_game();
void update_obstacles();
@@ -229,7 +234,7 @@ class DinoGameView : public View {
bool easy_mode = false;
Button button_difficulty{
{UI_POS_X_CENTER(13), 195, 13 * 8, 20},
{70, 195, 100, 20},
"Mode: HARD"};
app_settings::SettingsManager settings_{
@@ -239,7 +244,7 @@ class DinoGameView : public View {
{"easy_mode"sv, &easy_mode}}};
Button dummy{
{screen_width, 0, 0, 0},
{240, 0, 0, 0},
""};
MessageHandlerRegistration message_handler_frame_sync{
@@ -251,4 +256,4 @@ class DinoGameView : public View {
} // namespace ui::external_app::dinogame
#endif /* __UI_DINOGAME_H__ */
#endif /* __UI_DINOGAME_H__ */
+62 -23
View File
@@ -17,6 +17,7 @@ int SCREEN_HEIGHT = 0;
int RENDER_HEIGHT = 0;
int HALF_WIDTH = 0;
int HALF_HEIGHT = 0;
int HUD_HEIGHT = 40;
#define LEVEL_WIDTH_BASE 6
#define LEVEL_WIDTH (1 << LEVEL_WIDTH_BASE)
#define LEVEL_HEIGHT 57
@@ -24,7 +25,7 @@ int HALF_HEIGHT = 0;
#define MAX_RENDER_DEPTH 12
#define MIN_ENTITIES 15
#define MAX_ENTITIES 25
#define RES_DIVIDER 2
int RES_DIVIDER = 2;
#define DISTANCE_MULTIPLIER 20
#define ROT_SPEED 0.25
#define MOV_SPEED 1.0
@@ -32,10 +33,10 @@ int HALF_HEIGHT = 0;
#define MAX_ENTITY_DISTANCE 200
#define ITEM_COLLIDER_DIST 6
#include "mathdef.hpp"
#define GUN_WIDTH 30
#define GUN_HEIGHT 40
#define GUN_TARGET_POS 24
#define GUN_SHOT_POS (GUN_TARGET_POS + 6)
int GUN_WIDTH = 30;
int GUN_HEIGHT = 40;
int GUN_TARGET_POS = 24;
int GUN_SHOT_POS = 30;
#define ENEMY_SIZE 16
#define ENEMY_SPEED 0.03
#define ENEMY_MELEE_DIST 20
@@ -145,7 +146,7 @@ static bool up, down, left, right, fired;
static double jogging, view_height;
static bool needs_redraw = false;
static bool needs_gun_redraw = false;
static uint8_t gun_pos = GUN_TARGET_POS;
static uint8_t gun_pos = 24;
static bool gun_fired = false;
static int prev_gun_x = 0;
static int prev_gun_y = 0;
@@ -850,13 +851,23 @@ void render_entities(Painter& painter) {
}
}
void render_map(Painter& painter, bool full_clear, int16_t x_start = 0, int16_t x_end = SCREEN_WIDTH) {
void render_map(Painter& painter, bool full_clear, int16_t x_start = 0, int16_t x_end = -1) {
if (x_end == -1) x_end = SCREEN_WIDTH;
// Add safety bounds
x_start = fmax(0, x_start);
x_end = fmin(SCREEN_WIDTH, x_end);
// Dynamically adjust RES_DIVIDER based on screen size
int dynamic_res = (SCREEN_WIDTH > 400) ? 4 : (SCREEN_WIDTH > 300) ? 3
: RES_DIVIDER;
if (full_clear) {
painter.fill_rectangle({0, 0, SCREEN_WIDTH, RENDER_HEIGHT / 2}, Color(64, 64, 128));
painter.fill_rectangle({0, RENDER_HEIGHT / 2, SCREEN_WIDTH, RENDER_HEIGHT / 2}, Color(32, 32, 32));
}
for (uint8_t x = x_start; x < x_end; x += RES_DIVIDER) {
for (uint16_t x = x_start; x < x_end; x += dynamic_res) {
double camera_x = 2 * (double)x / SCREEN_WIDTH - 1;
double ray_x = player.dir.x + player.plane.x * camera_x;
double ray_y = player.dir.y + player.plane.y * camera_x;
@@ -985,15 +996,15 @@ void render_map(Painter& painter, bool full_clear, int16_t x_start = 0, int16_t
wall_color = Color(brightness / 5, (brightness - noise) * 0.8, brightness / 5);
}
painter.fill_rectangle({x, start_y, RES_DIVIDER, end_y - start_y}, wall_color);
painter.fill_rectangle({x, start_y, dynamic_res, end_y - start_y}, wall_color);
}
if (!full_clear && hit) {
if (start_y > 0) {
painter.fill_rectangle({x, 0, RES_DIVIDER, start_y}, Color(64, 64, 128));
painter.fill_rectangle({x, 0, dynamic_res, start_y}, Color(64, 64, 128));
}
if (end_y < RENDER_HEIGHT) {
painter.fill_rectangle({x, end_y, RES_DIVIDER, RENDER_HEIGHT - end_y}, Color(32, 32, 32));
painter.fill_rectangle({x, end_y, dynamic_res, RENDER_HEIGHT - end_y}, Color(32, 32, 32));
}
}
}
@@ -1001,11 +1012,28 @@ void render_map(Painter& painter, bool full_clear, int16_t x_start = 0, int16_t
DoomView::DoomView(NavigationView& nav)
: nav_{nav} {
SCREEN_WIDTH = screen_width;
SCREEN_HEIGHT = screen_height;
RENDER_HEIGHT = screen_height - 40;
HALF_WIDTH = screen_width / 2;
SCREEN_WIDTH = ui::screen_width;
SCREEN_HEIGHT = ui::screen_height;
HUD_HEIGHT = 40;
RENDER_HEIGHT = SCREEN_HEIGHT - HUD_HEIGHT;
HALF_WIDTH = SCREEN_WIDTH / 2;
HALF_HEIGHT = RENDER_HEIGHT / 2;
// More aggressive resolution reduction for larger screens
if (SCREEN_WIDTH > 400) {
RES_DIVIDER = 4;
} else if (SCREEN_WIDTH > 300) {
RES_DIVIDER = 3;
} else {
RES_DIVIDER = 2;
}
// Cap gun size to prevent overflow
GUN_WIDTH = fmin(60, SCREEN_WIDTH / 8);
GUN_HEIGHT = fmin(50, SCREEN_HEIGHT / 8);
GUN_TARGET_POS = fmin(40, RENDER_HEIGHT / 13);
GUN_SHOT_POS = GUN_TARGET_POS + 6;
add_children({&dummy});
game_timer.attach(&game_timer_check, 1.0 / 60.0);
}
@@ -1034,26 +1062,32 @@ void DoomView::paint(Painter& painter) {
if (scene == 0) {
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
painter.draw_string({50, 40}, style_yellow, "* * * DOOM * * *");
painter.draw_string({UI_POS_X_CENTER(15), 40}, style_yellow, "* * * DOOM * * *");
auto style_green = *ui::Theme::getInstance()->fg_green;
painter.draw_string({15, 240}, style_green, "** PRESS SELECT TO START **");
painter.draw_string({UI_POS_X_CENTER(26), SCREEN_HEIGHT - 80}, style_green, "** PRESS SELECT TO START **");
} else if (scene == 1) {
if (needs_redraw || (needs_gun_redraw && jogging > 0)) {
bool full_clear = (player.velocity == 0 || !prev_velocity_moving);
render_map(painter, full_clear);
render_entities(painter);
render_gun(painter, gun_pos, jogging);
// Draw HUD
painter.fill_rectangle({0, RENDER_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT - RENDER_HEIGHT}, pp_colors[COLOR_BACKGROUND]);
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_red = *ui::Theme::getInstance()->fg_red;
auto style_blue = *ui::Theme::getInstance()->fg_blue;
painter.draw_string({5, RENDER_HEIGHT + 5}, style_yellow, "Health: " + std::to_string(player.health));
painter.draw_string({100, RENDER_HEIGHT + 5}, style_red, "Kills: " + std::to_string(kills));
painter.draw_string({170, RENDER_HEIGHT + 5}, style_blue, "Ammo: " + std::to_string(player.ammo));
int hud_y = RENDER_HEIGHT + 5;
painter.draw_string({5, hud_y}, style_yellow, "Health: " + std::to_string(player.health));
painter.draw_string({UI_POS_X_CENTER(15), hud_y}, style_red, "Kills: " + std::to_string(kills));
painter.draw_string({UI_POS_X_RIGHT(80), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo));
prev_velocity_moving = (player.velocity != 0);
needs_redraw = false;
needs_gun_redraw = false;
} else if (needs_gun_redraw) {
// Partial redraw for gun only
int current_x = HALF_WIDTH - GUN_WIDTH / 2 + sin(LPC_RTC->CTIME0 * JOGGING_SPEED) * 10 * jogging;
int current_y = RENDER_HEIGHT - gun_pos - 29 + fabs(cos(LPC_RTC->CTIME0 * JOGGING_SPEED)) * 8 * jogging;
int flash_height = (gun_pos > GUN_TARGET_POS) ? (gun_pos - GUN_TARGET_POS) * 2 + 10 : 0;
@@ -1069,13 +1103,18 @@ void DoomView::paint(Painter& painter) {
render_map(painter, false, min_x, max_x);
render_entities(painter);
render_gun(painter, gun_pos, jogging);
// Redraw HUD
painter.fill_rectangle({0, RENDER_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT - RENDER_HEIGHT}, pp_colors[COLOR_BACKGROUND]);
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_red = *ui::Theme::getInstance()->fg_red;
auto style_blue = *ui::Theme::getInstance()->fg_blue;
painter.draw_string({5, RENDER_HEIGHT + 5}, style_yellow, "Health: " + std::to_string(player.health));
painter.draw_string({100, RENDER_HEIGHT + 5}, style_red, "Kills: " + std::to_string(kills));
painter.draw_string({170, RENDER_HEIGHT + 5}, style_blue, "Ammo: " + std::to_string(player.ammo));
int hud_y = RENDER_HEIGHT + 5;
painter.draw_string({5, hud_y}, style_yellow, "Health: " + std::to_string(player.health));
painter.draw_string({UI_POS_X_CENTER(15), hud_y}, style_red, "Kills: " + std::to_string(kills));
painter.draw_string({UI_POS_X_RIGHT(80), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo));
needs_gun_redraw = false;
}
}
+1 -1
View File
@@ -70,7 +70,7 @@ class DoomView : public View {
private:
NavigationView& nav_;
Button dummy{{screen_width, 0, 0, 0}, ""};
Button dummy{{0, 0, 0, 0}, ""};
bool initialized{false};
bool prev_velocity_moving{false};
MessageHandlerRegistration message_handler_frame_sync{
+1 -1
View File
@@ -118,7 +118,7 @@ class ERTAppView : public View {
// Prevent painting of region covered entirely by a child.
// TODO: Add flag to View that specifies view does not need to be cleared before painting.
void paint(Painter&) override{};
void paint(Painter&) override {};
void focus() override;
+128 -75
View File
@@ -1,259 +1,302 @@
set(EXTCPPSRC
#afsk_rx
#afsk_rx 16 byte
external/afsk_rx/main.cpp
external/afsk_rx/ui_afsk_rx.cpp
#calculator
#calculator 632 bytes
external/calculator/main.cpp
external/calculator/ui_calculator.cpp
#font_viewer
#font_viewer 8 byte?!
external/font_viewer/main.cpp
external/font_viewer/ui_font_viewer.cpp
#blespam
#blespam 336 bytes - array initializers?
external/blespam/main.cpp
external/blespam/ui_blespam.cpp
#analogtv
#analogtv 552 bytes
external/analogtv/main.cpp
external/analogtv/analog_tv_app.cpp
external/analogtv/ui_tv.cpp
#nrf_rx
#nrf_rx 40 byte
external/nrf_rx/main.cpp
external/nrf_rx/ui_nrf_rx.cpp
#coasterp
#coasterp 0 byte
external/coasterp/main.cpp
external/coasterp/ui_coasterp.cpp
#lge
#lge 120 byte
external/lge/main.cpp
external/lge/lge_app.cpp
external/lge/rfm69.cpp
#lcr
#lcr - 460 byte flash
external/lcr/main.cpp
external/lcr/ui_lcr.cpp
#jammer
#jammer 144 byte
external/jammer/main.cpp
external/jammer/ui_jammer.cpp
#gpssim
#gpssim 160 byte
external/gpssim/main.cpp
external/gpssim/gps_sim_app.cpp
#spainter
#spainter 464 byte
external/spainter/main.cpp
external/spainter/ui_spectrum_painter.cpp
external/spainter/ui_spectrum_painter_text.cpp
external/spainter/ui_spectrum_painter_image.cpp
#keyfob
#keyfob 216 byte
external/keyfob/main.cpp
external/keyfob/ui_keyfob.cpp
external/keyfob/ui_keyfob.hpp
#tetris
#tetris 88 byte
external/tetris/main.cpp
external/tetris/ui_tetris.cpp
#extsensors
#extsensors 192 byte
external/extsensors/main.cpp
external/extsensors/ui_extsensors.cpp
external/extsensors/ui_extsensors.hpp
#foxhunt
#foxhunt 0
external/foxhunt/main.cpp
external/foxhunt/ui_foxhunt_rx.cpp
external/foxhunt/ui_foxhunt_rx.hpp
#audio_test
#audio_test 192 byte
external/audio_test/main.cpp
external/audio_test/ui_audio_test.cpp
#wardrivemap
#wardrivemap 64 byte
external/wardrivemap/main.cpp
external/wardrivemap/ui_wardrivemap.cpp
#tpmsrx
#tpmsrx 920 byte- possible shared part with baseband
external/tpmsrx/main.cpp
external/tpmsrx/tpms_app.cpp
#protoview
#protoview 8 byte
external/protoview/main.cpp
external/protoview/ui_protoview.cpp
#adsbtx
#adsbtx 3544 byte - adsb shared part
external/adsbtx/main.cpp
external/adsbtx/ui_adsb_tx.cpp
#morse_tx
external/morse_tx/main.cpp
external/morse_tx/ui_morse.cpp
#morse_tx 768 bytes -- disabled because of the new morse tx app with more functions
#external/morse_tx/main.cpp
#external/morse_tx/ui_morse.cpp
#sstvtx
#sstvtx 456 bytes
external/sstvtx/main.cpp
external/sstvtx/ui_sstvtx.cpp
#random
#sstvrx
external/sstvrx/main.cpp
external/sstvrx/ui_sstvrx.cpp
#random 464 bytes.
external/random_password/main.cpp
external/random_password/ui_random_password.cpp
external/random_password/sha512.cpp
external/random_password/sha512.h
#acars
external/acars_rx/main.cpp
external/acars_rx/acars_app.cpp
#external/acars_rx/main.cpp
#external/acars_rx/acars_app.cpp
#wefax_rx
#wefax_rx 192 bytes
external/wefax_rx/main.cpp
external/wefax_rx/ui_wefax_rx.cpp
#noaaapt_rx
#noaaapt_rx 72 bytes
external/noaaapt_rx/main.cpp
external/noaaapt_rx/ui_noaaapt_rx.cpp
#shoppingcart_lock
#shoppingcart_lock 272 bytes
external/shoppingcart_lock/main.cpp
external/shoppingcart_lock/shoppingcart_lock.cpp
#ookbrute
#ookbrute 80 byte
external/ookbrute/main.cpp
external/ookbrute/ui_ookbrute.cpp
#ook_editor
#ook_editor 1808 bytes
external/ook_editor/main.cpp
external/ook_editor/ui_ook_editor.cpp
#cvs_spam
#cvs_spam 24 byte
external/cvs_spam/main.cpp
external/cvs_spam/cvs_spam.cpp
#flippertx
#flippertx 712 bytes
external/flippertx/main.cpp
external/flippertx/ui_flippertx.cpp
#remote
#remote 1664 bytes
external/remote/main.cpp
external/remote/ui_remote.cpp
#mcu_temperature
#mcu_temperature 112
external/mcu_temperature/main.cpp
external/mcu_temperature/mcu_temperature.cpp
#fmradio
#fmradio 640
external/fmradio/main.cpp
external/fmradio/ui_fmradio.cpp
#tuner
#tuner 384
external/tuner/main.cpp
external/tuner/ui_tuner.cpp
#metronome
#metronome 696 bytes
external/metronome/main.cpp
external/metronome/ui_metronome.cpp
#app_manager
#app_manager 40 bytes
external/app_manager/main.cpp
external/app_manager/ui_app_manager.cpp
#hopper
#hopper 472 bytes
external/hopper/main.cpp
external/hopper/ui_hopper.cpp
# whip calculator
# whip calculator 48 bytes
external/antenna_length/main.cpp
external/antenna_length/ui_whipcalc.cpp
# wav viewer
# wav viewer 1232 bytes
external/wav_view/main.cpp
external/wav_view/ui_view_wav.cpp
# wipe sdcard
# wipe sdcard 16 byte
external/sd_wipe/main.cpp
external/sd_wipe/ui_sd_wipe.cpp
# playlist editor
# playlist editor 232 bytes
external/playlist_editor/main.cpp
external/playlist_editor/ui_playlist_editor.cpp
#snake
#snake 240 bytes
external/snake/main.cpp
external/snake/ui_snake.cpp
#stopwatch
#stopwatch 0
external/stopwatch/main.cpp
external/stopwatch/ui_stopwatch.cpp
#breakout
#breakout 1144 bytes
external/breakout/main.cpp
external/breakout/ui_breakout.cpp
#dinogame
#dinogame 0
external/dinogame/main.cpp
external/dinogame/ui_dinogame.cpp
#doom
#doom 224
external/doom/main.cpp
external/doom/ui_doom.cpp
#debug_pmem
#debug_pmem 944 byte
external/debug_pmem/main.cpp
external/debug_pmem/ui_debug_pmem.cpp
#scanner
#scanner 520 byte
external/scanner/main.cpp
external/scanner/ui_scanner.cpp
#level
#level 24 byte
external/level/main.cpp
external/level/ui_level.cpp
#gfxEQ
#gfxEQ 80 byte
external/gfxeq/main.cpp
external/gfxeq/ui_gfxeq.cpp
external/gfxeq/ui_gfxeq.cpp
#detector_rx
#waterfall designer
external/waterfall_designer/main.cpp
external/waterfall_designer/ui_waterfall_designer.cpp
#detector_rx 168 byte
external/detector_rx/main.cpp
external/detector_rx/ui_detector_rx.cpp
#space_invaders
#space_invaders 0 byte
external/spaceinv/main.cpp
external/spaceinv/ui_spaceinv.cpp
#blackjack
#blackjack 24 byte
external/blackjack/main.cpp
external/blackjack/ui_blackjack.cpp
#battleship
#battleship 256 byte
external/battleship/main.cpp
external/battleship/ui_battleship.cpp
#ert
#ert 3040 bytes - has common with baseband, could be renamed the namespace, so both could have it, but not kept in fw.
external/ert/main.cpp
external/ert/ert_app.cpp
#epirb_rx
#epirb_rx 168 byte flash
external/epirb_rx/main.cpp
external/epirb_rx/ui_epirb_rx.cpp
#soundboard
#soundboard 272byte - 1236 bytes
external/soundboard/main.cpp
external/soundboard/soundboard_app.cpp
#game2048
#game2048 - 168 byte flash
external/game2048/main.cpp
external/game2048/ui_game2048.cpp
#bht_tx
#bht_tx - 3920 byte flash, unknown
external/bht_tx/main.cpp
external/bht_tx/ui_bht_tx.cpp
external/bht_tx/bht.cpp
#morse_practice - 80 byte flash - bc of array initializers
external/morse_practice/main.cpp
external/morse_practice/ui_morse_practice.cpp
#adult_toys_controller 144 bytes
external/adult_toys_controller/main.cpp
external/adult_toys_controller/ui_adult_toys_controller.cpp
#flex_rx
external/flex_rx/main.cpp
external/flex_rx/ui_flex_rx.cpp
#subcarrx
external/subcarrx/main.cpp
external/subcarrx/ui_subcar.cpp
#siggen
external/siggen/main.cpp
external/siggen/ui_siggen.cpp
#sdusb
external/sdusb/main.cpp
external/sdusb/ui_sd_over_usb.cpp
#morse_radio
external/morse_radio/main.cpp
external/morse_radio/ui_morse_radio.cpp
#morseradiotx
external/morseradiotx/main.cpp
external/morseradiotx/ui_morse_radiotx.cpp
)
set(EXTAPPLIST
@@ -261,8 +304,8 @@ set(EXTAPPLIST
calculator
font_viewer
blespam
nrf_rx
analogtv
nrf_rx
coasterp
lge
lcr
@@ -275,19 +318,20 @@ set(EXTAPPLIST
foxhunt_rx
audio_test
wardrivemap
cvs_spam
tpmsrx
protoview
adsbtx
morse_tx
#morse_tx
sstvtx
sstvrx
random_password
# acars_rx
ookbrute
ook_editor
# acars_rx --not working
wefax_rx
noaaapt_rx
shoppingcart_lock
ookbrute
ook_editor
cvs_spam
flippertx
remote
mcu_temperature
@@ -309,6 +353,7 @@ set(EXTAPPLIST
scanner
level
gfxeq
waterfall_designer
detector_rx
spaceinv
blackjack
@@ -318,4 +363,12 @@ set(EXTAPPLIST
soundboard
game2048
bht_tx
morse_practice
adult_toys_controller
flex_rx
subcarrx
siggen
sdusb
morse_radio
morseradiotx
)
+71 -3
View File
@@ -83,9 +83,17 @@ MEMORY
ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k
ram_external_app_soundboard (rwx) : org = 0xADEB0000, len = 32k
ram_external_app_game2048 (rwx) : org = 0xADEC0000, len = 32k
ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k
ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k
ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k
ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k
ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k
ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k
ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k
}
SECTIONS
@@ -396,6 +404,12 @@ SECTIONS
*(*ui*external_app*gfxeq*);
} > ram_external_app_gfxeq
.external_app_waterfall_designer : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_waterfall_designer.application_information));
*(*ui*external_app*waterfall_designer*);
} > ram_external_app_waterfall_designer
.external_app_detector_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_detector_rx.application_information));
@@ -458,5 +472,59 @@ SECTIONS
} > ram_external_app_bht_tx
.external_app_morse_practice : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_practice.application_information));
*(*ui*external_app*morse_practice*);
} > ram_external_app_morse_practice
.external_app_adult_toys_controller : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_adult_toys_controller.application_information));
*(*ui*external_app*adult_toys_controller*);
} > ram_external_app_adult_toys_controller
.external_app_flex_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_flex_rx.application_information));
*(*ui*external_app*flex_rx*);
} > ram_external_app_flex_rx
.external_app_sstvrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sstvrx.application_information));
*(*ui*external_app*sstvrx*);
} > ram_external_app_sstvrx
.external_app_subcarrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_subcarrx.application_information));
*(*ui*external_app*subcarrx*);
} > ram_external_app_subcarrx
.external_app_siggen : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_siggen.application_information));
*(*ui*external_app*siggen*);
} > ram_external_app_siggen
.external_app_sdusb : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sdusb.application_information));
*(*ui*external_app*sdusb*);
} > ram_external_app_sdusb
.external_app_morse_radio : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_radio.application_information));
*(*ui*external_app*morse_radio*);
} > ram_external_app_morse_radio
.external_app_morseradiotx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morseradiotx.application_information));
*(*ui*external_app*morseradiotx*);
} > ram_external_app_morseradiotx
}
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2025 HTotoo
*
* 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_flex_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::flex_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<FlexAppView>();
}
} // namespace ui::external_app::flex_rx
extern "C" {
__attribute__((section(".external_app.app_flex_rx.application_information"), used)) application_information_t _application_information_flex_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::flex_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "FLEX RX",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0x02,
0x40,
0xFA,
0x5F,
0x02,
0x40,
0xF2,
0x4F,
0x02,
0x40,
0xE2,
0x47,
0x02,
0x40,
0xC2,
0x43,
0x02,
0x40,
0x82,
0x41,
0x02,
0x40,
0xFE,
0x7F,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_flex */ {'P', 'F', 'L', 'X'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+129
View File
@@ -0,0 +1,129 @@
#include "ui_flex_rx.hpp"
#include "baseband_api.hpp"
#include "portapack_persistent_memory.hpp"
#include "string_format.hpp"
#include "memory_map.hpp"
using namespace portapack;
namespace ui::external_app::flex_rx {
FlexAppView::FlexAppView(NavigationView& nav)
: nav_{nav} {
// Load baseband image for FLEX decoding
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&field_frequency,
&field_rf_amp,
&field_lna,
&field_vga,
&rssi,
&console});
// Restore saved frequency
field_frequency.set_value(frequency_value);
receiver_model.set_target_frequency(frequency_value);
// Frequency change callback
field_frequency.updated = [this](rf::Frequency f) {
update_freq(f);
};
// Configure receiver
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.enable();
receiver_model.set_squelch_level(0);
// Initialize FLEX baseband
baseband::set_flex_config();
console.writeln("Ready");
}
FlexAppView::~FlexAppView() {
receiver_model.disable();
baseband::shutdown();
}
void FlexAppView::focus() {
field_frequency.focus();
}
// Redraw all messages to console
void FlexAppView::redraw_console() {
console.clear(true);
bool first = true;
for (const auto& msg : messages) {
if (!first) {
console.writeln(""); // Blank line between messages
}
first = false;
console.writeln(msg);
}
}
// Add message to log with automatic line wrapping
void FlexAppView::log_message(const std::string& message) {
const size_t chars_per_line = screen_width / 8;
// Console height accounts for status bar and controls row
const size_t console_lines = (screen_height - 2 * 16) / 16;
messages.push_back(message);
// Calculate total lines used (messages + blank lines between them)
size_t total_lines = 0;
for (size_t i = 0; i < messages.size(); i++) {
if (i > 0) total_lines++; // Count blank line separator
size_t msg_lines = (messages[i].length() + chars_per_line - 1) / chars_per_line;
if (msg_lines == 0) msg_lines = 1;
total_lines += msg_lines;
}
// If console would overflow, remove oldest messages and redraw
if (total_lines > console_lines) {
while (total_lines > console_lines && !messages.empty()) {
const auto& oldest = messages.front();
size_t oldest_lines = (oldest.length() + chars_per_line - 1) / chars_per_line;
if (oldest_lines == 0) oldest_lines = 1;
total_lines -= oldest_lines;
if (messages.size() > 1) total_lines--; // Remove separator line too
messages.erase(messages.begin());
}
redraw_console();
} else {
// Just append new message
if (messages.size() > 1) {
console.writeln(""); // Blank line before new message
}
console.writeln(message);
}
}
// Update frequency and save for persistence
void FlexAppView::update_freq(rf::Frequency f) {
frequency_value = f;
receiver_model.set_target_frequency(f);
}
// Handle decoded FLEX packet from baseband
void FlexAppView::on_packet(const FlexPacketMessage* message) {
log_message(message->packet.message);
}
// Handle stats message (currently unused)
void FlexAppView::on_stats(const FlexStatsMessage*) {
}
// Debug handler - uncomment to see baseband debug messages
void FlexAppView::on_debug(const FlexDebugMessage* message) {
(void)message; // Suppress unused parameter warning
// std::string text = "DBG: ";
// text += message->text;
// text += " " + to_string_hex(message->val1, 8);
// text += " " + to_string_hex(message->val2, 8);
// log_message(text);
}
} // namespace ui::external_app::flex_rx
+97
View File
@@ -0,0 +1,97 @@
#ifndef __UI_FLEX_RX_H__
#define __UI_FLEX_RX_H__
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_freq_field.hpp"
#include "ui_rssi.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include <string>
#include <vector>
namespace ui::external_app::flex_rx {
class FlexAppView : public View {
public:
FlexAppView(NavigationView& nav);
~FlexAppView();
void focus() override;
std::string title() const override { return "FLEX RX"; };
private:
NavigationView& nav_;
// Saved settings
rf::Frequency frequency_value{931740000}; // Default FLEX frequency
RxRadioState radio_state_{};
// Message storage for console redraw
static constexpr size_t MAX_MESSAGES = 20;
std::vector<std::string> messages{};
// Helper methods
void log_message(const std::string& message);
void redraw_console();
void update_freq(rf::Frequency f);
// UI Elements - Row 0, dynamically positioned
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
nav_};
RFAmpField field_rf_amp{
{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{
{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{
{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{
{UI_POS_X(21), 0, UI_POS_WIDTH(9), 4}};
// Message display area (below controls, account for status bar)
Console console{
{0, 1 * 16, screen_width, screen_height - 2 * 16}};
// Persistent settings manager
app_settings::SettingsManager settings_{
"rx_flex",
app_settings::Mode::RX,
{{"frequency", &frequency_value}}};
// Message handlers
void on_packet(const FlexPacketMessage* message);
void on_stats(const FlexStatsMessage* message);
void on_debug(const FlexDebugMessage* message);
// Message handler registrations
MessageHandlerRegistration message_handler_packet{
Message::ID::FlexPacket,
[this](const Message* const p) {
const auto message = *static_cast<const FlexPacketMessage*>(p);
this->on_packet(&message);
}};
MessageHandlerRegistration message_handler_stats{
Message::ID::FlexStats,
[this](const Message* const p) {
const auto message = *static_cast<const FlexStatsMessage*>(p);
this->on_stats(&message);
}};
MessageHandlerRegistration message_handler_debug{
Message::ID::FlexDebug,
[this](const Message* const p) {
const auto message = *static_cast<const FlexDebugMessage*>(p);
this->on_debug(&message);
}};
};
} // namespace ui::external_app::flex_rx
#endif /*__UI_FLEX_RX_H__*/
+1 -1
View File
@@ -79,7 +79,7 @@ class FlipperTxView : public View {
void on_tx_progress(const bool done);
bool on_file_changed(std::filesystem::path new_file_path);
std::filesystem::path filename = {};
std::filesystem::path filename = "";
FlipperProto proto = FLIPPER_PROTO_UNSUPPORTED;
FlipperPreset preset = FLIPPER_PRESET_UNK;
uint16_t te = 0; // for binraw
-1
View File
@@ -41,7 +41,6 @@ gfxEQView::gfxEQView(NavigationView& nav)
audio::set_rate(audio::Rate::Hz_48000);
audio::output::start();
receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack
//
receiver_model.enable();
receiver_model.set_target_frequency(frequency_value); // Retune to actual freq
+9 -7
View File
@@ -46,6 +46,7 @@ class gfxEQView : public View {
NavigationView& nav_;
uint32_t current_theme{0};
const std::array<ColorTheme, 20> themes{
ColorTheme{Color(255, 0, 255), Color(255, 255, 255)},
ColorTheme{Color(0, 255, 0), Color(255, 0, 0)},
@@ -68,13 +69,14 @@ class gfxEQView : public View {
ColorTheme{Color(64, 64, 64), Color(255, 0, 0)},
ColorTheme{Color(255, 192, 0), Color(0, 64, 128)}};
ButtonWithEncoder button_frequency{{UI_POS_X(0), UI_POS_Y(0) + 4, 11 * 8, 1 * 8}, ""};
RFAmpField field_rf_amp{{13 * 8, UI_POS_Y(0)}};
LNAGainField field_lna{{15 * 8, UI_POS_Y(0)}};
VGAGainField field_vga{{18 * 8, UI_POS_Y(0)}};
Button button_mood{{21 * 8, 0, 6 * 8, 16}, "MOOD"};
AudioVolumeField field_volume{{screen_width - 2 * 8, UI_POS_Y(0)}};
GraphEq gr{{2, UI_POS_DEFAULT_HEIGHT, UI_POS_MAXWIDTH - 4, UI_POS_HEIGHT_REMAINING(2)}, false};
// Widgets with positions using UI_POS macros
ButtonWithEncoder button_frequency{{UI_POS_X(0), UI_POS_Y(0) + 4, UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, ""};
RFAmpField field_rf_amp{{UI_POS_X(12), UI_POS_Y(0) + 4}};
LNAGainField field_lna{{UI_POS_X(14), UI_POS_Y(0) + 4}};
VGAGainField field_vga{{UI_POS_X(17), UI_POS_Y(0) + 4}};
Button button_mood{{UI_POS_X(21), UI_POS_Y(0) + 4, UI_POS_WIDTH(4), UI_POS_HEIGHT(1)}, "MOOD"};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0) + 4}};
GraphEq gr{{2, UI_POS_Y(1) + 8, UI_POS_MAXWIDTH, UI_POS_Y_BOTTOM(1) - 28}, false};
rf::Frequency frequency_value{93100000};
@@ -24,6 +24,8 @@
#include "crc.hpp"
#include "portapack_shared_memory.hpp"
namespace ui::external_app::lge {
uint32_t RFM69::gen_frame(std::vector<uint8_t>& payload) {
CRC<16> crc{0x1021, 0x1D0F, 0xFFFF};
std::vector<uint8_t> frame{};
@@ -74,3 +76,5 @@ uint32_t RFM69::gen_frame(std::vector<uint8_t>& payload) {
// Copy for baseband
return frame.size();
}
} /* namespace ui::external_app::lge */
@@ -28,6 +28,8 @@
#include "utility.hpp"
namespace ui::external_app::lge {
class RFM69 {
public:
RFM69(const uint8_t num_preamble, const uint16_t sync_word, const bool CRC, const bool manchester)
@@ -54,4 +56,6 @@ class RFM69 {
bool manchester_{false};
};
} /* namespace ui::external_app::lge */
#endif /*__RFM69_H__*/
+87
View File
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2025 Pezsma
*
* 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_morse_practice.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::morse_practice {
void initialize_app(NavigationView& nav) {
nav.push<MorsePracticeView>();
}
} // namespace ui::external_app::morse_practice
extern "C" {
__attribute__((section(".external_app.app_morse_practice.application_information"), used))
application_information_t _application_information_morse_practice = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::morse_practice::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Morse P",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0xFF,
0xFF,
0xBB,
0xD0,
0xFF,
0xFF,
0xFF,
0xFF,
0x0B,
0xE1,
0xFF,
0xFF,
0xFF,
0xFF,
0xEB,
0xD0,
0xFF,
0xFF,
0xFE,
0x7F,
0x70,
0x00,
0x30,
0x00,
0x10,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::GAMES,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'A', 'B', 'P'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
} // extern "C"
@@ -0,0 +1,363 @@
#ifndef __MORSEDECODER_HPP__
#define __MORSEDECODER_HPP__
/*
* Copyright (C) 2025 Pezsma
*
* 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 <cstdint>
#include <string>
#include "string_format.hpp"
namespace ui::external_app::morse_practice {
class MorseRingBuffer {
public:
MorseRingBuffer()
: head_(0), tail_(0), count_(0) {}
void push_back(const uint32_t& value) {
data_[head_] = value;
head_ = (head_ + 1) % 40;
if (count_ < 40) {
count_++;
} else {
// overwrite oldest element
tail_ = (tail_ + 1) % 40;
}
}
void pop_front() {
if (count_ > 0) {
tail_ = (tail_ + 1) % 40;
count_--;
}
}
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
const uint32_t& front() const { return data_[tail_]; }
// Access by index (0 = oldest)
uint32_t operator[](size_t idx) const {
return data_[(tail_ + idx) % 40];
}
// Convert to vector-like access for sorting etc.
void copy_to_array(uint32_t* out) const {
for (size_t i = 0; i < count_; ++i)
out[i] = (*this)[i];
}
private:
uint32_t data_[40];
size_t head_;
size_t tail_;
size_t count_;
};
class MorseDecoder {
public:
struct DecodeResult {
std::string text = "";
double confidence = 0.0;
bool isValid() const {
return !text.empty();
}
};
struct MorseEntry {
std::string code;
std::string letter;
};
MorseDecoder() {}
DecodeResult
handleInput(int32_t duration_ms) {
DecodeResult result = {"", 0.0};
if (duration_ms < 5 && duration_ms > -5) return result;
if (duration_ms > 0) {
pulse_history_.push_back(duration_ms);
double dah_prob = getDahProbability(duration_ms);
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
last_sequence_ = current_sequence_;
} else {
uint32_t gap_duration = -duration_ms;
pulse_gaps_.push_back(gap_duration);
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
result.text = lookupMorse(current_sequence_);
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
if (gap_duration >= getInterWordThreshold()) {
result.text += " ";
}
current_sequence_ = "";
}
}
updateLearning();
return result;
}
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
inline double getCurrentTimeUnit() { return time_unit_ms_; }
inline std::string getLastSequence() { return last_sequence_; }
private:
std::string current_sequence_ = "";
std::string last_sequence_ = "";
double time_unit_ms_ = 119.0;
double last_confidence_ = 0.0;
MorseRingBuffer pulse_history_{};
MorseRingBuffer pulse_gaps_{};
std::string lookupMorse(const std::string& seq) {
for (size_t i = 0; i < morse_table_size_; i++) {
if (seq == morse_table_[i].code)
return morse_table_[i].letter;
}
return "{" + seq + "}"; // not found
}
double getDahProbability(uint32_t duration_ms) {
double start_interp = 1.5 * time_unit_ms_;
double end_interp = 2.5 * time_unit_ms_;
if (duration_ms <= start_interp) return 0.0;
if (duration_ms >= end_interp) return 1.0;
return ((double)(duration_ms)-start_interp) / (end_interp - start_interp);
}
size_t findDecisionBoundary(uint32_t* sorted_data, size_t sorted_data_size) {
if (sorted_data_size < 4) return 0;
size_t best_split_index = 0;
uint32_t max_diff = 0;
for (size_t i = 1; i < sorted_data_size; ++i) {
uint32_t diff = sorted_data[i] - sorted_data[i - 1];
if (diff > sorted_data[i - 1] * 0.5 && diff > max_diff) {
max_diff = diff;
best_split_index = i;
}
}
return best_split_index;
}
bool calculatePulseUnit(double& unit, double& confidence) {
if (pulse_history_.size() < 10) return false;
uint32_t sorted_pulses[pulse_history_.size()];
pulse_history_.copy_to_array(sorted_pulses);
sort_uint32(sorted_pulses, pulse_history_.size());
size_t split_index = findDecisionBoundary(sorted_pulses, pulse_history_.size());
if (split_index == 0 || split_index < 3 || (pulse_history_.size() - split_index) < 2) {
return false;
}
double dit_sum = sum_uint32_range(sorted_pulses, 0, split_index);
double dah_sum = sum_uint32_range(sorted_pulses, split_index, pulse_history_.size());
double avg_dit = dit_sum / split_index;
double avg_dah = dah_sum / (pulse_history_.size() - split_index);
if (avg_dah <= avg_dit) return false;
double ratio = avg_dah / avg_dit;
if (ratio > 1.5 && ratio < 5.0) {
unit = avg_dit;
double tmpabs = ratio - 3.0;
if (tmpabs < 0) tmpabs *= -1;
tmpabs /= 3.0;
tmpabs = 1.0 - tmpabs;
if (tmpabs < 0) tmpabs = 0;
confidence = tmpabs; // 0..1
return true;
}
return false;
}
bool calculateGapUnit(double& unit, double& confidence) {
if (pulse_gaps_.size() < 10) return false;
double threshold = getInterElementThreshold();
double valid_gaps[pulse_gaps_.size()];
size_t valid_count = 0;
for (size_t i = 0; i < pulse_gaps_.size(); i++) {
double gap = pulse_gaps_[i];
if (gap <= threshold) {
valid_gaps[valid_count++] = gap;
}
}
if (valid_count < 2) {
return false;
}
double sum = sum_double_range(valid_gaps, 0, valid_count);
size_t count_to_average = valid_count;
if (count_to_average > 0) {
unit = sum / count_to_average;
confidence = 0.8;
return true;
}
return false;
}
double sum_uint32_range(const uint32_t* data, size_t start, size_t end) {
double sum = 0.0;
for (size_t i = start; i < end; i++) {
sum += data[i];
}
return sum;
}
double sum_double_range(const double* data, size_t start, size_t end) {
double sum = 0.0;
for (size_t i = start; i < end; i++) {
sum += data[i];
}
return sum;
}
void sort_uint32(uint32_t* data, size_t size) {
if (size < 2)
return;
for (size_t i = 1; i < size; i++) {
uint32_t key = data[i];
size_t j = i;
while (j > 0 && data[j - 1] > key) {
data[j] = data[j - 1];
j--;
}
data[j] = key;
}
}
double clamp_double(double value, double min_val, double max_val) {
if (value < min_val)
return min_val;
else if (value > max_val)
return max_val;
else
return value;
}
void updateLearning() {
double pulse_unit = -1.0, pulse_confidence = 0.0;
double gap_unit = -1.0, gap_confidence = 0.0;
bool pulse_success = calculatePulseUnit(pulse_unit, pulse_confidence);
bool gap_success = calculateGapUnit(gap_unit, gap_confidence);
double new_time_unit = -1.0;
if (pulse_success && pulse_confidence > 0.5) {
new_time_unit = pulse_unit;
} else if (pulse_success && gap_success) {
gap_confidence = 0.2;
double total_confidence = pulse_confidence + gap_confidence;
new_time_unit = (pulse_unit * pulse_confidence + gap_unit * gap_confidence) / total_confidence;
} else if (gap_success) {
new_time_unit = gap_unit;
} else {
return;
}
double max_change = time_unit_ms_ * 0.25;
new_time_unit = clamp_double(new_time_unit, time_unit_ms_ - max_change, time_unit_ms_ + max_change);
double DEFAULT_TIME_UNIT = 160.0;
double BASE_LEARNING_RATE = 0.05;
double MAX_LEARNING_RATE = 0.25;
double tudeltaabs = new_time_unit - DEFAULT_TIME_UNIT;
if (tudeltaabs < 0) tudeltaabs *= -1;
double deviation_from_default = tudeltaabs / DEFAULT_TIME_UNIT;
double tpp = deviation_from_default * 2.0;
if (tpp > 1) tpp = 1;
double learning_factor = BASE_LEARNING_RATE + (MAX_LEARNING_RATE - BASE_LEARNING_RATE) * tpp;
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
}
size_t morse_table_size_ = 50;
MorseEntry morse_table_[50] = {
{".-", "A"},
{"-...", "B"},
{"-.-.", "C"},
{"-..", "D"},
{".", "E"},
{"..-.", "F"},
{"--.", "G"},
{"....", "H"},
{"..", "I"},
{".---", "J"},
{"-.-", "K"},
{".-..", "L"},
{"--", "M"},
{"-.", "N"},
{"---", "O"},
{".--.", "P"},
{"--.-", "Q"},
{".-.", "R"},
{"...", "S"},
{"-", "T"},
{"..-", "U"},
{"...-", "V"},
{".--", "W"},
{"-..-", "X"},
{"-.--", "Y"},
{"--..", "Z"},
{".----", "1"},
{"..---", "2"},
{"...--", "3"},
{"....-", "4"},
{".....", "5"},
{"-....", "6"},
{"--...", "7"},
{"---..", "8"},
{"----.", "9"},
{"-----", "0"},
{".-.-.-", "."},
{"..--..", "?"},
{"-.-.--", "!"},
{"--..--", ","},
{"-...-", "="},
{"-..-.", "/"},
{".--.-.", "@"},
{"---...", ":"},
{"-....-", "-"},
{".----.", "'"},
{".-..-.", "\""},
{"-.--.", "("},
{"-.--.-", ")"},
{".-.-.", "+"}};
};
} // namespace ui::external_app::morse_practice
#endif // __MORSEDECODER_HPP__
@@ -0,0 +1,152 @@
#include "ui_morse_practice.hpp"
using namespace portapack;
namespace ui::external_app::morse_practice {
MorsePracticeView::MorsePracticeView(ui::NavigationView& nav)
: nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&btn_tt,
&txt_last,
&btn_clear,
&console_text,
&field_volume});
initial_switch_config_ = get_switches_repeat_config();
SwitchesState config = initial_switch_config_;
config[toUType(Switch::Sel)] = false;
set_switches_repeat_config(config);
audio::set_rate(audio::Rate::Hz_24000);
btn_tt.on_select = [this](Button&) {
if (button_touch) {
button_touch = false;
return;
}
button_was_selected = true;
onPress();
};
btn_tt.on_touch_press = [this](Button&) {
button_touch = true;
button_was_selected = false;
onPress();
};
btn_tt.on_touch_release = [this](Button&) {
button_touch = true;
button_was_selected = false;
onRelease();
};
btn_clear.on_select = [this](Button&) {
console_text.clear(true);
txt_last.set("");
};
audio::output::start();
auto vol = field_volume.value();
field_volume.set_value(0);
field_volume.set_value(vol);
}
MorsePracticeView::~MorsePracticeView() {
set_switches_repeat_config(initial_switch_config_);
receiver_model.disable();
baseband::shutdown();
audio::output::stop();
}
void MorsePracticeView::on_show() {
console_text.write("Morse Practice ready\n");
start_time = 0;
end_time = 0;
}
void MorsePracticeView::focus() {
btn_tt.focus();
}
void MorsePracticeView::onPress() {
start_time = chTimeNow();
if (end_time != 0) {
int64_t gap_delta = (chTimeNow() - end_time);
auto result = morse_decoder_.handleInput(-gap_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
end_time = 0;
decode_timeout_calc = false;
baseband::request_audio_beep(1000, 24000, 2000);
}
void MorsePracticeView::onRelease() {
end_time = chTimeNow();
if (start_time != 0) {
int32_t press_delta = (end_time - start_time);
auto result = morse_decoder_.handleInput(press_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
start_time = 0;
decode_timeout_calc = true;
baseband::request_beep_stop();
}
void MorsePracticeView::writeCharToConsole(const std::string& ch, double confidence) {
if (ch.empty()) {
return;
}
txt_last.set(morse_decoder_.getLastSequence().c_str());
last_color_id = color_id;
std::string color = "";
if (ch == " ") {
color_id = 0;
} else if (ch[0] == '{') { // no match
color_id = 0;
} else {
if (confidence < 0.8)
color_id = 1;
else if (confidence < 0.9)
color_id = 2;
else
color_id = 3;
}
color = arr_color[color_id];
last_color_id = color_id;
console_text.write(color + ch);
}
inline bool MorsePracticeView::tx_button_held() {
const auto switches_state = get_switches_state();
return switches_state[(size_t)ui::KeyEvent::Select];
}
void MorsePracticeView::on_framesync() {
if (button_was_selected && !button_touch && !tx_button_held()) {
button_was_selected = false;
onRelease();
}
if (end_time != 0 && decode_timeout_calc) {
int64_t gap_delta = (chTimeNow() - end_time);
if (gap_delta >= morse_decoder_.getInterCharThreshold()) {
auto result = morse_decoder_.handleInput(-(int32_t)gap_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
if (gap_delta >= morse_decoder_.getInterWordThreshold()) {
writeCharToConsole(" ", 1.0);
end_time = 0;
decode_timeout_calc = false;
}
}
}
} // namespace ui::external_app::morse_practice
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2025 Pezsma
*
* 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 __MORSE_PRACTICE_H__
#define __MORSE_PRACTICE_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_language.hpp"
#include "ui_painter.hpp"
#include "ui_receiver.hpp"
#include "string_format.hpp"
#include "morsedecoder.hpp"
#include "irq_controls.hpp"
#include "radio_state.hpp"
#include "portapack.hpp"
#include "message.hpp"
#include "volume.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "external_app.hpp"
#include <ch.h>
namespace ui::external_app::morse_practice {
class MorsePracticeView : public ui::View {
public:
MorsePracticeView(ui::NavigationView& nav);
~MorsePracticeView();
std::string title() const override { return "Morse Practice"; }
void focus() override;
void on_show() override;
private:
void onPress();
void onRelease();
void on_framesync();
void writeCharToConsole(const std::string& ch, double confidence);
bool tx_button_held();
ui::NavigationView& nav_;
MorseDecoder morse_decoder_{};
int64_t start_time = 0;
int64_t end_time = 0;
ui::Button btn_tt{{UI_POS_X_CENTER(12), UI_POS_Y(3), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "KEY"};
ui::Text txt_last{{UI_POS_X(0), UI_POS_Y(6), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, ""};
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(2), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)}, "CLR"};
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(10)}};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
uint8_t last_color_id = 255;
uint8_t color_id = 255;
std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN};
bool button_touch = false;
bool button_was_selected = false;
bool decode_timeout_calc = false;
SwitchesState initial_switch_config_{};
MessageHandlerRegistration message_handler_framesync{
Message::ID::DisplayFrameSync,
[this](const Message* const p) {
(void)p;
this->on_framesync();
}};
};
} // namespace ui::external_app::morse_practice
#endif // __MORSE_PRACTICE_H__
+86
View File
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2026 Pezsma
*
* 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_morse_radio.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::morse_radio {
void initialize_app(NavigationView& nav) {
nav.push<MorseRadioView>();
}
} // namespace ui::external_app::morse_radio
extern "C" {
__attribute__((section(".external_app.app_morse_radio.application_information"), used))
application_information_t _application_information_morse_radio = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::morse_radio::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Morse",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0xFF,
0xFF,
0xBB,
0xD0,
0xFF,
0xFF,
0xFF,
0xFF,
0x0B,
0xE1,
0xFF,
0xFF,
0xFF,
0xFF,
0xEB,
0xD0,
0xFF,
0xFF,
0xFE,
0x7F,
0x70,
0x00,
0x30,
0x00,
0x10,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_morse_radio */ {'P', 'M', 'R', 'S'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
@@ -0,0 +1,378 @@
#ifndef __MORSEDECODER_HPP__
#define __MORSEDECODER_HPP__
/*
* Copyright (C) 2025 Pezsma
*
* 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 <cstdint>
#include <string>
#include "string_format.hpp"
namespace ui::external_app::morse_radio {
class MorseRingBuffer {
public:
MorseRingBuffer()
: head_(0), tail_(0), count_(0) {}
void push_back(const uint32_t& value) {
data_[head_] = value;
head_ = (head_ + 1) % 40;
if (count_ < 40) {
count_++;
} else {
// overwrite oldest element
tail_ = (tail_ + 1) % 40;
}
}
void pop_front() {
if (count_ > 0) {
tail_ = (tail_ + 1) % 40;
count_--;
}
}
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
const uint32_t& front() const { return data_[tail_]; }
// Access by index (0 = oldest)
uint32_t operator[](size_t idx) const {
return data_[(tail_ + idx) % 40];
}
// Convert to vector-like access for sorting etc.
void copy_to_array(uint32_t* out) const {
for (size_t i = 0; i < count_; ++i)
out[i] = (*this)[i];
}
void clear() {
head_ = 0;
tail_ = 0;
count_ = 0;
}
private:
uint32_t data_[40];
size_t head_;
size_t tail_;
size_t count_;
};
class MorseDecoder {
public:
struct DecodeResult {
std::string text = "";
double confidence = 0.0;
bool isValid() const {
return !text.empty();
}
};
struct MorseEntry {
std::string code;
std::string letter;
};
MorseDecoder() {}
void resetLearning() {
time_unit_ms_ = 119.0;
current_sequence_ = "";
last_sequence_ = "";
last_confidence_ = 0.0;
pulse_history_.clear();
pulse_gaps_.clear();
}
DecodeResult
handleInput(int32_t duration_ms) {
DecodeResult result = {"", 0.0};
if (duration_ms < 5 && duration_ms > -5) return result;
if (duration_ms > 0) {
pulse_history_.push_back(duration_ms);
double dah_prob = getDahProbability(duration_ms);
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
last_sequence_ = current_sequence_;
} else {
uint32_t gap_duration = -duration_ms;
pulse_gaps_.push_back(gap_duration);
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
result.text = lookupMorse(current_sequence_);
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
if (gap_duration >= getInterWordThreshold()) {
result.text += " ";
}
current_sequence_ = "";
}
}
updateLearning();
return result;
}
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
inline double getCurrentTimeUnit() { return time_unit_ms_; }
inline std::string getLastSequence() { return last_sequence_; }
private:
std::string current_sequence_ = "";
std::string last_sequence_ = "";
double time_unit_ms_ = 119.0;
double last_confidence_ = 0.0;
MorseRingBuffer pulse_history_{};
MorseRingBuffer pulse_gaps_{};
std::string lookupMorse(const std::string& seq) {
for (size_t i = 0; i < morse_table_size_; i++) {
if (seq == morse_table_[i].code)
return morse_table_[i].letter;
}
return "{" + seq + "}"; // not found
}
double getDahProbability(uint32_t duration_ms) {
double start_interp = 1.5 * time_unit_ms_;
double end_interp = 2.5 * time_unit_ms_;
if (duration_ms <= start_interp) return 0.0;
if (duration_ms >= end_interp) return 1.0;
return ((double)(duration_ms)-start_interp) / (end_interp - start_interp);
}
size_t findDecisionBoundary(uint32_t* sorted_data, size_t sorted_data_size) {
if (sorted_data_size < 4) return 0;
size_t best_split_index = 0;
uint32_t max_diff = 0;
for (size_t i = 1; i < sorted_data_size; ++i) {
uint32_t diff = sorted_data[i] - sorted_data[i - 1];
if (diff > sorted_data[i - 1] * 0.5 && diff > max_diff) {
max_diff = diff;
best_split_index = i;
}
}
return best_split_index;
}
bool calculatePulseUnit(double& unit, double& confidence) {
if (pulse_history_.size() < 10) return false;
uint32_t sorted_pulses[pulse_history_.size()];
pulse_history_.copy_to_array(sorted_pulses);
sort_uint32(sorted_pulses, pulse_history_.size());
size_t split_index = findDecisionBoundary(sorted_pulses, pulse_history_.size());
if (split_index == 0 || split_index < 3 || (pulse_history_.size() - split_index) < 2) {
return false;
}
double dit_sum = sum_uint32_range(sorted_pulses, 0, split_index);
double dah_sum = sum_uint32_range(sorted_pulses, split_index, pulse_history_.size());
double avg_dit = dit_sum / split_index;
double avg_dah = dah_sum / (pulse_history_.size() - split_index);
if (avg_dah <= avg_dit) return false;
double ratio = avg_dah / avg_dit;
if (ratio > 1.5 && ratio < 5.0) {
unit = avg_dit;
double tmpabs = ratio - 3.0;
if (tmpabs < 0) tmpabs *= -1;
tmpabs /= 3.0;
tmpabs = 1.0 - tmpabs;
if (tmpabs < 0) tmpabs = 0;
confidence = tmpabs; // 0..1
return true;
}
return false;
}
bool calculateGapUnit(double& unit, double& confidence) {
if (pulse_gaps_.size() < 10) return false;
double threshold = getInterElementThreshold();
double valid_gaps[pulse_gaps_.size()];
size_t valid_count = 0;
for (size_t i = 0; i < pulse_gaps_.size(); i++) {
double gap = pulse_gaps_[i];
if (gap <= threshold) {
valid_gaps[valid_count++] = gap;
}
}
if (valid_count < 2) {
return false;
}
double sum = sum_double_range(valid_gaps, 0, valid_count);
size_t count_to_average = valid_count;
if (count_to_average > 0) {
unit = sum / count_to_average;
confidence = 0.8;
return true;
}
return false;
}
double sum_uint32_range(const uint32_t* data, size_t start, size_t end) {
double sum = 0.0;
for (size_t i = start; i < end; i++) {
sum += data[i];
}
return sum;
}
double sum_double_range(const double* data, size_t start, size_t end) {
double sum = 0.0;
for (size_t i = start; i < end; i++) {
sum += data[i];
}
return sum;
}
void sort_uint32(uint32_t* data, size_t size) {
if (size < 2)
return;
for (size_t i = 1; i < size; i++) {
uint32_t key = data[i];
size_t j = i;
while (j > 0 && data[j - 1] > key) {
data[j] = data[j - 1];
j--;
}
data[j] = key;
}
}
double clamp_double(double value, double min_val, double max_val) {
if (value < min_val)
return min_val;
else if (value > max_val)
return max_val;
else
return value;
}
void updateLearning() {
double pulse_unit = -1.0, pulse_confidence = 0.0;
double gap_unit = -1.0, gap_confidence = 0.0;
bool pulse_success = calculatePulseUnit(pulse_unit, pulse_confidence);
bool gap_success = calculateGapUnit(gap_unit, gap_confidence);
double new_time_unit = -1.0;
if (pulse_success && pulse_confidence > 0.5) {
new_time_unit = pulse_unit;
} else if (pulse_success && gap_success) {
gap_confidence = 0.2;
double total_confidence = pulse_confidence + gap_confidence;
new_time_unit = (pulse_unit * pulse_confidence + gap_unit * gap_confidence) / total_confidence;
} else if (gap_success) {
new_time_unit = gap_unit;
} else {
return;
}
double max_change = time_unit_ms_ * 0.25;
new_time_unit = clamp_double(new_time_unit, time_unit_ms_ - max_change, time_unit_ms_ + max_change);
double DEFAULT_TIME_UNIT = 160.0;
double BASE_LEARNING_RATE = 0.05;
double MAX_LEARNING_RATE = 0.25;
double tudeltaabs = new_time_unit - DEFAULT_TIME_UNIT;
if (tudeltaabs < 0) tudeltaabs *= -1;
double deviation_from_default = tudeltaabs / DEFAULT_TIME_UNIT;
double tpp = deviation_from_default * 2.0;
if (tpp > 1) tpp = 1;
double learning_factor = BASE_LEARNING_RATE + (MAX_LEARNING_RATE - BASE_LEARNING_RATE) * tpp;
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
}
size_t morse_table_size_ = 50;
MorseEntry morse_table_[50] = {
{".-", "A"},
{"-...", "B"},
{"-.-.", "C"},
{"-..", "D"},
{".", "E"},
{"..-.", "F"},
{"--.", "G"},
{"....", "H"},
{"..", "I"},
{".---", "J"},
{"-.-", "K"},
{".-..", "L"},
{"--", "M"},
{"-.", "N"},
{"---", "O"},
{".--.", "P"},
{"--.-", "Q"},
{".-.", "R"},
{"...", "S"},
{"-", "T"},
{"..-", "U"},
{"...-", "V"},
{".--", "W"},
{"-..-", "X"},
{"-.--", "Y"},
{"--..", "Z"},
{".----", "1"},
{"..---", "2"},
{"...--", "3"},
{"....-", "4"},
{".....", "5"},
{"-....", "6"},
{"--...", "7"},
{"---..", "8"},
{"----.", "9"},
{"-----", "0"},
{".-.-.-", "."},
{"..--..", "?"},
{"-.-.--", "!"},
{"--..--", ","},
{"-...-", "="},
{"-..-.", "/"},
{".--.-.", "@"},
{"---...", ":"},
{"-....-", "-"},
{".----.", "'"},
{".-..-.", "\""},
{"-.--.", "("},
{"-.--.-", ")"},
{".-.-.", "+"}};
};
} // namespace ui::external_app::morse_radio
#endif // __MORSEDECODER_HPP__
@@ -0,0 +1,274 @@
#include "ui_morse_radio.hpp"
#include "portapack_persistent_memory.hpp"
using namespace portapack;
namespace ui::external_app::morse_radio {
MorseRadioView::MorseRadioView(ui::NavigationView& nav)
: nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&rssi,
&field_rf_amp,
&field_lna,
&field_vga,
&field_volume,
&field_frequency,
&txt_last,
&txt_speed,
&txt_freq,
&options_mode,
&btn_clear,
&console_text,
&labels,
&chk_log,
&field_squelch});
field_frequency.set_step(100);
btn_clear.on_select = [this](Button&) {
console_text.clear(true);
txt_last.set("");
time_stamp = false;
if (logger && save_log)
logger->init_daily_log(logs_dir);
};
audio::output::start();
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.enable();
options_mode.on_change = [this](size_t, int32_t value) {
current_mode = (uint8_t)value;
morse_decoder_.resetLearning();
if (current_mode == 0) {
receiver_model.set_am_configuration(4);
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
field_squelch.set_style(Theme::getInstance()->option_active);
field_squelch.set_focusable(true);
receiver_model.set_squelch_level(field_squelch.value());
audio::set_rate(audio::Rate::Hz_24000);
} else {
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
receiver_model.set_am_configuration(current_mode + 7);
receiver_model.set_squelch_level(0);
field_squelch.set_style(Theme::getInstance()->fg_dark);
field_squelch.set_focusable(false);
audio::set_rate(audio::Rate::Hz_12000);
}
baseband::set_moreserx_config(current_mode);
};
field_squelch.set_value(receiver_model.squelch_level(), false); // will be sent later, no need to send 2x
field_squelch.on_change = [this](int32_t v) {
if (current_mode == 0)
receiver_model.set_squelch_level(v);
};
options_mode.set_selected_index(current_mode, true);
auto vol = field_volume.value(); // audio volume fix
field_volume.set_value(0);
field_volume.set_value(vol);
logger = std::make_unique<MorseLogger>();
chk_log.on_select = [this](Checkbox&, bool save) {
save_log = save;
if (logger && save_log)
logger->init_daily_log(logs_dir);
};
}
void MorseLogger::init_daily_log(const std::filesystem::path& log_dir) {
auto now = rtc_time::now();
std::string pattern = "morse_";
uint16_t y = now.year();
pattern += (char)('0' + (y / 1000) % 10);
pattern += (char)('0' + (y / 100) % 10);
pattern += (char)('0' + (y / 10) % 10);
pattern += (char)('0' + (y % 10));
uint8_t m = now.month();
pattern += (char)('0' + (m / 10) % 10);
pattern += (char)('0' + (m % 10));
uint8_t d = now.day();
pattern += (char)('0' + (d / 10) % 10);
pattern += (char)('0' + (d % 10));
pattern += "_???.TXT";
auto full_pattern_path = log_dir / pattern;
auto final_path = next_filename_matching_pattern(full_pattern_path);
if (!final_path.empty()) {
this->append(final_path);
}
}
void MorseLogger::radio_set_log(uint8_t current_mode) {
int64_t freq = receiver_model.target_frequency();
std::string header = "Freq:" + to_string_rounded_freq(freq, 4);
header += "MHz, ";
header += "RX MODE:" + std::string(current_mode == 0 ? "CW/FM" : (current_mode == 1 ? "USB" : "LSB"));
header += "\r\nMessage:";
log_file.write_raw(header);
}
bool MorseLogger::on_packet(const std::string& content, bool time, uint8_t current_mode) {
if (!time) {
log_file.write_raw_no_newline("\r\n\r\n");
auto timestamp = to_string_datetime(rtc_time::now(), YMDHMS);
log_file.write_raw("[" + timestamp + "] ");
radio_set_log(current_mode);
char_count = 0;
time = true;
}
if (content.empty()) return time;
std::string s;
for (char c : content) {
s = c;
if ((char_count >= 35 && c == ' ') || (char_count >= 47)) {
log_file.write_raw_no_newline("\r\n");
if (c != ' ') {
log_file.write_raw_no_newline(s);
char_count = 1;
} else {
char_count = 0;
}
} else {
log_file.write_raw_no_newline(s);
char_count++;
}
}
return time;
}
MorseRadioView::~MorseRadioView() {
receiver_model.disable();
baseband::shutdown();
audio::output::stop();
}
void MorseRadioView::focus() {
field_frequency.focus();
}
void MorseRadioView::check_for_timeout() {
uint64_t now = chTimeNow();
if (last_activity_time == 0) {
last_activity_time = now;
return;
}
uint64_t quiet_duration = now - last_activity_time;
if (quiet_duration >= 60000) {
morse_decoder_.resetLearning();
time_stamp = false;
last_activity_time = now;
}
}
int32_t MorseRadioView::ProcessSignal(int32_t sig_time_us) {
int8_t sign = (sig_time_us > 0) ? 1 : ((sig_time_us < 0) ? -1 : 0);
int32_t result = 0;
if (last_sign_ == 0) {
last_sign_ = sign;
accumulator_us_ = sig_time_us;
long_pause_sent_ = false;
return 0;
}
if (sign == last_sign_) {
accumulator_us_ += sig_time_us;
if (sign < 0) {
int32_t threshold_us = morse_decoder_.getInterWordThreshold() * 1000;
if (!long_pause_sent_ && accumulator_us_ <= -threshold_us) {
result = accumulator_us_ / 1000;
long_pause_sent_ = true;
}
}
} else {
// state change
if (!long_pause_sent_) {
result = accumulator_us_ / 1000;
} else {
result = 0;
}
accumulator_us_ = sig_time_us;
last_sign_ = sign;
long_pause_sent_ = false;
}
return result;
}
void MorseRadioView::on_data(const MorseRXDataMessage* message) {
int32_t r;
for (uint8_t i = 0; i <= message->state_cnt; ++i) {
r = ProcessSignal(message->state_durations[i]);
auto result = morse_decoder_.handleInput((r != 0) ? r : 0);
if (result.isValid()) {
last_activity_time = chTimeNow(); // start reset timer on valid input
writeCharToConsole(result.text, result.confidence);
if (logger && save_log) {
time_stamp = logger->on_packet(result.text, time_stamp, current_mode);
}
float dah_time = morse_decoder_.getCurrentTimeUnit() * 3.0f;
if (dah_time > 0) {
uint16_t wpm = (uint16_t)(3600.0f / (dah_time + 18.0f) + 0.5f);
txt_speed.set(to_string_dec_uint(wpm));
}
}
}
}
void MorseRadioView::on_freq(const MorseRXfreqMessage* message) {
std::string ret = " ";
uint32_t freq = message->measured_frequency;
if (freq < 301) {
ret = "<";
freq = 300;
}
if (freq > 2299) {
freq = 2300;
ret = ">";
}
if (freq < 400 || freq > 1400)
txt_freq.set_style(Theme::getInstance()->fg_red);
else if (freq <= 580 || freq >= 1220)
txt_freq.set_style(Theme::getInstance()->fg_yellow);
else
txt_freq.set_style(Theme::getInstance()->fg_green);
ret += to_string_dec_uint(freq);
txt_freq.set(ret);
}
void MorseRadioView::writeCharToConsole(const std::string& ch, double confidence) {
if (ch.empty()) {
return;
}
txt_last.set(morse_decoder_.getLastSequence().c_str());
last_color_id = color_id;
std::string color = "";
if (ch == " ") {
color_id = 0;
} else if (ch[0] == '{') { // no match
color_id = 0;
} else {
if (confidence < 0.8)
color_id = 1;
else if (confidence < 0.9)
color_id = 2;
else
color_id = 3;
}
color = arr_color[color_id];
last_color_id = color_id;
console_text.write(color + ch);
}
} // namespace ui::external_app::morse_radio
@@ -0,0 +1,172 @@
/*
* Copyright (C) 2026 Pezsma
*
* 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 __MORSE_RADIO_H__
#define __MORSE_RADIO_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "string_format.hpp"
#include "external_app.hpp"
#include "ui_freq_field.hpp"
#include "radio_state.hpp"
#include "portapack.hpp"
#include "message.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "receiver_model.hpp"
#include "tone_key.hpp"
#include "log_file.hpp"
#include "file_path.hpp"
#include "file.hpp"
#include "rtc_time.hpp"
#include "morsedecoder.hpp"
#include <cstdint>
namespace ui::external_app::morse_radio {
class MorseRadioView;
class MorseLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void init_daily_log(const std::filesystem::path& log_dir);
bool on_packet(const std::string& content, bool time, uint8_t current_mode);
void radio_set_log(uint8_t current_mode);
private:
LogFile log_file{};
uint8_t char_count{0}; // log file line break
};
class MorseRadioView : public ui::View {
public:
MorseRadioView(ui::NavigationView& nav);
~MorseRadioView();
std::string title() const override {
return "Morse";
}
void focus() override;
private:
void writeCharToConsole(const std::string& ch, double confidence);
void on_data(const MorseRXDataMessage* message);
void on_freq(const MorseRXfreqMessage* message);
void on_message(const Message* const p);
void check_for_timeout();
int32_t ProcessSignal(int32_t sig_time_us);
ui::NavigationView& nav_;
MorseDecoder morse_decoder_{};
RxRadioState radio_state_{};
std::unique_ptr<MorseLogger> logger{};
uint8_t current_mode{0}; // 0=CW/FM, 1=USB, 2=LSB
app_settings::SettingsManager settings_{
"rx_morese_radio",
app_settings::Mode::RX,
{{"cwmode"sv, &current_mode}}};
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
nav_};
RFAmpField field_rf_amp{
{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{
{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{
{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}};
AudioVolumeField field_volume{
{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
NumberField field_squelch{
{UI_POS_X(10), UI_POS_Y(1)},
2,
{0, 99},
1,
' ',
};
ui::Text txt_speed{{UI_POS_X(23), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}, "??"};
ui::Text txt_last{{UI_POS_X(12), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(12), UI_POS_HEIGHT(1)}, ""};
ui::Text txt_freq{{UI_POS_X(21), UI_POS_Y(2), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "??"};
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(7)}};
ui::Labels labels{
{{UI_POS_X(0), UI_POS_Y(1)}, "Squelch:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(15), UI_POS_Y(1)}, "Speed:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(26), UI_POS_Y(1)}, "wpm", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(4)}, "Last seq.:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(15), UI_POS_Y(2)}, "Tone:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(27), UI_POS_Y(2)}, "Hz", Theme::getInstance()->fg_light->foreground}};
ui::OptionsField options_mode{
{UI_POS_X(9), UI_POS_Y(2)},
5,
{{"CW/FM", 0}, {"USB", 1}, {"LSB", 2}}};
Checkbox chk_log{{UI_POS_X(0), UI_POS_Y(2)}, 12, "Log", false};
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(2), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)}, "CLR"};
std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN};
bool long_pause_sent_{false};
bool save_log{false};
bool reset_triggered_{false};
bool time_stamp{false};
uint8_t last_color_id{255};
uint8_t color_id{255};
int8_t last_sign_{0};
uint8_t space_timer{0};
int32_t accumulator_us_{0};
uint64_t last_activity_time{0};
MessageHandlerRegistration message_handler_packet{
Message::ID::MorseRXData,
[this](Message* const p) {
const auto message = static_cast<const MorseRXDataMessage*>(p);
this->on_data(message);
}};
MessageHandlerRegistration message_handler_freq{
Message::ID::MorseRXfreq,
[this](Message* const p) {
const auto message = static_cast<const MorseRXfreqMessage*>(p);
this->on_freq(message);
}};
MessageHandlerRegistration message_handler_framesync{
Message::ID::DisplayFrameSync,
[this](const Message* const p) {
(void)p;
this->check_for_timeout();
}};
};
} // namespace ui::external_app::morse_radio
#endif // __MORSE_RADIO_H__
+15 -14
View File
@@ -45,12 +45,9 @@ static msg_t ookthread_fn(void* arg) {
uint8_t* message_symbols = shared_memory.bb_data.tones_data.message;
uint8_t symbol;
MorseView* arg_c = (MorseView*)arg;
chRegSetThreadName("ookthread");
for (uint32_t i = 0; i < arg_c->symbol_count; i++) {
if (chThdShouldTerminate()) break;
symbol = message_symbols[i];
v = (symbol < 2) ? 1 : 0; // TX on for dot or dash, off for pause
@@ -61,9 +58,7 @@ static msg_t ookthread_fn(void* arg) {
} else {
gpio_og_tx.write(v); // in hackrf <=r6, "1" means tx , "0" no tx.
}
arg_c->on_tx_progress(i, false);
chThdSleepMilliseconds(delay * arg_c->time_unit_ms);
}
@@ -82,19 +77,14 @@ static msg_t ookthread_fn(void* arg) {
static msg_t loopthread_fn(void* arg) {
MorseView* arg_c = (MorseView*)arg;
uint32_t wait = arg_c->loop;
chRegSetThreadName("loopthread");
for (uint32_t i = 0; i < wait; i++) {
if (chThdShouldTerminate()) break;
arg_c->on_loop_progress(i, false);
chThdSleepMilliseconds(1000);
}
arg_c->on_loop_progress(0, true);
if (!chThdShouldTerminate()) arg_c->on_loop_progress(0, true);
chThdExit(0);
return 0;
}
@@ -132,6 +122,15 @@ void MorseView::focus() {
}
MorseView::~MorseView() {
if (ookthread) {
chThdTerminate(ookthread);
ookthread = nullptr;
}
if (loopthread) {
chThdTerminate(loopthread);
chThdWait(loopthread);
loopthread = nullptr;
}
transmitter_model.disable();
baseband::shutdown();
}
@@ -181,6 +180,9 @@ void MorseView::update_tx_duration() {
void MorseView::on_tx_progress(const uint32_t progress, const bool done) {
if (done) {
if (ookthread) {
ookthread = nullptr;
}
transmitter_model.disable();
progressbar.set_value(0);
@@ -190,10 +192,10 @@ void MorseView::on_tx_progress(const uint32_t progress, const bool done) {
progressbar.set_value(0);
if (loopthread) {
chThdRelease(loopthread);
chThdTerminate(loopthread);
chThdWait(loopthread);
loopthread = nullptr;
}
loopthread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO, loopthread_fn, this);
} else {
tx_view.set_transmitting(false);
@@ -219,7 +221,6 @@ void MorseView::set_foxhunt(size_t i) {
MorseView::MorseView(
NavigationView& nav)
: nav_(nav) {
// baseband::run_image(portapack::spi_flash::image_tag_tones);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&labels,
+1 -1
View File
@@ -110,7 +110,7 @@ class MorseView : public View {
bool run{false};
Labels labels{
{{4 * 8, 6 * 8}, "Speed: wps", Theme::getInstance()->fg_light->foreground},
{{4 * 8, 6 * 8}, "Speed: wpm", Theme::getInstance()->fg_light->foreground},
{{4 * 8, 8 * 8}, "Tone: Hz", Theme::getInstance()->fg_light->foreground},
{{4 * 8, 10 * 8}, "Modulation:", Theme::getInstance()->fg_light->foreground},
{{4 * 8, 12 * 8}, "Loop:", Theme::getInstance()->fg_light->foreground},
+87
View File
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2026 Pezsma
*
* 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_morse_radiotx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::morseradiotx {
void initialize_app(NavigationView& nav) {
nav.push<MorseRadiotxView>();
}
} // namespace ui::external_app::morseradiotx
extern "C" {
__attribute__((section(".external_app.app_morseradiotx.application_information"), used))
application_information_t _application_information_morseradiotx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::morseradiotx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Morse TX",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0xFF,
0xFF,
0xBB,
0xD0,
0xFF,
0xFF,
0xFF,
0xFF,
0x0B,
0xE1,
0xFF,
0xFF,
0xFF,
0xFF,
0xEB,
0xD0,
0xFF,
0xFF,
0xFE,
0x7F,
0x70,
0x00,
0x30,
0x00,
0x10,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'M', 'R', 'T'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
} // extern "C"
@@ -0,0 +1,389 @@
#ifndef __MORSEDECODER_HPP__
#define __MORSEDECODER_HPP__
/*
* Copyright (C) 2025 Pezsma
*
* 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 <cstdint>
#include <string>
#include "string_format.hpp"
namespace ui::external_app::morseradiotx {
class MorseRingBuffer {
public:
MorseRingBuffer()
: head_(0), tail_(0), count_(0) {}
void push_back(const uint32_t& value) {
data_[head_] = value;
head_ = (head_ + 1) % 40;
if (count_ < 40) {
count_++;
} else {
// overwrite oldest element
tail_ = (tail_ + 1) % 40;
}
}
void pop_front() {
if (count_ > 0) {
tail_ = (tail_ + 1) % 40;
count_--;
}
}
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
const uint32_t& front() const { return data_[tail_]; }
// Access by index (0 = oldest)
uint32_t operator[](size_t idx) const {
return data_[(tail_ + idx) % 40];
}
// Convert to vector-like access for sorting etc.
void copy_to_array(uint32_t* out) const {
for (size_t i = 0; i < count_; ++i)
out[i] = (*this)[i];
}
void clear() {
head_ = 0;
tail_ = 0;
count_ = 0;
}
private:
uint32_t data_[40];
size_t head_;
size_t tail_;
size_t count_;
};
class MorseDecoder {
public:
struct DecodeResult {
std::string text = "";
double confidence = 0.0;
bool isValid() const {
return !text.empty();
}
};
struct MorseEntry {
std::string code;
std::string letter;
};
MorseDecoder() {}
void resetLearning() {
time_unit_ms_ = 119.0;
current_sequence_ = "";
last_sequence_ = "";
last_confidence_ = 0.0;
pulse_history_.clear();
pulse_gaps_.clear();
}
const char* get_morse_pattern(char c) {
char upper_c = (c >= 'a' && c <= 'z') ? c - 32 : c;
for (size_t i = 0; i < morse_table_size_; ++i) {
if (morse_table_[i].letter[0] == upper_c) {
return morse_table_[i].code.c_str();
}
}
return nullptr;
}
DecodeResult
handleInput(int32_t duration_ms) {
DecodeResult result = {"", 0.0};
if (duration_ms < 5 && duration_ms > -5) return result;
if (duration_ms > 0) {
pulse_history_.push_back(duration_ms);
double dah_prob = getDahProbability(duration_ms);
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
last_sequence_ = current_sequence_;
} else {
uint32_t gap_duration = -duration_ms;
pulse_gaps_.push_back(gap_duration);
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
result.text = lookupMorse(current_sequence_);
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
if (gap_duration >= getInterWordThreshold()) {
result.text += " ";
}
current_sequence_ = "";
}
}
updateLearning();
return result;
}
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
inline double getCurrentTimeUnit() { return time_unit_ms_; }
inline std::string getLastSequence() { return last_sequence_; }
private:
std::string current_sequence_ = "";
std::string last_sequence_ = "";
double time_unit_ms_ = 119.0;
double last_confidence_ = 0.0;
MorseRingBuffer pulse_history_{};
MorseRingBuffer pulse_gaps_{};
std::string lookupMorse(const std::string& seq) {
for (size_t i = 0; i < morse_table_size_; i++) {
if (seq == morse_table_[i].code)
return morse_table_[i].letter;
}
return "{" + seq + "}"; // not found
}
double getDahProbability(uint32_t duration_ms) {
double start_interp = 1.5 * time_unit_ms_;
double end_interp = 2.5 * time_unit_ms_;
if (duration_ms <= start_interp) return 0.0;
if (duration_ms >= end_interp) return 1.0;
return ((double)(duration_ms)-start_interp) / (end_interp - start_interp);
}
size_t findDecisionBoundary(uint32_t* sorted_data, size_t sorted_data_size) {
if (sorted_data_size < 4) return 0;
size_t best_split_index = 0;
uint32_t max_diff = 0;
for (size_t i = 1; i < sorted_data_size; ++i) {
uint32_t diff = sorted_data[i] - sorted_data[i - 1];
if (diff > sorted_data[i - 1] * 0.5 && diff > max_diff) {
max_diff = diff;
best_split_index = i;
}
}
return best_split_index;
}
bool calculatePulseUnit(double& unit, double& confidence) {
if (pulse_history_.size() < 10) return false;
uint32_t sorted_pulses[pulse_history_.size()];
pulse_history_.copy_to_array(sorted_pulses);
sort_uint32(sorted_pulses, pulse_history_.size());
size_t split_index = findDecisionBoundary(sorted_pulses, pulse_history_.size());
if (split_index == 0 || split_index < 3 || (pulse_history_.size() - split_index) < 2) {
return false;
}
double dit_sum = sum_uint32_range(sorted_pulses, 0, split_index);
double dah_sum = sum_uint32_range(sorted_pulses, split_index, pulse_history_.size());
double avg_dit = dit_sum / split_index;
double avg_dah = dah_sum / (pulse_history_.size() - split_index);
if (avg_dah <= avg_dit) return false;
double ratio = avg_dah / avg_dit;
if (ratio > 1.5 && ratio < 5.0) {
unit = avg_dit;
double tmpabs = ratio - 3.0;
if (tmpabs < 0) tmpabs *= -1;
tmpabs /= 3.0;
tmpabs = 1.0 - tmpabs;
if (tmpabs < 0) tmpabs = 0;
confidence = tmpabs; // 0..1
return true;
}
return false;
}
bool calculateGapUnit(double& unit, double& confidence) {
if (pulse_gaps_.size() < 10) return false;
double threshold = getInterElementThreshold();
double valid_gaps[pulse_gaps_.size()];
size_t valid_count = 0;
for (size_t i = 0; i < pulse_gaps_.size(); i++) {
double gap = pulse_gaps_[i];
if (gap <= threshold) {
valid_gaps[valid_count++] = gap;
}
}
if (valid_count < 2) {
return false;
}
double sum = sum_double_range(valid_gaps, 0, valid_count);
size_t count_to_average = valid_count;
if (count_to_average > 0) {
unit = sum / count_to_average;
confidence = 0.8;
return true;
}
return false;
}
double sum_uint32_range(const uint32_t* data, size_t start, size_t end) {
double sum = 0.0;
for (size_t i = start; i < end; i++) {
sum += data[i];
}
return sum;
}
double sum_double_range(const double* data, size_t start, size_t end) {
double sum = 0.0;
for (size_t i = start; i < end; i++) {
sum += data[i];
}
return sum;
}
void sort_uint32(uint32_t* data, size_t size) {
if (size < 2)
return;
for (size_t i = 1; i < size; i++) {
uint32_t key = data[i];
size_t j = i;
while (j > 0 && data[j - 1] > key) {
data[j] = data[j - 1];
j--;
}
data[j] = key;
}
}
double clamp_double(double value, double min_val, double max_val) {
if (value < min_val)
return min_val;
else if (value > max_val)
return max_val;
else
return value;
}
void updateLearning() {
double pulse_unit = -1.0, pulse_confidence = 0.0;
double gap_unit = -1.0, gap_confidence = 0.0;
bool pulse_success = calculatePulseUnit(pulse_unit, pulse_confidence);
bool gap_success = calculateGapUnit(gap_unit, gap_confidence);
double new_time_unit = -1.0;
if (pulse_success && pulse_confidence > 0.5) {
new_time_unit = pulse_unit;
} else if (pulse_success && gap_success) {
gap_confidence = 0.2;
double total_confidence = pulse_confidence + gap_confidence;
new_time_unit = (pulse_unit * pulse_confidence + gap_unit * gap_confidence) / total_confidence;
} else if (gap_success) {
new_time_unit = gap_unit;
} else {
return;
}
double max_change = time_unit_ms_ * 0.25;
new_time_unit = clamp_double(new_time_unit, time_unit_ms_ - max_change, time_unit_ms_ + max_change);
double DEFAULT_TIME_UNIT = 160.0;
double BASE_LEARNING_RATE = 0.05;
double MAX_LEARNING_RATE = 0.25;
double tudeltaabs = new_time_unit - DEFAULT_TIME_UNIT;
if (tudeltaabs < 0) tudeltaabs *= -1;
double deviation_from_default = tudeltaabs / DEFAULT_TIME_UNIT;
double tpp = deviation_from_default * 2.0;
if (tpp > 1) tpp = 1;
double learning_factor = BASE_LEARNING_RATE + (MAX_LEARNING_RATE - BASE_LEARNING_RATE) * tpp;
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
}
size_t morse_table_size_ = 50;
MorseEntry morse_table_[50] = {
{".-", "A"},
{"-...", "B"},
{"-.-.", "C"},
{"-..", "D"},
{".", "E"},
{"..-.", "F"},
{"--.", "G"},
{"....", "H"},
{"..", "I"},
{".---", "J"},
{"-.-", "K"},
{".-..", "L"},
{"--", "M"},
{"-.", "N"},
{"---", "O"},
{".--.", "P"},
{"--.-", "Q"},
{".-.", "R"},
{"...", "S"},
{"-", "T"},
{"..-", "U"},
{"...-", "V"},
{".--", "W"},
{"-..-", "X"},
{"-.--", "Y"},
{"--..", "Z"},
{".----", "1"},
{"..---", "2"},
{"...--", "3"},
{"....-", "4"},
{".....", "5"},
{"-....", "6"},
{"--...", "7"},
{"---..", "8"},
{"----.", "9"},
{"-----", "0"},
{".-.-.-", "."},
{"..--..", "?"},
{"-.-.--", "!"},
{"--..--", ","},
{"-...-", "="},
{"-..-.", "/"},
{".--.-.", "@"},
{"---...", ":"},
{"-....-", "-"},
{".----.", "'"},
{".-..-.", "\""},
{"-.--.", "("},
{"-.--.-", ")"},
{".-.-.", "+"}};
};
} // namespace ui::external_app::morseradiotx
#endif // __MORSEDECODER_HPP__
@@ -0,0 +1,437 @@
#include "ui_morse_radiotx.hpp"
using namespace portapack;
namespace ui::external_app::morseradiotx {
static msg_t tx_thread_fn(void* arg) {
auto view = reinterpret_cast<MorseRadiotxView*>(arg);
chRegSetThreadName("morse_tx_thread");
view->transmit_morse_message();
return 0;
}
MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav)
: current_timings(calculate_morse_timings(20)),
nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&tx_view,
&field_volume,
&labels,
&options_mode,
&tone_,
&wpm_,
&txt_msg,
&btn_message,
&btn_calls,
&chk_trans,
&bandwidth,
&chk_callsgn,
&txt_last,
&console_text,
&btn_clear,
&btn_ptt});
initial_switch_config_ = get_switches_repeat_config();
SwitchesState config = initial_switch_config_;
config[toUType(Switch::Sel)] = false;
set_switches_repeat_config(config);
audio::set_rate(audio::Rate::Hz_24000);
btn_clear.on_select = [this](Button&) {
console_text.clear(true);
txt_last.set("");
};
options_mode.on_change = [this](size_t, int32_t value) {
current_mode = (uint8_t)value;
baseband::set_morsetx_config(current_mode, tone, band);
ui_toggle();
};
tone_.on_change = [this](int32_t v) {
tone = static_cast<uint32_t>(v);
baseband::set_morsetx_config(current_mode, tone, band);
};
wpm_.on_change = [this](int16_t v) {
wpm = v;
current_timings = calculate_morse_timings(wpm);
};
bandwidth.on_change = [this](float v) {
band = v;
baseband::set_morsetx_config(current_mode, tone, band);
};
options_mode.set_selected_index(current_mode, true);
tone_.set_value(tone, true);
wpm_.set_value(wpm, true);
bandwidth.set_value(band, true);
btn_ptt.on_select = [this](Button&) {
if (button_touch) {
button_touch = false;
return;
}
button_was_selected = true;
onPress();
};
btn_ptt.on_touch_press = [this](Button&) {
button_touch = true;
button_was_selected = false;
onPress();
};
btn_ptt.on_touch_release = [this](Button&) {
if (chk_trans.value() && transmit) {
button_touch = true;
button_was_selected = false;
onRelease();
}
};
btn_message.on_select = [this, &nav](Button&) {
if (!transmit)
text_prompt(nav, msg_buffer, 27, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) {
msg_buffer = buffer;
txt_msg.set("[" + msg_buffer + "] ");
});
};
btn_calls.on_select = [this, &nav](Button&) {
if (!transmit) {
text_prompt(nav, call_sign, 10, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) {
call_sign = buffer;
if (call_sign.empty())
btn_calls.set_text("call sign?");
else
btn_calls.set_text(call_sign);
});
}
};
audio::output::start();
auto vol = field_volume.value();
field_volume.set_value(0);
field_volume.set_value(vol);
tx_view.on_start = [this]() {
if (!chk_trans.value() && msg_buffer.empty()) {
tx_view.set_transmitting(false);
return;
}
tx_view.focus();
if (!tx_thread && !thread_running) {
thread_running = true;
tx_thread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO + 5, tx_thread_fn, this);
tx_view.set_transmitting(true);
}
else
tx_view.set_transmitting(false);
};
tx_view.on_stop = [this]() {
baseband::set_morsetx_key(false);
transmitter_model.disable();
if (tx_thread) {
chThdTerminate(tx_thread);
chThdWait(tx_thread);
}
transmit = false;
transmit_time = 0;
tx_thread = nullptr;
thread_running = false;
tx_view.set_transmitting(false);
ui_toggle();
};
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [this](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
};
};
}
MorseRadiotxView::~MorseRadiotxView() {
set_switches_repeat_config(initial_switch_config_);
if (tx_thread) {
chThdTerminate(tx_thread);
chThdWait(tx_thread);
if (!thread_running) tx_thread = nullptr;
}
transmitter_model.disable();
baseband::shutdown();
audio::output::stop();
}
void MorseRadiotxView::on_show() {
ptt_button_visibility(true);
start_time = 0;
end_time = 0;
transmit_time = 0;
}
void MorseRadiotxView::focus() {
options_mode.focus();
}
MorseRadiotxView::MorseTimings MorseRadiotxView::calculate_morse_timings(uint32_t wpm) {
MorseRadiotxView::MorseTimings t;
if (wpm < 10) wpm = 10;
if (wpm > 45) wpm = 45;
t.dot_ms = 1200 / wpm;
t.dash_ms = 3 * t.dot_ms;
t.symbol_gap = t.dot_ms;
t.char_gap = 3 * t.dot_ms;
t.word_gap = 7 * t.dot_ms;
return t;
}
void MorseRadiotxView::transmit_morse_message() {
std::string full_message = "";
// cal sign
if (chk_callsgn.value() && !call_sign.empty()) {
full_message = call_sign;
if (!chk_trans.value() && !msg_buffer.empty()) {
full_message += " " + msg_buffer;
}
} else {
if (!chk_trans.value()) full_message = msg_buffer;
}
if (full_message.empty() && !chk_trans.value()) {
ptt_button_visibility(false);
return;
}
// enable transmit
if (!transmit) {
transmit = true;
transmitter_model.enable();
ui_toggle();
}
for (size_t i = 0; i < full_message.length(); i++) {
if (chThdShouldTerminate()) break;
char c = full_message[i];
std::string s_char(1, c);
// space
if (c == ' ') {
console_text.write(" ");
chThdSleepMilliseconds(current_timings.word_gap);
continue;
}
const char* pattern = morse_decoder_.get_morse_pattern(c);
if (pattern != nullptr) {
txt_last.set(pattern);
// write blue char to console
console_text.write(STR_COLOR_BLUE + s_char);
// Morze (dih/dah) send
for (int j = 0; pattern[j] != '\0'; j++) {
baseband::set_morsetx_key(true);
if (pattern[j] == '.') {
chThdSleepMilliseconds(current_timings.dot_ms);
} else if (pattern[j] == '-') {
chThdSleepMilliseconds(current_timings.dash_ms);
}
if (chThdShouldTerminate()) break;
// pause between signs
baseband::set_morsetx_key(false);
chThdSleepMilliseconds(current_timings.symbol_gap);
}
if (chThdShouldTerminate()) break;
if (current_timings.char_gap > current_timings.symbol_gap) {
chThdSleepMilliseconds(current_timings.char_gap - current_timings.symbol_gap);
}
}
}
if (chk_trans.value()) ptt_button_visibility(false);
baseband::set_morsetx_key(false);
transmit_time = chTimeNow();
decode_timeout_calc = false;
thread_running = false;
}
void MorseRadiotxView::onPress() {
start_time = chTimeNow();
if (!transmit) {
transmit = true;
transmitter_model.enable();
}
baseband::set_morsetx_key(true);
if (end_time != 0) {
int64_t gap_delta = (chTimeNow() - end_time);
auto result = morse_decoder_.handleInput(-gap_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
end_time = 0;
transmit_time = 0;
decode_timeout_calc = false;
}
void MorseRadiotxView::onRelease() {
end_time = chTimeNow();
transmit_time = end_time;
baseband::set_morsetx_key(false);
if (start_time != 0) {
int32_t press_delta = (end_time - start_time);
auto result = morse_decoder_.handleInput(press_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
start_time = 0;
decode_timeout_calc = true;
baseband::request_beep_stop();
}
void MorseRadiotxView::writeCharToConsole(const std::string& ch, double confidence) {
if (ch.empty()) {
return;
}
txt_last.set(morse_decoder_.getLastSequence().c_str());
last_color_id = color_id;
std::string color = "";
if (ch == " ") {
color_id = 0;
} else if (ch[0] == '{') { // no match
color_id = 0;
} else {
if (confidence == 1.5)
color_id = 4;
else if (confidence < 0.8)
color_id = 1;
else if (confidence < 0.9)
color_id = 2;
else
color_id = 3;
}
color = arr_color[color_id];
last_color_id = color_id;
console_text.write(color + ch);
}
void MorseRadiotxView::ptt_button_visibility(bool hidden) {
bool hiddenorig = btn_ptt.hidden();
if (hiddenorig != hidden) {
btn_ptt.hidden(hidden);
if (!hidden) btn_ptt.focus();
if (hidden) {
// hack fix until widget hidig problem is not solved.
auto r = btn_ptt.screen_rect();
Painter p;
p.fill_rectangle_unrolled8(r, Theme::getInstance()->fg_light->background);
}
}
}
void MorseRadiotxView::ui_toggle() {
// 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
if (transmit) {
options_mode.set_style(Theme::getInstance()->fg_dark);
options_mode.set_focusable(false);
tone_.set_style(Theme::getInstance()->fg_dark);
tone_.set_focusable(false);
wpm_.set_style(Theme::getInstance()->fg_dark);
wpm_.set_focusable(false);
btn_message.set_style(Theme::getInstance()->fg_dark);
btn_calls.set_style(Theme::getInstance()->fg_dark);
chk_trans.set_style(Theme::getInstance()->fg_dark);
chk_trans.set_focusable(false);
bandwidth.set_style(Theme::getInstance()->fg_dark);
bandwidth.set_focusable(false);
chk_callsgn.set_style(Theme::getInstance()->fg_dark);
chk_callsgn.set_focusable(false);
} else {
options_mode.set_style(Theme::getInstance()->bg_darker);
options_mode.set_focusable(true);
wpm_.set_style(Theme::getInstance()->bg_darker);
wpm_.set_focusable(true);
btn_message.set_style(Theme::getInstance()->bg_darker);
btn_calls.set_style(Theme::getInstance()->bg_darker);
chk_trans.set_style(Theme::getInstance()->bg_darker);
chk_trans.set_focusable(true);
chk_callsgn.set_style(Theme::getInstance()->bg_darker);
chk_callsgn.set_focusable(true);
ptt_button_visibility(true);
tone_.set_style(Theme::getInstance()->bg_darker);
tone_.set_focusable(true);
if (current_mode == 1) { // FM mode
bandwidth.set_style(Theme::getInstance()->bg_darker);
bandwidth.set_focusable(true);
} else {
bandwidth.set_style(Theme::getInstance()->fg_dark);
bandwidth.set_focusable(false);
}
} // transmit false
}
inline bool MorseRadiotxView::tx_button_held() {
const auto switches_state = get_switches_state();
return switches_state[(size_t)ui::KeyEvent::Select];
}
void MorseRadiotxView::on_framesync() {
if (button_was_selected && !button_touch && !tx_button_held()) {
button_was_selected = false;
onRelease();
}
if (end_time != 0 && decode_timeout_calc) {
int64_t gap_delta = (chTimeNow() - end_time);
if (gap_delta >= morse_decoder_.getInterCharThreshold()) {
auto result = morse_decoder_.handleInput(-(int32_t)gap_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
if (gap_delta >= morse_decoder_.getInterWordThreshold()) {
writeCharToConsole(" ", 1.0);
end_time = 0;
decode_timeout_calc = false;
}
}
if (transmit_time != 0 && transmit) { // Tx disable if time is up
int64_t gap_delta = (chTimeNow() - transmit_time);
if (gap_delta >= ((morse_decoder_.getInterWordThreshold() * ((chk_trans.value()) ? 10 : 1)))) {
if (tx_thread) {
chThdTerminate(tx_thread);
chThdWait(tx_thread);
}
transmit = false;
tx_view.set_transmitting(false);
transmitter_model.disable();
thread_running = false;
tx_thread = nullptr;
transmit_time = 0;
if (!chk_trans.value()) writeCharToConsole(" ", 1.0);
ui_toggle();
}
}
}
} // namespace ui::external_app::morseradiotx
@@ -0,0 +1,171 @@
/*
* Copyright (C) 2026 Pezsma
*
* 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 __MORSE_RADIOTX_H__
#define __MORSE_RADIOTX_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_language.hpp"
#include "ui_painter.hpp"
#include "ui_freq_field.hpp"
#include "ui_transmitter.hpp"
#include "ui_textentry.hpp"
#include "string_format.hpp"
#include "morsedecoder.hpp"
#include "irq_controls.hpp"
#include "radio_state.hpp"
#include "portapack.hpp"
#include "message.hpp"
#include "volume.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "external_app.hpp"
#include "string_format.hpp"
#include <ch.h>
#include <hal.h>
namespace ui::external_app::morseradiotx {
class MorseRadiotxView : public ui::View {
public:
MorseRadiotxView(ui::NavigationView& nav);
~MorseRadiotxView();
MorseRadiotxView(const MorseRadiotxView&) = delete;
MorseRadiotxView(MorseRadiotxView&&) = delete;
MorseRadiotxView& operator=(const MorseRadiotxView&) = delete;
MorseRadiotxView& operator=(MorseRadiotxView&&) = delete;
std::string title() const override { return "Morse Tx"; }
void focus() override;
void on_show() override;
void transmit_morse_message();
private:
struct MorseTimings {
uint32_t dot_ms;
uint32_t dash_ms;
uint32_t symbol_gap;
uint32_t char_gap;
uint32_t word_gap;
};
MorseTimings current_timings{};
std::string msg_indicator{""};
MorseTimings calculate_morse_timings(uint32_t wpm);
void onPress();
void onRelease();
void on_framesync();
void writeCharToConsole(const std::string& ch, double confidence);
void ui_toggle();
bool tx_button_held();
void ptt_button_visibility(bool hidden);
ui::NavigationView& nav_;
MorseDecoder morse_decoder_{};
TxRadioState radio_state_{};
Thread* tx_thread{nullptr};
std::string msg_buffer{"PORTAPACK"};
uint8_t current_mode{0}; // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
uint8_t wpm{20};
uint32_t tone{700};
float band{5.8};
std::string call_sign{""};
app_settings::SettingsManager settings_{
"tx_morseradio",
app_settings::Mode::TX,
{
{"cmode"sv, &current_mode},
{"audtone"sv, &tone},
{"wpm"sv, &wpm},
{"message"sv, &msg_buffer},
{"bandwith"sv, &band},
{"call_sign"sv, &call_sign},
}};
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
10000,
0, false};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y_BOTTOM(5)}};
ui::OptionsField options_mode{
{UI_POS_X(5), UI_POS_Y(0)},
3,
{{"AM", 0}, {"FM", 1}, {"DSB", 2}, {"USB", 3}, {"LSB", 4}}};
NumberField tone_{{UI_POS_X(14), UI_POS_Y(0)}, 4, {400, 1400}, 10, ' ', true};
NumberField wpm_{{UI_POS_X(25), UI_POS_Y(0)}, 2, {10, 45}, 1, ' ', true};
FloatField bandwidth{{UI_POS_X(20), UI_POS_Y(3)}, 4, {1.0, 16.0}, 0.1, ' ', true, 1};
ui::Text txt_msg{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "[" + msg_buffer + "] "};
ui::Button btn_message{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, "Message"};
ui::Button btn_calls{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, (call_sign.empty()) ? "call sign?" : call_sign};
Checkbox chk_trans{{UI_POS_X(14), UI_POS_Y(2)}, 13, "Manual trans.", true};
Checkbox chk_callsgn{{UI_POS_X(14), UI_POS_Y(4)}, 13, "Call sign", true};
ui::Text txt_last{{UI_POS_X(10), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)}, ""};
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(14)}};
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "CLR"};
ui::Button btn_ptt{{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(7), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "PTT"};
ui::Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "Mode:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(9), UI_POS_Y(0)}, "Tone:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(18), UI_POS_Y(0)}, "Hz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(21), UI_POS_Y(0)}, "WPM:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(14), UI_POS_Y(3)}, "BandW:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(24), UI_POS_Y(3)}, "kHz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(5)}, "Last seq:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(6)}, "Sent Message:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_RIGHT(7), UI_POS_Y_BOTTOM(5)}, "Vol.:", Theme::getInstance()->fg_light->foreground},
};
uint8_t last_color_id{255};
uint8_t color_id{255};
std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN};
bool button_touch{false};
bool button_was_selected{false};
bool decode_timeout_calc{false};
bool transmit{false};
bool thread_running = false;
uint8_t send_indicator{5};
int64_t start_time{0};
int64_t end_time{0};
int64_t transmit_time{0};
SwitchesState initial_switch_config_{};
MessageHandlerRegistration message_handler_framesync{
Message::ID::DisplayFrameSync,
[this](const Message* const p) {
(void)p;
this->on_framesync();
}};
};
} // namespace ui::external_app::morseradiotx
#endif // __MORSE_RADIOTX_H__
@@ -231,7 +231,7 @@ void RandomPasswordView::on_data(uint32_t value, bool is_data) {
}
void RandomPasswordView::clean_buffer() {
seeds_deque = {};
seeds_deque.clear();
}
void RandomPasswordView::on_freqchg(int64_t freq) {
@@ -247,6 +247,18 @@ void RandomPasswordView::set_random_freq() {
field_frequency.set_value(random_freq);
}
bool RandomPasswordView::islower(char c) {
return (c >= 'a' && c <= 'z');
}
bool RandomPasswordView::isupper(char c) {
return (c >= 'A' && c <= 'Z');
}
bool RandomPasswordView::isdigit(char c) {
return (c >= '0' && c <= '9');
}
void RandomPasswordView::new_password() {
if (seeds_deque.size() < MAX_DIGITS * 2) {
seeds_buffer_not_full = true;
@@ -261,21 +273,21 @@ void RandomPasswordView::new_password() {
int password_length = field_digits.value();
/// charset worker
if (check_digits.value())
charset += "0123456789";
if (check_latin_lower.value())
charset += "abcdefghijklmnopqrstuvwxyz";
if (check_latin_upper.value())
charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
if (check_digits.value()) {
charset += "23456789";
if (check_allow_confusable_chars.value()) charset += "01";
}
if (check_latin_lower.value()) {
charset += "abcdefghijkmnpqrstuvwxyz";
if (check_allow_confusable_chars.value()) charset += "ol";
}
if (check_latin_upper.value()) {
charset += "ABCDEFGHIJKLMNPQRSTUVWXYZ";
if (check_allow_confusable_chars.value()) charset += "O";
}
if (check_punctuation.value())
charset += ".,-!?";
if (!check_allow_confusable_chars.value()) {
charset.erase(std::remove_if(charset.begin(), charset.end(),
[](char c) { return c == '0' || c == 'O' || c == 'o' || c == '1' || c == 'l'; }),
charset.end());
}
if (charset.empty()) {
text_generated_passwd.set("generate failed,");
text_char_type_hints.set("select at least 1 type");
@@ -306,11 +318,11 @@ void RandomPasswordView::new_password() {
/// hint text worker
for (char c : password_chars) {
password += c;
if (std::isdigit(c)) {
if (isdigit(c)) {
char_type_hints += "1";
} else if (std::islower(c)) {
} else if (islower(c)) {
char_type_hints += "a";
} else if (std::isupper(c)) {
} else if (isupper(c)) {
char_type_hints += "A";
} else {
char_type_hints += ",";
@@ -360,11 +372,11 @@ void RandomPasswordView::paint_password_hints() {
for (size_t i = 0; i < password.length(); i++) {
char c = password[i];
Color color;
if (std::isdigit(c)) {
if (isdigit(c)) {
color = Color::red();
} else if (std::islower(c)) {
} else if (islower(c)) {
color = Color::green();
} else if (std::isupper(c)) {
} else if (isupper(c)) {
color = Color::blue();
} else {
color = Color::white();
@@ -379,8 +391,8 @@ void RandomPasswordView::paint_password_hints() {
std::string RandomPasswordView::generate_log_line() {
std::string seeds_set = "";
for (auto seed : seeds_deque) {
seeds_set += std::to_string(seed);
for (unsigned int seed : seeds_deque) {
seeds_set += to_string_dec_uint(seed);
seeds_set += " ";
}
std::string line = "\npassword=" + password +
@@ -92,6 +92,10 @@ class RandomPasswordView : public View {
std::string generate_log_line();
void paint_password_hints();
bool islower(char c);
bool isupper(char c);
bool isdigit(char c);
NavigationView& nav_;
RxRadioState radio_state_{};
app_settings::SettingsManager settings_{
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2024 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_sd_over_usb.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::sdusb {
void initialize_app(ui::NavigationView& nav) {
nav.push<SdOverUsbView>();
}
} // namespace ui::external_app::sdusb
extern "C" {
__attribute__((section(".external_app.app_sdusb.application_information"), used)) application_information_t _application_information_sdusb = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::sdusb::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "SD Over USB",
/*.bitmap_data = */ {
0xF0,
0x0F,
0x10,
0x08,
0x50,
0x0A,
0x10,
0x08,
0x10,
0x08,
0x10,
0x08,
0xF8,
0x1F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF0,
0x0F,
0x80,
0x01,
0x80,
0x01,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::UTILITIES,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_usb_sd */ {'P', 'U', 'S', 'B'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
@@ -23,7 +23,7 @@
#include "ui_sd_over_usb.hpp"
#include "portapack_shared_memory.hpp"
namespace ui {
namespace ui::external_app::sdusb {
SdOverUsbView::SdOverUsbView(NavigationView& nav)
: nav_(nav) {
@@ -46,7 +46,7 @@ SdOverUsbView::SdOverUsbView(NavigationView& nav)
sdcStop(&SDCD1);
portapack::shutdown(true);
m4_init(portapack::spi_flash::image_tag_usb_sd, portapack::memory::map::m4_code, false);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
m0_halt();
/* will not return*/
};
@@ -56,4 +56,4 @@ void SdOverUsbView::focus() {
button_run.focus();
}
} /* namespace ui */
} // namespace ui::external_app::sdusb
@@ -32,7 +32,7 @@
#include <cstdint>
namespace ui {
namespace ui::external_app::sdusb {
class SdOverUsbView : public View {
public:
@@ -58,6 +58,6 @@ class SdOverUsbView : public View {
"Run"};
};
} /* namespace ui */
} // namespace ui::external_app::sdusb
#endif /*__UI_SD_OVER_USB_H__*/
+82
View File
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2023 Bernd Herzog
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_siggen.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::siggen {
void initialize_app(ui::NavigationView& nav) {
nav.push<SigGenView>();
}
} // namespace ui::external_app::siggen
extern "C" {
__attribute__((section(".external_app.app_siggen.application_information"), used)) application_information_t _application_information_siggen = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::siggen::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Signal gen",
/*.bitmap_data = */ {
0x18,
0x00,
0x24,
0x00,
0x42,
0x00,
0x42,
0x00,
0x42,
0x00,
0x42,
0x00,
0x81,
0x00,
0xAB,
0x6A,
0x80,
0x40,
0x00,
0x21,
0x00,
0x21,
0x00,
0x21,
0x00,
0x21,
0x00,
0x12,
0x00,
0x0C,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_siggen */ {'P', 'S', 'I', 'G'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
@@ -31,7 +31,7 @@
using namespace portapack;
namespace ui {
namespace ui::external_app::siggen {
void SigGenView::focus() {
options_shape.focus();
@@ -75,7 +75,7 @@ void SigGenView::on_tx_progress(const uint32_t progress, const bool done) {
SigGenView::SigGenView(
NavigationView& nav) {
baseband::run_image(portapack::spi_flash::image_tag_siggen);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&labels,
&options_mod,
@@ -172,4 +172,4 @@ SigGenView::SigGenView(
};
}
} /* namespace ui */
} // namespace ui::external_app::siggen
@@ -33,7 +33,7 @@
#include "portapack.hpp"
#include "message.hpp"
namespace ui {
namespace ui::external_app::siggen {
class SigGenView : public View {
public:
@@ -139,6 +139,6 @@ class SigGenView : public View {
}};
};
} /* namespace ui */
} // namespace ui::external_app::siggen
#endif /*__SIGGEN_H__*/
+166 -149
View File
@@ -10,22 +10,27 @@
namespace ui::external_app::spaceinv {
// Game constants
#define PLAYER_WIDTH 26
#define PLAYER_HEIGHT 16
#define PLAYER_Y 280
#define BULLET_WIDTH 3
#define BULLET_HEIGHT 10
#define BULLET_SPEED 8
#define MAX_BULLETS 3
#define INVADER_WIDTH 20
#define INVADER_HEIGHT 16
#define INVADER_ROWS 5 // Classic 5 rows
#define INVADER_COLS 6 // Reduced for more movement space on narrow PP screen
#define INVADER_GAP_X 10
#define INVADER_GAP_Y 8 // Gap between invaders
#define MAX_ENEMY_BULLETS 3
#define ENEMY_BULLET_SPEED 3
// Dynamic game constants
static int SCREEN_WIDTH = 240;
static int SCREEN_HEIGHT = 320;
static int PLAYER_WIDTH = 26;
static int PLAYER_HEIGHT = 16;
static int PLAYER_Y = 280;
static int BULLET_WIDTH = 3;
static int BULLET_HEIGHT = 10;
static int BULLET_SPEED = 8;
static int MAX_BULLETS = 3;
static int INVADER_WIDTH = 20;
static int INVADER_HEIGHT = 16;
static int INVADER_ROWS = 5;
static int INVADER_COLS = 6;
static int INVADER_GAP_X = 10;
static int INVADER_GAP_Y = 8;
static int MAX_ENEMY_BULLETS = 3;
static int ENEMY_BULLET_SPEED = 3;
static int INFO_BAR_HEIGHT = 50;
static int INVADER_MOVE_AMOUNT = 8;
static int INVADER_DROP_AMOUNT = 15;
// Game state
static int game_state = 0; // 0=menu, 1=playing, 2=game over, 3=wave_complete
@@ -38,18 +43,18 @@ static uint32_t speed_bonus = 0;
static uint32_t wave_complete_timer = 0;
// Cannon Bullets
static int bullet_x[MAX_BULLETS] = {0, 0, 0};
static int bullet_y[MAX_BULLETS] = {0, 0, 0};
static bool bullet_active[MAX_BULLETS] = {false, false, false};
static int bullet_x[3] = {0, 0, 0};
static int bullet_y[3] = {0, 0, 0};
static bool bullet_active[3] = {false, false, false};
// Enemy bullets
static int enemy_bullet_x[MAX_ENEMY_BULLETS] = {0, 0, 0};
static int enemy_bullet_y[MAX_ENEMY_BULLETS] = {0, 0, 0};
static bool enemy_bullet_active[MAX_ENEMY_BULLETS] = {false, false, false};
static int enemy_bullet_x[3] = {0, 0, 0};
static int enemy_bullet_y[3] = {0, 0, 0};
static bool enemy_bullet_active[3] = {false, false, false};
static uint32_t enemy_fire_counter = 0;
// Invaders
static bool invaders[INVADER_ROWS][INVADER_COLS];
static bool invaders[5][11]; // Max possible columns
static int invaders_x = 40;
static int invaders_y = 60;
static int invader_direction = 1;
@@ -94,15 +99,47 @@ void Ticker::detach() {
game_update_callback = nullptr;
}
static void init_dimensions() {
SCREEN_WIDTH = ui::screen_width;
SCREEN_HEIGHT = ui::screen_height;
// Scale player based on screen size
PLAYER_WIDTH = SCREEN_WIDTH / 9;
PLAYER_HEIGHT = SCREEN_HEIGHT / 20;
PLAYER_Y = SCREEN_HEIGHT - 40;
// Keep invader size fixed for proper pixel art
INVADER_WIDTH = 20;
INVADER_HEIGHT = 16;
// Calculate columns based on available width
int available_width = SCREEN_WIDTH - 40; // 20px margins on each side
INVADER_COLS = available_width / (INVADER_WIDTH + INVADER_GAP_X);
if (INVADER_COLS > 11) INVADER_COLS = 11;
if (INVADER_COLS < 6) INVADER_COLS = 6;
// Center the invader formation
int formation_width = INVADER_COLS * INVADER_WIDTH + (INVADER_COLS - 1) * INVADER_GAP_X;
invaders_x = (SCREEN_WIDTH - formation_width) / 2;
invaders_y = INFO_BAR_HEIGHT + 10;
// Scale movement
INVADER_MOVE_AMOUNT = SCREEN_WIDTH / 30;
INVADER_DROP_AMOUNT = SCREEN_HEIGHT / 21;
}
static void init_invaders() {
// Initialize invader array
// Initialize invader array based on dynamic columns
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
invaders[row][col] = true;
}
}
invaders_x = 40;
invaders_y = 60;
// Reset position to center
int formation_width = INVADER_COLS * INVADER_WIDTH + (INVADER_COLS - 1) * INVADER_GAP_X;
invaders_x = (SCREEN_WIDTH - formation_width) / 2;
invaders_y = INFO_BAR_HEIGHT + 10;
invader_direction = 1;
invader_move_counter = 0;
@@ -125,138 +162,114 @@ static void save_high_score() {
}
static void init_menu() {
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
painter.fill_rectangle({0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}, Color::black());
auto style_green = *ui::Theme::getInstance()->fg_green;
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
auto style_red = *ui::Theme::getInstance()->fg_red;
int16_t screen_width = 240;
int16_t title_x = (screen_width - 15 * 8) / 2;
int16_t divider_width = 24 * 8;
int16_t divider_x = (screen_width - divider_width) / 2;
int16_t instruction_width = 22 * 8;
int16_t instruction_x = (screen_width - instruction_width) / 2;
int16_t prompt_width = 12 * 8;
prompt_x = (screen_width - prompt_width) / 2;
// Title section
painter.draw_string({UI_POS_X_CENTER(15), 30}, style_green, "SPACE INVADERS");
painter.draw_string({UI_POS_X_CENTER(24), 55}, style_yellow, "========================");
painter.fill_rectangle({0, 30, screen_width, 30}, Color::black());
painter.draw_string({title_x, 42}, style_green, "SPACE INVADERS");
// Instructions box
int box_width = 22 * 8;
int box_x = UI_POS_X_CENTER(22);
painter.fill_rectangle({box_x - 5, 85, box_width + 10, 80}, Color::black());
painter.draw_rectangle({box_x - 5, 85, box_width + 10, 80}, Color::white());
painter.draw_string({divider_x, 70}, style_yellow, "========================");
painter.draw_string({box_x, 95}, style_cyan, " ROTARY: MOVE SHIP");
painter.draw_string({box_x, 115}, style_cyan, " SELECT: FIRE");
painter.draw_string({box_x, 135}, style_cyan, " DEFEND EARTH!");
painter.fill_rectangle({instruction_x - 5, 100, instruction_width + 10, 80}, Color::black());
painter.draw_rectangle({instruction_x - 5, 100, instruction_width + 10, 80}, Color::white());
// Point values
painter.draw_string({UI_POS_X_CENTER(16), 175}, style_red, "TOP ROW = 30 PTS");
painter.draw_string({UI_POS_X_CENTER(16), 190}, style_yellow, "MID ROW = 20 PTS");
painter.draw_string({UI_POS_X_CENTER(16), 205}, style_green, "BOT ROW = 10 PTS");
painter.draw_string({instruction_x, 110}, style_cyan, " ROTARY: MOVE SHIP");
painter.draw_string({instruction_x, 130}, style_cyan, " SELECT: FIRE");
painter.draw_string({instruction_x, 150}, style_cyan, " DEFEND EARTH!");
// Draw point values
auto style_purple = *ui::Theme::getInstance()->fg_magenta;
painter.draw_string({50, 190}, style_purple, "TOP ROW = 30 PTS");
painter.draw_string({50, 205}, style_yellow, "MID ROW = 20 PTS");
painter.draw_string({50, 220}, style_green, "BOT ROW = 10 PTS");
// Draw high score
// High score
if (current_instance) {
auto style_white = *ui::Theme::getInstance()->fg_light;
std::string high_score_text = "HIGH SCORE: " + std::to_string(current_instance->highScore);
int16_t high_score_x = (screen_width - high_score_text.length() * 8) / 2;
painter.draw_string({high_score_x, 240}, style_white, high_score_text);
painter.draw_string({UI_POS_X_CENTER(high_score_text.length()), 225}, style_white, high_score_text);
}
// Set prompt position (will be used for blinking text)
prompt_x = UI_POS_X_CENTER(12);
menu_initialized = true;
}
static void init_game_over() {
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
painter.fill_rectangle({0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}, Color::black());
auto style_red = *ui::Theme::getInstance()->fg_red;
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
auto style_white = *ui::Theme::getInstance()->fg_light;
int16_t screen_width = 240;
// Game Over title
int16_t title_x = (screen_width - 9 * 8) / 2;
painter.draw_string({title_x, 42}, style_red, "GAME OVER");
// Divider
int16_t divider_width = 24 * 8;
int16_t divider_x = (screen_width - divider_width) / 2;
painter.draw_string({divider_x, 70}, style_yellow, "========================");
painter.draw_string({UI_POS_X_CENTER(9), 40}, style_red, "GAME OVER");
painter.draw_string({UI_POS_X_CENTER(24), 65}, style_yellow, "========================");
// Score box
int16_t box_width = 22 * 8;
int16_t box_x = (screen_width - box_width) / 2;
painter.fill_rectangle({box_x - 5, 100, box_width + 10, 80}, Color::black());
painter.draw_rectangle({box_x - 5, 100, box_width + 10, 80}, Color::white());
int box_width = 22 * 8;
int box_x = UI_POS_X_CENTER(22);
painter.fill_rectangle({box_x - 5, 90, box_width + 10, 80}, Color::black());
painter.draw_rectangle({box_x - 5, 90, box_width + 10, 80}, Color::white());
// Display scores
std::string score_text = "SCORE: " + std::to_string(score);
int16_t score_x = (screen_width - score_text.length() * 8) / 2;
painter.draw_string({score_x, 120}, style_cyan, score_text);
painter.draw_string({UI_POS_X_CENTER(score_text.length()), 110}, style_cyan, score_text);
std::string wave_text = "WAVE: " + std::to_string(wave);
int16_t wave_x = (screen_width - wave_text.length() * 8) / 2;
painter.draw_string({wave_x, 140}, style_cyan, wave_text);
painter.draw_string({UI_POS_X_CENTER(wave_text.length()), 130}, style_cyan, wave_text);
// High score section
if (current_instance) {
if (score > current_instance->highScore) {
// New high score!
std::string new_high = "NEW HIGH SCORE!";
int16_t new_high_x = (screen_width - new_high.length() * 8) / 2;
painter.draw_string({new_high_x, 200}, style_yellow, new_high);
painter.draw_string({UI_POS_X_CENTER(15), 190}, style_yellow, "NEW HIGH SCORE!");
std::string high_score_text = std::to_string(score);
int16_t high_score_x = (screen_width - high_score_text.length() * 8) / 2;
painter.draw_string({high_score_x, 220}, style_yellow, high_score_text);
painter.draw_string({UI_POS_X_CENTER(high_score_text.length()), 210}, style_yellow, high_score_text);
} else {
// Show existing high score
std::string high_label = "HIGH SCORE:";
int16_t label_x = (screen_width - high_label.length() * 8) / 2;
painter.draw_string({label_x, 200}, style_white, high_label);
painter.draw_string({UI_POS_X_CENTER(11), 190}, style_white, "HIGH SCORE:");
std::string high_score_text = std::to_string(current_instance->highScore);
int16_t high_score_x = (screen_width - high_score_text.length() * 8) / 2;
painter.draw_string({high_score_x, 220}, style_white, high_score_text);
painter.draw_string({UI_POS_X_CENTER(high_score_text.length()), 210}, style_white, high_score_text);
}
}
// Set prompt position for blinking text
prompt_x = UI_POS_X_CENTER(12);
game_over_initialized = true;
}
static void draw_player() {
// Clear the entire player area
painter.fill_rectangle({0, PLAYER_Y - 4, 240, PLAYER_HEIGHT + 8}, Color::black());
painter.fill_rectangle({0, PLAYER_Y - 4, SCREEN_WIDTH, PLAYER_HEIGHT + 8}, Color::black());
// Draw the classic Space Invaders cannon
Color green = Color::green();
// Scale the drawing based on player size
int unit_x = PLAYER_WIDTH / 26;
int unit_y = PLAYER_HEIGHT / 16;
// Top part - the cannon barrel
painter.draw_hline({player_x + 12, PLAYER_Y}, 2, green);
painter.draw_hline({player_x + 12, PLAYER_Y + 1}, 2, green);
painter.fill_rectangle({player_x + 12 * unit_x, PLAYER_Y, 2 * unit_x, 2 * unit_y}, green);
// Upper body
painter.draw_hline({player_x + 11, PLAYER_Y + 2}, 4, green);
painter.draw_hline({player_x + 11, PLAYER_Y + 3}, 4, green);
painter.draw_hline({player_x + 10, PLAYER_Y + 4}, 6, green);
painter.draw_hline({player_x + 10, PLAYER_Y + 5}, 6, green);
painter.fill_rectangle({player_x + 11 * unit_x, PLAYER_Y + 2 * unit_y, 4 * unit_x, 2 * unit_y}, green);
painter.fill_rectangle({player_x + 10 * unit_x, PLAYER_Y + 4 * unit_y, 6 * unit_x, 2 * unit_y}, green);
// Main body
painter.draw_hline({player_x + 2, PLAYER_Y + 6}, 22, green);
painter.draw_hline({player_x + 2, PLAYER_Y + 7}, 22, green);
painter.draw_hline({player_x + 1, PLAYER_Y + 8}, 24, green);
painter.draw_hline({player_x + 1, PLAYER_Y + 9}, 24, green);
painter.draw_hline({player_x, PLAYER_Y + 10}, 26, green);
painter.draw_hline({player_x, PLAYER_Y + 11}, 26, green);
painter.draw_hline({player_x, PLAYER_Y + 12}, 26, green);
painter.draw_hline({player_x, PLAYER_Y + 13}, 26, green);
painter.draw_hline({player_x, PLAYER_Y + 14}, 26, green);
painter.draw_hline({player_x, PLAYER_Y + 15}, 26, green);
painter.fill_rectangle({player_x + 2 * unit_x, PLAYER_Y + 6 * unit_y, 22 * unit_x, 10 * unit_y}, green);
painter.fill_rectangle({player_x + unit_x, PLAYER_Y + 8 * unit_y, 24 * unit_x, 8 * unit_y}, green);
painter.fill_rectangle({player_x, PLAYER_Y + 10 * unit_y, 26 * unit_x, 6 * unit_y}, green);
}
static void draw_invader(int row, int col) {
@@ -452,14 +465,14 @@ static void fire_enemy_bullet() {
static void update_enemy_bullets() {
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
if (enemy_bullet_active[i]) {
// Clear old position - but protect the border line
if (enemy_bullet_y[i] != 49) { // Don't clear if we're exactly on the border line
// Clear old position
if (enemy_bullet_y[i] != INFO_BAR_HEIGHT - 1) {
painter.fill_rectangle({enemy_bullet_x[i], enemy_bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::black());
}
enemy_bullet_y[i] += ENEMY_BULLET_SPEED;
if (enemy_bullet_y[i] > 320) {
if (enemy_bullet_y[i] > SCREEN_HEIGHT) {
enemy_bullet_active[i] = false;
} else {
// Draw at new position
@@ -498,7 +511,7 @@ static void update_invaders() {
// Clear old positions
clear_all_invaders();
// Check bounds - find the actual leftmost and rightmost invaders
// Check bounds
int leftmost = INVADER_COLS;
int rightmost = -1;
@@ -513,25 +526,25 @@ static void update_invaders() {
bool hit_edge = false;
if (invader_direction > 0) {
// Moving right - check rightmost invader
// Moving right - check rightmost invader with margin
int right_edge = invaders_x + rightmost * (INVADER_WIDTH + INVADER_GAP_X) + INVADER_WIDTH;
if (right_edge >= 240) {
if (right_edge >= SCREEN_WIDTH - 5) { // 5px margin
hit_edge = true;
}
} else {
// Moving left - check leftmost invader
// Moving left - check leftmost invader with margin
int left_edge = invaders_x + leftmost * (INVADER_WIDTH + INVADER_GAP_X);
if (left_edge <= 0) {
if (left_edge <= 5) { // 5px margin
hit_edge = true;
}
}
// Move invaders
if (hit_edge) {
invaders_y += 15; // Move down
invaders_y += INVADER_DROP_AMOUNT;
invader_direction = -invader_direction;
} else {
invaders_x += invader_direction * 8; // Horizontal movement
invaders_x += invader_direction * INVADER_MOVE_AMOUNT;
}
// Toggle animation frame
@@ -541,7 +554,7 @@ static void update_invaders() {
draw_all_invaders();
}
// Enemy firing logic - only in hard mode or always with lower frequency
// Enemy firing logic
if (!current_instance || !current_instance->easy_mode) {
if (++enemy_fire_counter >= 120) { // Fire every 2 seconds at 60fps
enemy_fire_counter = 0;
@@ -568,7 +581,7 @@ static void update_bullets() {
bullet_y[i] -= BULLET_SPEED;
if (bullet_y[i] < 50) {
if (bullet_y[i] < INFO_BAR_HEIGHT) {
bullet_active[i] = false;
} else {
painter.fill_rectangle({bullet_x[i], bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::white());
@@ -600,7 +613,7 @@ static void check_collisions() {
invaders[row][col] = false;
painter.fill_rectangle({inv_x, inv_y, INVADER_WIDTH, INVADER_HEIGHT}, Color::black());
// Score based on row: top=30, middle=20, bottom=10
// Score based on row
if (row == 0) {
score += 30;
} else if (row == 1 || row == 2) {
@@ -635,23 +648,20 @@ static void check_wave_complete() {
speed_bonus = (wave - 1) * 3; // Each wave is slightly faster
if (speed_bonus > 15) speed_bonus = 15; // Cap the speed increase
// Clear any enemy bullets before transitioning
// Clear any enemy bullets
clear_all_enemy_bullets();
// Set state to wave complete and start timer
// Set state to wave complete
game_state = 3;
wave_complete_timer = 60; // 1 second at 60fps
wave_complete_timer = 60; // 1 second
// Clear screen and show wave message - but preserve the border line
painter.fill_rectangle({0, 51, 240, 269}, Color::black()); // Start at 51 to preserve line at 49
// Redraw the border line to ensure it's intact - stupid bullet clearing wants to damage it
painter.draw_hline({0, 49}, 240, Color::white());
// Clear screen and show wave message
painter.fill_rectangle({0, INFO_BAR_HEIGHT + 1, SCREEN_WIDTH, SCREEN_HEIGHT - INFO_BAR_HEIGHT - 1}, Color::black());
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
auto style = *ui::Theme::getInstance()->fg_green;
std::string wave_text = "WAVE " + std::to_string(wave);
int wave_x = (240 - wave_text.length() * 8) / 2;
painter.draw_string({wave_x, 150}, style, wave_text);
painter.draw_string({UI_POS_X_CENTER(wave_text.length()), SCREEN_HEIGHT / 2}, style, wave_text);
return;
}
@@ -661,8 +671,8 @@ static void check_wave_complete() {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) {
int y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
if (y + INVADER_HEIGHT >= PLAYER_Y) { // Actual collision with player
game_state = 2; // Game over
if (y + INVADER_HEIGHT >= PLAYER_Y) {
game_state = 2; // Game over
save_high_score();
return;
}
@@ -683,10 +693,12 @@ void game_timer_check() {
blink_state = !blink_state;
auto style = *ui::Theme::getInstance()->fg_red;
int blink_y = 250; // Position below the high score
if (blink_state) {
painter.draw_string({prompt_x, 260}, style, "PRESS SELECT");
painter.draw_string({prompt_x, blink_y}, style, "PRESS SELECT");
} else {
painter.fill_rectangle({prompt_x, 260, 16 * 8, 20}, Color::black());
painter.fill_rectangle({prompt_x, blink_y, 12 * 8, 20}, Color::black());
}
}
} else if (game_state == 1) {
@@ -704,16 +716,14 @@ void game_timer_check() {
auto style = *ui::Theme::getInstance()->fg_green;
painter.fill_rectangle({5, 10, 100, 20}, Color::black());
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
// Redraw border line after score update (in case it was damaged) - stupid bullet clearing wants to damage it
painter.draw_hline({0, 49}, 240, Color::white());
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
}
// Periodically redraw the border line to fix any damage because it's a pain in the ass
// Periodically redraw the border line
static uint32_t border_redraw_counter = 0;
if (++border_redraw_counter >= 60) { // Once per second - might make less if causing flickering
if (++border_redraw_counter >= 60) {
border_redraw_counter = 0;
painter.draw_hline({0, 49}, 240, Color::white());
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
}
} else if (game_state == 2) {
// Game over state
@@ -726,30 +736,29 @@ void game_timer_check() {
blink_state = !blink_state;
auto style = *ui::Theme::getInstance()->fg_red;
int blink_y = 250; // Position below high score section
if (blink_state) {
painter.draw_string({prompt_x, 260}, style, "PRESS SELECT");
painter.draw_string({prompt_x, blink_y}, style, "PRESS SELECT");
} else {
painter.fill_rectangle({prompt_x, 260, 16 * 8, 20}, Color::black());
painter.fill_rectangle({prompt_x, blink_y, 12 * 8, 20}, Color::black());
}
}
} else if (game_state == 3) {
// Wave complete state - wait for timer
if (--wave_complete_timer <= 0) {
// Timer expired, start next wave
game_state = 1;
// Clear and redraw game area - preserve border line
painter.fill_rectangle({0, 51, 240, 269}, Color::black()); // Start at 51 to preserve line
// Clear and redraw game area
painter.fill_rectangle({0, INFO_BAR_HEIGHT + 1, SCREEN_WIDTH, SCREEN_HEIGHT - INFO_BAR_HEIGHT - 1}, Color::black());
// Restore UI
auto style = *ui::Theme::getInstance()->fg_green;
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
painter.draw_string({5, 30}, style, "Lives: " + std::to_string(lives));
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
// Redraw the border line in case it was damaged again
painter.draw_hline({0, 49}, 240, Color::white());
// Initialize new wave of invaders
// Initialize new wave
init_invaders();
draw_all_invaders();
draw_player();
@@ -760,7 +769,16 @@ void game_timer_check() {
SpaceInvadersView::SpaceInvadersView(NavigationView& nav)
: nav_{nav} {
add_children({&dummy, &button_difficulty});
// Initialize dimensions first
current_instance = this;
init_dimensions();
// Now reposition button with proper centering
int button_y = SCREEN_HEIGHT - 45; // 45px from bottom
int button_x = (SCREEN_WIDTH - 100) / 2; // Center horizontally
button_difficulty.set_parent_rect({button_x, button_y, 100, 20});
game_timer.attach(&game_timer_check, 1.0 / 60.0);
// Update button text based on loaded setting
@@ -769,12 +787,10 @@ SpaceInvadersView::SpaceInvadersView(NavigationView& nav)
button_difficulty.on_select = [this](Button&) {
easy_mode = !easy_mode;
button_difficulty.set_text(easy_mode ? "Mode: EASY" : "Mode: HARD");
// Settings will be saved when the view is destroyed
};
}
SpaceInvadersView::~SpaceInvadersView() {
// Settings are automatically saved when destroyed
current_instance = nullptr;
}
@@ -791,7 +807,7 @@ void SpaceInvadersView::paint(Painter& painter) {
game_over_initialized = false;
blink_state = true;
blink_counter = 0;
player_x = 107;
player_x = SCREEN_WIDTH / 2 - PLAYER_WIDTH / 2;
score = 0;
lives = 3;
wave = 1;
@@ -821,12 +837,13 @@ void SpaceInvadersView::frame_sync() {
bool SpaceInvadersView::on_encoder(const EncoderEvent delta) {
if (game_state == 1) {
int move_speed = SCREEN_WIDTH / 48; // Scale movement speed
if (delta > 0) {
player_x += 5;
if (player_x > 214) player_x = 214;
player_x += move_speed;
if (player_x > SCREEN_WIDTH - PLAYER_WIDTH) player_x = SCREEN_WIDTH - PLAYER_WIDTH;
draw_player();
} else if (delta < 0) {
player_x -= 5;
player_x -= move_speed;
if (player_x < 0) player_x = 0;
draw_player();
}
@@ -850,8 +867,8 @@ bool SpaceInvadersView::on_key(const KeyEvent key) {
enemy_fire_counter = 0;
init_invaders();
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
painter.draw_hline({0, 49}, 240, Color::white());
painter.fill_rectangle({0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}, Color::black());
painter.draw_hline({0, INFO_BAR_HEIGHT - 1}, SCREEN_WIDTH, Color::white());
auto style = *ui::Theme::getInstance()->fg_green;
painter.draw_string({5, 10}, style, "Score: 0");
@@ -876,4 +893,4 @@ bool SpaceInvadersView::on_key(const KeyEvent key) {
return true;
}
} // namespace ui::external_app::spaceinv
} // namespace ui::external_app::spaceinv
+2 -2
View File
@@ -54,7 +54,7 @@ class SpaceInvadersView : public View {
NavigationView& nav_;
Button button_difficulty{
{70, 285, 100, 20},
{70, 275, 100, 20},
"Mode: HARD"};
app_settings::SettingsManager settings_{
@@ -76,4 +76,4 @@ class SpaceInvadersView : public View {
} // namespace ui::external_app::spaceinv
#endif /* __UI_SPACEINV_H__ */
#endif /* __UI_SPACEINV_H__ */

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