From b15bb59678fbec6864f207166002a0f81ece9cc4 Mon Sep 17 00:00:00 2001 From: Totoo Date: Mon, 1 Sep 2025 11:50:46 +0200 Subject: [PATCH] Standalone app api v3 (#2772) Added file io, and updated some ui elements. Also added Digital Rain standalone app for an example. --- firmware/application/apps/ble_rx_app.cpp | 1 + .../application/apps/ui_standalone_view.cpp | 123 +- .../application/apps/ui_standalone_view.hpp | 2 + firmware/application/theme.cpp | 6 + firmware/application/theme.hpp | 2 +- firmware/application/ui/ui_geomap.cpp | 6 +- .../ui_external_items_menu_loader.cpp | 2 +- firmware/common/standalone_app.hpp | 50 +- firmware/standalone/CMakeLists.txt | 3 +- .../standalone/digitalrain/CMakeLists.txt | 234 ++ .../standalone/digitalrain/digitalrain.cpp | 153 + .../standalone/digitalrain/digitalrain.hpp | 221 ++ firmware/standalone/digitalrain/external.ld | 118 + firmware/standalone/digitalrain/ff.h | 312 ++ firmware/standalone/digitalrain/ffconf.h | 242 ++ firmware/standalone/digitalrain/fileext.hpp | 42 + firmware/standalone/digitalrain/integer.h | 38 + firmware/standalone/digitalrain/main.cpp | 178 + firmware/standalone/digitalrain/ui/bmp.hpp | 53 + .../standalone/digitalrain/ui/bmpfile.cpp | 306 ++ .../standalone/digitalrain/ui/bmpfile.hpp | 71 + .../digitalrain/ui/circular_buffer.hpp | 120 + .../standalone/digitalrain/ui/complex.hpp | 114 + firmware/standalone/digitalrain/ui/file.cpp | 641 ++++ firmware/standalone/digitalrain/ui/file.hpp | 368 ++ .../standalone/digitalrain/ui/file_path.cpp | 55 + .../standalone/digitalrain/ui/file_path.hpp | 59 + .../standalone/digitalrain/ui/file_reader.cpp | 48 + .../standalone/digitalrain/ui/file_reader.hpp | 151 + .../digitalrain/ui/file_wrapper.hpp | 543 +++ .../standalone/digitalrain/ui/mathdef.hpp | 4 + .../standalone/digitalrain/ui/optional.hpp | 68 + firmware/standalone/digitalrain/ui/result.hpp | 84 + .../standalone/digitalrain/ui/sine_table.hpp | 147 + .../digitalrain/ui/standalone_application.hpp | 1 + .../digitalrain/ui/string_format.cpp | 569 +++ .../digitalrain/ui/string_format.hpp | 106 + firmware/standalone/digitalrain/ui/theme.cpp | 870 +++++ firmware/standalone/digitalrain/ui/theme.hpp | 122 + firmware/standalone/digitalrain/ui/ui.cpp | 132 + firmware/standalone/digitalrain/ui/ui.hpp | 410 ++ .../standalone/digitalrain/ui/ui_focus.cpp | 211 ++ .../standalone/digitalrain/ui/ui_focus.hpp | 45 + .../digitalrain/ui/ui_font_fixed_5x8.cpp | 45 + .../digitalrain/ui/ui_font_fixed_5x8.hpp | 36 + .../digitalrain/ui/ui_font_fixed_8x16.cpp | 45 + .../digitalrain/ui/ui_font_fixed_8x16.hpp | 36 + .../standalone/digitalrain/ui/ui_geomap.cpp | 985 +++++ .../standalone/digitalrain/ui/ui_geomap.hpp | 378 ++ .../standalone/digitalrain/ui/ui_painter.cpp | 177 + .../standalone/digitalrain/ui/ui_painter.hpp | 113 + .../standalone/digitalrain/ui/ui_text.cpp | 74 + .../standalone/digitalrain/ui/ui_text.hpp | 110 + .../standalone/digitalrain/ui/ui_widget.cpp | 3310 +++++++++++++++++ .../standalone/digitalrain/ui/ui_widget.hpp | 1046 ++++++ .../standalone/digitalrain/ui/utility.cpp | 244 ++ .../standalone/digitalrain/ui/utility.hpp | 222 ++ firmware/standalone/pacman/ff.h | 312 ++ firmware/standalone/pacman/ffconf.h | 242 ++ firmware/standalone/pacman/fileext.hpp | 42 + firmware/standalone/pacman/integer.h | 38 + 61 files changed, 14474 insertions(+), 12 deletions(-) create mode 100644 firmware/standalone/digitalrain/CMakeLists.txt create mode 100644 firmware/standalone/digitalrain/digitalrain.cpp create mode 100644 firmware/standalone/digitalrain/digitalrain.hpp create mode 100644 firmware/standalone/digitalrain/external.ld create mode 100644 firmware/standalone/digitalrain/ff.h create mode 100644 firmware/standalone/digitalrain/ffconf.h create mode 100644 firmware/standalone/digitalrain/fileext.hpp create mode 100644 firmware/standalone/digitalrain/integer.h create mode 100644 firmware/standalone/digitalrain/main.cpp create mode 100644 firmware/standalone/digitalrain/ui/bmp.hpp create mode 100644 firmware/standalone/digitalrain/ui/bmpfile.cpp create mode 100644 firmware/standalone/digitalrain/ui/bmpfile.hpp create mode 100644 firmware/standalone/digitalrain/ui/circular_buffer.hpp create mode 100644 firmware/standalone/digitalrain/ui/complex.hpp create mode 100644 firmware/standalone/digitalrain/ui/file.cpp create mode 100644 firmware/standalone/digitalrain/ui/file.hpp create mode 100644 firmware/standalone/digitalrain/ui/file_path.cpp create mode 100644 firmware/standalone/digitalrain/ui/file_path.hpp create mode 100644 firmware/standalone/digitalrain/ui/file_reader.cpp create mode 100644 firmware/standalone/digitalrain/ui/file_reader.hpp create mode 100644 firmware/standalone/digitalrain/ui/file_wrapper.hpp create mode 100644 firmware/standalone/digitalrain/ui/mathdef.hpp create mode 100644 firmware/standalone/digitalrain/ui/optional.hpp create mode 100644 firmware/standalone/digitalrain/ui/result.hpp create mode 100644 firmware/standalone/digitalrain/ui/sine_table.hpp create mode 100644 firmware/standalone/digitalrain/ui/standalone_application.hpp create mode 100644 firmware/standalone/digitalrain/ui/string_format.cpp create mode 100644 firmware/standalone/digitalrain/ui/string_format.hpp create mode 100644 firmware/standalone/digitalrain/ui/theme.cpp create mode 100644 firmware/standalone/digitalrain/ui/theme.hpp create mode 100644 firmware/standalone/digitalrain/ui/ui.cpp create mode 100644 firmware/standalone/digitalrain/ui/ui.hpp create mode 100644 firmware/standalone/digitalrain/ui/ui_focus.cpp create mode 100644 firmware/standalone/digitalrain/ui/ui_focus.hpp create mode 100644 firmware/standalone/digitalrain/ui/ui_font_fixed_5x8.cpp create mode 100644 firmware/standalone/digitalrain/ui/ui_font_fixed_5x8.hpp create mode 100644 firmware/standalone/digitalrain/ui/ui_font_fixed_8x16.cpp create mode 100644 firmware/standalone/digitalrain/ui/ui_font_fixed_8x16.hpp create mode 100644 firmware/standalone/digitalrain/ui/ui_geomap.cpp create mode 100644 firmware/standalone/digitalrain/ui/ui_geomap.hpp create mode 100644 firmware/standalone/digitalrain/ui/ui_painter.cpp create mode 100644 firmware/standalone/digitalrain/ui/ui_painter.hpp create mode 100644 firmware/standalone/digitalrain/ui/ui_text.cpp create mode 100644 firmware/standalone/digitalrain/ui/ui_text.hpp create mode 100644 firmware/standalone/digitalrain/ui/ui_widget.cpp create mode 100644 firmware/standalone/digitalrain/ui/ui_widget.hpp create mode 100644 firmware/standalone/digitalrain/ui/utility.cpp create mode 100644 firmware/standalone/digitalrain/ui/utility.hpp create mode 100644 firmware/standalone/pacman/ff.h create mode 100644 firmware/standalone/pacman/ffconf.h create mode 100644 firmware/standalone/pacman/fileext.hpp create mode 100644 firmware/standalone/pacman/integer.h diff --git a/firmware/application/apps/ble_rx_app.cpp b/firmware/application/apps/ble_rx_app.cpp index 34297972c..6fd4e6363 100644 --- a/firmware/application/apps/ble_rx_app.cpp +++ b/firmware/application/apps/ble_rx_app.cpp @@ -640,6 +640,7 @@ BLERxView::BLERxView(NavigationView& nav) }; options_filter.on_change = [this](size_t index, int32_t v) { + (void)v; filter_index = (uint8_t)index; recent.clear(); recent_entries_view.set_dirty(); diff --git a/firmware/application/apps/ui_standalone_view.cpp b/firmware/application/apps/ui_standalone_view.cpp index 5fc1e229b..ed67317cb 100644 --- a/firmware/application/apps/ui_standalone_view.cpp +++ b/firmware/application/apps/ui_standalone_view.cpp @@ -27,6 +27,9 @@ #include "ui_font_fixed_5x8.hpp" #include "ui_font_fixed_8x16.hpp" +#include "portapack.hpp" + +#include "file.hpp" namespace ui { @@ -93,8 +96,90 @@ bool i2c_read(uint8_t* cmd, size_t cmd_len, uint8_t* data, size_t data_len) { return dev->i2c_read(cmd, cmd_len, data, data_len); } +// v3 +// Version 3 +FRESULT ext_f_open(FIL* fp, const TCHAR* path, BYTE mode) { + return f_open(fp, path, mode); +} +FRESULT ext_f_close(FIL* fp) { + return f_close(fp); +} +FRESULT ext_f_read(FIL* fp, void* buff, UINT btr, UINT* br) { + return f_read(fp, buff, btr, br); +} +FRESULT ext_f_write(FIL* fp, const void* buff, UINT btw, UINT* bw) { + return f_write(fp, buff, btw, bw); +} +FRESULT ext_f_lseek(FIL* fp, FSIZE_t ofs) { + return f_lseek(fp, ofs); +} +FRESULT ext_f_truncate(FIL* fp) { + return f_truncate(fp); +} +FRESULT ext_f_sync(FIL* fp) { + return f_sync(fp); +} +FRESULT ext_f_opendir(DIR* dp, const TCHAR* path) { + return f_opendir(dp, path); +} +FRESULT ext_f_closedir(DIR* dp) { + return f_closedir(dp); +} +FRESULT ext_f_readdir(DIR* dp, FILINFO* fno) { + return f_readdir(dp, fno); +} +FRESULT ext_f_findfirst(DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern) { + return f_findfirst(dp, fno, path, pattern); +} +FRESULT ext_f_findnext(DIR* dp, FILINFO* fno) { + return f_findnext(dp, fno); +} +FRESULT ext_f_mkdir(const TCHAR* path) { + return f_mkdir(path); +} +FRESULT ext_f_unlink(const TCHAR* path) { + return f_unlink(path); +} +FRESULT ext_f_rename(const TCHAR* path_old, const TCHAR* path_new) { + return f_rename(path_old, path_new); +} +FRESULT ext_f_stat(const TCHAR* path, FILINFO* fno) { + return f_stat(path, fno); +} +FRESULT ext_f_utime(const TCHAR* path, const FILINFO* fno) { + return f_utime(path, fno); +} +FRESULT ext_f_getfree(const TCHAR* path, DWORD* nclst, FATFS** fatfs) { + return f_getfree(path, nclst, fatfs); +} +FRESULT ext_f_mount(FATFS* fs, const TCHAR* path, BYTE opt) { + return f_mount(fs, path, opt); +} +int ext_f_putc(TCHAR c, FIL* fp) { + return f_putc(c, fp); +} +int ext_f_puts(const TCHAR* str, FIL* cp) { + return f_puts(str, cp); +} +int ext_f_printf(FIL* fp, const TCHAR* str, ...) { + return f_printf(fp, str); +} +TCHAR* ext_f_gets(TCHAR* buff, int len, FIL* fp) { + return f_gets(buff, len, fp); +} +void ext_draw_pixels(const ui::Rect r, const ui::Color* const colors, const size_t count) { + portapack::display.draw_pixels(r, colors, count); +} +void ext_draw_pixel(const ui::Point p, const ui::Color color) { + portapack::display.draw_pixel(p, color); +} + StandaloneView* standaloneView = nullptr; +void exit_app() { + if (standaloneView) standaloneView->exit(); +} + void set_dirty() { if (standaloneView != nullptr) standaloneView->set_dirty(); @@ -121,6 +206,33 @@ standalone_application_api_t api = { /* .i2c_read = */ &i2c_read, /* .panic = */ &chDbgPanic, /* .set_dirty = */ &set_dirty, + // Version 3 + .f_open = &ext_f_open, + .f_close = &ext_f_close, + .f_read = &ext_f_read, + .f_write = &ext_f_write, + .f_lseek = &ext_f_lseek, + .f_truncate = &ext_f_truncate, + .f_sync = &ext_f_sync, + .f_opendir = &ext_f_opendir, + .f_closedir = &ext_f_closedir, + .f_readdir = &ext_f_readdir, + .f_findfirst = &ext_f_findfirst, + .f_findnext = &ext_f_findnext, + .f_mkdir = &ext_f_mkdir, + .f_unlink = &ext_f_unlink, + .f_rename = &ext_f_rename, + .f_stat = &ext_f_stat, + .f_utime = &ext_f_utime, + .f_getfree = &ext_f_getfree, + .f_mount = &ext_f_mount, + .f_putc = &ext_f_putc, + .f_puts = &ext_f_puts, + .f_printf = &ext_f_printf, + .f_gets = &ext_f_gets, + .draw_pixels = &ext_draw_pixels, + .draw_pixel = &ext_draw_pixel, + .exit_app = &exit_app, }; StandaloneView::StandaloneView(NavigationView& nav, uint8_t* app_image) @@ -158,22 +270,21 @@ bool StandaloneView::on_key(const KeyEvent key) { if (get_application_information()->header_version > 1) { return get_application_information()->OnKeyEvent((uint8_t)key); } - - return false; + return true; } bool StandaloneView::on_encoder(const EncoderEvent event) { if (get_application_information()->header_version > 1) { return get_application_information()->OnEncoder((int32_t)event); } - return false; + return true; } 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 true; + return false; } bool StandaloneView::on_keyboard(const KeyboardEvent event) { @@ -202,4 +313,8 @@ void StandaloneView::on_before_detach() { context().focus_manager().clearMirror(); } +void StandaloneView::exit() { + nav_.pop(); +} + } // namespace ui diff --git a/firmware/application/apps/ui_standalone_view.hpp b/firmware/application/apps/ui_standalone_view.hpp index f5a8a78b3..76599591d 100644 --- a/firmware/application/apps/ui_standalone_view.hpp +++ b/firmware/application/apps/ui_standalone_view.hpp @@ -51,6 +51,8 @@ class StandaloneView : public View { void frame_sync(); + void exit(); + private: bool initialized = false; NavigationView& nav_; diff --git a/firmware/application/theme.cpp b/firmware/application/theme.cpp index 22b42f887..43319251b 100644 --- a/firmware/application/theme.cpp +++ b/firmware/application/theme.cpp @@ -4,6 +4,12 @@ namespace ui { ThemeTemplate* Theme::current = nullptr; +void Theme::destroy() { + if (current != nullptr) + delete current; + current = nullptr; +} + ThemeTemplate* Theme::getInstance() { if (current == nullptr) SetTheme(DefaultGrey); return Theme::current; diff --git a/firmware/application/theme.hpp b/firmware/application/theme.hpp index b4ecf0973..a03b7d887 100644 --- a/firmware/application/theme.hpp +++ b/firmware/application/theme.hpp @@ -113,7 +113,7 @@ class Theme { static void SetTheme(ThemeId theme); static ThemeTemplate* current; - + static void destroy(); // used from standalone app, to prevent memleak private: }; diff --git a/firmware/application/ui/ui_geomap.cpp b/firmware/application/ui/ui_geomap.cpp index 8e3ca5cd7..b5de12ce5 100644 --- a/firmware/application/ui/ui_geomap.cpp +++ b/firmware/application/ui/ui_geomap.cpp @@ -256,7 +256,7 @@ void GeoMap::map_read_line_bin(ui::Color* buffer, uint16_t pixels) { for (int i = 0; i < geomap_rect_width; i++) { buffer[i] = zoom_out_buffer[i * (-map_zoom)]; } - delete zoom_out_buffer; + delete[] zoom_out_buffer; } } @@ -660,7 +660,7 @@ bool GeoMap::init() { map_height = 32768; } - map_visible = map_opened; + map_visible = map_opened || has_osm; map_center_x = map_width >> 1; map_center_y = map_height >> 1; @@ -670,7 +670,7 @@ bool GeoMap::init() { map_bottom = sin(-85.05 * pi / 180); // Map bitmap only goes from about -85 to 85 lat map_world_lon = map_width / (2 * pi); map_offset = (map_world_lon / 2 * log((1 + map_bottom) / (1 - map_bottom))); - return map_opened || has_osm; + return map_opened; } void GeoMap::set_mode(GeoMapMode mode) { diff --git a/firmware/application/ui_external_items_menu_loader.cpp b/firmware/application/ui_external_items_menu_loader.cpp index 6ec1202bc..f1f5390a1 100644 --- a/firmware/application/ui_external_items_menu_loader.cpp +++ b/firmware/application/ui_external_items_menu_loader.cpp @@ -347,7 +347,7 @@ namespace ui { return false; // TODO: move this to m4 memory space - auto app_image = reinterpret_cast(portapack::memory::map::m4_code.end() - app.size()); + auto app_image = reinterpret_cast(portapack::memory::map::local_sram_0.base()); // read file in 512 byte chunks for (size_t file_read_index = 0; file_read_index < app.size(); file_read_index += std::filesystem::max_file_block_size) { diff --git a/firmware/common/standalone_app.hpp b/firmware/common/standalone_app.hpp index 5de9e19d3..d11c575b8 100644 --- a/firmware/common/standalone_app.hpp +++ b/firmware/common/standalone_app.hpp @@ -27,8 +27,9 @@ #include #include "ui.hpp" +#include "file.hpp" -#define CURRENT_STANDALONE_APPLICATION_API_VERSION 2 +#define CURRENT_STANDALONE_APPLICATION_API_VERSION 3 struct standalone_application_api_t { // Version 1 @@ -58,7 +59,52 @@ struct standalone_application_api_t { void (*panic)(const char* msg); void (*set_dirty)(); - // TODO: add filesystem access functions + // Version 3 + FRESULT(*f_open) + (FIL* fp, const TCHAR* path, BYTE mode); + FRESULT(*f_close) + (FIL* fp); + FRESULT(*f_read) + (FIL* fp, void* buff, UINT btr, UINT* br); + FRESULT(*f_write) + (FIL* fp, const void* buff, UINT btw, UINT* bw); + FRESULT(*f_lseek) + (FIL* fp, FSIZE_t ofs); + FRESULT(*f_truncate) + (FIL* fp); + FRESULT(*f_sync) + (FIL* fp); + FRESULT(*f_opendir) + (DIR* dp, const TCHAR* path); + FRESULT(*f_closedir) + (DIR* dp); + FRESULT(*f_readdir) + (DIR* dp, FILINFO* fno); + FRESULT(*f_findfirst) + (DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern); + FRESULT(*f_findnext) + (DIR* dp, FILINFO* fno); + FRESULT(*f_mkdir) + (const TCHAR* path); + FRESULT(*f_unlink) + (const TCHAR* path); + FRESULT(*f_rename) + (const TCHAR* path_old, const TCHAR* path_new); + FRESULT(*f_stat) + (const TCHAR* path, FILINFO* fno); + FRESULT(*f_utime) + (const TCHAR* path, const FILINFO* fno); + FRESULT(*f_getfree) + (const TCHAR* path, DWORD* nclst, FATFS** fatfs); + FRESULT(*f_mount) + (FATFS* fs, const TCHAR* path, BYTE opt); + int (*f_putc)(TCHAR c, FIL* fp); + int (*f_puts)(const TCHAR* str, FIL* cp); + int (*f_printf)(FIL* fp, const TCHAR* str, ...); + TCHAR* (*f_gets)(TCHAR* buff, int len, FIL* fp); + void (*draw_pixels)(const ui::Rect r, const ui::Color* const colors, const size_t count); + void (*draw_pixel)(const ui::Point p, const ui::Color color); + void (*exit_app)(); // TODO: add baseband access functions // HOW TO extend this interface: diff --git a/firmware/standalone/CMakeLists.txt b/firmware/standalone/CMakeLists.txt index e6e603d5c..ccbeac88f 100644 --- a/firmware/standalone/CMakeLists.txt +++ b/firmware/standalone/CMakeLists.txt @@ -3,9 +3,10 @@ cmake_minimum_required(VERSION 3.16) project(standalone_apps) add_subdirectory(pacman) +add_subdirectory(digitalrain) add_custom_target( standalone_apps - DEPENDS pacman_app + DEPENDS pacman_app digitalrain_app ) diff --git a/firmware/standalone/digitalrain/CMakeLists.txt b/firmware/standalone/digitalrain/CMakeLists.txt new file mode 100644 index 000000000..b332cd3de --- /dev/null +++ b/firmware/standalone/digitalrain/CMakeLists.txt @@ -0,0 +1,234 @@ +# +# 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. +# + +############################################################################## +# Build global options +# NOTE: Can be overridden externally. +# + +enable_language(C CXX ASM) + +include(CheckCXXCompilerFlag) + +project(digitalrain_app) + +# Compiler options here. +set(USE_OPT "-Os -g --specs=nano.specs --specs=nosys.specs") + +# C specific options here (added to USE_OPT). +set(USE_COPT "-std=gnu99") + +# C++ specific options here (added to USE_OPT). +check_cxx_compiler_flag("-std=c++20" cpp20_supported) +if(cpp20_supported) + set(USE_CPPOPT "-std=c++20") +else() + set(USE_CPPOPT "-std=c++17") +endif() +set(USE_CPPOPT "${USE_CPPOPT} -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized -fno-use-cxa-atexit") + +# Enable this if you want the linker to remove unused code and data +set(USE_LINK_GC yes) + +# Linker extra options here. +set(USE_LDOPT) + +# Enable this if you want link time optimizations (LTO) - this flag affects chibios only +set(USE_LTO no) + +# If enabled, this option allows to compile the application in THUMB mode. +set(USE_THUMB yes) + +# Enable this if you want to see the full log while compiling. +set(USE_VERBOSE_COMPILE no) + +# +# Build global options +############################################################################## + +############################################################################## +# Architecture or project specific options +# + +# Enables the use of FPU on Cortex-M4 (no, softfp, hard). +set(USE_FPU no) + +# +# Architecture or project specific options +############################################################################## + +############################################################################## +# Project, sources and paths +# + +# Define linker script file here +set(LDSCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/external.ld) + + +# C sources that can be compiled in ARM or THUMB mode depending on the global +# setting. +FILE(GLOB_RECURSE Sources_C ${CMAKE_CURRENT_LIST_DIR}/*.c) +set(CSRC + ${Sources_C} +) + +# C++ sources that can be compiled in ARM or THUMB mode depending on the global +# setting. +FILE(GLOB_RECURSE Sources_CPP ${CMAKE_CURRENT_LIST_DIR}/*.cpp) +set(CPPSRC + ${Sources_CPP} +) + +# C sources to be compiled in ARM mode regardless of the global setting. +# NOTE: Mixing ARM and THUMB mode enables the -mthumb-interwork compiler +# option that results in lower performance and larger code size. +set(ACSRC) + +# C++ sources to be compiled in ARM mode regardless of the global setting. +# NOTE: Mixing ARM and THUMB mode enables the -mthumb-interwork compiler +# option that results in lower performance and larger code size. +set(ACPPSRC) + +# C sources to be compiled in THUMB mode regardless of the global setting. +# NOTE: Mixing ARM and THUMB mode enables the -mthumb-interwork compiler +# option that results in lower performance and larger code size. +set(TCSRC) + +# C sources to be compiled in THUMB mode regardless of the global setting. +# NOTE: Mixing ARM and THUMB mode enables the -mthumb-interwork compiler +# option that results in lower performance and larger code size. +set(TCPPSRC) + +# List ASM source files here +set(ASMSRC) + +set(INCDIR + ${CMAKE_CURRENT_SOURCE_DIR} + ${COMMON} + ${COMMON}/../application + ${COMMON}/../application/hw +) + +# +# Project, sources and paths +############################################################################## + +############################################################################## +# Compiler settings +# + +# TODO: Entertain using MCU=cortex-m0.small-multiply for LPC43xx M0 core. +# However, on GCC-ARM-Embedded 4.9 2015q2, it seems to produce non-functional +# binaries. +set(MCU cortex-m0) + +# ARM-specific options here +set(AOPT) + +# THUMB-specific options here +set(TOPT "-mthumb -DTHUMB") + +# Define C warning options here +set(CWARN "-Wall -Wextra -Wstrict-prototypes") + +# Define C++ warning options here +set(CPPWARN "-Wall -Wextra -Wno-psabi") + +# +# Compiler settings +############################################################################## + +############################################################################## +# Start of default section +# + +# List all default C defines here, like -D_DEBUG=1 +# TODO: Switch -DCRT0_INIT_DATA depending on load from RAM or SPIFI? +# NOTE: _RANDOM_TCC to kill a GCC 4.9.3 error with std::max argument types +set(DDEFS "-DLPC43XX -DLPC43XX_M0 -D__NEWLIB__ -DHACKRF_ONE -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0") + +# List all default ASM defines here, like -D_DEBUG=1 +set(DADEFS) + +# List all default directories to look for include files here +set(DINCDIR) + +# List the default directory to look for the libraries here +set(DLIBDIR) + +# List all default libraries here +set(DLIBS) + +# +# End of default section +############################################################################## + +############################################################################## +# Start of user section +# + +# List all user C define here, like -D_DEBUG=1 +set(UDEFS) + +# Define ASM defines here +set(UADEFS) + +# List all user directories here +set(UINCDIR) + +# List the user directory to look for the libraries here +set(ULIBDIR) + +# List all user libraries here +set(ULIBS) + +# +# End of user defines +############################################################################## + +set(RULESPATH ${CHIBIOS}/os/ports/GCC/ARMCMx) +include(${RULESPATH}/rules.cmake) + +############################################################################## + + +add_executable(${PROJECT_NAME}.elf ${CSRC} ${CPPSRC} ${ASMSRC}) +set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT}) +add_definitions(${DEFS}) +include_directories(. ${INCDIR}) +link_directories(${LLIBDIR}) +target_link_libraries(${PROJECT_NAME}.elf -Wl,-Map=${PROJECT_NAME}.map) + +# redirect std lib memory allocations +target_link_libraries(${PROJECT_NAME}.elf "-Wl,-wrap,_malloc_r") +target_link_libraries(${PROJECT_NAME}.elf "-Wl,-wrap,_free_r") +target_link_libraries(${PROJECT_NAME}.elf "-Wl,--print-memory-usage") + +add_custom_command( + OUTPUT ${PROJECT_NAME}.ppmp + COMMAND ${CMAKE_OBJCOPY} -v -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.ppmp + DEPENDS ${PROJECT_NAME}.elf +) + +add_custom_target( + ${PROJECT_NAME} + DEPENDS ${PROJECT_NAME}.ppmp +) diff --git a/firmware/standalone/digitalrain/digitalrain.cpp b/firmware/standalone/digitalrain/digitalrain.cpp new file mode 100644 index 000000000..fc91e9850 --- /dev/null +++ b/firmware/standalone/digitalrain/digitalrain.cpp @@ -0,0 +1,153 @@ +/* + * 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 "digitalrain.hpp" + +#include +#include + +StandaloneViewMirror* standaloneViewMirror = nullptr; +ui::Context* context = nullptr; + +void initialize(const standalone_application_api_t& api) { + _api = &api; + + context = new ui::Context(); + standaloneViewMirror = new StandaloneViewMirror(*context, {0, 16, 240, 304}); +} + +// event 1 == frame sync. called each 1/60th of second, so 6 = 100ms + +void on_event(const uint32_t& events) { + if ((events & 1) == 1) standaloneViewMirror->need_refresh(); +} + +void shutdown() { + delete standaloneViewMirror; + delete context; +} + +void PaintViewMirror() { + ui::Painter painter; + if (standaloneViewMirror) + painter.paint_widget_tree(standaloneViewMirror); +} + +ui::Widget* touch_widget(ui::Widget* const w, ui::TouchEvent event) { + if (!w->hidden()) { + // To achieve reverse depth ordering (last object drawn is + // considered "top"), descend first. + for (const auto child : w->children()) { + const auto touched_widget = touch_widget(child, event); + if (touched_widget) { + return touched_widget; + } + } + + const auto r = w->screen_rect(); + if (r.contains(event.point)) { + if (w->on_touch(event)) { + // This widget responded. Return it up the call stack. + return w; + } + } + } + return nullptr; +} + +ui::Widget* captured_widget{nullptr}; + +void OnTouchEvent(int, int, uint32_t) { + if (standaloneViewMirror) { + _api->exit_app(); + /* + ui::TouchEvent event{{x, y}, static_cast(type)}; + + if (event.type == ui::TouchEvent::Type::Start) { + captured_widget = touch_widget(standaloneViewMirror, event); + + if (captured_widget) { + captured_widget->focus(); + captured_widget->set_dirty(); + } + } + + if (captured_widget) + captured_widget->on_touch(event); + */ + } +} + +void OnFocus() { + if (standaloneViewMirror) + standaloneViewMirror->focus(); +} + +bool OnKeyEvent(uint8_t) { + // ui::KeyEvent key = (ui::KeyEvent)key_val; + if (context) { + _api->exit_app(); + /* auto focus_widget = context->focus_manager().focus_widget(); + + if (focus_widget) { + if (focus_widget->on_key(key)) + return true; + + context->focus_manager().update(standaloneViewMirror, key); + + if (focus_widget != context->focus_manager().focus_widget()) + return true; + else { + if (key == ui::KeyEvent::Up || key == ui::KeyEvent::Back || key == ui::KeyEvent::Left) { + focus_widget->blur(); + return false; + } + } + }*/ + } + return false; +} + +bool OnEncoder(int32_t) { + if (context) { + _api->exit_app(); + /* + auto focus_widget = context->focus_manager().focus_widget(); + + if (focus_widget) return focus_widget->on_encoder((ui::EncoderEvent)delta); + */ + } + + return false; +} + +bool OnKeyboad(uint8_t) { + if (context) { + _api->exit_app(); + /* + auto focus_widget = context->focus_manager().focus_widget(); + + if (focus_widget) + return focus_widget->on_keyboard((ui::KeyboardEvent)key); + */ + } + return false; +} diff --git a/firmware/standalone/digitalrain/digitalrain.hpp b/firmware/standalone/digitalrain/digitalrain.hpp new file mode 100644 index 000000000..7c5a45bd5 --- /dev/null +++ b/firmware/standalone/digitalrain/digitalrain.hpp @@ -0,0 +1,221 @@ +/* + * 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. + */ + +#pragma once + +#include "standalone_app.hpp" + +#include "ui/ui_widget.hpp" +#include "ui/theme.hpp" +#include "ui/string_format.hpp" +#include +#include "ui/ui_font_fixed_5x8.hpp" +#include "ui/ui_painter.hpp" +#include +#include +#include // for std::rand() and std::srand() +#include // for std::time() + +void initialize(const standalone_application_api_t& api); +void on_event(const uint32_t& events); +void shutdown(); +void OnFocus(); +bool OnKeyEvent(uint8_t); +bool OnEncoder(int32_t); +void OnTouchEvent(int, int, uint32_t); +bool OnKeyboad(uint8_t); +void PaintViewMirror(); + +extern const standalone_application_api_t* _api; + +class DigitalRain { + private: + ui::Painter painter{}; + static const int WIDTH = 240; + static const int HEIGHT = 325; + static const int MARGIN_TOP = 20; + static const int CHAR_WIDTH = 5; + static const int CHAR_HEIGHT = 8; + static const int COLS = WIDTH / CHAR_WIDTH; + static const int ROWS = (HEIGHT - MARGIN_TOP) / CHAR_HEIGHT; + static const int MAX_DROPS = 36; + + const ui::Font& font = ui::font::fixed_5x8(); + + struct Drop { + uint8_t x; + int16_t y; + uint8_t length; + uint8_t speed; + uint8_t morph_counter[16]; + char chars[16]; + int16_t old_y; // Track previous position for clearing + bool active; + }; + + Drop drops[MAX_DROPS]; + const char char_set[16] = { + '@', '#', '$', '0', '1', '2', '>', '<', + '/', '\\', '[', ']', '{', '}', '.', ' '}; + + inline int random(int min, int max) { + return min + (std::rand() % (max - min + 1)); + } + + void init_drop(uint8_t index, bool force_top = false) { + drops[index].x = random(0, COLS - 1); + drops[index].y = force_top ? -random(0, 5) : -5; + drops[index].old_y = drops[index].y; // Initialize old position + drops[index].length = random(5, 15); + drops[index].speed = random(1, 3); + drops[index].active = true; + + for (uint8_t i = 0; i < 16; i++) { + drops[index].chars[i] = char_set[random(0, 15)]; + drops[index].morph_counter[i] = random(2, 6); + } + } + + void clear_drop_trail(const Drop& drop) { + // Convert to int16_t for consistent type comparison + int16_t start_y = std::max(0, drop.old_y - drop.length + 1); + int16_t end_y = std::min(ROWS - 1, drop.old_y); + + if (start_y <= end_y) { + int16_t pixel_y = start_y * CHAR_HEIGHT + MARGIN_TOP; + uint16_t height = (end_y - start_y + 1) * CHAR_HEIGHT; + + painter.fill_rectangle_unrolled8( + {static_cast(drop.x * CHAR_WIDTH), + pixel_y, + CHAR_WIDTH, + height}, + ui::Color::black()); + } + } + + void morph_characters(Drop& drop) { + for (uint8_t i = 0; i < drop.length; i++) { + if (--drop.morph_counter[i] == 0) { + drop.chars[i] = char_set[random(0, 15)]; + drop.morph_counter[i] = random(2, 6); + } + } + } + + public: + DigitalRain() { + std::srand(0); + + for (uint8_t i = 0; i < MAX_DROPS; ++i) { + init_drop(i, true); + } + } + + void update() { + for (uint8_t i = 0; i < MAX_DROPS; ++i) { + if (!drops[i].active) continue; + + // Store old position before updating + drops[i].old_y = drops[i].y; + + // Update position + drops[i].y += drops[i].speed; + morph_characters(drops[i]); + + // Reset drop if off screen + if (drops[i].y - drops[i].length > ROWS) { + clear_drop_trail(drops[i]); // Clear final position + init_drop(i); + } + } + } + + void render() { + for (uint8_t i = 0; i < MAX_DROPS; ++i) { + if (!drops[i].active) continue; + + // Clear previous position + clear_drop_trail(drops[i]); + + // Draw new position + for (uint8_t j = 0; j < drops[i].length; ++j) { + int y = drops[i].y - j; + if (y >= 0 && y < ROWS) { + ui::Point p{ + static_cast(drops[i].x * CHAR_WIDTH), + static_cast(y * CHAR_HEIGHT + MARGIN_TOP)}; + + ui::Color fg; + if (j == 0) { + fg = ui::Color::white(); + } else if (j < 3) { + fg = ui::Color(0, 255, 0); + } else { + uint8_t intensity = std::max(40, 180 - (j * 15)); + fg = ui::Color(0, intensity, 0); + } + + std::string ch(1, drops[i].chars[j]); + painter.draw_string( + p, + font, + fg, + ui::Color::black(), + ch); + } + } + } + } +}; + +class StandaloneViewMirror : public ui::View { + public: + StandaloneViewMirror(ui::Context& context, const ui::Rect parent_rect) + : View{parent_rect}, context_(context) { + set_style(ui::Theme::getInstance()->bg_darkest); + } + ~StandaloneViewMirror() { + ui::Theme::destroy(); + } + ui::Context& context() const override { + return context_; + } + + void focus() override { + } + + bool need_refresh() { + update++; + if (update % 2 == 0) { + return false; + } + digitalRain.update(); + digitalRain.render(); + return true; + } + + private: + ui::Context& context_; + ui::Console console{{0, 0, 240, 320}}; + DigitalRain digitalRain{}; + uint8_t update = 0; +}; diff --git a/firmware/standalone/digitalrain/external.ld b/firmware/standalone/digitalrain/external.ld new file mode 100644 index 000000000..4048f9383 --- /dev/null +++ b/firmware/standalone/digitalrain/external.ld @@ -0,0 +1,118 @@ +/* + Copyright (C) 2024 Bernd Herzog + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +MEMORY +{ + ram : org = 0xADB10000, len = 64k /* DO NOT CHANGE the address. We make the image relocateable on load. It needs to be 0xADB10000 */ +} + +__ram_start__ = ORIGIN(ram); +__ram_size__ = LENGTH(ram); +__ram_end__ = __ram_start__ + __ram_size__; + +SECTIONS +{ + . = 0; + _text = .; + startup : ALIGN(16) SUBALIGN(16) + { + KEEP(*(.standalone_application_information)); + } > ram + + constructors : ALIGN(4) SUBALIGN(4) + { + PROVIDE(__init_array_start = .); + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array)) + PROVIDE(__init_array_end = .); + } > ram + + destructors : ALIGN(4) SUBALIGN(4) + { + PROVIDE(__fini_array_start = .); + KEEP(*(.fini_array)) + KEEP(*(SORT(.fini_array.*))) + PROVIDE(__fini_array_end = .); + } > ram + + .text : ALIGN(16) SUBALIGN(16) + { + *(.text.startup.*) + *(.text) + *(.text.*) + *(.rodata) + *(.rodata.*) + *(.glue_7t) + *(.glue_7) + *(.gcc*) + } > ram + + .ARM.extab : + { + *(.ARM.extab* .gnu.linkonce.armextab.*) + } > ram + + .ARM.exidx : { + PROVIDE(__exidx_start = .); + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + PROVIDE(__exidx_end = .); + } > ram + + .eh_frame_hdr : + { + *(.eh_frame_hdr) + } > ram + + .eh_frame : ONLY_IF_RO + { + *(.eh_frame) + } > ram + + .textalign : ONLY_IF_RO + { + . = ALIGN(8); + } > ram + + .bss ALIGN(4) : ALIGN(4) + { + . = ALIGN(4); + PROVIDE(_bss_start = .); + *(.bss) + *(.bss.*) + *(COMMON) + . = ALIGN(4); + PROVIDE(_bss_end = .); + } > ram + + . = ALIGN(4); + _etext = .; + _textdata = _etext; + + .data ALIGN(4) : AT (_textdata) + { + . = ALIGN(4); + PROVIDE(_data = .); + *(.data) + *(.data.*) + *(.ramtext) + . = ALIGN(4); + PROVIDE(_edata = .); + } > ram +} + +PROVIDE(end = .); +_end = .; + diff --git a/firmware/standalone/digitalrain/ff.h b/firmware/standalone/digitalrain/ff.h new file mode 100644 index 000000000..f322e7888 --- /dev/null +++ b/firmware/standalone/digitalrain/ff.h @@ -0,0 +1,312 @@ +/*----------------------------------------------------------------------------/ +/ FatFs - Generic FAT file system module R0.12c / +/-----------------------------------------------------------------------------/ +/ +/ Copyright (C) 2017, ChaN, all right reserved. +/ +/ FatFs module is an open source software. Redistribution and use of FatFs in +/ source and binary forms, with or without modification, are permitted provided +/ that the following condition is met: + +/ 1. Redistributions of source code must retain the above copyright notice, +/ this condition and the following disclaimer. +/ +/ This software is provided by the copyright holder and contributors "AS IS" +/ and any warranties related to this software are DISCLAIMED. +/ The copyright owner or contributors be NOT LIABLE for any damages caused +/ by use of this software. +/----------------------------------------------------------------------------*/ + +#ifndef _FATFS +#define _FATFS 68300 /* Revision ID */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "integer.h" /* Basic integer types */ +#include "ffconf.h" /* FatFs configuration options */ + +#if _FATFS != _FFCONF +#error Wrong configuration file (ffconf.h). +#endif + +/* Definitions of volume management */ + +#if _MULTI_PARTITION /* Multiple partition configuration */ +typedef struct { + BYTE pd; /* Physical drive number */ + BYTE pt; /* Partition: 0:Auto detect, 1-4:Forced partition) */ +} PARTITION; +extern PARTITION VolToPart[]; /* Volume - Partition resolution table */ +#endif + +/* Type of path name strings on FatFs API */ + +#if _LFN_UNICODE /* Unicode (UTF-16) string */ +#if _USE_LFN == 0 +#error _LFN_UNICODE must be 0 at non-LFN cfg. +#endif +#ifndef _INC_TCHAR +typedef WCHAR TCHAR; +#define _T(x) L##x +#define _TEXT(x) L##x +#endif +#else /* ANSI/OEM string */ +#ifndef _INC_TCHAR +typedef char TCHAR; +#define _T(x) x +#define _TEXT(x) x +#endif +#endif + +/* Type of file size variables */ + +#if _FS_EXFAT +#if _USE_LFN == 0 +#error LFN must be enabled when enable exFAT +#endif +typedef QWORD FSIZE_t; +#else +typedef DWORD FSIZE_t; +#endif + +/* File system object structure (FATFS) */ + +typedef struct { + BYTE fs_type; /* File system type (0:N/A) */ + BYTE drv; /* Physical drive number */ + BYTE n_fats; /* Number of FATs (1 or 2) */ + BYTE wflag; /* win[] flag (b0:dirty) */ + BYTE fsi_flag; /* FSINFO flags (b7:disabled, b0:dirty) */ + WORD id; /* File system mount ID */ + WORD n_rootdir; /* Number of root directory entries (FAT12/16) */ + WORD csize; /* Cluster size [sectors] */ +#if _MAX_SS != _MIN_SS + WORD ssize; /* Sector size (512, 1024, 2048 or 4096) */ +#endif +#if _USE_LFN != 0 + WCHAR* lfnbuf; /* LFN working buffer */ +#endif +#if _FS_EXFAT + BYTE* dirbuf; /* Directory entry block scratchpad buffer */ +#endif +#if _FS_REENTRANT + _SYNC_t sobj; /* Identifier of sync object */ +#endif +#if !_FS_READONLY + DWORD last_clst; /* Last allocated cluster */ + DWORD free_clst; /* Number of free clusters */ +#endif +#if _FS_RPATH != 0 + DWORD cdir; /* Current directory start cluster (0:root) */ +#if _FS_EXFAT + DWORD cdc_scl; /* Containing directory start cluster (invalid when cdir is 0) */ + DWORD cdc_size; /* b31-b8:Size of containing directory, b7-b0: Chain status */ + DWORD cdc_ofs; /* Offset in the containing directory (invalid when cdir is 0) */ +#endif +#endif + DWORD n_fatent; /* Number of FAT entries (number of clusters + 2) */ + DWORD fsize; /* Size of an FAT [sectors] */ + DWORD volbase; /* Volume base sector */ + DWORD fatbase; /* FAT base sector */ + DWORD dirbase; /* Root directory base sector/cluster */ + DWORD database; /* Data base sector */ + DWORD winsect; /* Current sector appearing in the win[] */ + BYTE win[_MAX_SS]; /* Disk access window for Directory, FAT (and file data at tiny cfg) */ +} FATFS; + +/* Object ID and allocation information (_FDID) */ + +typedef struct { + FATFS* fs; /* Pointer to the owner file system object */ + WORD id; /* Owner file system mount ID */ + BYTE attr; /* Object attribute */ + BYTE stat; /* Object chain status (b1-0: =0:not contiguous, =2:contiguous (no data on FAT), =3:flagmented in this session, b2:sub-directory stretched) */ + DWORD sclust; /* Object start cluster (0:no cluster or root directory) */ + FSIZE_t objsize; /* Object size (valid when sclust != 0) */ +#if _FS_EXFAT + DWORD n_cont; /* Size of first fragment, clusters - 1 (valid when stat == 3) */ + DWORD n_frag; /* Size of last fragment needs to be written (valid when not zero) */ + DWORD c_scl; /* Containing directory start cluster (valid when sclust != 0) */ + DWORD c_size; /* b31-b8:Size of containing directory, b7-b0: Chain status (valid when c_scl != 0) */ + DWORD c_ofs; /* Offset in the containing directory (valid when sclust != 0 and non-directory object) */ +#endif +#if _FS_LOCK != 0 + UINT lockid; /* File lock ID origin from 1 (index of file semaphore table Files[]) */ +#endif +} _FDID; + +/* File object structure (FIL) */ + +typedef struct { + _FDID obj; /* Object identifier (must be the 1st member to detect invalid object pointer) */ + BYTE flag; /* File status flags */ + BYTE err; /* Abort flag (error code) */ + FSIZE_t fptr; /* File read/write pointer (Zeroed on file open) */ + DWORD clust; /* Current cluster of fpter (invalid when fptr is 0) */ + DWORD sect; /* Sector number appearing in buf[] (0:invalid) */ +#if !_FS_READONLY + DWORD dir_sect; /* Sector number containing the directory entry */ + BYTE* dir_ptr; /* Pointer to the directory entry in the win[] */ +#endif +#if _USE_FASTSEEK + DWORD* cltbl; /* Pointer to the cluster link map table (nulled on open, set by application) */ +#endif +#if !_FS_TINY + BYTE buf[_MAX_SS]; /* File private data read/write window */ +#endif +} FIL; + +/* Directory object structure (DIR) */ + +typedef struct { + _FDID obj; /* Object identifier */ + DWORD dptr; /* Current read/write offset */ + DWORD clust; /* Current cluster */ + DWORD sect; /* Current sector (0:Read operation has terminated) */ + BYTE* dir; /* Pointer to the directory item in the win[] */ + BYTE fn[12]; /* SFN (in/out) {body[8],ext[3],status[1]} */ +#if _USE_LFN != 0 + DWORD blk_ofs; /* Offset of current entry block being processed (0xFFFFFFFF:Invalid) */ +#endif +#if _USE_FIND + const TCHAR* pat; /* Pointer to the name matching pattern */ +#endif +} DIR; + +/* File information structure (FILINFO) */ + +typedef struct { + FSIZE_t fsize; /* File size */ + WORD fdate; /* Modified date */ + WORD ftime; /* Modified time */ + BYTE fattrib; /* File attribute */ +#if _USE_LFN != 0 + TCHAR altname[13]; /* Altenative file name */ + TCHAR fname[_MAX_LFN + 1]; /* Primary file name */ +#else + TCHAR fname[13]; /* File name */ +#endif +} FILINFO; + +/* File function return code (FRESULT) */ + +typedef enum { + FR_OK = 0, /* (0) Succeeded */ + FR_DISK_ERR, /* (1) A hard error occurred in the low level disk I/O layer */ + FR_INT_ERR, /* (2) Assertion failed */ + FR_NOT_READY, /* (3) The physical drive cannot work */ + FR_NO_FILE, /* (4) Could not find the file */ + FR_NO_PATH, /* (5) Could not find the path */ + FR_INVALID_NAME, /* (6) The path name format is invalid */ + FR_DENIED, /* (7) Access denied due to prohibited access or directory full */ + FR_EXIST, /* (8) Access denied due to prohibited access */ + FR_INVALID_OBJECT, /* (9) The file/directory object is invalid */ + FR_WRITE_PROTECTED, /* (10) The physical drive is write protected */ + FR_INVALID_DRIVE, /* (11) The logical drive number is invalid */ + FR_NOT_ENABLED, /* (12) The volume has no work area */ + FR_NO_FILESYSTEM, /* (13) There is no valid FAT volume */ + FR_MKFS_ABORTED, /* (14) The f_mkfs() aborted due to any problem */ + FR_TIMEOUT, /* (15) Could not get a grant to access the volume within defined period */ + FR_LOCKED, /* (16) The operation is rejected according to the file sharing policy */ + FR_NOT_ENOUGH_CORE, /* (17) LFN working buffer could not be allocated */ + FR_TOO_MANY_OPEN_FILES, /* (18) Number of open files > _FS_LOCK */ + FR_INVALID_PARAMETER /* (19) Given parameter is invalid */ +} FRESULT; + +/*--------------------------------------------------------------*/ +/* FatFs module application interface */ + +/* +//MOVED TO API + +FRESULT f_open(FIL* fp, const TCHAR* path, BYTE mode); / Open or create a file * +FRESULT f_close(FIL* fp); / Close an open file object * +FRESULT f_read(FIL* fp, void* buff, UINT btr, UINT* br); / Read data from the file * +FRESULT f_write(FIL* fp, const void* buff, UINT btw, UINT* bw); / Write data to the file * +FRESULT f_lseek(FIL* fp, FSIZE_t ofs); / Move file pointer of the file object * +FRESULT f_truncate(FIL* fp); / Truncate the file * +FRESULT f_sync(FIL* fp); / Flush cached data of the writing file * +FRESULT f_opendir(DIR* dp, const TCHAR* path); / Open a directory * +FRESULT f_closedir(DIR* dp); / Close an open directory * +FRESULT f_readdir(DIR* dp, FILINFO* fno); / Read a directory item * +FRESULT f_findfirst(DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern); / Find first file * +FRESULT f_findnext(DIR* dp, FILINFO* fno); / Find next file * +FRESULT f_mkdir(const TCHAR* path); / Create a sub diectory * +FRESULT f_unlink(const TCHAR* path); / Delete an existing fileor directory * +FRESULT f_rename(const TCHAR* path_old, const TCHAR* path_new); / Rename/Move a file or directory * +FRESULT f_stat(const TCHAR* path, FILINFO* fno); / Get file status * +FRESULT f_chmod(const TCHAR* path, BYTE attr, BYTE mask); / Change attribute of a file/dir * +FRESULT f_utime(const TCHAR* path, const FILINFO* fno); / Change timestamp of a file/dir * +FRESULT f_chdir(const TCHAR* path); / Change current directory * +FRESULT f_chdrive(const TCHAR* path); / Change current drive * +FRESULT f_getcwd(TCHAR* buff, UINT len); / Get current directory * +FRESULT f_getfree(const TCHAR* path, DWORD* nclst, FATFS** fatfs); / Get number of free clusters on the drive * +FRESULT f_getlabel(const TCHAR* path, TCHAR* label, DWORD* vsn); / Get volume label * +FRESULT f_setlabel(const TCHAR* label); / Set volume label * +FRESULT f_forward(FIL* fp, UINT (*func)(const BYTE*, UINT), UINT btf, UINT* bf); / Forward data to the stream * +FRESULT f_expand(FIL* fp, FSIZE_t szf, BYTE opt); / Allocate a contiguous block to the file * +FRESULT f_mount(FATFS* fs, const TCHAR* path, BYTE opt); / Mount/Unmount a logical drive * +FRESULT f_mkfs(const TCHAR* path, BYTE opt, DWORD au, void* work, UINT len); / Create a FAT volume * +FRESULT f_fdisk(BYTE pdrv, const DWORD* szt, void* work); / Divide a physical drive into some partitions * +int f_putc(TCHAR c, FIL* fp); / Put a character to the file * +int f_puts(const TCHAR* str, FIL* cp); / Put a string to the file * +int f_printf(FIL* fp, const TCHAR* str, ...); / Put a formatted string to the file * +TCHAR* f_gets(TCHAR* buff, int len, FIL* fp); / Get a string from the file * + +*/ +#define f_eof(fp) ((int)((fp)->fptr == (fp)->obj.objsize)) +#define f_error(fp) ((fp)->err) +#define f_tell(fp) ((fp)->fptr) +#define f_size(fp) ((fp)->obj.objsize) +#define f_rewind(fp) f_lseek((fp), 0) +#define f_rewinddir(dp) f_readdir((dp), 0) +#define f_rmdir(path) f_unlink(path) + +#ifndef EOF +#define EOF (-1) +#endif + +/*--------------------------------------------------------------*/ +/* Flags and offset address */ + +/* File access mode and open method flags (3rd argument of f_open) */ +#define FA_READ 0x01 +#define FA_WRITE 0x02 +#define FA_OPEN_EXISTING 0x00 +#define FA_CREATE_NEW 0x04 +#define FA_CREATE_ALWAYS 0x08 +#define FA_OPEN_ALWAYS 0x10 +#define FA_OPEN_APPEND 0x30 + +/* Fast seek controls (2nd argument of f_lseek) */ +#define CREATE_LINKMAP ((FSIZE_t)0 - 1) + +/* Format options (2nd argument of f_mkfs) */ +#define FM_FAT 0x01 +#define FM_FAT32 0x02 +#define FM_EXFAT 0x04 +#define FM_ANY 0x07 +#define FM_SFD 0x08 + +/* Filesystem type (FATFS.fs_type) */ +#define FS_FAT12 1 +#define FS_FAT16 2 +#define FS_FAT32 3 +#define FS_EXFAT 4 + +/* File attribute bits for directory entry (FILINFO.fattrib) */ +#define AM_RDO 0x01 /* Read only */ +#define AM_HID 0x02 /* Hidden */ +#define AM_SYS 0x04 /* System */ +#define AM_DIR 0x10 /* Directory */ +#define AM_ARC 0x20 /* Archive */ + +#ifdef __cplusplus +} +#endif + +#include "fileext.hpp" + +#endif /* _FATFS */ diff --git a/firmware/standalone/digitalrain/ffconf.h b/firmware/standalone/digitalrain/ffconf.h new file mode 100644 index 000000000..68f3035e7 --- /dev/null +++ b/firmware/standalone/digitalrain/ffconf.h @@ -0,0 +1,242 @@ +/* CHIBIOS FIX */ + +/*---------------------------------------------------------------------------/ +/ FatFs - FAT file system module configuration file +/---------------------------------------------------------------------------*/ + +#define _FFCONF 68300 /* Revision ID */ + +/*---------------------------------------------------------------------------/ +/ Function Configurations +/---------------------------------------------------------------------------*/ + +#define _FS_READONLY 0 +/* This option switches read-only configuration. (0:Read/Write or 1:Read-only) +/ Read-only configuration removes writing API functions, f_write(), f_sync(), +/ f_unlink(), f_mkdir(), f_chmod(), f_rename(), f_truncate(), f_getfree() +/ and optional writing functions as well. */ + +#define _FS_MINIMIZE 0 +/* This option defines minimization level to remove some basic API functions. +/ +/ 0: All basic functions are enabled. +/ 1: f_stat(), f_getfree(), f_unlink(), f_mkdir(), f_truncate() and f_rename() +/ are removed. +/ 2: f_opendir(), f_readdir() and f_closedir() are removed in addition to 1. +/ 3: f_lseek() function is removed in addition to 2. */ + +#define _USE_STRFUNC 1 +/* This option switches string functions, f_gets(), f_putc(), f_puts() and +/ f_printf(). +/ +/ 0: Disable string functions. +/ 1: Enable without LF-CRLF conversion. +/ 2: Enable with LF-CRLF conversion. */ + +#define _USE_FIND 1 +/* This option switches filtered directory read functions, f_findfirst() and +/ f_findnext(). (0:Disable, 1:Enable 2:Enable with matching altname[] too) */ + +#define _USE_MKFS 0 +/* This option switches f_mkfs() function. (0:Disable or 1:Enable) */ + +#define _USE_FASTSEEK 1 +/* This option switches fast seek function. (0:Disable or 1:Enable) */ + +#define _USE_EXPAND 0 +/* This option switches f_expand function. (0:Disable or 1:Enable) */ + +#define _USE_CHMOD 1 +/* This option switches attribute manipulation functions, f_chmod() and f_utime(). +/ (0:Disable or 1:Enable) Also _FS_READONLY needs to be 0 to enable this option. */ + +#define _USE_LABEL 0 +/* This option switches volume label functions, f_getlabel() and f_setlabel(). +/ (0:Disable or 1:Enable) */ + +#define _USE_FORWARD 0 +/* This option switches f_forward() function. (0:Disable or 1:Enable) */ + +/*---------------------------------------------------------------------------/ +/ Locale and Namespace Configurations +/---------------------------------------------------------------------------*/ + +#define _CODE_PAGE 437 +/* This option specifies the OEM code page to be used on the target system. +/ Incorrect setting of the code page can cause a file open failure. +/ +/ 1 - ASCII (No support of extended character. Non-LFN cfg. only) +/ 437 - U.S. +/ 720 - Arabic +/ 737 - Greek +/ 771 - KBL +/ 775 - Baltic +/ 850 - Latin 1 +/ 852 - Latin 2 +/ 855 - Cyrillic +/ 857 - Turkish +/ 860 - Portuguese +/ 861 - Icelandic +/ 862 - Hebrew +/ 863 - Canadian French +/ 864 - Arabic +/ 865 - Nordic +/ 866 - Russian +/ 869 - Greek 2 +/ 932 - Japanese (DBCS) +/ 936 - Simplified Chinese (DBCS) +/ 949 - Korean (DBCS) +/ 950 - Traditional Chinese (DBCS) +*/ + +#define _USE_LFN 3 +#define _MAX_LFN 255 +/* The _USE_LFN switches the support of long file name (LFN). +/ +/ 0: Disable support of LFN. _MAX_LFN has no effect. +/ 1: Enable LFN with static working buffer on the BSS. Always NOT thread-safe. +/ 2: Enable LFN with dynamic working buffer on the STACK. +/ 3: Enable LFN with dynamic working buffer on the HEAP. +/ +/ To enable the LFN, Unicode handling functions (option/unicode.c) must be added +/ to the project. The working buffer occupies (_MAX_LFN + 1) * 2 bytes and +/ additional 608 bytes at exFAT enabled. _MAX_LFN can be in range from 12 to 255. +/ It should be set 255 to support full featured LFN operations. +/ When use stack for the working buffer, take care on stack overflow. When use heap +/ memory for the working buffer, memory management functions, ff_memalloc() and +/ ff_memfree(), must be added to the project. */ + +#define _LFN_UNICODE 1 +/* This option switches character encoding on the API. (0:ANSI/OEM or 1:UTF-16) +/ To use Unicode string for the path name, enable LFN and set _LFN_UNICODE = 1. +/ This option also affects behavior of string I/O functions. */ + +#define _STRF_ENCODE 3 +/* When _LFN_UNICODE == 1, this option selects the character encoding ON THE FILE to +/ be read/written via string I/O functions, f_gets(), f_putc(), f_puts and f_printf(). +/ +/ 0: ANSI/OEM +/ 1: UTF-16LE +/ 2: UTF-16BE +/ 3: UTF-8 +/ +/ This option has no effect when _LFN_UNICODE == 0. */ + +#define _FS_RPATH 0 +/* This option configures support of relative path. +/ +/ 0: Disable relative path and remove related functions. +/ 1: Enable relative path. f_chdir() and f_chdrive() are available. +/ 2: f_getcwd() function is available in addition to 1. +*/ + +/*---------------------------------------------------------------------------/ +/ Drive/Volume Configurations +/---------------------------------------------------------------------------*/ + +#define _VOLUMES 1 +/* Number of volumes (logical drives) to be used. (1-10) */ + +#define _STR_VOLUME_ID 0 +#define _VOLUME_STRS "RAM", "NAND", "CF", "SD", "SD2", "USB", "USB2", "USB3" +/* _STR_VOLUME_ID switches string support of volume ID. +/ When _STR_VOLUME_ID is set to 1, also pre-defined strings can be used as drive +/ number in the path name. _VOLUME_STRS defines the drive ID strings for each +/ logical drives. Number of items must be equal to _VOLUMES. Valid characters for +/ the drive ID strings are: A-Z and 0-9. */ + +#define _MULTI_PARTITION 0 +/* This option switches support of multi-partition on a physical drive. +/ By default (0), each logical drive number is bound to the same physical drive +/ number and only an FAT volume found on the physical drive will be mounted. +/ When multi-partition is enabled (1), each logical drive number can be bound to +/ arbitrary physical drive and partition listed in the VolToPart[]. Also f_fdisk() +/ funciton will be available. */ + +#define _MIN_SS 512 +#define _MAX_SS 512 +/* These options configure the range of sector size to be supported. (512, 1024, +/ 2048 or 4096) Always set both 512 for most systems, generic memory card and +/ harddisk. But a larger value may be required for on-board flash memory and some +/ type of optical media. When _MAX_SS is larger than _MIN_SS, FatFs is configured +/ to variable sector size and GET_SECTOR_SIZE command needs to be implemented to +/ the disk_ioctl() function. */ + +#define _USE_TRIM 0 +/* This option switches support of ATA-TRIM. (0:Disable or 1:Enable) +/ To enable Trim function, also CTRL_TRIM command should be implemented to the +/ disk_ioctl() function. */ + +#define _FS_NOFSINFO 0 +/* If you need to know correct free space on the FAT32 volume, set bit 0 of this +/ option, and f_getfree() function at first time after volume mount will force +/ a full FAT scan. Bit 1 controls the use of last allocated cluster number. +/ +/ bit0=0: Use free cluster count in the FSINFO if available. +/ bit0=1: Do not trust free cluster count in the FSINFO. +/ bit1=0: Use last allocated cluster number in the FSINFO if available. +/ bit1=1: Do not trust last allocated cluster number in the FSINFO. +*/ + +/*---------------------------------------------------------------------------/ +/ System Configurations +/---------------------------------------------------------------------------*/ + +#define _FS_TINY 0 +/* This option switches tiny buffer configuration. (0:Normal or 1:Tiny) +/ At the tiny configuration, size of file object (FIL) is shrinked _MAX_SS bytes. +/ Instead of private sector buffer eliminated from the file object, common sector +/ buffer in the file system object (FATFS) is used for the file data transfer. */ + +#define _FS_EXFAT 1 +/* This option switches support of exFAT file system. (0:Disable or 1:Enable) +/ When enable exFAT, also LFN needs to be enabled. (_USE_LFN >= 1) +/ Note that enabling exFAT discards ANSI C (C89) compatibility. */ + +#define _FS_NORTC 0 +#define _NORTC_MON 1 +#define _NORTC_MDAY 1 +#define _NORTC_YEAR 2016 +/* The option _FS_NORTC switches timestamp functiton. If the system does not have +/ any RTC function or valid timestamp is not needed, set _FS_NORTC = 1 to disable +/ the timestamp function. All objects modified by FatFs will have a fixed timestamp +/ defined by _NORTC_MON, _NORTC_MDAY and _NORTC_YEAR in local time. +/ To enable timestamp function (_FS_NORTC = 0), get_fattime() function need to be +/ added to the project to get current time form real-time clock. _NORTC_MON, +/ _NORTC_MDAY and _NORTC_YEAR have no effect. +/ These options have no effect at read-only configuration (_FS_READONLY = 1). */ + +#define _FS_LOCK 0 +/* The option _FS_LOCK switches file lock function to control duplicated file open +/ and illegal operation to open objects. This option must be 0 when _FS_READONLY +/ is 1. +/ +/ 0: Disable file lock function. To avoid volume corruption, application program +/ should avoid illegal open, remove and rename to the open objects. +/ >0: Enable file lock function. The value defines how many files/sub-directories +/ can be opened simultaneously under file lock control. Note that the file +/ lock control is independent of re-entrancy. */ + +#define _FS_REENTRANT 1 +#define _FS_TIMEOUT 1000 +#define _SYNC_t void* +/* The option _FS_REENTRANT switches the re-entrancy (thread safe) of the FatFs +/ module itself. Note that regardless of this option, file access to different +/ volume is always re-entrant and volume control functions, f_mount(), f_mkfs() +/ and f_fdisk() function, are always not re-entrant. Only file/directory access +/ to the same volume is under control of this function. +/ +/ 0: Disable re-entrancy. _FS_TIMEOUT and _SYNC_t have no effect. +/ 1: Enable re-entrancy. Also user provided synchronization handlers, +/ ff_req_grant(), ff_rel_grant(), ff_del_syncobj() and ff_cre_syncobj() +/ function, must be added to the project. Samples are available in +/ option/syscall.c. +/ +/ The _FS_TIMEOUT defines timeout period in unit of time tick. +/ The _SYNC_t defines O/S dependent sync object type. e.g. HANDLE, ID, OS_EVENT*, +/ SemaphoreHandle_t and etc. A header file for O/S definitions needs to be +/ included somewhere in the scope of ff.h. */ + +/* #include // O/S definitions */ + +/*--- End of configuration options ---*/ diff --git a/firmware/standalone/digitalrain/fileext.hpp b/firmware/standalone/digitalrain/fileext.hpp new file mode 100644 index 000000000..23f884039 --- /dev/null +++ b/firmware/standalone/digitalrain/fileext.hpp @@ -0,0 +1,42 @@ + +extern "C" FRESULT f_open(FIL* fp, const TCHAR* path, BYTE mode); +extern "C" FRESULT f_close(FIL* fp); +extern "C" FRESULT f_read(FIL* fp, void* buff, UINT btr, UINT* br); +extern "C" FRESULT f_write(FIL* fp, const void* buff, UINT btw, UINT* bw); +extern "C" FRESULT f_lseek(FIL* fp, FSIZE_t ofs); +extern "C" FRESULT f_truncate(FIL* fp); + +extern "C" FRESULT f_sync(FIL* fp); +extern "C" FRESULT f_opendir(DIR* dp, const TCHAR* path); +extern "C" FRESULT f_closedir(DIR* dp); +extern "C" FRESULT f_readdir(DIR* dp, FILINFO* fno); + +extern "C" FRESULT f_findfirst(DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern); +extern "C" FRESULT f_findnext(DIR* dp, FILINFO* fno); + +extern "C" FRESULT f_mkdir(const TCHAR* path); +extern "C" FRESULT f_unlink(const TCHAR* path); +extern "C" FRESULT f_rename(const TCHAR* path_old, const TCHAR* path_new); +extern "C" FRESULT f_stat(const TCHAR* path, FILINFO* fno); + +extern "C" FRESULT f_chmod(const TCHAR* path, BYTE attr, BYTE mask); +extern "C" FRESULT f_utime(const TCHAR* path, const FILINFO* fno); +extern "C" FRESULT f_chdir(const TCHAR* path); +extern "C" FRESULT f_chdrive(const TCHAR* path); + +extern "C" FRESULT f_getcwd(TCHAR* buff, UINT len); +extern "C" FRESULT f_getfree(const TCHAR* path, DWORD* nclst, FATFS** fatfs); +extern "C" FRESULT f_getlabel(const TCHAR* path, TCHAR* label, DWORD* vsn); +extern "C" FRESULT f_setlabel(const TCHAR* label); +extern "C" FRESULT f_forward(FIL* fp, UINT (*func)(const BYTE*, UINT), UINT btf, UINT* bf); +extern "C" FRESULT f_expand(FIL* fp, FSIZE_t szf, BYTE opt); + +extern "C" FRESULT f_mount(FATFS* fs, const TCHAR* path, BYTE opt); +extern "C" FRESULT f_mkfs(const TCHAR* path, BYTE opt, DWORD au, void* work, UINT len); + +extern "C" FRESULT f_fdisk(BYTE pdrv, const DWORD* szt, void* work); +extern "C" int f_putc(TCHAR c, FIL* fp); + +extern "C" int f_puts(const TCHAR* str, FIL* cp); +extern "C" int f_printf(FIL* fp, const TCHAR* str, ...); +extern "C" TCHAR* f_gets(TCHAR* buff, int len, FIL* fp); diff --git a/firmware/standalone/digitalrain/integer.h b/firmware/standalone/digitalrain/integer.h new file mode 100644 index 000000000..d8e911cf1 --- /dev/null +++ b/firmware/standalone/digitalrain/integer.h @@ -0,0 +1,38 @@ +/*-------------------------------------------*/ +/* Integer type definitions for FatFs module */ +/*-------------------------------------------*/ + +#ifndef _FF_INTEGER +#define _FF_INTEGER + +#ifdef _WIN32 /* FatFs development platform */ + +#include +#include +typedef unsigned __int64 QWORD; + + +#else /* Embedded platform */ + +/* These types MUST be 16-bit or 32-bit */ +typedef int INT; +typedef unsigned int UINT; + +/* This type MUST be 8-bit */ +typedef unsigned char BYTE; + +/* These types MUST be 16-bit */ +typedef short SHORT; +typedef unsigned short WORD; +typedef unsigned short WCHAR; + +/* These types MUST be 32-bit */ +typedef long LONG; +typedef unsigned long DWORD; + +/* This type MUST be 64-bit (Remove this for ANSI C (C89) compatibility) */ +typedef unsigned long long QWORD; + +#endif + +#endif diff --git a/firmware/standalone/digitalrain/main.cpp b/firmware/standalone/digitalrain/main.cpp new file mode 100644 index 000000000..78772fc31 --- /dev/null +++ b/firmware/standalone/digitalrain/main.cpp @@ -0,0 +1,178 @@ +/* + * 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 "standalone_app.hpp" +#include "digitalrain.hpp" +#include + +const standalone_application_api_t* _api; + +extern "C" { +__attribute__((section(".standalone_application_information"), used)) standalone_application_information_t _standalone_application_information = { + /*.header_version = */ 2, + + /*.app_name = */ "DigitalRain", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFC, + 0x3F, + 0xFE, + 0x7F, + 0x02, + 0x40, + 0xBA, + 0x45, + 0x02, + 0x40, + 0xFE, + 0x7F, + 0xFE, + 0x7F, + 0x92, + 0x7C, + 0x92, + 0x7C, + 0xFC, + 0x3F, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + }, + /*.icon_color = 16 bit: 5R 6G 5B*/ 0x0000FFE0, + /*.menu_location = */ app_location_t::GAMES, + + /*.initialize_app = */ initialize, + /*.on_event = */ on_event, + /*.shutdown = */ shutdown, + /*PaintViewMirror */ PaintViewMirror, + /*OnTouchEvent */ OnTouchEvent, + /*OnFocus */ OnFocus, + /*OnKeyEvent */ OnKeyEvent, + /*OnEncoder */ OnEncoder, + /*OnKeyboard */ OnKeyboad}; +} + +/* Implementing abort() eliminates requirement for _getpid(), _kill(), _exit(). */ +extern "C" void abort() { + while (true); +} + +// replace memory allocations to use heap from chibios +extern "C" void* malloc(size_t size) { + return _api->malloc(size); +} +extern "C" void* calloc(size_t num, size_t size) { + return _api->calloc(num, size); +} +extern "C" void* realloc(void* p, size_t size) { + return _api->realloc(p, size); +} +extern "C" void free(void* p) { + _api->free(p); +} + +// redirect std lib memory allocations (sprintf, etc.) +extern "C" void* __wrap__malloc_r(size_t size) { + return _api->malloc(size); +} +extern "C" void __wrap__free_r(void* p) { + _api->free(p); +} + +// redirect file I/O + +extern "C" FRESULT f_open(FIL* fp, const TCHAR* path, BYTE mode) { + return _api->f_open(fp, path, mode); +} +extern "C" FRESULT f_close(FIL* fp) { + return _api->f_close(fp); +} +extern "C" FRESULT f_read(FIL* fp, void* buff, UINT btr, UINT* br) { + return _api->f_read(fp, buff, btr, br); +} +extern "C" FRESULT f_write(FIL* fp, const void* buff, UINT btw, UINT* bw) { + return _api->f_write(fp, buff, btw, bw); +} +extern "C" FRESULT f_lseek(FIL* fp, FSIZE_t ofs) { + return _api->f_lseek(fp, ofs); +} +extern "C" FRESULT f_truncate(FIL* fp) { + return _api->f_truncate(fp); +} +extern "C" FRESULT f_sync(FIL* fp) { + return _api->f_sync(fp); +} +extern "C" FRESULT f_opendir(DIR* dp, const TCHAR* path) { + return _api->f_opendir(dp, path); +} +extern "C" FRESULT f_closedir(DIR* dp) { + return _api->f_closedir(dp); +} +extern "C" FRESULT f_readdir(DIR* dp, FILINFO* fno) { + return _api->f_readdir(dp, fno); +} +extern "C" FRESULT f_findfirst(DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern) { + return _api->f_findfirst(dp, fno, path, pattern); +} +extern "C" FRESULT f_findnext(DIR* dp, FILINFO* fno) { + return _api->f_findnext(dp, fno); +} +extern "C" FRESULT f_mkdir(const TCHAR* path) { + return _api->f_mkdir(path); +} +extern "C" FRESULT f_unlink(const TCHAR* path) { + return _api->f_unlink(path); +} +extern "C" FRESULT f_rename(const TCHAR* path_old, const TCHAR* path_new) { + return _api->f_rename(path_old, path_new); +} +extern "C" FRESULT f_stat(const TCHAR* path, FILINFO* fno) { + return _api->f_stat(path, fno); +} +extern "C" FRESULT f_utime(const TCHAR* path, const FILINFO* fno) { + return _api->f_utime(path, fno); +} +extern "C" FRESULT f_getfree(const TCHAR* path, DWORD* nclst, FATFS** fatfs) { + return _api->f_getfree(path, nclst, fatfs); +} +extern "C" FRESULT f_mount(FATFS* fs, const TCHAR* path, BYTE opt) { + return _api->f_mount(fs, path, opt); +} +extern "C" int f_putc(TCHAR c, FIL* fp) { + return _api->f_putc(c, fp); +} +extern "C" int f_puts(const TCHAR* str, FIL* cp) { + return _api->f_puts(str, cp); +} +extern "C" int f_printf(FIL* fp, const TCHAR* str, ...) { + return _api->f_printf(fp, str); +} +extern "C" TCHAR* f_gets(TCHAR* buff, int len, FIL* fp) { + return _api->f_gets(buff, len, fp); +} diff --git a/firmware/standalone/digitalrain/ui/bmp.hpp b/firmware/standalone/digitalrain/ui/bmp.hpp new file mode 100644 index 000000000..dd032fb66 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/bmp.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * 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. + */ + +#pragma pack(push, 1) +struct bmp_header_t { + uint16_t signature; + uint32_t size; + uint16_t reserved_1; + uint16_t reserved_2; + uint32_t image_data; + uint32_t BIH_size; + uint32_t width; + int32_t height; // can be negative, to signal the bottom-up or reserve status + uint16_t planes; + uint16_t bpp; + uint32_t compression; + uint32_t data_size; + uint32_t h_res; + uint32_t v_res; + uint32_t colors_count; + uint32_t icolors_count; +}; +#pragma pack(pop) + +#pragma pack(push, 1) +struct bmp_palette_t { + struct color_t { + uint8_t B; + uint8_t G; + uint8_t R; + uint8_t A; + } color[16]; +}; +#pragma pack(pop) diff --git a/firmware/standalone/digitalrain/ui/bmpfile.cpp b/firmware/standalone/digitalrain/ui/bmpfile.cpp new file mode 100644 index 000000000..bc8e51340 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/bmpfile.cpp @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2024 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 "bmpfile.hpp" + +bool BMPFile::is_loaded() { + return is_opened; +} + +// fix height info +uint32_t BMPFile::get_real_height() { + if (!is_opened) return 0; + return bmp_header.height >= 0 ? (uint32_t)bmp_header.height : (uint32_t)(-1 * bmp_header.height); +} + +// get bmp width +uint32_t BMPFile::get_width() { + if (!is_opened) return 0; + return bmp_header.width; +} + +// get if the rows are bottom up (for most bmp), or up to bottom (negative height, we use it for write) +bool BMPFile::is_bottomup() { + return (bmp_header.height >= 0); +} + +BMPFile::~BMPFile() { + close(); +} + +// closes file +void BMPFile::close() { + is_opened = false; + bmpimage.close(); +} + +// creates a new bmp file. hardcoded to 3 byte (24-bit) colour depth +bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y) { + is_opened = false; + is_read_only = true; + bmpimage.close(); // if already open, close before open a new + if (file_exists(file)) { + delete_file(file); // overwrite + } + auto result = bmpimage.open(file, false, true); + if (!result.value().ok()) return false; + file_pos = 0; + byte_per_row = (x * 3 % 4 == 0) ? x * 3 : (x * 3 + (4 - ((x * 3) % 4))); // with padding + bmpimage.seek(file_pos); + bmp_header.signature = 0x4D42; + bmp_header.planes = 1; + bmp_header.compression = 0; + bmp_header.bpp = 24; // 3 byte depth + bmp_header.width = x; + bmp_header.height = 0; // for now, will expand + bmp_header.image_data = 0x36; + bmp_header.BIH_size = 0x28; + bmp_header.h_res = 100; + bmp_header.v_res = 100; + byte_per_px = 3; + type = 1; + bmp_header.size = sizeof(bmp_header) + get_real_height() * byte_per_row; // with padding! --will update later with expand + bmp_header.data_size = bmp_header.size - sizeof(bmp_header_t); + bmp_header.colors_count = 0; + bmp_header.icolors_count = 0; + + bmpimage.write(&bmp_header, sizeof(bmp_header_t)); + file_pos = bmp_header.image_data; + is_opened = true; + is_read_only = false; + if (!expand_y(y)) return false; // will fill with 0, and update header data + seek(0, 0); + return true; +} + +// opens the file and parses header data. return true on success +bool BMPFile::open(const std::filesystem::path& file, bool readonly) { + is_opened = false; + is_read_only = true; + bmpimage.close(); + + auto result = bmpimage.open(file, readonly, false); + if (!result.value().ok()) return false; + file_pos = 0; + bmpimage.seek(file_pos); + bmpimage.read(&bmp_header, sizeof(bmp_header_t)); + if (!((bmp_header.signature == 0x4D42) && + (bmp_header.planes == 1) && + (bmp_header.compression == 0 || bmp_header.compression == 3))) { + return false; + } + + switch (bmp_header.bpp) { + case 8: + type = 4; + byte_per_px = 1; + // bmpimage.seek(sizeof(bmp_header_t)); + // bmpimage.read(color_palette, 1024); + return false; // niy + break; + + case 16: + byte_per_px = 2; + type = 5; + if (bmp_header.compression == 3) { + return false; + } // niy + + break; + case 24: + type = 1; + byte_per_px = 3; + break; + case 32: + type = 2; + byte_per_px = 4; + break; + default: + // not supported + return false; + } + byte_per_row = (bmp_header.width * byte_per_px + 3) & ~3; + file_pos = bmp_header.image_data; + is_opened = true; + is_read_only = readonly; + currx = 0; + curry = 0; + return true; +} + +// jumps to next pixel. false on the end +bool BMPFile::advance_curr_px(uint32_t num = 1) { + if (curry >= get_real_height()) return false; + uint32_t rowsToAdvance = (currx + num) / bmp_header.width; + uint32_t nx = (currx + num) % bmp_header.width; + uint32_t ny = curry + rowsToAdvance; + if (ny >= get_real_height()) { + return false; + } + seek(nx, ny); + return true; +} + +// reads next px, then advance the pos (and seek). return false on error +bool BMPFile::read_next_px(ui::Color& px, bool seek = true) { + if (!is_opened) return false; + uint8_t buffer[4]; + auto res = bmpimage.read(buffer, byte_per_px); + if (res.is_error()) return false; + switch (type) { + case 5: { + // ARGB1555 + uint16_t val = buffer[0] | (buffer[1] << 8); + // Extract components + //*a = (val >> 15) & 0x01; // 1-bit alpha + uint8_t r = (val >> 10) & 0x1F; // 5-bit red + uint8_t g = (val >> 5) & 0x1F; // 5-bit green + uint8_t b = (val)&0x1F; // 5-bit blue + // expand + r = (r << 3) | (r >> 2); + g = (g << 3) | (g >> 2); + b = (b << 3) | (b >> 2); + px = ui::Color(r, g, b); + break; + } + case 2: // 32 + px = ui::Color(buffer[2], buffer[1], buffer[0]); + break; + + case 4: { // 8-bit + // uint8_t index = buffer[0]; + // px = ui::Color(color_palette[index][2], color_palette[index][1], color_palette[index][0]); // Palette is BGR + // px = ui::Color(buffer[0]); // niy, since needs a lot of ram for the palette + break; + } + case 1: // 24 + default: + px = ui::Color(buffer[2], buffer[1], buffer[0]); + break; + } + if (seek) advance_curr_px(); + return true; +} + +// if you set this, then the expanded part (or the newly created) will be filled with this color. but the expansion or the creation will be slower. +void BMPFile::set_bg_color(ui::Color background) { + bg = background; + use_bg = true; +} + +// delete bg color. default. creation or expansion will be fast, but the file will contain random garbage. no problem if you write all pixels later. +void BMPFile::delete_bg_color() { + use_bg = false; +} + +// writes a color data to the current position, and advances 1 px. true on success, false on error +bool BMPFile::write_next_px(ui::Color& px) { + if (!is_opened || is_read_only) return false; + uint8_t buffer[4]; + switch (type) { + case 5: + case 0: // R5G6B5 + case 3: // A1R5G5B5 + if (!type) { + buffer[0] = (px.r() << 3) | (px.g() >> 3); + buffer[1] = (px.g() << 5) | px.b(); + } else { + buffer[0] = (1 << 7) | (px.r() << 2) | (px.g() >> 3); + buffer[1] = (px.g() << 5) | px.b(); + } + break; + case 1: // 24 + default: + buffer[2] = px.r(); + buffer[1] = px.g(); + buffer[0] = px.b(); + break; + case 2: // 32 + buffer[2] = px.r(); + buffer[1] = px.g(); + buffer[0] = px.b(); + buffer[3] = 255; + break; + case 4: // 8-bit + return false; + } + auto res = bmpimage.write(buffer, byte_per_px); + if (res.is_error()) return false; + advance_curr_px(); + return true; +} + +// positions in the file to the given pixel. 0 based indexing +bool BMPFile::seek(uint32_t x, uint32_t y) { + if (!is_opened) return false; + if (x >= bmp_header.width) return false; + if (y >= get_real_height()) return false; + if (!BMPFile::is_bottomup()) { + file_pos = bmp_header.image_data; // nav to start pos. + file_pos += y * byte_per_row; + file_pos += x * byte_per_px; + bmpimage.seek(file_pos); + currx = x; + curry = y; + } else { + file_pos = bmp_header.image_data; // nav to start pos. + file_pos += (get_real_height() - y - 1) * byte_per_row; + file_pos += x * byte_per_px; + bmpimage.seek(file_pos); + currx = x; + curry = y; + } + return true; +} + +// expands the image with a delta (y). also seek's t it's begining. in bottumup format, it should be used carefully! +bool BMPFile::expand_y_delta(uint32_t delta_y) { + return expand_y(get_real_height() + delta_y); +} + +// expands the image to a new y size. also seek's t it's begining. in bottumup format, it should be used carefully! +bool BMPFile::expand_y(uint32_t new_y) { + if (!is_opened) return false; // not yet opened + uint32_t old_height = get_real_height(); + if (new_y < old_height) return true; // already bigger + if (is_read_only) return false; // can't expand + uint32_t delta = (new_y - old_height) * byte_per_row; + bmp_header.size += delta; + bmp_header.data_size += delta; + bmp_header.height = -1 * new_y; //-1*, so no bottom-up structure needed. easier to expand. + bmpimage.seek(0); + bmpimage.write(&bmp_header, sizeof(bmp_header)); // overwrite header + bmpimage.seek(bmp_header.size); // seek to new end to expand + // fill with bg color if needed + if (use_bg) { + seek(0, old_height); // to the new begin + size_t newpxcount = ((new_y - old_height) * bmp_header.width); + for (size_t i = 0; i < newpxcount; ++i) + write_next_px(bg); + } + + if (is_bottomup()) { + seek(0, new_y - old_height); // seek to the new chunk begin + } else { + seek(0, curry + 1); // seek to the begin of the new chunk + } + return true; +} diff --git a/firmware/standalone/digitalrain/ui/bmpfile.hpp b/firmware/standalone/digitalrain/ui/bmpfile.hpp new file mode 100644 index 000000000..f3709bd23 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/bmpfile.hpp @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 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. + */ + +#ifndef __BMPFILE__H +#define __BMPFILE__H + +#include +#include + +#include "file.hpp" +#include "bmp.hpp" +#include "ui.hpp" + +class BMPFile { + public: + ~BMPFile(); + bool open(const std::filesystem::path& file, bool readonly); + bool create(const std::filesystem::path& file, uint32_t x, uint32_t y); + void close(); + bool is_loaded(); + bool seek(uint32_t x, uint32_t y); + bool expand_y(uint32_t new_y); + bool expand_y_delta(uint32_t delta_y); + uint32_t getbpr() { return byte_per_row; }; + + bool read_next_px(ui::Color& px, bool seek); + bool write_next_px(ui::Color& px); + uint32_t get_real_height(); + uint32_t get_width(); + bool is_bottomup(); + void set_bg_color(ui::Color background); + void delete_bg_color(); + + private: + bool advance_curr_px(uint32_t num); + bool is_opened = false; + bool is_read_only = true; + + File bmpimage{}; + size_t file_pos = 0; + bmp_header_t bmp_header{}; + uint8_t type = 0; + uint8_t byte_per_px = 1; + uint32_t byte_per_row = 0; + + uint32_t currx = 0; + uint32_t curry = 0; + ui::Color bg{}; + // uint8_t color_palette[256][4]; + bool use_bg = false; +}; + +#endif \ No newline at end of file diff --git a/firmware/standalone/digitalrain/ui/circular_buffer.hpp b/firmware/standalone/digitalrain/ui/circular_buffer.hpp new file mode 100644 index 000000000..f455cd8ee --- /dev/null +++ b/firmware/standalone/digitalrain/ui/circular_buffer.hpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __CIRCULAR_BUFFER_H__ +#define __CIRCULAR_BUFFER_H__ + +#include // For size_t +#include + +/* Implements a fixed-size, circular buffer. + * NB: Holds Capacity - 1 items. + * There are no bounds checks on accessors so ensure there are + * items in the buffer before accessing front/back/operator[]. */ +template +class CircularBuffer { + public: + CircularBuffer() = default; + + CircularBuffer(const CircularBuffer&) = delete; + CircularBuffer(CircularBuffer&&) = delete; + CircularBuffer& operator=(const CircularBuffer&) = delete; + CircularBuffer& operator=(CircularBuffer&&) = delete; + + void push_front(T val) { + head_ = head_ > 0 ? head_ - 1 : last_index; + if (head_ == end_) + pop_back_internal(); + + data_[head_] = std::move(val); + } + + void pop_front() { + if (!empty()) + pop_front_internal(); + } + + void push_back(T val) { + data_[end_] = std::move(val); + + end_ = end_ < last_index ? end_ + 1 : 0; + if (head_ == end_) + pop_front_internal(); + } + + void pop_back() { + if (!empty()) + pop_back_internal(); + } + + T& operator[](size_t ix) & { + ix += head_; + if (ix >= Capacity) + ix -= Capacity; + return data_[ix]; + } + + const T& operator[](size_t ix) const& { + return const_cast(this)->operator[](ix); + } + + const T& front() const& { + return data_[head_]; + } + + const T& back() const& { + auto end = end_ > 0 ? end_ - 1 : last_index; + return data_[end]; + } + + size_t size() const& { + auto end = end_; + if (end < head_) + end += Capacity; + return end - head_; + } + + bool empty() const { + return head_ == end_; + } + + void clear() { + head_ = 0; + end_ = 0; + } + + private: + void pop_front_internal() { + head_ = head_ < last_index ? head_ + 1 : 0; + } + + void pop_back_internal() { + end_ = end_ > 0 ? end_ - 1 : last_index; + } + + static constexpr size_t last_index = Capacity - 1; + size_t head_{0}; + size_t end_{0}; + + T data_[Capacity]{}; +}; + +#endif /*__CIRCULAR_BUFFER_H__*/ \ No newline at end of file diff --git a/firmware/standalone/digitalrain/ui/complex.hpp b/firmware/standalone/digitalrain/ui/complex.hpp new file mode 100644 index 000000000..ac24b0ab5 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/complex.hpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __COMPLEX_H__ +#define __COMPLEX_H__ + +#include +#include +#include + +constexpr float pi{3.141592653589793238462643383279502884f}; + +namespace std { + +template <> +struct complex { + public: + typedef int8_t value_type; + typedef uint16_t rep_type; + + constexpr complex( + int8_t re = 0, + int8_t im = 0) + : _v{re, im} { + } + + constexpr int8_t real() const { return _v[0]; } + constexpr int8_t imag() const { return _v[1]; } + + void real(int8_t v) { _v[0] = v; } + void imag(int8_t v) { _v[1] = v; } + + constexpr uint16_t __rep() const { + return _rep; + } + + private: + union { + value_type _v[2]; + rep_type _rep; + }; +}; + +template <> +struct complex { + public: + typedef int16_t value_type; + typedef uint32_t rep_type; + + constexpr complex( + int16_t re = 0, + int16_t im = 0) + : _v{re, im} { + } + + constexpr int16_t real() const { return _v[0]; } + constexpr int16_t imag() const { return _v[1]; } + + void real(int16_t v) { _v[0] = v; } + void imag(int16_t v) { _v[1] = v; } + + template + complex& operator+=(const complex& other) { + _v[0] += other.real(); + _v[1] += other.imag(); + return *this; + } + + constexpr uint32_t __rep() const { + return _rep; + } + + constexpr operator std::complex() const { + return { + static_cast(_v[0]), + static_cast(_v[1])}; + } + + private: + union { + value_type _v[2]; + rep_type _rep; + }; +}; + +} /* namespace std */ + +using complex8_t = std::complex; +using complex16_t = std::complex; +using complex32_t = std::complex; + +static_assert(sizeof(complex8_t) == 2, "complex8_t size wrong"); +static_assert(sizeof(complex16_t) == 4, "complex16_t size wrong"); +static_assert(sizeof(complex32_t) == 8, "complex32_t size wrong"); + +#endif /*__COMPLEX_H__*/ diff --git a/firmware/standalone/digitalrain/ui/file.cpp b/firmware/standalone/digitalrain/ui/file.cpp new file mode 100644 index 000000000..df0f22849 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/file.cpp @@ -0,0 +1,641 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * 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 "file.hpp" +#include "complex.hpp" + +#include +#include +#include +#include + +namespace fs = std::filesystem; +static const fs::path c8_ext{u".C8"}; +static const fs::path c16_ext{u".C16"}; + +Optional File::open_fatfs(const std::filesystem::path& filename, BYTE mode) { + auto result = f_open(&f, reinterpret_cast(filename.c_str()), mode); + if (result == FR_OK) { + if (mode & FA_OPEN_ALWAYS) { + const auto result = f_lseek(&f, f_size(&f)); + if (result != FR_OK) { + f_close(&f); + } + } + } + + if (result == FR_OK) { + return {}; + } else { + return {result}; + } +} + +/* + * @param read_only: open in readonly mode + * @param create: create if it doesnt exist + */ +Optional File::open(const std::filesystem::path& filename, bool read_only, bool create) { + BYTE mode = read_only ? FA_READ : FA_READ | FA_WRITE; + if (create) + mode |= FA_OPEN_ALWAYS; + + return open_fatfs(filename, mode); +} + +Optional File::append(const std::filesystem::path& filename) { + return open_fatfs(filename, FA_WRITE | FA_OPEN_ALWAYS); +} + +Optional File::create(const std::filesystem::path& filename) { + return open_fatfs(filename, FA_WRITE | FA_CREATE_ALWAYS); +} + +File::~File() { + f_close(&f); +} + +void File::close() { + f_close(&f); +} + +File::Result File::read(void* data, Size bytes_to_read) { + UINT bytes_read = 0; + const auto result = f_read(&f, data, bytes_to_read, &bytes_read); + if (result == FR_OK) { + return {static_cast(bytes_read)}; + } else { + return {static_cast(result)}; + } +} + +File::Result File::write(const void* data, Size bytes_to_write) { + UINT bytes_written = 0; + const auto result = f_write(&f, data, bytes_to_write, &bytes_written); + if (result == FR_OK) { + if (bytes_to_write == bytes_written) { + return {static_cast(bytes_written)}; + } else { + return Error{FR_DISK_FULL}; + } + } else { + return {static_cast(result)}; + } +} + +File::Offset File::tell() const { + return f_tell(&f); +} + +File::Result File::eof() { + return f_eof(&f); +} + +File::Result File::seek(Offset new_position) { + /* NOTE: Returns *old* position, not new position */ + const auto old_position = tell(); + const auto result = (old_position == new_position) ? FR_OK : f_lseek(&f, new_position); + if (result != FR_OK) { + return {static_cast(result)}; + } + if (f_tell(&f) != new_position) { + return {static_cast(FR_BAD_SEEK)}; + } + return {static_cast(old_position)}; +} + +File::Result File::truncate() { + const auto position = f_tell(&f); + const auto result = f_truncate(&f); + if (result != FR_OK) { + return {static_cast(result)}; + } + return {static_cast(position)}; +} + +File::Size File::size() const { + return f_size(&f); +} + +Optional File::write_line(const std::string& s) { + const auto result_s = write(s.c_str(), s.size()); + if (result_s.is_error()) { + return {result_s.error()}; + } + + const auto result_crlf = write("\r\n", 2); + if (result_crlf.is_error()) { + return {result_crlf.error()}; + } + + return {}; +} + +Optional File::sync() { + const auto result = f_sync(&f); + if (result == FR_OK) { + return {}; + } else { + return {result}; + } +} + +File::Result File::read_file(const std::filesystem::path& filename) { + constexpr size_t buffer_size = 0x80; + char* buffer[buffer_size]; + + File f; + auto error = f.open(filename); + if (error) + return *error; + + std::string content; + content.resize(f.size()); + auto str = &content[0]; + auto total_read = 0u; + + while (true) { + auto read = f.read(buffer, buffer_size); + if (!read) + return read.error(); + + memcpy(str, buffer, *read); + str += *read; + total_read += *read; + + if (*read < buffer_size) + break; + } + + content.resize(total_read); + return content; +} + +/* Range used for filename matching. + * Start and end are inclusive positions of "???" */ +struct pattern_range { + size_t start; + size_t end; +}; + +/* Finds the last file matching the specified pattern that + * can be automatically incremented (digits in pattern). + * NB: assumes a patten with contiguous '?' like "FOO_???.txt". */ +static std::filesystem::path find_last_ordinal_match( + const std::filesystem::path& folder, + const std::filesystem::path& pattern, + pattern_range range) { + auto last_match = std::filesystem::path(); + auto can_increment = [range](const auto& path) { + for (auto i = range.start; i <= range.end; ++i) + if (!isdigit(path.native()[i])) + return false; + + return true; + }; + + for (const auto& entry : std::filesystem::directory_iterator(folder, pattern)) { + if (std::filesystem::is_regular_file(entry.status()) && can_increment(entry.path())) { + const auto& match = entry.path(); + if (match > last_match) { + last_match = match; + } + } + } + + return last_match; +} + +/* Given a file path like "FOO_0001.txt" increment it to "FOO_0002.txt". */ +static std::filesystem::path increment_filename_ordinal( + const std::filesystem::path& path, + pattern_range range) { + auto name = path.filename().native(); + + for (auto i = range.end; i >= range.start; --i) { + auto& c = name[i]; + + // Not a digit or would overflow the counter. + if (c < u'0' || c > u'9' || (c == u'9' && i == range.start)) + return {}; + + if (c == u'9') + c = '0'; + else { + c++; + break; + } + } + + return {name}; +} + +std::filesystem::path next_filename_matching_pattern(const std::filesystem::path& filename_pattern) { + auto path = filename_pattern.parent_path(); + auto pattern = filename_pattern.filename(); + auto range = pattern_range{ + pattern.native().find_first_of(u'?'), + pattern.native().find_last_of(u'?')}; + + const auto match = find_last_ordinal_match(path, pattern, range); + + if (match.empty()) { + auto pattern_str = pattern.native(); + for (auto i = range.start; i <= range.end; ++i) + pattern_str[i] = u'0'; + return path / pattern_str; + } + + auto next_name = increment_filename_ordinal(match, range); + return next_name.empty() ? next_name : path / next_name; +} + +std::vector scan_root_files(const std::filesystem::path& directory, + const std::filesystem::path& extension) { + std::vector file_list{}; + scan_root_files(directory, extension, [&file_list](const std::filesystem::path& p) { + file_list.push_back(p); + }); + + return file_list; +} + +std::vector scan_root_directories(const std::filesystem::path& directory) { + std::vector directory_list{}; + + for (const auto& entry : std::filesystem::directory_iterator(directory, "*")) { + if (std::filesystem::is_directory(entry.status())) { + directory_list.push_back(entry.path()); + } + } + + return directory_list; +} + +std::filesystem::filesystem_error delete_file(const std::filesystem::path& file_path) { + return {f_unlink(reinterpret_cast(file_path.c_str()))}; +} + +std::filesystem::filesystem_error rename_file( + const std::filesystem::path& file_path, + const std::filesystem::path& new_name) { + return {f_rename(reinterpret_cast(file_path.c_str()), reinterpret_cast(new_name.c_str()))}; +} + +std::filesystem::filesystem_error copy_file( + const std::filesystem::path& file_path, + const std::filesystem::path& dest_path) { + constexpr size_t buffer_size = std::filesystem::max_file_block_size; + uint8_t buffer[buffer_size]; + File src; + File dst; + + auto error = src.open(file_path); + if (error) return error.value(); + + error = dst.create(dest_path); + if (error) return error.value(); + + while (true) { + auto result = src.read(buffer, buffer_size); + if (result.is_error()) return result.error(); + + result = dst.write(buffer, *result); + if (result.is_error()) return result.error(); + + if (*result < buffer_size) + break; + } + + return {}; +} + +FATTimestamp file_created_date(const std::filesystem::path& file_path) { + FILINFO filinfo; + + f_stat(reinterpret_cast(file_path.c_str()), &filinfo); + + return {filinfo.fdate, filinfo.ftime}; +} + +std::filesystem::filesystem_error file_update_date(const std::filesystem::path& file_path, FATTimestamp timestamp) { + FILINFO filinfo{}; + + filinfo.fdate = timestamp.FAT_date; + filinfo.ftime = timestamp.FAT_time; + return f_utime(reinterpret_cast(file_path.c_str()), &filinfo); +} + +std::filesystem::filesystem_error make_new_file( + const std::filesystem::path& file_path) { + File f; + auto error = f.create(file_path); + if (error) + return *error; + + return {}; +} + +std::filesystem::filesystem_error make_new_directory( + const std::filesystem::path& dir_path) { + return {f_mkdir(reinterpret_cast(dir_path.c_str()))}; +} + +std::filesystem::filesystem_error ensure_directory( + const std::filesystem::path& dir_path) { + if (dir_path.empty() || std::filesystem::file_exists(dir_path)) + return {}; + + auto result = ensure_directory(dir_path.parent_path()); + if (result.code()) + return result; + + return make_new_directory(dir_path); +} + +namespace std { +namespace filesystem { + +std::string filesystem_error::what() const { + switch (err) { + case FR_OK: + return "ok"; + case FR_DISK_ERR: + return "disk error"; + case FR_INT_ERR: + return "insanity detected"; + case FR_NOT_READY: + return "SD card not ready"; + case FR_NO_FILE: + return "no file"; + case FR_NO_PATH: + return "no path"; + case FR_INVALID_NAME: + return "invalid name"; + case FR_DENIED: + return "denied"; + case FR_EXIST: + return "exists"; + case FR_INVALID_OBJECT: + return "invalid object"; + case FR_WRITE_PROTECTED: + return "write protected"; + case FR_INVALID_DRIVE: + return "invalid drive"; + case FR_NOT_ENABLED: + return "not enabled"; + case FR_NO_FILESYSTEM: + return "no filesystem"; + case FR_MKFS_ABORTED: + return "mkfs aborted"; + case FR_TIMEOUT: + return "timeout"; + case FR_LOCKED: + return "locked"; + case FR_NOT_ENOUGH_CORE: + return "not enough core"; + case FR_TOO_MANY_OPEN_FILES: + return "too many open files"; + case FR_INVALID_PARAMETER: + return "invalid parameter"; + case FR_EOF: + return "end of file"; + case FR_DISK_FULL: + return "disk full"; + case FR_BAD_SEEK: + return "bad seek"; + case FR_UNEXPECTED: + return "unexpected"; + default: + return "unknown"; + } +} + +path path::parent_path() const { + const auto index = _s.find_last_of(preferred_separator); + if (index == _s.npos) { + return {}; // NB: Deviation from STL. + } else { + return _s.substr(0, index); + } +} + +path path::extension() const { + const auto t = filename().native(); + const auto index = t.find_last_of(u'.'); + if (index == t.npos) { + return {}; + } else { + return t.substr(index); + } +} + +path path::filename() const { + const auto index = _s.find_last_of(preferred_separator); + if (index == _s.npos) { + return _s; + } else { + return _s.substr(index + 1); + } +} + +path path::stem() const { + const auto t = filename().native(); + const auto index = t.find_last_of(u'.'); + if (index == t.npos) { + return t; + } else { + return t.substr(0, index); + } +} + +std::string path::string() const { + std::wstring_convert, path::value_type> conv; + return conv.to_bytes(native()); +} + +// appends a string to the end of filename, but leaves the extension asd.txt + "fg" -> asdfg.txt +path& path::append_filename(const string_type& str) { + const auto t = extension().native(); + _s.erase(_s.size() - t.size()); // remove extension + _s += str; // append string + _s += t; // add back extension + return *this; +} + +path& path::replace_extension(const path& replacement) { + const auto t = extension().native(); + _s.erase(_s.size() - t.size()); + if (!replacement._s.empty()) { + if (replacement._s.front() != u'.') { + _s += u'.'; + } + _s += replacement._s; + } + return *this; +} + +bool operator==(const path& lhs, const path& rhs) { + return lhs.native() == rhs.native(); +} + +bool operator!=(const path& lhs, const path& rhs) { + return !(lhs == rhs); +} + +bool operator<(const path& lhs, const path& rhs) { + return lhs.native() < rhs.native(); +} + +bool operator>(const path& lhs, const path& rhs) { + return lhs.native() > rhs.native(); +} + +path operator+(const path& lhs, const path& rhs) { + path result = lhs; + result += rhs; + return result; +} + +path operator/(const path& lhs, const path& rhs) { + path result = lhs; + result /= rhs; + return result; +} + +bool path_iequal( + const path& lhs, + const path& rhs) { + const auto& lhs_str = lhs.native(); + const auto& rhs_str = rhs.native(); + + // NB: Not correct for Unicode/locales. + if (lhs_str.length() == rhs_str.length()) { + for (size_t i = 0; i < lhs_str.length(); ++i) + if (towupper(lhs_str[i]) != towupper(rhs_str[i])) + return false; + + return true; + } + + return false; +} + +bool is_cxx_capture_file(const path& filename) { + auto ext = filename.extension(); + return path_iequal(c8_ext, ext) || path_iequal(c16_ext, ext); +} + +uint8_t capture_file_sample_size(const path& filename) { + if (path_iequal(filename.extension(), c8_ext)) + return sizeof(complex8_t); + if (path_iequal(filename.extension(), c16_ext)) + return sizeof(complex16_t); + return 0; +} + +directory_iterator::directory_iterator( + const std::filesystem::path& path, + const std::filesystem::path& wild) + : path_{path}, wild_{wild} { + impl = std::make_shared(); + auto result = f_findfirst(&impl->dir, &impl->filinfo, + path_.tchar(), wild_.tchar()); + if (result != FR_OK || impl->filinfo.fname[0] == (TCHAR)'\0') { + impl.reset(); + // TODO: Throw exception if/when I enable exceptions... + } +} + +directory_iterator& directory_iterator::operator++() { + const auto result = f_findnext(&impl->dir, &impl->filinfo); + if ((result != FR_OK) || (impl->filinfo.fname[0] == 0)) { + impl.reset(); + } + return *this; +} + +bool is_directory(const file_status s) { + return (s & AM_DIR); +} + +bool is_regular_file(const file_status s) { + return !(s & AM_DIR); +} + +bool file_exists(const path& file_path) { + FILINFO filinfo; + auto fr = f_stat(reinterpret_cast(file_path.c_str()), &filinfo); + + return fr == FR_OK; +} + +bool is_directory(const path& file_path) { + FILINFO filinfo; + auto fr = f_stat(reinterpret_cast(file_path.c_str()), &filinfo); + + return fr == FR_OK && is_directory(static_cast(filinfo.fattrib)); +} + +bool is_empty_directory(const path& file_path) { + DIR dir; + FILINFO filinfo; + + if (!is_directory(file_path)) + return false; + + auto result = f_findfirst(&dir, &filinfo, reinterpret_cast(file_path.c_str()), (const TCHAR*)u"*"); + return !((result == FR_OK) && (filinfo.fname[0] != (TCHAR)'\0')); +} + +int file_count(const path& directory) { + int count{0}; + + for (auto& entry : std::filesystem::directory_iterator(directory, (const TCHAR*)u"*")) { + (void)entry; // avoid unused warning + ++count; + } + + return count; +} + +space_info space(const path& p) { + DWORD free_clusters{0}; + FATFS* fs; + if (f_getfree(reinterpret_cast(p.c_str()), &free_clusters, &fs) == FR_OK) { +#if _MAX_SS != _MIN_SS + static_assert(false, "FatFs not configured for fixed sector size"); +#else + const std::uintmax_t cluster_bytes = fs->csize * _MIN_SS; + return { + (fs->n_fatent - 2) * cluster_bytes, + free_clusters * cluster_bytes, + free_clusters * cluster_bytes, + }; +#endif + } else { + return {0, 0, 0}; + } +} + +} /* namespace filesystem */ +} /* namespace std */ diff --git a/firmware/standalone/digitalrain/ui/file.hpp b/firmware/standalone/digitalrain/ui/file.hpp new file mode 100644 index 000000000..45c2cfc68 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/file.hpp @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __FILE_H__ +#define __FILE_H__ + +#include "ff.h" + +#include "optional.hpp" +#include "result.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace std { +namespace filesystem { + +struct filesystem_error { + constexpr filesystem_error() = default; + + constexpr filesystem_error( + FRESULT fatfs_error) + : err{fatfs_error} { + } + + constexpr filesystem_error( + unsigned int other_error) + : err{other_error} { + } + + uint32_t code() const { + return err; + } + + std::string what() const; + + bool ok() const { + return err == FR_OK; + } + + private: + uint32_t err{FR_OK}; +}; + +struct path { + using string_type = std::u16string; + using value_type = string_type::value_type; + + static constexpr value_type preferred_separator = u'/'; + + path() + : _s{} { + } + + path(const path& p) + : _s{p._s} { + } + + path(path&& p) + : _s{std::move(p._s)} { + } + + template + path(const Source& source) + : path{std::begin(source), std::end(source)} { + } + + template + path(InputIt first, + InputIt last) + : _s{first, last} { + } + + path(const char16_t* const s) + : _s{s} { + } + + path(const TCHAR* const s) + : _s{reinterpret_cast(s)} { + } + + path& operator=(const path& p) { + _s = p._s; + return *this; + } + + path& operator=(path&& p) { + _s = std::move(p._s); + return *this; + } + + path parent_path() const; + path extension() const; + path filename() const; + path stem() const; + + bool empty() const { + return _s.empty(); + } + + const value_type* c_str() const { + return native().c_str(); + } + + const TCHAR* tchar() const { + return reinterpret_cast(native().c_str()); + } + + const string_type& native() const { + return _s; + } + + std::string string() const; + + path& operator+=(const path& p) { + _s += p._s; + return *this; + } + + path& operator+=(const string_type& str) { + _s += str; + return *this; + } + + path& operator/=(const path& p) { + if (_s.back() != preferred_separator && p._s.front() != preferred_separator) + _s += preferred_separator; + _s += p._s; + return *this; + } + + path& replace_extension(const path& replacement = path()); + + path& append_filename(const string_type& str); + + private: + string_type _s; +}; + +bool operator==(const path& lhs, const path& rhs); +bool operator!=(const path& lhs, const path& rhs); +bool operator<(const path& lhs, const path& rhs); +bool operator>(const path& lhs, const path& rhs); +path operator+(const path& lhs, const path& rhs); +path operator/(const path& lhs, const path& rhs); + +/* Case insensitive path equality on underlying "native" string. */ +bool path_iequal(const path& lhs, const path& rhs); +bool is_cxx_capture_file(const path& filename); +uint8_t capture_file_sample_size(const path& filename); + +using file_status = BYTE; + +/* The largest block that can be read/written to a file. */ +constexpr uint16_t max_file_block_size = 512; + +static_assert(sizeof(path::value_type) == 2, "sizeof(std::filesystem::path::value_type) != 2"); +static_assert(sizeof(path::value_type) == sizeof(TCHAR), "FatFs TCHAR size != std::filesystem::path::value_type"); + +struct space_info { + static_assert(sizeof(std::uintmax_t) >= 8, "std::uintmax_t too small ( impl{}; + std::filesystem::path path_{}; + std::filesystem::path wild_{}; + + friend bool operator!=(const directory_iterator& lhs, const directory_iterator& rhs); + + public: + using difference_type = std::ptrdiff_t; + using value_type = directory_entry; + using pointer = const directory_entry*; + using reference = const directory_entry&; + using iterator_category = std::input_iterator_tag; + + directory_iterator() noexcept {}; + directory_iterator(const std::filesystem::path& path, + const std::filesystem::path& wild); + + ~directory_iterator() {} + + directory_iterator& operator++(); + + reference operator*() const { + // TODO: Exception or assert if impl == nullptr. + return impl->filinfo; + } +}; + +inline const directory_iterator& begin(const directory_iterator& iter) noexcept { + return iter; +}; +inline directory_iterator end(const directory_iterator&) noexcept { + return {}; +}; + +inline bool operator!=(const directory_iterator& lhs, const directory_iterator& rhs) { + return lhs.impl != rhs.impl; +}; + +bool is_directory(const file_status s); +bool is_regular_file(const file_status s); +bool file_exists(const path& file_path); +bool is_directory(const path& file_path); +bool is_empty_directory(const path& file_path); + +int file_count(const path& dir_path); + +space_info space(const path& p); + +} /* namespace filesystem */ +} /* namespace std */ + +struct FATTimestamp { + uint16_t FAT_date; + uint16_t FAT_time; +}; + +std::filesystem::filesystem_error delete_file(const std::filesystem::path& file_path); +std::filesystem::filesystem_error rename_file(const std::filesystem::path& file_path, const std::filesystem::path& new_name); +std::filesystem::filesystem_error copy_file(const std::filesystem::path& file_path, const std::filesystem::path& dest_path); + +FATTimestamp file_created_date(const std::filesystem::path& file_path); +std::filesystem::filesystem_error file_update_date(const std::filesystem::path& file_path, FATTimestamp timestamp); +std::filesystem::filesystem_error make_new_file(const std::filesystem::path& file_path); +std::filesystem::filesystem_error make_new_directory(const std::filesystem::path& dir_path); +std::filesystem::filesystem_error ensure_directory(const std::filesystem::path& dir_path); + +template +void scan_root_files(const std::filesystem::path& directory, const std::filesystem::path& extension, const TCallback& fn) { + for (const auto& entry : std::filesystem::directory_iterator(directory, extension)) { + if (std::filesystem::is_regular_file(entry.status())) + fn(entry.path()); + } +} +std::vector scan_root_files(const std::filesystem::path& directory, const std::filesystem::path& extension); +std::vector scan_root_directories(const std::filesystem::path& directory); + +/* Gets an auto incrementing filename stem. + * Pattern should be like "FOO_???.txt" where ??? will be replaced by digits. + * Pattern may also contain a folder path like "LOGS/FOO_???.txt". + * Pattern '?' must be contiguous (bad: "FOO?_??") + * Returns empty path if a filename could not be created. */ +std::filesystem::path next_filename_matching_pattern(const std::filesystem::path& pattern); + +/* Values added to FatFs FRESULT enum, values outside the FRESULT data type */ +static_assert(sizeof(FIL::err) == 1, "FatFs FIL::err size not expected."); + +/* Dangerous to expose these, as FatFs native error values are byte-sized. However, + * my filesystem_error implementation is fine with it. */ +#define FR_DISK_FULL (0x100) +#define FR_EOF (0x101) +#define FR_BAD_SEEK (0x102) +#define FR_UNEXPECTED (0x103) + +/* NOTE: sizeof(File) == 556 bytes because of the FIL's buf member. */ +class File { + public: + using Size = uint64_t; + using Offset = uint64_t; + using Timestamp = uint32_t; + using Error = std::filesystem::filesystem_error; + + template + using Result = Result; + + File() {}; + ~File(); + + File(File&& other) { + std::swap(f, other.f); + } + File& operator=(File&& other) { + std::swap(f, other.f); + return *this; + } + + /* Prevent copies */ + File(const File&) = delete; + File& operator=(const File&) = delete; + + // TODO: Return Result<>. + Optional open(const std::filesystem::path& filename, bool read_only = true, bool create = false); + void close(); + Optional append(const std::filesystem::path& filename); + Optional create(const std::filesystem::path& filename); + + Result read(void* data, const Size bytes_to_read); + Result write(const void* data, Size bytes_to_write); + + Offset tell() const; + Result seek(uint64_t Offset); + Result truncate(); + Size size() const; + Result eof(); + + template + Result write(const std::array& data) { + return write(data.data(), N); + } + + Optional write_line(const std::string& s); + + // TODO: Return Result<>. + Optional sync(); + + /* Reads the entire file contents to a string. + * NB: This will likely fail for files larger than ~10kB. */ + static Result read_file(const std::filesystem::path& filename); + + private: + FIL f{}; + + Optional open_fatfs(const std::filesystem::path& filename, BYTE mode); +}; + +#endif /*__FILE_H__*/ diff --git a/firmware/standalone/digitalrain/ui/file_path.cpp b/firmware/standalone/digitalrain/ui/file_path.cpp new file mode 100644 index 000000000..a08811fb6 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/file_path.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 Mark Thompson + * + * 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 "file_path.hpp" +#include "file.hpp" + +const std::filesystem::path adsb_dir = u"ADSB"; +const std::filesystem::path ais_dir = u"AIS"; +const std::filesystem::path apps_dir = u"APPS"; +const std::filesystem::path aprs_dir = u"APRS"; +const std::filesystem::path audio_dir = u"AUDIO"; +const std::filesystem::path blerx_dir = u"BLERX"; +const std::filesystem::path bletx_dir = u"BLETX"; +const std::filesystem::path captures_dir = u"CAPTURES"; +const std::filesystem::path cvsfiles_dir = u"CVSFILES"; +const std::filesystem::path debug_dir = u"DEBUG"; +const std::filesystem::path firmware_dir = u"FIRMWARE"; +const std::filesystem::path freqman_dir = u"FREQMAN"; +const std::filesystem::path gps_dir = u"GPS"; +const std::filesystem::path logs_dir = u"LOGS"; +const std::filesystem::path looking_glass_dir = u"LOOKINGGLASS"; +const std::filesystem::path playlist_dir = u"PLAYLIST"; +const std::filesystem::path remotes_dir = u"REMOTES"; +const std::filesystem::path repeat_rec_path = u"CAPTURES"; +const std::filesystem::path samples_dir = u"SAMPLES"; +const std::filesystem::path screenshots_dir = u"SCREENSHOTS"; +const std::filesystem::path settings_dir = u"SETTINGS"; +const std::filesystem::path spectrum_dir = u"SPECTRUM"; +const std::filesystem::path splash_dir = u"SPLASH"; +const std::filesystem::path sstv_dir = u"SSTV"; +const std::filesystem::path wav_dir = u"WAV"; +const std::filesystem::path whipcalc_dir = u"WHIPCALC"; +const std::filesystem::path ook_editor_dir = u"OOKFILES"; +const std::filesystem::path hopper_dir = u"HOPPER"; +const std::filesystem::path subghz_dir = u"SUBGHZ"; +const std::filesystem::path waterfalls_dir = u"WATERFALLS"; +const std::filesystem::path macaddress_dir = u"MACADDRESS"; diff --git a/firmware/standalone/digitalrain/ui/file_path.hpp b/firmware/standalone/digitalrain/ui/file_path.hpp new file mode 100644 index 000000000..0f8548ee0 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/file_path.hpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 Mark Thompson + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __FILE_PATH_H__ +#define __FILE_PATH_H__ + +#include "file.hpp" + +extern const std::filesystem::path adsb_dir; +extern const std::filesystem::path ais_dir; +extern const std::filesystem::path apps_dir; +extern const std::filesystem::path aprs_dir; +extern const std::filesystem::path audio_dir; +extern const std::filesystem::path blerx_dir; +extern const std::filesystem::path bletx_dir; +extern const std::filesystem::path captures_dir; +extern const std::filesystem::path cvsfiles_dir; +extern const std::filesystem::path debug_dir; +extern const std::filesystem::path firmware_dir; +extern const std::filesystem::path freqman_dir; +extern const std::filesystem::path gps_dir; +extern const std::filesystem::path logs_dir; +extern const std::filesystem::path looking_glass_dir; +extern const std::filesystem::path playlist_dir; +extern const std::filesystem::path remotes_dir; +extern const std::filesystem::path repeat_rec_path; +extern const std::filesystem::path samples_dir; +extern const std::filesystem::path screenshots_dir; +extern const std::filesystem::path settings_dir; +extern const std::filesystem::path spectrum_dir; +extern const std::filesystem::path splash_dir; +extern const std::filesystem::path sstv_dir; +extern const std::filesystem::path wav_dir; +extern const std::filesystem::path whipcalc_dir; +extern const std::filesystem::path ook_editor_dir; +extern const std::filesystem::path hopper_dir; +extern const std::filesystem::path subghz_dir; +extern const std::filesystem::path waterfalls_dir; +extern const std::filesystem::path macaddress_dir; + +#endif /* __FILE_PATH_H__ */ diff --git a/firmware/standalone/digitalrain/ui/file_reader.cpp b/firmware/standalone/digitalrain/ui/file_reader.cpp new file mode 100644 index 000000000..3984b9920 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/file_reader.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "file_reader.hpp" +#include +#include + +/* Splits the string on the specified char and returns + * a vector of string_views. NB: the lifetime of the + * string to split must be maintained while the views + * are used or they will dangle. */ +std::vector split_string(std::string_view str, char c) { + std::vector cols; + size_t start = 0; + + while (start < str.length()) { + auto it = str.find(c, start); + + if (it == str.npos) + break; + + cols.emplace_back(&str[start], it - start); + start = it + 1; + } + + if (start <= str.length() && !str.empty()) + cols.emplace_back(&str[start], str.length() - start); + + return cols; +} diff --git a/firmware/standalone/digitalrain/ui/file_reader.hpp b/firmware/standalone/digitalrain/ui/file_reader.hpp new file mode 100644 index 000000000..bcd9f1517 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/file_reader.hpp @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __FILE_READER_HPP__ +#define __FILE_READER_HPP__ + +#include "file.hpp" +#include +#include +#include +#include +#include + +/* BufferType requires the following members + * Size size() + * Result read(void* data, Size bytes_to_read) + * Result seek(uint32_t offset) + */ + +/* Iterates lines in buffer split on '\n'. + * NB: very basic iterator impl, don't try anything fancy with it. + * For example, you _must_ deref the iterator after advancing it. */ +template +class BufferLineReader { + public: + struct iterator { + bool operator!=(const iterator& other) { + return this->pos_ != other.pos_ || this->reader_ != other.reader_; + } + + const std::string& operator*() { + if (!cached_) { + bool ok = reader_->read_line(*this); + cached_ = true; + + if (!ok) *this = reader_->end(); + } + + return line_data_; + } + + iterator& operator++() { + const auto size = reader_->size(); + + if (pos_ < size) { + cached_ = false; + pos_ += line_data_.length(); + } + + if (pos_ >= size) + *this = reader_->end(); + + return *this; + } + + typename BufferType::Size pos_{}; + BufferLineReader* reader_{}; + bool cached_ = false; + std::string line_data_{}; + }; + + BufferLineReader(BufferType& buffer) + : buffer_{buffer} {} + + iterator begin() { return {0, this}; } + iterator end() { return {size(), this}; } + + typename BufferType::Size size() const { return buffer_.size(); } + + private: + BufferType& buffer_; + + bool read_line(iterator& it) { + constexpr size_t buf_size = 0x80; + char buf[buf_size]; + uint32_t offset = 0; + + it.line_data_.resize(buf_size); + buffer_.seek(it.pos_); + + while (true) { + auto read = buffer_.read(buf, buf_size); + if (!read) + return false; + + // Find newline. + auto len = 0u; + for (; len < *read; ++len) { + if (buf[len] == '\n') { + ++len; + break; + } + } + + // Reallocate if needed. + if (offset + len >= it.line_data_.length()) + it.line_data_.resize(offset + len); + + std::strncpy(&it.line_data_[offset], buf, len); + offset += len; + + if (len < buf_size) + break; + } + + it.line_data_.resize(offset); + return true; + } +}; + +using FileLineReader = BufferLineReader; + +/* Splits the string on the specified char and returns + * a vector of string_views. NB: the lifetime of the + * string to split must be maintained while the views + * are used or they will dangle. */ +std::vector split_string(std::string_view str, char c); + +/* Returns the number of lines in a file. */ +template +uint32_t count_lines(BufferLineReader& reader) { + uint32_t count = 0; + auto it = reader.begin(); + + do { + *it; // Necessary to force the file read. + ++count; + } while (++it != reader.end()); + + return count; +} + +#endif diff --git a/firmware/standalone/digitalrain/ui/file_wrapper.hpp b/firmware/standalone/digitalrain/ui/file_wrapper.hpp new file mode 100644 index 000000000..1c120430c --- /dev/null +++ b/firmware/standalone/digitalrain/ui/file_wrapper.hpp @@ -0,0 +1,543 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __FILE_WRAPPER_HPP__ +#define __FILE_WRAPPER_HPP__ + +#include "circular_buffer.hpp" +#include "file.hpp" +#include "optional.hpp" + +#include +#include +#include + +enum class LineEnding : uint8_t { + LF, + CRLF +}; + +/* TODO: + * - CRLF handling. + * - Avoid full re-read on edits. + * - Would need to read old/new text when editing to track newlines. + * - How to surface errors? Exceptions? + */ + +/* FatFs docs http://elm-chan.org/fsw/ff/00index_e.html */ + +/* BufferType requires the following members + * Size size() + * Result read(void* data, Size bytes_to_read) + * Result write(const void* data, Size bytes_to_write) + * Result seek(uint32_t offset) + * Result truncate() + * Optional sync() + */ + +/* Wraps a buffer and provides an API for accessing lines efficiently. */ +template +class BufferWrapper { + public: + using Offset = uint32_t; + using Line = uint32_t; + using Column = uint32_t; + using Size = File::Size; + using Range = struct { + // Offset of the start, inclusive. + Offset start; + // Offset of the end, exclusive. + Offset end; + + Offset length() const { return end - start; } + }; + + BufferWrapper(BufferType* buffer) + : wrapped_{buffer} { + initialize(); + } + virtual ~BufferWrapper() {} + + std::function on_read_progress{}; + + /* Prevent copies */ + BufferWrapper(const BufferWrapper&) = delete; + BufferWrapper& operator=(const BufferWrapper&) = delete; + + Optional get_text(Line line, Column col, Offset length) { + std::string buffer; + buffer.resize(length); + + auto result = get_text(line, col, &buffer[0], length); + if (!result) + return {}; + + buffer.resize(*result); + return buffer; + } + + Optional get_text(Line line, Column col, char* output, Offset length) { + auto range = line_range(line); + int32_t to_read = length; + + if (!range) + return {}; + + // Don't read past end of line. + if (range->start + col + to_read >= range->end) + to_read = range->end - col - range->start; + + if (to_read <= 0) + return {}; + + return read(range->start + col, output, to_read); + } + + /* Gets the size of the buffer in bytes. */ + Size size() const { return wrapped_->size(); } + + /* Get the count of the lines in the buffer. */ + uint32_t line_count() const { return line_count_; } + + /* Gets the range of the line if valid. */ + Optional line_range(Line line) { + ensure_cached(line); + + auto index = index_for_line(line); + if (!index) + return {}; + + auto start = *index == 0 ? start_offset_ : (newlines_[*index - 1] + 1); + auto end = newlines_[*index] + 1; + + return Range{start, end}; + } + + /* Gets the length of the line, or 0 if invalid. */ + Offset line_length(Line line) { + auto range = line_range(line); + return range ? range->length() : 0; + } + + /* Gets the buffer offset of the line & col if valid. */ + Optional get_offset(Line line, Column col) { + auto range = line_range(line); + + if (range) + return range->start + col; + + return {}; + } + + /* Gets the index of the first line in the cache. + * Only really useful for unit testing or diagnostics. */ + Offset start_line() { return start_line_; }; + + /* Inserts a line before the specified line or at the + * end of the buffer if line >= line_count. */ + void insert_line(Line line) { + auto range = line_range(line); + + if (range) + replace_range({range->start, range->start}, "\n"); + else if (line >= line_count_) + replace_range({(Offset)size(), (Offset)size()}, "\n"); + } + + /* Deletes the specified line. */ + void delete_line(Line line) { + auto range = line_range(line); + + if (range) + replace_range(*range, {}); + } + + /* Replace the specified range with the string contents. + * A range with start/end set to the same value will insert. + * A range with an empty string will delete. */ + void replace_range(Range range, std::string_view value) { + if (range.start > size() || range.end > size() || range.start > range.end) + return; + + /* If delta_length == 0, it's an overwrite. Could still have + * added or removed newlines so caches will need to be rebuilt. + * If delta_length > 0, the file needs to grow and content needs + * to be shifted forward until the end of the range. + * If delta_length < 0, the file needs to be truncated and the + * content after the value needs to be shifted backward. */ + int32_t delta_length = value.length() - range.length(); + if (delta_length > 0) + expand(range.end, delta_length); + else if (delta_length < 0) + shrink(range.end, delta_length); + + write(range.start, value); + wrapped_->sync(); + rebuild_cache(); + } + + protected: + BufferWrapper() {} + + void set_buffer(BufferType* buffer) { + wrapped_ = buffer; + initialize(); + } + + private: + /* Number of newline offsets to cache. */ + static constexpr Offset max_newlines = CacheSize; + + /* Size of stack buffer used for reading/writing. */ + static constexpr Offset buffer_size = 512; + + void initialize() { + start_offset_ = 0; + start_line_ = 0; + line_count_ = 0; + rebuild_cache(); + } + + void rebuild_cache() { + newlines_.clear(); + + // Special case for empty files to keep them consistent. + if (size() == 0) { + line_count_ = 1; + newlines_.push_back(0); + return; + } + + // TODO: think through this for edit cases. + // E.g. don't read to end, maybe could specify + // a range to re-read because it should be possible + // to tell where the dirty regions are. After the + // dirty region, it should be possible to fixup + // the line_count data. + // TODO: seems like shrink/expand could do this while + // they are running. + + line_count_ = start_line_; + Offset offset = start_offset_; + + // Report progress every N lines. + constexpr auto report_interval = 100u; + auto result = next_newline(offset); + auto next_report = report_interval; + + while (result) { + ++line_count_; + if (newlines_.size() < max_newlines) + newlines_.push_back(*result); + offset = *result + 1; + + if (on_read_progress && line_count_ > next_report) { + on_read_progress(offset, size()); + next_report = line_count_ + report_interval; + } + + result = next_newline(offset); + } + } + + Optional read(Offset offset, char* buffer, Offset length) { + if (offset + length > size()) + return {}; + + wrapped_->seek(offset); + + auto result = wrapped_->read(buffer, length); + if (result.is_error()) + return {}; + + return *result; + } + + bool write(Offset offset, std::string_view value) { + wrapped_->seek(offset); + auto result = wrapped_->write(value.data(), value.length()); + + return result.is_ok(); + } + + /* Returns the index of the line in the newline cache if valid. */ + Optional index_for_line(Line line) const { + if (line >= line_count_) + return {}; + + Offset actual = line - start_line_; + if (actual >= newlines_.size()) // NB: underflow wrap. + return {}; + + return actual; + } + + /* Ensure specified line is in the newline cache. */ + void ensure_cached(Line line) { + if (line >= line_count_) + return; + + auto index = index_for_line(line); + if (index) + return; + + if (line < start_line_) { + while (line < start_line_ && start_offset_ >= 2) { + // start_offset_ - 1 should be a newline. Need to + // find the new value for start_offset_. start_line_ + // has to be > 0 to get into this block so there should + // always be one newline before start_offset_. + auto offset = previous_newline(start_offset_ - 2); + newlines_.push_front(start_offset_ - 1); + + if (!offset) { + // Must be at beginning. + start_line_ = 0; + start_offset_ = 0; + } else { + // Found an previous newline, the new start_line_ + // starts at the newline offset + 1. + start_line_--; + start_offset_ = *offset + 1; + } + } + } else { + while (line >= start_line_ + newlines_.size()) { + auto offset = next_newline(newlines_.back() + 1); + if (offset) { + start_line_++; + start_offset_ = newlines_.front() + 1; + newlines_.push_back(*offset); + } /* else at the EOF. */ + } + } + } + + /* Finding the first newline backward from offset. */ + Optional previous_newline(Offset offset) { + char buffer[buffer_size]; + auto to_read = buffer_size; + + do { + if (offset < to_read) { + // NB: Char at 'offset' was read in the previous iteration. + to_read = offset; + offset = 0; + } else + offset -= to_read; + + wrapped_->seek(offset); + + auto result = wrapped_->read(buffer, to_read); + if (result.is_error()) + break; + + // Find newlines in the buffer backwards. + for (int32_t i = *result - 1; i >= 0; --i) { + switch (buffer[i]) { + case '\n': + return offset + i; + } + } + + if (offset == 0) + break; + + } while (true); + + return {}; // Didn't find one. + } + + /* Finding the first newline forward from offset. */ + Optional next_newline(Offset offset) { + // EOF, no more newlines to find. + if (offset >= size()) + return {}; + + char buffer[buffer_size]; + wrapped_->seek(offset); + + while (true) { + auto result = wrapped_->read(buffer, buffer_size); + if (result.is_error()) + return {}; + + // Find newlines in the buffer. + for (Offset i = 0; i < *result; ++i) { + switch (buffer[i]) { + case '\n': + return offset + i; + } + } + + offset += *result; + + if (*result < buffer_size) + break; + } + + // For consistency, treat the end of the file as a "newline". + return size() - 1; + } + + /* Grow the file and move file content so that the + * content at src is shifted forward by 'delta'. */ + void expand(Offset src, int32_t delta) { + if (delta <= 0) // Not an expand. + return; + + char buffer[buffer_size]; + auto to_read = buffer_size; + + // Number of bytes left to shift. + Offset remaining = size() - src; + Offset offset = size(); + Size report_total = remaining; + Size report_interval = report_total / 8; + Size next_report = remaining - report_interval; + + while (remaining > 0) { + offset -= std::min(remaining, buffer_size); + to_read = std::min(remaining, buffer_size); + + wrapped_->seek(offset); + auto result = wrapped_->read(buffer, to_read); + if (result.is_error()) + break; + + wrapped_->seek(offset + delta); + result = wrapped_->write(buffer, *result); + if (result.is_error()) + break; + + remaining -= *result; + + if (on_read_progress && remaining <= next_report) { + on_read_progress(report_total - remaining, report_total); + next_report = remaining > report_interval ? remaining - report_interval : 0; + } + } + } + + /* Shrink the file and move file content so that the + * content at src is shifted backward by 'delta'. */ + void shrink(Offset src, int32_t delta) { + if (delta >= 0) // Not a shrink. + return; + + char buffer[buffer_size]; + auto offset = src; + Size report_total = size(); + Size report_interval = report_total / 8; + Size next_report = offset + report_interval; + + while (true) { + wrapped_->seek(offset); + auto result = wrapped_->read(buffer, buffer_size); + if (result.is_error()) + break; + + wrapped_->seek(offset + delta); + result = wrapped_->write(buffer, *result); + + if (result.is_error() || *result < buffer_size) + break; + + offset += *result; + + if (on_read_progress && offset >= next_report) { + on_read_progress(offset, report_total); + next_report = offset + report_interval; + } + } + + // Delete the extra bytes at the end of the file. + wrapped_->truncate(); + } + + BufferType* wrapped_{}; + + /* Total number of lines in the buffer. */ + Offset line_count_{0}; + + /* The offset and line of the newlines cache. */ + Offset start_offset_{0}; + Offset start_line_{0}; + + LineEnding line_ending_{LineEnding::LF}; + CircularBuffer newlines_{}; +}; + +/* A BufferWrapper over a file. */ +class FileWrapper : public BufferWrapper { + public: + template + using Result = File::Result; + using Error = File::Error; + static Result> open( + const std::filesystem::path& path, + bool create = false, + std::function on_read_progress = nullptr) { + auto fw = std::unique_ptr(new FileWrapper()); + auto error = fw->file_.open(path, /*read_only*/ false, create); + + if (error) + return *error; + + if (on_read_progress) + fw->on_read_progress = on_read_progress; + + fw->initialize(); + return fw; + } + + /* Underlying file. */ + File& file() { return file_; } + + /* Swaps out the underlying file for the specified file. + * The swapped file is expected have the same contents. + * For copy-on-write scenario with a temp file. */ + bool assume_file(const std::filesystem::path& path) { + File file; + auto error = file.open(path, /*read_only*/ false); + + if (error) + return false; + + file_ = std::move(file); + return true; + } + + private: + FileWrapper() {} + void initialize() { + set_buffer(&file_); + } + + File file_{}; +}; + +template +BufferWrapper wrap_buffer(T& buffer) { + return {&buffer}; +} + +#endif // __FILE_WRAPPER_HPP__ \ No newline at end of file diff --git a/firmware/standalone/digitalrain/ui/mathdef.hpp b/firmware/standalone/digitalrain/ui/mathdef.hpp new file mode 100644 index 000000000..52283bcac --- /dev/null +++ b/firmware/standalone/digitalrain/ui/mathdef.hpp @@ -0,0 +1,4 @@ +#pragma once + +#define PI 3.1415926535897932384626433832795 +#define M_PI PI \ No newline at end of file diff --git a/firmware/standalone/digitalrain/ui/optional.hpp b/firmware/standalone/digitalrain/ui/optional.hpp new file mode 100644 index 000000000..38e492f69 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/optional.hpp @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2016 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __OPTIONAL_H__ +#define __OPTIONAL_H__ + +#include + +template +class Optional { + public: + constexpr Optional() + : value_{}, valid_{false} {} + constexpr Optional(const T& value) + : value_{value}, valid_{true} {} + constexpr Optional(T&& value) + : value_{std::move(value)}, valid_{true} {} + + constexpr Optional& operator=(const T& value) { + value_ = value; + valid_ = true; + return *this; + } + constexpr Optional& operator=(T&& value) { + value_ = std::move(value); + valid_ = true; + return *this; + } + + bool is_valid() const { return valid_; } + operator bool() const { return valid_; } + + // TODO: Throw if not valid? + T& value() & { return value_; } + T& operator*() & { return value_; } + const T& value() const& { return value_; } + const T& operator*() const& { return value_; } + + T&& value() && { return std::move(value_); } + T&& operator*() && { return std::move(value_); } + + T* operator->() { return &value_; } + const T* operator->() const { return &value_; } + + private: + T value_; + bool valid_; +}; + +#endif /*__OPTIONAL_H__*/ diff --git a/firmware/standalone/digitalrain/ui/result.hpp b/firmware/standalone/digitalrain/ui/result.hpp new file mode 100644 index 000000000..0c87c6cb8 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/result.hpp @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 Kyle Reed + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * 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 + +template +struct Result { + enum class Type { + Success, + Error, + } type; + + union { + TValue value_; + TError error_; + }; + + bool is_ok() const { + return type == Type::Success; + } + + operator bool() const { + return is_ok(); + } + + bool is_error() const { + return type == Type::Error; + } + + const TValue& value() const& { + return value_; + } + + const TValue& operator*() const& { + return value_; + } + + TValue&& operator*() && { + return std::move(value_); + } + + const TError& error() const& { + return error_; + } + + Result() = delete; + + constexpr Result(TValue&& value) + : type{Type::Success}, + value_{std::forward(value)} { + } + + constexpr Result(TError error) + : type{Type::Error}, + error_{error} { + } + + ~Result() { + if (is_ok()) + value_.~TValue(); + else + error_.~TError(); + } +}; \ No newline at end of file diff --git a/firmware/standalone/digitalrain/ui/sine_table.hpp b/firmware/standalone/digitalrain/ui/sine_table.hpp new file mode 100644 index 000000000..b0d72e6a0 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/sine_table.hpp @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __SINE_TABLE_H__ +#define __SINE_TABLE_H__ + +// TODO: Including only for pi. Need separate math.hpp... +#include "complex.hpp" + +#include +#include + +/* +import numpy +length = 256 +w = numpy.arange(length, dtype=numpy.float64) * (2 * numpy.pi / length) +v = numpy.sin(w) +print(v) +*/ +constexpr uint16_t sine_table_f32_period = 256; +// periode is 256 . means sine_table_f32[0]= sine_table_f32[0+256], sine_table_f32[1]=sine_table_f32[1+256] (those two added manualy) +// Then table has 258 values ,256:[0,..255] + [256] and [257], those two are used when we interpolate[255] with [255+1], and [256] with [256+1] +// [256] index is needed in the function sin_f32() when we are inputing very small radian values , example , sin_f32((-1e-14) in radians) + +static constexpr std::array sine_table_f32{ + 0.00000000e+00, 2.45412285e-02, 4.90676743e-02, + 7.35645636e-02, 9.80171403e-02, 1.22410675e-01, + 1.46730474e-01, 1.70961889e-01, 1.95090322e-01, + 2.19101240e-01, 2.42980180e-01, 2.66712757e-01, + 2.90284677e-01, 3.13681740e-01, 3.36889853e-01, + 3.59895037e-01, 3.82683432e-01, 4.05241314e-01, + 4.27555093e-01, 4.49611330e-01, 4.71396737e-01, + 4.92898192e-01, 5.14102744e-01, 5.34997620e-01, + 5.55570233e-01, 5.75808191e-01, 5.95699304e-01, + 6.15231591e-01, 6.34393284e-01, 6.53172843e-01, + 6.71558955e-01, 6.89540545e-01, 7.07106781e-01, + 7.24247083e-01, 7.40951125e-01, 7.57208847e-01, + 7.73010453e-01, 7.88346428e-01, 8.03207531e-01, + 8.17584813e-01, 8.31469612e-01, 8.44853565e-01, + 8.57728610e-01, 8.70086991e-01, 8.81921264e-01, + 8.93224301e-01, 9.03989293e-01, 9.14209756e-01, + 9.23879533e-01, 9.32992799e-01, 9.41544065e-01, + 9.49528181e-01, 9.56940336e-01, 9.63776066e-01, + 9.70031253e-01, 9.75702130e-01, 9.80785280e-01, + 9.85277642e-01, 9.89176510e-01, 9.92479535e-01, + 9.95184727e-01, 9.97290457e-01, 9.98795456e-01, + 9.99698819e-01, 1.00000000e+00, 9.99698819e-01, + 9.98795456e-01, 9.97290457e-01, 9.95184727e-01, + 9.92479535e-01, 9.89176510e-01, 9.85277642e-01, + 9.80785280e-01, 9.75702130e-01, 9.70031253e-01, + 9.63776066e-01, 9.56940336e-01, 9.49528181e-01, + 9.41544065e-01, 9.32992799e-01, 9.23879533e-01, + 9.14209756e-01, 9.03989293e-01, 8.93224301e-01, + 8.81921264e-01, 8.70086991e-01, 8.57728610e-01, + 8.44853565e-01, 8.31469612e-01, 8.17584813e-01, + 8.03207531e-01, 7.88346428e-01, 7.73010453e-01, + 7.57208847e-01, 7.40951125e-01, 7.24247083e-01, + 7.07106781e-01, 6.89540545e-01, 6.71558955e-01, + 6.53172843e-01, 6.34393284e-01, 6.15231591e-01, + 5.95699304e-01, 5.75808191e-01, 5.55570233e-01, + 5.34997620e-01, 5.14102744e-01, 4.92898192e-01, + 4.71396737e-01, 4.49611330e-01, 4.27555093e-01, + 4.05241314e-01, 3.82683432e-01, 3.59895037e-01, + 3.36889853e-01, 3.13681740e-01, 2.90284677e-01, + 2.66712757e-01, 2.42980180e-01, 2.19101240e-01, + 1.95090322e-01, 1.70961889e-01, 1.46730474e-01, + 1.22410675e-01, 9.80171403e-02, 7.35645636e-02, + 4.90676743e-02, 2.45412285e-02, 1.22464680e-16, + -2.45412285e-02, -4.90676743e-02, -7.35645636e-02, + -9.80171403e-02, -1.22410675e-01, -1.46730474e-01, + -1.70961889e-01, -1.95090322e-01, -2.19101240e-01, + -2.42980180e-01, -2.66712757e-01, -2.90284677e-01, + -3.13681740e-01, -3.36889853e-01, -3.59895037e-01, + -3.82683432e-01, -4.05241314e-01, -4.27555093e-01, + -4.49611330e-01, -4.71396737e-01, -4.92898192e-01, + -5.14102744e-01, -5.34997620e-01, -5.55570233e-01, + -5.75808191e-01, -5.95699304e-01, -6.15231591e-01, + -6.34393284e-01, -6.53172843e-01, -6.71558955e-01, + -6.89540545e-01, -7.07106781e-01, -7.24247083e-01, + -7.40951125e-01, -7.57208847e-01, -7.73010453e-01, + -7.88346428e-01, -8.03207531e-01, -8.17584813e-01, + -8.31469612e-01, -8.44853565e-01, -8.57728610e-01, + -8.70086991e-01, -8.81921264e-01, -8.93224301e-01, + -9.03989293e-01, -9.14209756e-01, -9.23879533e-01, + -9.32992799e-01, -9.41544065e-01, -9.49528181e-01, + -9.56940336e-01, -9.63776066e-01, -9.70031253e-01, + -9.75702130e-01, -9.80785280e-01, -9.85277642e-01, + -9.89176510e-01, -9.92479535e-01, -9.95184727e-01, + -9.97290457e-01, -9.98795456e-01, -9.99698819e-01, + -1.00000000e+00, -9.99698819e-01, -9.98795456e-01, + -9.97290457e-01, -9.95184727e-01, -9.92479535e-01, + -9.89176510e-01, -9.85277642e-01, -9.80785280e-01, + -9.75702130e-01, -9.70031253e-01, -9.63776066e-01, + -9.56940336e-01, -9.49528181e-01, -9.41544065e-01, + -9.32992799e-01, -9.23879533e-01, -9.14209756e-01, + -9.03989293e-01, -8.93224301e-01, -8.81921264e-01, + -8.70086991e-01, -8.57728610e-01, -8.44853565e-01, + -8.31469612e-01, -8.17584813e-01, -8.03207531e-01, + -7.88346428e-01, -7.73010453e-01, -7.57208847e-01, + -7.40951125e-01, -7.24247083e-01, -7.07106781e-01, + -6.89540545e-01, -6.71558955e-01, -6.53172843e-01, + -6.34393284e-01, -6.15231591e-01, -5.95699304e-01, + -5.75808191e-01, -5.55570233e-01, -5.34997620e-01, + -5.14102744e-01, -4.92898192e-01, -4.71396737e-01, + -4.49611330e-01, -4.27555093e-01, -4.05241314e-01, + -3.82683432e-01, -3.59895037e-01, -3.36889853e-01, + -3.13681740e-01, -2.90284677e-01, -2.66712757e-01, + -2.42980180e-01, -2.19101240e-01, -1.95090322e-01, + -1.70961889e-01, -1.46730474e-01, -1.22410675e-01, + -9.80171403e-02, -7.35645636e-02, -4.90676743e-02, + -2.45412285e-02, 0.00000000e+00, 2.45412285e-02}; + +inline float sin_f32(const float w) { + const float x = w / (2 * pi); // normalization + const float x_frac = x - std::floor(x); // [0, 1] + + const float n = x_frac * sine_table_f32_period; + const uint16_t n_int = static_cast(n); + const float n_frac = n - n_int; + + const float p0 = sine_table_f32[n_int]; + const float p1 = sine_table_f32[n_int + 1]; + const float diff = p1 - p0; + const float result = p0 + n_frac * diff; // linear interpolation + + return result; +} + +#endif /*__SINE_TABLE_H__*/ diff --git a/firmware/standalone/digitalrain/ui/standalone_application.hpp b/firmware/standalone/digitalrain/ui/standalone_application.hpp new file mode 100644 index 000000000..715fbed40 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/standalone_application.hpp @@ -0,0 +1 @@ +#include "standalone_app.hpp" \ No newline at end of file diff --git a/firmware/standalone/digitalrain/ui/string_format.cpp b/firmware/standalone/digitalrain/ui/string_format.cpp new file mode 100644 index 000000000..ddfa29ff4 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/string_format.cpp @@ -0,0 +1,569 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "string_format.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::literals; + +/* This takes a pointer to the end of a buffer + * and fills it backwards towards the front. + * The return value 'q' is a pointer to the start. + * TODO: use std::array for all this. */ +template +static char *to_string_dec_uint_internal( + char *p, + Int n) +{ + *p = 0; + auto q = p; + + do + { + *(--q) = n % 10 + '0'; + n /= 10; + } while (n != 0); + + return q; +} + +static char *to_string_dec_uint_pad_internal( + char *const term, + const uint32_t n, + const int32_t l, + const char fill) +{ + auto q = to_string_dec_uint_internal(term, n); + + // Fill with padding if needed. + // TODO: use std::array instead. There's no + // bounds checks on any of this! + if (fill) + { + while ((term - q) < l) + { + *(--q) = fill; + } + } + + return q; +} + +static char *to_string_dec_uint_internal(uint64_t n, StringFormatBuffer &buffer, size_t &length) +{ + auto end = &buffer.back(); + auto start = to_string_dec_uint_internal(end, n); + length = end - start; + return start; +} + +char *to_string_dec_uint(uint64_t n, StringFormatBuffer &buffer, size_t &length) +{ + return to_string_dec_uint_internal(n, buffer, length); +} + +char *to_string_dec_int(int64_t n, StringFormatBuffer &buffer, size_t &length) +{ + bool negative = n < 0; + auto start = to_string_dec_uint(negative ? -n : n, buffer, length); + + if (negative) + { + *(--start) = '-'; + ++length; + } + + return start; +} + +std::string to_string_dec_int(int64_t n) +{ + StringFormatBuffer b{}; + size_t len{}; + char *str = to_string_dec_int(n, b, len); + return std::string(str, len); +} + +std::string to_string_dec_uint(uint64_t n) +{ + StringFormatBuffer b{}; + size_t len{}; + char *str = to_string_dec_uint(n, b, len); + return std::string(str, len); +} + +std::string to_string_bin( + const uint32_t n, + const uint8_t l) +{ + char p[33]; + for (uint8_t c = 0; c < l; c++) + { + if (n & (1 << (l - 1 - c))) + p[c] = '1'; + else + p[c] = '0'; + } + p[l] = 0; + return p; +} + +std::string to_string_dec_uint( + const uint32_t n, + const int32_t l, + const char fill) +{ + char p[16]; + auto term = p + sizeof(p) - 1; + auto q = to_string_dec_uint_pad_internal(term, n, l, fill); + + // Right justify. + // (This code is redundant and won't do anything if a fill character was specified) + while ((term - q) < l) + { + *(--q) = ' '; + } + + return q; +} + +std::string to_string_dec_int( + const int32_t n, + const int32_t l, + const char fill) +{ + const size_t negative = (n < 0) ? 1 : 0; + uint32_t n_abs = negative ? -n : n; + + char p[16]; + auto term = p + sizeof(p) - 1; + auto q = to_string_dec_uint_pad_internal(term, n_abs, l - negative, fill); + + // Add sign. + if (negative) + { + *(--q) = '-'; + } + + // Right justify. + // (This code is redundant and won't do anything if a fill character was specified) + while ((term - q) < l) + { + *(--q) = ' '; + } + + return q; +} + +std::string to_string_decimal(float decimal, int8_t precision) +{ + double integer_part; + double fractional_part; + + std::string result; + if (precision > 9) + precision = 9; // we will convert to uin32_t, and that is the max it can hold. + + fractional_part = modf(decimal, &integer_part) * pow(10, precision); + + if (fractional_part < 0) + { + fractional_part = -fractional_part; + } + + result = to_string_dec_int(integer_part) + "." + to_string_dec_uint(fractional_part, precision, '0'); + + return result; +} + +std::string to_string_decimal_padding(float decimal, int8_t precision, const int32_t l) +{ + double integer_part; + double fractional_part; + + std::string result; + if (precision > 9) + precision = 9; // we will convert to uin32_t, and that is the max it can hold. + + fractional_part = modf(decimal, &integer_part) * pow(10, precision); + + if (fractional_part < 0) + { + fractional_part = -fractional_part; + } + + result = to_string_dec_int(integer_part) + "." + to_string_dec_uint(fractional_part, precision, '0'); + + // Add padding with spaces to meet the length requirement + if (result.length() < (uint32_t)l) + { + int padding_length = l - result.length(); + std::string padding(padding_length, ' '); + result = padding + result; + } + + return result; +} + +// right-justified frequency in Hz, always 10 characters +std::string to_string_freq(const uint64_t f) +{ + std::string final_str{""}; + + if (f < 1000000) + final_str = to_string_dec_int(f, 10, ' '); + else + final_str = to_string_dec_int(f / 1000000, 4) + to_string_dec_int(f % 1000000, 6, '0'); + + return final_str; +} + +// right-justified frequency in MHz, rounded to 4 decimal places, always 9 characters +std::string to_string_short_freq(const uint64_t f) +{ + auto final_str = to_string_dec_int((f + 50) / 1000000, 4) + "." + to_string_dec_int(((f + 50) / 100) % 10000, 4, '0'); + return final_str; +} + +// non-justified non-padded frequency in MHz, rounded to specified number of decimal places +std::string to_string_rounded_freq(const uint64_t f, int8_t precision) +{ + std::string final_str{""}; + static constexpr uint32_t pow10[7] = { + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + }; + + if (precision < 1) + { + final_str = to_string_dec_uint(f / 1000000); + } + else + { + if (precision > 6) + precision = 6; + + uint32_t divisor = pow10[6 - precision]; + + final_str = to_string_dec_uint((f + (divisor / 2)) / 1000000) + "." + to_string_dec_int(((f + (divisor / 2)) / divisor) % pow10[precision], precision, '0'); + } + return final_str; +} + +std::string to_string_time_ms(const uint32_t ms) +{ + std::string final_str{""}; + + if (ms < 1000) + { + final_str = to_string_dec_uint(ms) + "ms"; + } + else + { + auto seconds = ms / 1000; + + if (seconds >= 60) + final_str = to_string_dec_uint(seconds / 60) + "m"; + + return final_str + to_string_dec_uint(seconds % 60) + "s"; + } + + return final_str; +} + +static char *to_string_hex_internal(char *ptr, uint64_t value, uint8_t length) +{ + if (length == 0) + return ptr; + + *(--ptr) = uint_to_char(value & 0xF, 16); + return to_string_hex_internal(ptr, value >> 4, length - 1); +} + +std::string to_string_hex(uint64_t value, int32_t length) +{ + constexpr uint8_t buffer_length = 33; + char buffer[buffer_length]; + + char *ptr = &buffer[buffer_length - 1]; + *ptr = '\0'; + + length = std::min(buffer_length - 1, length); + return to_string_hex_internal(ptr, value, length); +} + +std::string to_string_hex_array(uint8_t *array, int32_t length) +{ + std::string str_return; + str_return.reserve(length * 2); + + for (uint8_t i = 0; i < length; i++) + str_return += to_string_hex(array[i], 2); + + return str_return; +} +// TODO: wire standalone api: +/* +std::string to_string_datetime(const rtc::RTC &value, const TimeFormat format) +{ + std::string string{""}; + + if (format == YMDHMS) + { + string += to_string_dec_uint(value.year(), 4) + "-" + + to_string_dec_uint(value.month(), 2, '0') + "-" + + to_string_dec_uint(value.day(), 2, '0') + " "; + } + + string += to_string_dec_uint(value.hour(), 2, '0') + ":" + + to_string_dec_uint(value.minute(), 2, '0'); + + if ((format == YMDHMS) || (format == HMS)) + string += ":" + to_string_dec_uint(value.second(), 2, '0'); + + return string; +} + +std::string to_string_timestamp(const rtc::RTC &value) +{ + return to_string_dec_uint(value.year(), 4, '0') + + to_string_dec_uint(value.month(), 2, '0') + + to_string_dec_uint(value.day(), 2, '0') + + to_string_dec_uint(value.hour(), 2, '0') + + to_string_dec_uint(value.minute(), 2, '0') + + to_string_dec_uint(value.second(), 2, '0'); +} + +std::string to_string_FAT_timestamp(const FATTimestamp ×tamp) +{ + return to_string_dec_uint((timestamp.FAT_date >> 9) + 1980) + "-" + + to_string_dec_uint((timestamp.FAT_date >> 5) & 0xF, 2, '0') + "-" + + to_string_dec_uint((timestamp.FAT_date & 0x1F), 2, '0') + " " + + to_string_dec_uint((timestamp.FAT_time >> 11), 2, '0') + ":" + + to_string_dec_uint((timestamp.FAT_time >> 5) & 0x3F, 2, '0'); +} +*/ +std::string to_string_file_size(uint32_t file_size) +{ + static const std::string suffix[5] = {"B", "kB", "MB", "GB", "??"}; + size_t suffix_index = 0; + + while (file_size >= 1024) + { + file_size /= 1024; + suffix_index++; + } + + if (suffix_index > 4) + suffix_index = 4; + + return to_string_dec_uint(file_size) + suffix[suffix_index]; +} + +std::string to_string_mac_address(const uint8_t *macAddress, uint8_t length, bool noColon) +{ + std::string string; + + string += to_string_hex(macAddress[0], 2); + + for (int i = 1; i < length; i++) + { + string += noColon ? to_string_hex(macAddress[i], 2) : ":" + to_string_hex(macAddress[i], 2); + } + + return string; +} + +std::string to_string_formatted_mac_address(const char *macAddress) +{ + std::string formattedAddress; + + for (int i = 0; i < 12; i += 2) + { + if (i > 0) + { + formattedAddress += ':'; + } + formattedAddress += macAddress[i]; + formattedAddress += macAddress[i + 1]; + } + + return formattedAddress; +} + +void generateRandomMacAddress(char *macAddress) +{ + const char hexDigits[] = "0123456789ABCDEF"; + + // Generate 12 random hexadecimal characters + for (int i = 0; i < 12; i++) + { + int randomIndex = rand() % 16; + macAddress[i] = hexDigits[randomIndex]; + } + macAddress[12] = '\0'; // Null-terminate the string +} + +// TODO: wire standalone api: +/* +uint64_t readUntil(File &file, char *result, std::size_t maxBufferSize, char delimiter) +{ + std::size_t bytesRead = 0; + + while (true) + { + char ch; + File::Result readResult = file.read(&ch, 1); + + if (readResult.is_ok() && readResult.value() > 0) + { + if (ch == delimiter) + { + // Found a space character, stop reading + break; + } + else if (bytesRead < maxBufferSize) + { + // Append the character to the result if there's space + result[bytesRead++] = ch; + } + else + { + // Buffer is full, break to prevent overflow + break; + } + } + else + { + break; // End of file or error + } + } + + // Null-terminate the result string + result[bytesRead] = '\0'; + + return bytesRead; +} +*/ +std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision) +{ + const uint32_t powers_of_ten[5] = {1, 10, 100, 1000, 10000}; + std::string string{""}; + uint32_t prefix_index = base_unit; + double integer_part; + double fractional_part; + + precision = std::min((uint32_t)4, precision); + + while (n > 1000) + { + n /= 1000.0; + prefix_index++; + } + + fractional_part = modf(n, &integer_part) * powers_of_ten[precision]; + if (fractional_part < 0) + fractional_part = -fractional_part; + + string = to_string_dec_int(integer_part); + if (precision) + string += '.' + to_string_dec_uint(fractional_part, precision, '0'); + + if (prefix_index != 3) + string += unit_prefix[prefix_index]; + + return string; +} + +double get_decimals(double num, int16_t mult, bool round) +{ + num -= int(num); // keep decimals only + num *= mult; // Shift decimals into integers + if (!round) + return num; + int16_t intnum = int(num); // Round it up if necessary + num -= intnum; // Get decimal part + if (num > .5) + intnum++; // Round up + return intnum; +} + +static const char *whitespace_str = " \t\r\n"; + +std::string trim(std::string_view str) +{ + auto first = str.find_first_not_of(whitespace_str); + if (first == std::string::npos) + return {}; + + auto last = str.find_last_not_of(whitespace_str); + return std::string{str.substr(first, last - first + 1)}; +} + +std::string trimr(std::string_view str) +{ + size_t last = str.find_last_not_of(whitespace_str); + return std::string{last != std::string::npos ? str.substr(0, last + 1) : ""}; +} + +std::string truncate(std::string_view str, size_t length) +{ + return std::string{str.length() <= length ? str : str.substr(0, length)}; +} + +uint8_t char_to_uint(char c, uint8_t radix) +{ + uint8_t v = 0; + + if (c >= '0' && c <= '9') + v = c - '0'; + else if (c >= 'A' && c <= 'F') + v = c - 'A' + 10; // A is dec: 10 + else if (c >= 'a' && c <= 'f') + v = c - 'a' + 10; // A is dec: 10 + + return v < radix ? v : 0; +} + +char uint_to_char(uint8_t val, uint8_t radix) +{ + if (val >= radix) + return 0; + + if (val < 10) + return '0' + val; + else + return 'A' + val - 10; // A is dec: 10 +} diff --git a/firmware/standalone/digitalrain/ui/string_format.hpp b/firmware/standalone/digitalrain/ui/string_format.hpp new file mode 100644 index 000000000..e2dbb992b --- /dev/null +++ b/firmware/standalone/digitalrain/ui/string_format.hpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __STRING_FORMAT_H__ +#define __STRING_FORMAT_H__ + +#include +#include +#include +#include + +// #include "file.hpp" + +// BARF! rtc::RTC is leaking everywhere. +// #include "lpc43xx_cpp.hpp" +// using namespace lpc43xx; + +enum TimeFormat +{ + YMDHMS = 0, + HMS = 1, + HM = 2 +}; + +const char unit_prefix[7]{'n', 'u', 'm', 0, 'k', 'M', 'G'}; + +using StringFormatBuffer = std::array; + +/* Integer conversion without memory allocations. */ +char *to_string_dec_int(int64_t n, StringFormatBuffer &buffer, size_t &length); +char *to_string_dec_uint(uint64_t n, StringFormatBuffer &buffer, size_t &length); + +std::string to_string_dec_int(int64_t n); +std::string to_string_dec_uint(uint64_t n); + +std::string to_string_bin(const uint32_t n, const uint8_t l = 0); +std::string to_string_dec_uint(const uint32_t n, const int32_t l, const char fill = ' '); +std::string to_string_dec_int(const int32_t n, const int32_t l, const char fill = 0); +std::string to_string_decimal(float decimal, int8_t precision); +std::string to_string_decimal_padding(float decimal, int8_t precision, const int32_t l); + +std::string to_string_hex(uint64_t n, int32_t length); +std::string to_string_hex_array(uint8_t *array, int32_t length); + +/* Helper to select length based on type size. */ +template +std::string to_string_hex(T n) +{ + return to_string_hex(n, sizeof(T) * 2); // Two digits/byte. +} + +std::string to_string_freq(const uint64_t f); +std::string to_string_short_freq(const uint64_t f); +std::string to_string_rounded_freq(const uint64_t f, int8_t precision); +std::string to_string_time_ms(const uint32_t ms); + +// TODO: wire standalone api: std::string to_string_datetime(const rtc::RTC &value, const TimeFormat format = YMDHMS); +// TODO: wire standalone api: std::string to_string_timestamp(const rtc::RTC &value); +// TODO: wire standalone api: std::string to_string_FAT_timestamp(const FATTimestamp ×tamp); + +// Gets a human readable file size string. +std::string to_string_file_size(uint32_t file_size); + +// Converts Mac Address to string. +std::string to_string_mac_address(const uint8_t *macAddress, uint8_t length, bool noColon); +std::string to_string_formatted_mac_address(const char *macAddress); +void generateRandomMacAddress(char *macAddress); + +// TODO: wire standalone api: uint64_t readUntil(File &file, char *result, std::size_t maxBufferSize, char delimiter); + +/* Scales 'n' to be a value less than 1000. 'base_unit' is the index of the unit from + * 'unit_prefix' that 'n' is in initially. 3 is the index of the '1s' unit. */ +std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision); +double get_decimals(double num, int16_t mult, bool round = false); + +std::string trim(std::string_view str); // Remove whitespace at ends. +std::string trimr(std::string_view str); // Remove trailing spaces +std::string truncate(std::string_view, size_t length); + +/* Gets the int value for a character given the radix. + * e.g. '5' => 5, 'D' => 13. Out of bounds => 0. */ +uint8_t char_to_uint(char c, uint8_t radix = 10); + +/* Gets the int value for a character given the radix. + * e.g. 5 => '5', 13 => 'D'. Out of bounds => '\0'. */ +char uint_to_char(uint8_t val, uint8_t radix = 10); + +#endif /*__STRING_FORMAT_H__*/ diff --git a/firmware/standalone/digitalrain/ui/theme.cpp b/firmware/standalone/digitalrain/ui/theme.cpp new file mode 100644 index 000000000..27b2e5553 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/theme.cpp @@ -0,0 +1,870 @@ +#include "theme.hpp" + +namespace ui { + +ThemeTemplate* Theme::current = nullptr; + +ThemeTemplate* Theme::getInstance() { + if (current == nullptr) SetTheme(DefaultGrey); + return Theme::current; +} + +void Theme::destroy() { + if (current != nullptr) { + delete current; + current = nullptr; + } +} + +void Theme::SetTheme(ThemeId theme) { + if (current != nullptr) delete current; + switch (theme) { + case Yellow: + current = new ThemeYellow(); + break; + case Aqua: + current = new ThemeAqua(); + break; + case Green: + current = new ThemeGreen(); + break; + case Red: + current = new ThemeRed(); + break; + case Dark: + current = new ThemeDark(); + break; + case DefaultGrey: + default: + current = new ThemeDefault(); + break; + } +} + +ThemeTemplate::~ThemeTemplate() { + delete bg_lightest; + delete bg_lightest_small; + delete bg_light; + delete bg_medium; + delete bg_dark; + delete bg_darker; + delete bg_darkest; + delete bg_darkest_small; + delete bg_important_small; + delete error_dark; + delete warning_dark; + delete ok_dark; + delete fg_dark; + delete fg_medium; + delete fg_light; + delete fg_red; + delete fg_green; + delete fg_yellow; + delete fg_orange; + delete fg_blue; + delete fg_cyan; + delete fg_darkcyan; + delete fg_magenta; + delete option_active; + delete status_active; // green, the status bar icons when active + delete bg_table_header; +} + +ThemeYellow::ThemeYellow() { + bg_lightest = new Style{ + .font = font::fixed_8x16(), + .background = {255, 255, 204}, + .foreground = Color::black(), + }; + bg_lightest_small = new Style{ + .font = font::fixed_5x8(), + .background = {255, 255, 204}, + .foreground = Color::black(), + }; + bg_light = new Style{ + .font = font::fixed_8x16(), + .background = {255, 255, 102}, + .foreground = Color::white(), + }; + bg_medium = new Style{ + .font = font::fixed_8x16(), + .background = {204, 204, 0}, + .foreground = Color::white(), + }; + bg_dark = new Style{ + .font = font::fixed_8x16(), + .background = {153, 153, 0}, + .foreground = Color::white(), + }; + bg_darker = new Style{ + .font = font::fixed_8x16(), + .background = {102, 102, 0}, + .foreground = Color::white(), + }; + + bg_darkest = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::white(), + }; + bg_darkest_small = new Style{ + .font = font::fixed_5x8(), + .background = {31, 31, 0}, + .foreground = Color::white(), + }; + + bg_important_small = new Style{ + .font = font::fixed_5x8(), + .background = Color::yellow(), + .foreground = {31, 31, 0}, + }; + + error_dark = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::red(), + }; + warning_dark = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::yellow(), + }; + ok_dark = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::green(), + }; + + fg_dark = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = {153, 153, 0}, + }; + fg_medium = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = {204, 204, 0}, + }; + fg_light = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::light_grey(), + }; + + fg_red = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::red(), + }; + fg_green = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::green(), + }; + fg_yellow = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::yellow(), + }; + fg_orange = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::orange(), + }; + fg_blue = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::blue(), + }; + fg_cyan = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::cyan(), + }; + fg_darkcyan = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::dark_cyan(), + }; + fg_magenta = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::magenta(), + }; + + option_active = new Style{ + .font = font::fixed_8x16(), + .background = Color::orange(), + .foreground = Color::white(), + }; + + status_active = new Color{0, 255, 0}; // green, the status bar icons when active + + bg_table_header = new Color{205, 205, 0}; +} + +ThemeAqua::ThemeAqua() { + bg_lightest = new Style{ + .font = font::fixed_8x16(), + .background = {204, 255, 255}, + .foreground = Color::black(), + }; + bg_lightest_small = new Style{ + .font = font::fixed_5x8(), + .background = {204, 255, 255}, + .foreground = Color::black(), + }; + bg_light = new Style{ + .font = font::fixed_8x16(), + .background = {102, 255, 255}, + .foreground = Color::white(), + }; + bg_medium = new Style{ + .font = font::fixed_8x16(), + .background = {0, 144, 200}, + .foreground = Color::white(), + }; + bg_dark = new Style{ + .font = font::fixed_8x16(), + .background = {0, 153, 153}, + .foreground = Color::white(), + }; + bg_darker = new Style{ + .font = font::fixed_8x16(), + .background = {0, 102, 102}, + .foreground = Color::white(), + }; + + bg_darkest = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::white(), + }; + bg_darkest_small = new Style{ + .font = font::fixed_5x8(), + .background = {0, 31, 31}, + .foreground = Color::white(), + }; + + bg_important_small = new Style{ + .font = font::fixed_5x8(), + .background = Color::yellow(), + .foreground = {0, 31, 31}, + }; + + error_dark = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::red(), + }; + warning_dark = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::yellow(), + }; + ok_dark = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::green(), + }; + + fg_dark = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = {0, 153, 153}, + }; + fg_medium = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = {0, 204, 204}, + }; + fg_light = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::light_grey(), + }; + + fg_red = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::red(), + }; + fg_green = new Style{ + .font = font::fixed_8x16(), + .background = {31, 31, 0}, + .foreground = Color::green(), + }; + fg_yellow = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::yellow(), + }; + fg_orange = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::orange(), + }; + fg_blue = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::blue(), + }; + fg_cyan = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::cyan(), + }; + fg_darkcyan = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::dark_cyan(), + }; + fg_magenta = new Style{ + .font = font::fixed_8x16(), + .background = {0, 31, 31}, + .foreground = Color::magenta(), + }; + + option_active = new Style{ + .font = font::fixed_8x16(), + .background = Color::blue(), + .foreground = Color::white(), + }; + + status_active = new Color{0, 255, 0}; // green, the status bar icons when active + + bg_table_header = new Color{0, 205, 205}; +} + +ThemeDefault::ThemeDefault() { + bg_lightest = new Style{ + .font = font::fixed_8x16(), + .background = Color::white(), + .foreground = Color::black(), + }; + bg_lightest_small = new Style{ + .font = font::fixed_5x8(), + .background = Color::white(), + .foreground = Color::black(), + }; + bg_light = new Style{ + .font = font::fixed_8x16(), + .background = Color::light_grey(), + .foreground = Color::white(), + }; + bg_medium = new Style{ + .font = font::fixed_8x16(), + .background = Color::grey(), + .foreground = Color::white(), + }; + bg_dark = new Style{ + .font = font::fixed_8x16(), + .background = Color::dark_grey(), + .foreground = Color::white(), + }; + bg_darker = new Style{ + .font = font::fixed_8x16(), + .background = Color::darker_grey(), + .foreground = Color::white(), + }; + + bg_darkest = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::white(), + }; + bg_darkest_small = new Style{ + .font = font::fixed_5x8(), + .background = Color::black(), + .foreground = Color::white(), + }; + + bg_important_small = new Style{ + .font = font::fixed_5x8(), + .background = Color::yellow(), + .foreground = Color::black(), + }; + + error_dark = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::red(), + }; + warning_dark = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::yellow(), + }; + ok_dark = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::green(), + }; + + fg_dark = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::dark_grey(), + }; + fg_medium = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::grey(), + }; + fg_light = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::light_grey(), + }; + + fg_red = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::red(), + }; + fg_green = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::green(), + }; + fg_yellow = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::yellow(), + }; + fg_orange = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::orange(), + }; + fg_blue = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::blue(), + }; + fg_cyan = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::cyan(), + }; + fg_darkcyan = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::dark_cyan(), + }; + fg_magenta = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::magenta(), + }; + + option_active = new Style{ + .font = font::fixed_8x16(), + .background = Color::blue(), + .foreground = Color::white(), + }; + + status_active = new Color{0, 255, 0}; // green, the status bar icons when active + bg_table_header = new Color{0, 0, 255}; +} + +ThemeGreen::ThemeGreen() { + bg_lightest = new Style{ + .font = font::fixed_8x16(), + .background = {0, 245, 29}, + .foreground = Color::black(), + }; + bg_lightest_small = new Style{ + .font = font::fixed_5x8(), + .background = {0, 245, 29}, + .foreground = Color::black(), + }; + bg_light = new Style{ + .font = font::fixed_8x16(), + .background = {0, 212, 25}, + .foreground = Color::white(), + }; + bg_medium = new Style{ + .font = font::fixed_8x16(), + .background = {0, 143, 17}, + .foreground = Color::white(), + }; + bg_dark = new Style{ + .font = font::fixed_8x16(), + .background = {0, 99, 12}, + .foreground = Color::white(), + }; + bg_darker = new Style{ + .font = font::fixed_8x16(), + .background = {0, 79, 9}, + .foreground = Color::white(), + }; + + bg_darkest = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::white(), + }; + bg_darkest_small = new Style{ + .font = font::fixed_5x8(), + .background = {0, 33, 4}, + .foreground = Color::white(), + }; + + bg_important_small = new Style{ + .font = font::fixed_5x8(), + .background = Color::yellow(), + .foreground = {0, 33, 4}, + }; + + error_dark = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::red(), + }; + warning_dark = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::yellow(), + }; + ok_dark = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::green(), + }; + + fg_dark = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = {0, 99, 12}, + }; + fg_medium = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = {0, 143, 17}, + }; + fg_light = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::light_grey(), + }; + + fg_red = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::red(), + }; + fg_green = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::green(), + }; + fg_yellow = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::yellow(), + }; + fg_orange = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::orange(), + }; + fg_blue = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::blue(), + }; + fg_cyan = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::cyan(), + }; + fg_darkcyan = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::dark_cyan(), + }; + fg_magenta = new Style{ + .font = font::fixed_8x16(), + .background = {0, 33, 4}, + .foreground = Color::magenta(), + }; + + option_active = new Style{ + .font = font::fixed_8x16(), + .background = Color::orange(), + .foreground = Color::white(), + }; + + status_active = new Color{0, 255, 0}; // green, the status bar icons when active + + bg_table_header = new Color{0, 205, 30}; +} + +ThemeRed::ThemeRed() { + bg_lightest = new Style{ + .font = font::fixed_8x16(), + .background = {245, 29, 0}, + .foreground = Color::black(), + }; + bg_lightest_small = new Style{ + .font = font::fixed_5x8(), + .background = {245, 29, 0}, + .foreground = Color::black(), + }; + bg_light = new Style{ + .font = font::fixed_8x16(), + .background = {212, 25, 0}, + .foreground = Color::white(), + }; + bg_medium = new Style{ + .font = font::fixed_8x16(), + .background = {143, 17, 0}, + .foreground = Color::white(), + }; + bg_dark = new Style{ + .font = font::fixed_8x16(), + .background = {99, 12, 0}, + .foreground = Color::white(), + }; + bg_darker = new Style{ + .font = font::fixed_8x16(), + .background = {79, 9, 0}, + .foreground = Color::white(), + }; + + bg_darkest = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::white(), + }; + bg_darkest_small = new Style{ + .font = font::fixed_5x8(), + .background = {33, 4, 0}, + .foreground = Color::white(), + }; + + bg_important_small = new Style{ + .font = font::fixed_5x8(), + .background = Color::yellow(), + .foreground = {33, 4, 0}, + }; + + error_dark = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::red(), + }; + warning_dark = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::yellow(), + }; + ok_dark = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::green(), + }; + + fg_dark = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = {99, 12, 0}, + }; + fg_medium = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = {143, 17, 0}, + }; + fg_light = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::light_grey(), + }; + + fg_red = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::red(), + }; + fg_green = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::green(), + }; + fg_yellow = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::yellow(), + }; + fg_orange = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::orange(), + }; + fg_blue = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::blue(), + }; + fg_cyan = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::cyan(), + }; + fg_darkcyan = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::dark_cyan(), + }; + fg_magenta = new Style{ + .font = font::fixed_8x16(), + .background = {33, 4, 0}, + .foreground = Color::magenta(), + }; + + option_active = new Style{ + .font = font::fixed_8x16(), + .background = Color::orange(), + .foreground = Color::white(), + }; + + status_active = new Color{0, 255, 0}; // green, the status bar icons when active + + bg_table_header = new Color{205, 30, 0}; +} + +ThemeDark::ThemeDark() { + bg_lightest = new Style{ + .font = font::fixed_8x16(), + .background = {32, 32, 32}, + .foreground = Color::white(), + }; + bg_lightest_small = new Style{ + .font = font::fixed_5x8(), + .background = {32, 32, 32}, + .foreground = Color::white(), + }; + bg_light = new Style{ + .font = font::fixed_8x16(), + .background = {24, 24, 24}, + .foreground = Color::white(), + }; + bg_medium = new Style{ + .font = font::fixed_8x16(), + .background = {16, 16, 16}, + .foreground = Color::white(), + }; + bg_dark = new Style{ + .font = font::fixed_8x16(), + .background = {8, 8, 8}, + .foreground = Color::white(), + }; + bg_darker = new Style{ + .font = font::fixed_8x16(), + .background = {4, 4, 4}, + .foreground = Color::white(), + }; + + bg_darkest = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::white(), + }; + bg_darkest_small = new Style{ + .font = font::fixed_5x8(), + .background = Color::black(), + .foreground = Color::white(), + }; + + bg_important_small = new Style{ + .font = font::fixed_5x8(), + .background = {64, 64, 64}, + .foreground = Color::white(), + }; + + error_dark = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::red(), + }; + warning_dark = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::yellow(), + }; + ok_dark = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::green(), + }; + + fg_dark = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = {96, 96, 96}, + }; + fg_medium = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = {128, 128, 128}, + }; + fg_light = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::white(), + }; + + fg_red = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::red(), + }; + fg_green = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::green(), + }; + fg_yellow = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::yellow(), + }; + fg_orange = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::orange(), + }; + fg_blue = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::blue(), + }; + fg_cyan = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::cyan(), + }; + fg_darkcyan = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::dark_cyan(), + }; + fg_magenta = new Style{ + .font = font::fixed_8x16(), + .background = Color::black(), + .foreground = Color::magenta(), + }; + + option_active = new Style{ + .font = font::fixed_8x16(), + .background = {64, 64, 64}, + .foreground = Color::white(), + }; + + status_active = new Color{0, 255, 0}; + + bg_table_header = new Color{48, 48, 48}; +} + +} // namespace ui \ No newline at end of file diff --git a/firmware/standalone/digitalrain/ui/theme.hpp b/firmware/standalone/digitalrain/ui/theme.hpp new file mode 100644 index 000000000..9a1ddf469 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/theme.hpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 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. + */ + +#ifndef __THEME_H__ +#define __THEME_H__ +#include +#include +#include +#include "ui_painter.hpp" +#include "ui_font_fixed_5x8.hpp" +#include "ui_font_fixed_8x16.hpp" + +namespace ui { + +class ThemeTemplate { + public: + ~ThemeTemplate(); + Style* bg_lightest; + Style* bg_lightest_small; + Style* bg_light; + Style* bg_medium; + Style* bg_dark; + Style* bg_darker; + + Style* bg_darkest; + Style* bg_darkest_small; + + Style* bg_important_small; + + Style* error_dark; + Style* warning_dark; + Style* ok_dark; + + Style* fg_dark; + Style* fg_medium; + Style* fg_light; + + Style* fg_red; + Style* fg_green; + Style* fg_yellow; + Style* fg_orange; + Style* fg_blue; + Style* fg_cyan; + Style* fg_darkcyan; + Style* fg_magenta; + + Style* option_active; + + Color* status_active; // green, the status bar icons when active + Color* bg_table_header; +}; + +class ThemeDefault : public ThemeTemplate { + public: + ThemeDefault(); +}; + +class ThemeYellow : public ThemeTemplate { + public: + ThemeYellow(); +}; + +class ThemeAqua : public ThemeTemplate { + public: + ThemeAqua(); +}; + +class ThemeGreen : public ThemeTemplate { + public: + ThemeGreen(); +}; + +class ThemeRed : public ThemeTemplate { + public: + ThemeRed(); +}; + +class ThemeDark : public ThemeTemplate { + public: + ThemeDark(); +}; + +class Theme { + public: + enum ThemeId { + DefaultGrey = 0, + Yellow = 1, + Aqua = 2, + Green = 3, + Red = 4, + Dark = 5, + MAX + }; + static ThemeTemplate* getInstance(); + + static void SetTheme(ThemeId theme); + static ThemeTemplate* current; + static void destroy(); + + private: +}; + +} // namespace ui +#endif /*__THEME_H__*/ diff --git a/firmware/standalone/digitalrain/ui/ui.cpp b/firmware/standalone/digitalrain/ui/ui.cpp new file mode 100644 index 000000000..7ec4b9d79 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +// #include "irq_controls.hpp" +#include "sine_table.hpp" +#include "utility.hpp" + +#include + +namespace ui +{ + + // CGA palette + // Index into this table should match STR_COLOR_ escape string in ui.hpp + Color term_colors[16] = { + Color::black(), + Color::dark_blue(), + Color::dark_green(), + Color::dark_cyan(), + Color::dark_red(), + Color::dark_magenta(), + Color::dark_yellow(), + Color::light_grey(), + Color::dark_grey(), + Color::blue(), + Color::green(), + Color::cyan(), + Color::red(), + Color::magenta(), + Color::yellow(), + Color::white()}; + + bool Rect::contains(const Point p) const + { + return (p.x() >= left()) && (p.y() >= top()) && + (p.x() < right()) && (p.y() < bottom()); + } + + Rect Rect::intersect(const Rect &o) const + { + const auto x1 = std::max(left(), o.left()); + const auto x2 = std::min(right(), o.right()); + const auto y1 = std::max(top(), o.top()); + const auto y2 = std::min(bottom(), o.bottom()); + if ((x2 >= x1) && (y2 > y1)) + { + return {x1, y1, x2 - x1, y2 - y1}; + } + else + { + return {}; + } + } + + // TODO: This violates the principle of least surprise! + // This does a union, but that might not be obvious from "+=" syntax. + Rect &Rect::operator+=(const Rect &p) + { + if (is_empty()) + { + *this = p; + } + if (!p.is_empty()) + { + const auto x1 = std::min(left(), p.left()); + const auto y1 = std::min(top(), p.top()); + _pos = {x1, y1}; + const auto x2 = std::max(right(), p.right()); + const auto y2 = std::max(bottom(), p.bottom()); + _size = {x2 - x1, y2 - y1}; + } + return *this; + } + + Rect &Rect::operator+=(const Point &p) + { + _pos += p; + return *this; + } + + Rect &Rect::operator-=(const Point &p) + { + _pos -= p; + return *this; + } + + Point polar_to_point(float angle, uint32_t distance) + { + // polar to compass with y negated for screen drawing + return Point(sin_f32(DEG_TO_RAD(-angle) + pi) * distance, + sin_f32(DEG_TO_RAD(-angle) - (pi / 2)) * distance); + } + + Point fast_polar_to_point(int32_t angle, uint32_t distance) + { + // polar to compass with y negated for screen drawing + return Point((int16_sin_s4(((1 << 16) * (-angle + 180)) / 360) * distance) / (1 << 16), + (int16_sin_s4(((1 << 16) * (-angle - 90)) / 360) * distance) / (1 << 16)); + } + + bool key_is_long_pressed(KeyEvent key) + { + if (key < KeyEvent::Back) + { + // TODO: this would make more sense as a flag on KeyEvent + // passed up to the UI via event dispatch. + return false; // TODO: wire standalone api: switch_is_long_pressed(static_cast(key)); + } + + return false; + } + +} /* namespace ui */ diff --git a/firmware/standalone/digitalrain/ui/ui.hpp b/firmware/standalone/digitalrain/ui/ui.hpp new file mode 100644 index 000000000..36a7b837a --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui.hpp @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_H__ +#define __UI_H__ + +#include + +namespace ui { + +// Escape sequences for colored text; second character is index into term_colors[] +#define STR_COLOR_BLACK "\x1B\x00" +#define STR_COLOR_DARK_BLUE "\x1B\x01" +#define STR_COLOR_DARK_GREEN "\x1B\x02" +#define STR_COLOR_DARK_CYAN "\x1B\x03" +#define STR_COLOR_DARK_RED "\x1B\x04" +#define STR_COLOR_DARK_MAGENTA "\x1B\x05" +#define STR_COLOR_DARK_YELLOW "\x1B\x06" +#define STR_COLOR_LIGHT_GREY "\x1B\x07" +#define STR_COLOR_DARK_GREY "\x1B\x08" +#define STR_COLOR_BLUE "\x1B\x09" +#define STR_COLOR_GREEN "\x1B\x0A" +#define STR_COLOR_CYAN "\x1B\x0B" +#define STR_COLOR_RED "\x1B\x0C" +#define STR_COLOR_MAGENTA "\x1B\x0D" +#define STR_COLOR_YELLOW "\x1B\x0E" +#define STR_COLOR_WHITE "\x1B\x0F" +#define STR_COLOR_FOREGROUND "\x1B\x10" + +#define DEG_TO_RAD(d) (d * (2 * pi) / 360.0) + +using Coord = int16_t; +using Dim = int16_t; + +constexpr uint16_t screen_width = 240; +constexpr uint16_t screen_height = 320; + +/* Dimensions for the default font character. */ +constexpr uint16_t char_width = 8; +constexpr uint16_t char_height = 16; + +struct Color { + uint16_t v; // rrrrrGGGGGGbbbbb + + constexpr Color() + : v{0} { + } + + constexpr Color( + uint16_t v) + : v{v} { + } + + constexpr Color( + uint8_t r, + uint8_t g, + uint8_t b) + : v{ + static_cast( + ((r & 0xf8) << 8) | ((g & 0xfc) << 3) | ((b & 0xf8) >> 3))} { + } + + uint8_t r() { + return (uint8_t)((v >> 8) & 0xf8); + } + + uint8_t g() { + return (uint8_t)((v >> 3) & 0xfc); + } + + uint8_t b() { + return (uint8_t)((v << 3) & 0xf8); + } + + uint8_t to_greyscale() { + uint32_t r = (v >> 8) & 0xf8; + uint32_t g = (v >> 3) & 0xfc; + uint32_t b = (v << 3) & 0xf8; + + uint32_t grey = ((r * 306) + (g * 601) + (b * 117)) >> 10; + + return (uint8_t)grey; + } + + uint16_t dark() { + // stripping bits 4 & 5 from each of the colors R/G/B + return (v & ((0xc8 << 8) | (0xcc << 3) | (0xc8 >> 3))); + } + + Color operator-() const { + return (v ^ 0xffff); + } + + /* Converts a 32-bit color into a 16-bit color. + * High byte is ignored. */ + static constexpr Color RGB(uint32_t rgb) { + return {static_cast((rgb >> 16) & 0xff), + static_cast((rgb >> 8) & 0xff), + static_cast(rgb & 0xff)}; + } + + static constexpr Color black() { + return {0, 0, 0}; + } + + static constexpr Color red() { + return {255, 0, 0}; + } + static constexpr Color dark_red() { + return {159, 0, 0}; + } + + static constexpr Color orange() { + return {255, 175, 0}; + } + static constexpr Color dark_orange() { + return {191, 95, 0}; + } + + static constexpr Color yellow() { + return {255, 255, 0}; + } + static constexpr Color dark_yellow() { + return {191, 191, 0}; + } + + static constexpr Color green() { + return {0, 255, 0}; + } + static constexpr Color dark_green() { + return {0, 159, 0}; + } + + static constexpr Color blue() { + return {0, 0, 255}; + } + static constexpr Color dark_blue() { + return {0, 0, 191}; + } + + static constexpr Color cyan() { + return {0, 255, 255}; + } + static constexpr Color dark_cyan() { + return {0, 191, 191}; + } + + static constexpr Color magenta() { + return {255, 0, 255}; + } + static constexpr Color dark_magenta() { + return {191, 0, 191}; + } + + static constexpr Color white() { + return {255, 255, 255}; + } + + static constexpr Color light_grey() { + return {191, 191, 191}; + } + static constexpr Color grey() { + return {127, 127, 127}; + } + static constexpr Color dark_grey() { + return {63, 63, 63}; + } + static constexpr Color darker_grey() { + return {31, 31, 31}; + } + + static constexpr Color purple() { + return {204, 0, 102}; + } +}; + +extern Color term_colors[16]; + +struct ColorRGB888 { + uint8_t r; + uint8_t g; + uint8_t b; +}; + +struct Point { + private: + Coord _x; + Coord _y; + + public: + constexpr Point() + : _x{0}, + _y{0} { + } + + constexpr Point( + int x, + int y) + : _x{static_cast(x)}, + _y{static_cast(y)} { + } + + constexpr int x() const { + return _x; + } + + constexpr int y() const { + return _y; + } + + constexpr Point operator-() const { + return {-_x, -_y}; + } + + constexpr Point operator+(const Point& p) const { + return {_x + p._x, _y + p._y}; + } + + constexpr Point operator-(const Point& p) const { + return {_x - p._x, _y - p._y}; + } + + Point& operator+=(const Point& p) { + _x += p._x; + _y += p._y; + return *this; + } + + Point& operator-=(const Point& p) { + _x -= p._x; + _y -= p._y; + return *this; + } +}; + +struct Size { + private: + Dim _w; + Dim _h; + + public: + constexpr Size() + : _w{0}, + _h{0} { + } + + constexpr Size( + int w, + int h) + : _w{static_cast(w)}, + _h{static_cast(h)} { + } + + int width() const { + return _w; + } + + int height() const { + return _h; + } + + bool is_empty() const { + return (_w < 1) || (_h < 1); + } +}; + +struct Rect { + private: + Point _pos; + Size _size; + + public: + constexpr Rect() + : _pos{}, + _size{} { + } + + constexpr Rect( + int x, + int y, + int w, + int h) + : _pos{x, y}, + _size{w, h} { + } + + constexpr Rect( + Point pos, + Size size) + : _pos(pos), + _size(size) { + } + + Point location() const { + return _pos; + } + + Size size() const { + return _size; + } + + int top() const { + return _pos.y(); + } + + int bottom() const { + return _pos.y() + _size.height(); + } + + int left() const { + return _pos.x(); + } + + int right() const { + return _pos.x() + _size.width(); + } + + int width() const { + return _size.width(); + } + + int height() const { + return _size.height(); + } + + Point center() const { + return {_pos.x() + _size.width() / 2, _pos.y() + _size.height() / 2}; + } + + bool is_empty() const { + return _size.is_empty(); + } + + bool contains(const Point p) const; + + Rect intersect(const Rect& o) const; + + Rect operator+(const Point& p) const { + return {_pos + p, _size}; + } + + Rect& operator+=(const Rect& p); + Rect& operator+=(const Point& p); + Rect& operator-=(const Point& p); + + operator bool() const { + return !_size.is_empty(); + } +}; + +struct Bitmap { + const Size size; + const uint8_t* const data; +}; + +enum class KeyEvent : uint8_t { + /* Ordinals map to bit positions reported by CPLD */ + Right = 0, + Left = 1, + Down = 2, + Up = 3, + Select = 4, + Dfu = 5, + Back = 6, /* Left and Up together */ +}; + +using EncoderEvent = int32_t; +using KeyboardEvent = uint8_t; + +struct TouchEvent { + enum class Type : uint32_t { + Start = 0, + Move = 1, + End = 2, + }; + + Point point; + Type type; +}; + +Point polar_to_point(float angle, uint32_t distance); + +Point fast_polar_to_point(int32_t angle, uint32_t distance); + +/* Default font glyph size. */ +constexpr Size char_size{char_width, char_height}; + +bool key_is_long_pressed(KeyEvent key); + +} /* namespace ui */ + +#endif /*__UI_H__*/ diff --git a/firmware/standalone/digitalrain/ui/ui_focus.cpp b/firmware/standalone/digitalrain/ui/ui_focus.cpp new file mode 100644 index 000000000..c8015a700 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_focus.cpp @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * 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_focus.hpp" +#include "ui_widget.hpp" + +#include + +#include +#include +#include +#include + +#define pow2(x) ((x) * (x)) + +namespace ui { + +Widget* FocusManager::focus_widget() const { + return focus_widget_; +} + +void FocusManager::set_focus_widget(Widget* const new_focus_widget) { + // Widget already has focus. + if (new_focus_widget == focus_widget()) { + return; + } + + if (new_focus_widget) { + if (!new_focus_widget->focusable()) + return; + } + + // Blur old widget. + // NB: This will crash if the focus_widget is a destroyed instance. + if (focus_widget()) { + focus_widget()->on_blur(); + focus_widget()->set_dirty(); + } + + focus_widget_ = new_focus_widget; + + if (focus_widget()) { + focus_widget()->on_focus(); + focus_widget()->set_dirty(); + } +} + +using test_result_t = std::pair; +using test_fn = std::function; +using test_collection_t = std::vector; + +/* Walk all visible widgets in hierarchy, collecting those that pass test */ +template +static void widget_collect_visible(Widget* const w, TestFn test, test_collection_t& collection) { + for (auto child : w->children()) { + if (!child->hidden()) { + const auto result = test(child); + if (result.first) { + collection.push_back(result); + } + widget_collect_visible(child, test, collection); + } + } +} + +static int32_t rect_distances( + const KeyEvent direction, + const Rect& rect_start, + const Rect& rect_end) { + Coord on_axis_max, on_axis_min; + + switch (direction) { + case KeyEvent::Right: + on_axis_max = rect_end.left(); + on_axis_min = rect_start.right(); + break; + + case KeyEvent::Left: + on_axis_max = rect_start.left(); + on_axis_min = rect_end.right(); + break; + + case KeyEvent::Down: + on_axis_max = rect_end.top(); + on_axis_min = rect_start.bottom(); + break; + + case KeyEvent::Up: + on_axis_max = rect_start.top(); + on_axis_min = rect_end.bottom(); + break; + + default: + return -1; + } + + Coord on_axis_distance = on_axis_max - on_axis_min; + if (on_axis_distance < 0) { + return -1; + } + + Coord perpendicular_axis_start, perpendicular_axis_end; + + switch (direction) { + case KeyEvent::Right: + case KeyEvent::Left: + perpendicular_axis_start = rect_start.center().y(); + perpendicular_axis_end = rect_end.center().y(); + break; + + case KeyEvent::Up: + case KeyEvent::Down: + perpendicular_axis_start = rect_start.center().x(); + perpendicular_axis_end = rect_end.center().x(); + break; + + default: + return -1; + } + + switch (direction) { + case KeyEvent::Right: + case KeyEvent::Left: + return pow2(std::abs(perpendicular_axis_end - perpendicular_axis_start) + 1) * (on_axis_distance + 1); + break; + + case KeyEvent::Up: + case KeyEvent::Down: + return (std::abs(perpendicular_axis_end - perpendicular_axis_start) + 1) * pow2(on_axis_distance + 1); + break; + + default: + return 0; + } +} + +void FocusManager::update( + Widget* const top_widget, + const KeyEvent event) { + if (focus_widget()) { + const auto focus_screen_rect = focus_widget()->screen_rect(); + + const auto test_fn = [&focus_screen_rect, event](ui::Widget* const w) -> test_result_t { + // if( w->visible() && w->focusable() ) { + if (w->focusable()) { + const auto distance = rect_distances(event, focus_screen_rect, w->screen_rect()); + if (distance >= 0) { + return {w, distance}; + } + } + + return {nullptr, 0}; + }; + + const auto find_back_fn = [](ui::Widget* const w) -> test_result_t { + if (w->focusable() && (w->id == -1)) + return {w, 0}; + else + return {nullptr, 0}; + }; + + test_collection_t collection; + widget_collect_visible(top_widget, test_fn, collection); + + const auto compare_fn = [](const test_result_t& a, const test_result_t& b) { + return a.second < b.second; + }; + + const auto nearest = std::min_element(collection.cbegin(), collection.cend(), compare_fn); + // Up and left to indicate back + if (event == KeyEvent::Back) { + collection.clear(); + widget_collect_visible(top_widget, find_back_fn, collection); + if (!collection.empty()) + set_focus_widget(collection[0].first); + } else if (nearest != collection.cend()) { + // focus->blur(); + const auto new_focus = (*nearest).first; + set_focus_widget(new_focus); + } else { + if ((focus_widget()->id >= 0) && (event == KeyEvent::Left)) { + // Stuck left, move to back button + collection.clear(); + widget_collect_visible(top_widget, find_back_fn, collection); + if (!collection.empty()) + set_focus_widget(collection[0].first); + } + } + } +} + +} /* namespace ui */ diff --git a/firmware/standalone/digitalrain/ui/ui_focus.hpp b/firmware/standalone/digitalrain/ui/ui_focus.hpp new file mode 100644 index 000000000..2f9c45650 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_focus.hpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_FOCUS_H__ +#define __UI_FOCUS_H__ + +#include "ui.hpp" + +namespace ui { + +class Widget; + +class FocusManager { + public: + Widget* focus_widget() const; + void set_focus_widget(Widget* const new_focus_widget); + + void update(Widget* const top_widget, const KeyEvent event); + // void update(Widget* const top_widget, const TouchEvent event); + + private: + Widget* focus_widget_{nullptr}; +}; + +} /* namespace ui */ + +#endif /*__UI_FOCUS_H__*/ diff --git a/firmware/standalone/digitalrain/ui/ui_font_fixed_5x8.cpp b/firmware/standalone/digitalrain/ui/ui_font_fixed_5x8.cpp new file mode 100644 index 000000000..5a8078c0f --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_font_fixed_5x8.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_font_fixed_5x8.hpp" +#include +#include "standalone_app.hpp" + +namespace ui { +namespace font { +ui::Font* fixed_5x8_font = nullptr; + +ui::Font& fixed_5x8() { + if (fixed_5x8_font == nullptr) { + fixed_5x8_font = new ui::Font{ + 5, + 8, + _api->fixed_5x8_glyph_data, + 0x20, + 95, + }; + } + + return *fixed_5x8_font; +} + +} /* namespace font */ +} /* namespace ui */ diff --git a/firmware/standalone/digitalrain/ui/ui_font_fixed_5x8.hpp b/firmware/standalone/digitalrain/ui/ui_font_fixed_5x8.hpp new file mode 100644 index 000000000..3709378d4 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_font_fixed_5x8.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_FONT_FIXED_5X8_H__ +#define __UI_FONT_FIXED_5X8_H__ + +#include "ui_text.hpp" + +namespace ui +{ + namespace font + { + extern ui::Font &fixed_5x8(); + + } // namespace font +} // namespace ui + +#endif /*__UI_FONT_FIXED_5X8_H__*/ diff --git a/firmware/standalone/digitalrain/ui/ui_font_fixed_8x16.cpp b/firmware/standalone/digitalrain/ui/ui_font_fixed_8x16.cpp new file mode 100644 index 000000000..03c4e5f9a --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_font_fixed_8x16.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_font_fixed_8x16.hpp" + +#include + +namespace ui { +namespace font { +ui::Font* fixed_8x16_font = nullptr; + +ui::Font& fixed_8x16() { + if (fixed_8x16_font == nullptr) { + fixed_8x16_font = new ui::Font{ + 8, + 16, + _api->fixed_8x16_glyph_data, + 0x20, + 223, + }; + } + + return *fixed_8x16_font; +} + +} /* namespace font */ +} /* namespace ui */ diff --git a/firmware/standalone/digitalrain/ui/ui_font_fixed_8x16.hpp b/firmware/standalone/digitalrain/ui/ui_font_fixed_8x16.hpp new file mode 100644 index 000000000..b08241e3e --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_font_fixed_8x16.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_FONT_FIXED_8X16_H__ +#define __UI_FONT_FIXED_8X16_H__ + +#include "ui_text.hpp" + +namespace ui +{ + namespace font + { + extern ui::Font &fixed_8x16(); + + } /* namespace font */ +} // namespace ui + +#endif /*__UI_FONT_FIXED_8X16_H__*/ diff --git a/firmware/standalone/digitalrain/ui/ui_geomap.cpp b/firmware/standalone/digitalrain/ui/ui_geomap.cpp new file mode 100644 index 000000000..c44857b1c --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_geomap.cpp @@ -0,0 +1,985 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2017 Furrtek + * Copyright (C) 2024 Mark Thompson + * + * 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_geomap.hpp" +#include +#include +#include + +#include "string_format.hpp" +#include "complex.hpp" +#include "ui_font_fixed_5x8.hpp" +#include "file_path.hpp" + +namespace ui { +GeoPos::GeoPos( + const Point pos, + const alt_unit altitude_unit, + const spd_unit speed_unit) + : altitude_unit_(altitude_unit), speed_unit_(speed_unit) { + set_parent_rect({pos, {screen_width, 3 * 16}}); + + add_children({&labels_position, + &label_spd_position, + &field_altitude, + &field_speed, + &text_alt_unit, + &text_speed_unit, + &field_lat_degrees, + &field_lat_minutes, + &field_lat_seconds, + &text_lat_decimal, + &field_lon_degrees, + &field_lon_minutes, + &field_lon_seconds, + &text_lon_decimal}); + + // Defaults + set_altitude(0); + set_speed(0); + set_lat(0); + set_lon(0); + + const auto changed_fn = [this](int32_t) { + // Convert degrees/minutes/seconds fields to decimal (floating point) lat/lon degree + float lat_value = lat(); + float lon_value = lon(); + + text_lat_decimal.set(to_string_decimal(lat_value, 5)); + text_lon_decimal.set(to_string_decimal(lon_value, 5)); + + if (on_change && report_change) + on_change(altitude(), lat_value, lon_value, speed()); + }; + + field_altitude.on_change = changed_fn; + field_speed.on_change = changed_fn; + field_lat_degrees.on_change = changed_fn; + field_lat_minutes.on_change = changed_fn; + field_lat_seconds.on_change = changed_fn; + field_lon_degrees.on_change = changed_fn; + field_lon_minutes.on_change = changed_fn; + field_lon_seconds.on_change = changed_fn; + + const auto wrapped_lat_seconds = [this](int32_t v) { + field_lat_minutes.on_encoder(v); + }; + + const auto wrapped_lat_minutes = [this](int32_t v) { + field_lat_degrees.on_encoder((field_lat_degrees.value() >= 0) ? v : -v); + }; + + const auto wrapped_lon_seconds = [this](int32_t v) { + field_lon_minutes.on_encoder(v); + }; + + const auto wrapped_lon_minutes = [this](int32_t v) { + field_lon_degrees.on_encoder((field_lon_degrees.value() >= 0) ? v : -v); + }; + + field_lat_seconds.on_wrap = wrapped_lat_seconds; + field_lat_minutes.on_wrap = wrapped_lat_minutes; + field_lon_seconds.on_wrap = wrapped_lon_seconds; + field_lon_minutes.on_wrap = wrapped_lon_minutes; + + text_alt_unit.set(altitude_unit_ ? "m" : "ft"); + if (speed_unit_ == KMPH) text_speed_unit.set("kmph"); + if (speed_unit_ == MPH) text_speed_unit.set("mph"); + if (speed_unit_ == KNOTS) text_speed_unit.set("knots"); + if (speed_unit_ == HIDDEN) { + text_speed_unit.hidden(true); + label_spd_position.hidden(true); + field_speed.hidden(true); + } +} + +void GeoPos::set_read_only(bool v) { + // only setting altitude to read-only (allow manual panning via lat/lon fields) + field_altitude.set_focusable(!v); + field_speed.set_focusable(!v); +} + +// Stupid hack to avoid an event loop +void GeoPos::set_report_change(bool v) { + report_change = v; +} + +void GeoPos::focus() { + if (field_altitude.focusable()) + field_altitude.focus(); + else + field_lat_degrees.focus(); +} + +void GeoPos::hide_altandspeed() { + // Color altitude grey to indicate it's not updated in manual panning mode + field_altitude.set_style(Theme::getInstance()->fg_medium); + field_speed.set_style(Theme::getInstance()->fg_medium); +} + +void GeoPos::set_altitude(int32_t altitude) { + field_altitude.set_value(altitude); +} +void GeoPos::set_speed(int32_t speed) { + field_speed.set_value(speed); +} + +void GeoPos::set_lat(float lat) { + field_lat_degrees.set_value(lat); + field_lat_minutes.set_value((uint32_t)abs(lat / (1.0 / 60)) % 60); + field_lat_seconds.set_value((uint32_t)abs(lat / (1.0 / 3600)) % 60); +} + +void GeoPos::set_lon(float lon) { + field_lon_degrees.set_value(lon); + field_lon_minutes.set_value((uint32_t)abs(lon / (1.0 / 60)) % 60); + field_lon_seconds.set_value((uint32_t)abs(lon / (1.0 / 3600)) % 60); +} + +float GeoPos::lat() { + if (field_lat_degrees.value() < 0) { + return -1 * (-1 * field_lat_degrees.value() + (field_lat_minutes.value() / 60.0) + (field_lat_seconds.value() / 3600.0)); + } else { + return field_lat_degrees.value() + (field_lat_minutes.value() / 60.0) + (field_lat_seconds.value() / 3600.0); + } +}; + +float GeoPos::lon() { + if (field_lon_degrees.value() < 0) { + return -1 * (-1 * field_lon_degrees.value() + (field_lon_minutes.value() / 60.0) + (field_lon_seconds.value() / 3600.0)); + } else { + return field_lon_degrees.value() + (field_lon_minutes.value() / 60.0) + (field_lon_seconds.value() / 3600.0); + } +}; + +int32_t GeoPos::altitude() { + return field_altitude.value(); +}; + +int32_t GeoPos::speed() { + return field_speed.value(); +}; + +GeoMap::GeoMap( + Rect parent_rect) + : Widget{parent_rect}, markerListLen(0) { + has_osm = use_osm = find_osm_file_tile(); +} + +bool GeoMap::on_encoder(const EncoderEvent delta) { + // Valid map_zoom values are -2 to -MAX_MAP_ZOOM_OUT, and +1 to +MAX_MAP_ZOOM_IN (values of 0 and -1 are not permitted) + if (delta > 0) { + if (map_zoom < MAX_MAP_ZOOM_IN) { + if (map_zoom == -2) { + map_zoom = 1; + } else { + // zoom in faster after exceeding the map resolution limit + map_zoom += (map_zoom >= MAP_ZOOM_RESOLUTION_LIMIT) ? map_zoom : 1; + } + } + map_osm_zoom++; + if (has_osm) set_osm_max_zoom(); + } else if (delta < 0) { + if (map_zoom > -MAX_MAP_ZOOM_OUT) { + if (map_zoom == 1) { + map_zoom = -2; + } else { + if (map_zoom > MAP_ZOOM_RESOLUTION_LIMIT) + map_zoom /= 2; + else + map_zoom--; + } + } + if (map_osm_zoom > 0) map_osm_zoom--; + } else { + return false; + } + + map_visible = map_opened && (map_zoom <= MAP_ZOOM_RESOLUTION_LIMIT); + if (use_osm) { + map_visible = true; + zoom_pixel_offset = 0; + } else { + zoom_pixel_offset = (map_visible && (map_zoom > 1)) ? (float)map_zoom / 2 : 0.0f; + } + + // Trigger map redraw + redraw_map = true; + set_dirty(); + return true; +} + +void GeoMap::map_read_line_bin(ui::Color* buffer, uint16_t pixels) { + if (map_zoom == 1) { + map_file.read(buffer, pixels << 1); + } else if (map_zoom > 1) { + map_file.read(buffer, (pixels / map_zoom) << 1); + + // Zoom in: Expand each pixel to "map_zoom" number of pixels. + // Future TODO: Add dithering to smooth out the pixelation. + // As long as MOD(width,map_zoom)==0 then we don't need to check buffer overflow case when stretching last pixel; + // For 240 width, than means no check is needed for map_zoom values up to 6. + // (Rectangle height must also divide evenly into map_zoom or we get black lines at end of screen) + // Note that zooming in results in a map offset of (1/map_zoom) pixels to the right & downward directions (see zoom_pixel_offset). + for (int i = (geomap_rect_width / map_zoom) - 1; i >= 0; i--) { + for (int j = 0; j < map_zoom; j++) { + buffer[(i * map_zoom) + j] = buffer[i]; + } + } + } else { + ui::Color* zoom_out_buffer = new ui::Color[(pixels * (-map_zoom))]; + map_file.read(zoom_out_buffer, (pixels * (-map_zoom)) << 1); + + // Zoom out: Collapse each group of "-map_zoom" pixels into one pixel. + // Future TODO: Average each group of pixels (in both X & Y directions if possible). + for (int i = 0; i < geomap_rect_width; i++) { + buffer[i] = zoom_out_buffer[i * (-map_zoom)]; + } + delete[] zoom_out_buffer; + } +} + +void GeoMap::draw_markers(Painter& painter) { + for (int i = 0; i < markerListLen; ++i) { + draw_marker_item(painter, markerList[i], Color::blue(), Color::blue(), Color::magenta()); + } +} + +void GeoMap::draw_marker_item(Painter& painter, GeoMarker& item, const Color color, const Color fontColor, const Color backColor) { + const auto r = screen_rect(); + const ui::Point itemPoint = item_rect_pixel(item); + if ((itemPoint.x() >= 0) && (itemPoint.x() < r.width()) && + (itemPoint.y() > 10) && (itemPoint.y() < r.height())) // Dont draw within symbol size of top + { + draw_marker(painter, {itemPoint.x() + r.left(), itemPoint.y() + r.top()}, item.angle, item.tag, color, fontColor, backColor); + } +} + +// Calculate screen position of item, adjusted for zoom factor. +ui::Point GeoMap::item_rect_pixel(GeoMarker& item) { + if (!use_osm) { + const auto r = screen_rect(); + const auto geomap_rect_half_width = r.width() / 2; + const auto geomap_rect_half_height = r.height() / 2; + GeoPoint mapPoint = lat_lon_to_map_pixel(item.lat, item.lon); + float x = mapPoint.x - x_pos; + float y = mapPoint.y - y_pos; + if (map_zoom > 1) { + x = x * map_zoom + zoom_pixel_offset; + y = y * map_zoom + zoom_pixel_offset; + } else if (map_zoom < 0) { + x = x / (-map_zoom); + y = y / (-map_zoom); + } + x += geomap_rect_half_width; + y += geomap_rect_half_height; + return {(int16_t)x, (int16_t)y}; + } + // osm calculation + double y = lat_to_pixel_y_tile(item.lat, map_osm_zoom) - viewport_top_left_py; + double x = lon_to_pixel_x_tile(item.lon, map_osm_zoom) - viewport_top_left_px; + return {(int16_t)x, (int16_t)y}; +} + +/** + * @brief Converts longitude to a map tile's X-coordinate. + * @param lon The longitude in degrees. + * @param zoom The zoom level. + * @return The X-coordinate of the tile. + */ +int GeoMap::lon2tile(double lon, int zoom) { + return (int)floor((lon + 180.0) / 360.0 * pow(2.0, zoom)); +} + +/** + * @brief Converts latitude to a map tile's Y-coordinate. + * @param lat The latitude in degrees. + * @param zoom The zoom level. + * @return The Y-coordinate of the tile. + */ +int GeoMap::lat2tile(double lat, int zoom) { + // Convert latitude from degrees to radians for trigonometric functions + double lat_rad = lat * M_PI / 180.0; + // Perform the Mercator projection calculation + return (int)floor((1.0 - log(tan(lat_rad) + 1.0 / cos(lat_rad)) / M_PI) / 2.0 * pow(2.0, zoom)); +} + +void GeoMap::set_osm_max_zoom() { + if (map_osm_zoom > 20) map_osm_zoom = 20; + for (uint8_t i = map_osm_zoom; i > 0; i--) { + int tile_x = lon2tile(lon_, i); + int tile_y = lat2tile(lat_, i); + std::string filename = "/OSM/" + to_string_dec_int(i) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp"; + std::filesystem::path file_path(filename); + if (file_exists(file_path)) { + map_osm_zoom = i; + return; + } + } + map_osm_zoom = 0; // should not happen +} + +// checks if the tile file presents or not. to determine if we got osm or not +uint8_t GeoMap::find_osm_file_tile() { + std::string filename = "/OSM/" + to_string_dec_int(0) + "/" + to_string_dec_int(0) + "/" + to_string_dec_int(0) + ".bmp"; + std::filesystem::path file_path(filename); + if (file_exists(file_path)) return 1; + return 0; // not found +} + +// Converts latitude/longitude to pixel coordinates in binary map file. +// (Note that when map_zoom==1, one pixel in map file corresponds to 1 pixel on screen) +GeoPoint GeoMap::lat_lon_to_map_pixel(float lat, float lon) { + // Using WGS 84/Pseudo-Mercator projection + float x = (map_width * (lon + 180) / 360); + + // Latitude calculation based on https://stackoverflow.com/a/10401734/2278659 + double lat_rad = sin(lat * pi / 180); + float y = (map_height - ((map_world_lon / 2 * log((1 + lat_rad) / (1 - lat_rad))) - map_offset)); + + return {x, y}; +} + +// Draw grid in place of map (when zoom-in level is too high). +void GeoMap::draw_map_grid(ui::Rect r, Painter& painter) { + // Grid spacing is just based on zoom at the moment, and centered on screen. + // TODO: Maybe align with latitude/longitude seconds instead? + int grid_spacing = map_zoom * 2; + int x = (r.width() / 2) % grid_spacing; + int y = (r.height() / 2) % grid_spacing; + + if (map_zoom <= MAP_ZOOM_RESOLUTION_LIMIT) + return; + + painter.fill_rectangle({{0, r.top()}, {r.width(), r.height()}}, Theme::getInstance()->bg_darkest->background); + + for (uint16_t line = y; line < r.height(); line += grid_spacing) { + painter.fill_rectangle({{0, r.top() + line}, {r.width(), 1}}, Theme::getInstance()->bg_darker->background); + } + for (uint16_t column = x; column < r.width(); column += grid_spacing) { + painter.fill_rectangle({{column, r.top()}, {1, r.height()}}, Theme::getInstance()->bg_darker->background); + } +} + +double GeoMap::tile_pixel_x_to_lon(int x, int zoom) { + double map_width = pow(2.0, zoom) * TILE_SIZE; + return (x / map_width * 360.0) - 180.0; +} + +double GeoMap::tile_pixel_y_to_lat(int y, int zoom) { + double map_height = pow(2.0, zoom) * TILE_SIZE; + double n = M_PI * (1.0 - 2.0 * y / map_height); + return atan(sinh(n)) * 180.0 / M_PI; +} + +double GeoMap::lon_to_pixel_x_tile(double lon, int zoom) { + return ((lon + 180.0) / 360.0) * pow(2.0, zoom) * TILE_SIZE; +} + +double GeoMap::lat_to_pixel_y_tile(double lat, int zoom) { + double lat_rad = lat * M_PI / 180.0; + double sin_lat = sin(lat_rad); + return ((1.0 - log((1.0 + sin_lat) / (1.0 - sin_lat)) / (2.0 * M_PI)) / 2.0) * pow(2.0, zoom) * TILE_SIZE; +} + +bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y, Painter& painter) { + const ui::Rect r = screen_rect(); + // Early exit if the tile is completely outside the viewport + if (relative_x >= r.width() || relative_y >= r.height() || + relative_x + TILE_SIZE <= 0 || relative_y + TILE_SIZE <= 0) { + return true; + } + + BMPFile bmp{}; + bmp.open("/OSM/" + to_string_dec_int(zoom) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp", true); + // 1. Define the source and destination areas, starting with the full tile. + int src_x = 0; + int src_y = 0; + int dest_x = relative_x; + int dest_y = relative_y; + int clip_w = TILE_SIZE; + int clip_h = TILE_SIZE; + // 2. Clip left edge + if (dest_x < 0) { + src_x = -dest_x; + clip_w += dest_x; + dest_x = 0; + } + // 3. Clip top edge + if (dest_y < 0) { + src_y = -dest_y; + clip_h += dest_y; + dest_y = 0; + } + // 4. Clip right edge + if (dest_x + clip_w > r.width()) { + clip_w = r.width() - dest_x; + } + // 5. Clip bottom edge + if (dest_y + clip_h > r.height()) { + clip_h = r.height() - dest_y; + } + // 6. If clipping resulted in no visible area, we're done. + if (clip_w <= 0 || clip_h <= 0) { + return true; + } + + if (!bmp.is_loaded()) { + // Draw an error rectangle using the calculated clipped dimensions + ui::Rect error_rect{{dest_x + r.left(), dest_y + r.top()}, {clip_w, clip_h}}; + painter.fill_rectangle(error_rect, Theme::getInstance()->bg_lightest->background); + return false; + } + std::vector line(clip_w); + for (int y = 0; y < clip_h; ++y) { + int source_row = src_y + y; + int dest_row = dest_y + y; + bmp.seek(src_x, source_row); + for (int x = 0; x < clip_w; ++x) { + bmp.read_next_px(line[x], true); + } + painter.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line); + } + return true; +} + +void GeoMap::paint(Painter& painter) { + const auto r = screen_rect(); + std::array map_line_buffer; + int16_t zoom_seek_x, zoom_seek_y; + + if (!use_osm) { + // Ony redraw map if it moved by at least 1 pixel or the markers list was updated + if (map_zoom <= 1) { + // Zooming out, or no zoom + const int min_diff = abs(map_zoom); + if ((int)abs(x_pos - prev_x_pos) >= min_diff) + redraw_map = true; + else if ((int)abs(y_pos - prev_y_pos) >= min_diff) + redraw_map = true; + } else { + // Zooming in; magnify position differences (utilizing zoom_pixel_offset) + if ((int)(abs(x_pos - prev_x_pos) * map_zoom) >= 1) + redraw_map = true; + else if ((int)(abs(y_pos - prev_y_pos) * map_zoom) >= 1) + redraw_map = true; + } + } else { + // using osm; needs to be stricter with the redraws, it'll be checked on move + } + + if (redraw_map) { + redraw_map = false; + if (map_visible) { + if (!use_osm) { + prev_x_pos = x_pos; // Note x_pos/y_pos pixel position in map file now correspond to screen rect CENTER pixel + prev_y_pos = y_pos; + // Adjust starting corner position of map per zoom setting; + // When zooming in the map should technically by shifted left & up by another map_zoom/2 pixels but + // the map_read_line_bin() function doesn't handle that yet so we're adjusting markers instead (see zoom_pixel_offset). + if (map_zoom > 1) { + zoom_seek_x = x_pos - (float)r.width() / (2 * map_zoom); + zoom_seek_y = y_pos - (float)r.height() / (2 * map_zoom); + } else { + zoom_seek_x = x_pos - (r.width() * abs(map_zoom)) / 2; + zoom_seek_y = y_pos - (r.height() * abs(map_zoom)) / 2; + } + // Read from map file and display to zoomed scale + int duplicate_lines = (map_zoom < 0) ? 1 : map_zoom; + for (uint16_t line = 0; line < (r.height() / duplicate_lines); line++) { + uint16_t seek_line = zoom_seek_y + ((map_zoom >= 0) ? line : line * (-map_zoom)); + map_file.seek(4 + ((zoom_seek_x + (map_width * seek_line)) << 1)); + map_read_line_bin(map_line_buffer.data(), r.width()); + for (uint16_t j = 0; j < duplicate_lines; j++) { + painter.draw_pixels({0, r.top() + (line * duplicate_lines) + j, r.width(), 1}, map_line_buffer); + } + } + + } else { + // display osm tiles + // Convert center GPS to a global pixel coordinate + double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_zoom); + double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_zoom); + + // Find the top-left corner of the screen (viewport) in global pixel coordinates + viewport_top_left_px = global_center_px - (r.width() / 2.0); + viewport_top_left_py = global_center_py - (r.height() / 2.0); + + // Find the tile ID that contains the top-left corner of the viewport + int start_tile_x = floor(viewport_top_left_px / TILE_SIZE); + int start_tile_y = floor(viewport_top_left_py / TILE_SIZE); + + // Calculate the crucial render offset. + // This determines how much the first tile is shifted to align the map correctly. + // This value will almost always be negative or zero. + double render_offset_x = -(viewport_top_left_px - (start_tile_x * TILE_SIZE)); + double render_offset_y = -(viewport_top_left_py - (start_tile_y * TILE_SIZE)); + + // Determine how many tiles we need to draw to fill the screen + int tiles_needed_x = (r.width() / TILE_SIZE) + 2; + int tiles_needed_y = (r.height() / TILE_SIZE) + 2; + + for (int y = 0; y < tiles_needed_y; ++y) { + for (int x = 0; x < tiles_needed_x; ++x) { + int current_tile_x = start_tile_x + x; + int current_tile_y = start_tile_y + y; + + // Calculate the final on-screen drawing position for this tile. + // For the first tile (x=0, y=0), this will be the negative offset. + int draw_pos_x = round(render_offset_x + x * TILE_SIZE); + int draw_pos_y = round(render_offset_y + y * TILE_SIZE); + if (!draw_osm_file(map_osm_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y, painter)) { + // already blanked it. + } + } + } + } + + } else { + // No map data or excessive zoom; just draw a grid + draw_map_grid(r, painter); + } + // Draw crosshairs in center in manual panning mode + if (manual_panning_) { + painter.fill_rectangle({r.center() - Point(16, 1) + Point(zoom_pixel_offset, zoom_pixel_offset), {32, 2}}, Color::red()); + painter.fill_rectangle({r.center() - Point(1, 16) + Point(zoom_pixel_offset, zoom_pixel_offset), {2, 32}}, Color::red()); + } + + // Draw the other markers + draw_markers(painter); + if (!use_osm) draw_scale(painter); + draw_mypos(painter); + if (has_osm) draw_switcher(painter); + set_clean(); + } + + // Draw the marker in the center + if (!manual_panning_ && !hide_center_marker_) { + draw_marker(painter, r.center() + Point(zoom_pixel_offset, zoom_pixel_offset), angle_, tag_, Color::red(), Color::white(), Color::black()); + } +} + +void GeoMap::draw_switcher(Painter& painter) { + painter.fill_rectangle({screen_rect().left(), screen_rect().top(), 3 * 20, 20}, Theme::getInstance()->bg_darker->background); + std::string_view txt = (use_osm) ? "B I N" : "O S M"; + painter.draw_string({screen_rect().left() + 5, screen_rect().top() + 2}, *Theme::getInstance()->fg_light, txt); +} + +bool GeoMap::on_keyboard(KeyboardEvent key) { + if (key == '+' || key == ' ') return on_encoder(1); + if (key == '-') return on_encoder(-1); + + return false; +} + +bool GeoMap::on_touch(const TouchEvent event) { + if (has_osm && event.type == TouchEvent::Type::Start && event.point.x() < screen_rect().left() + 3 * 20 && event.point.y() < screen_rect().top() + 20) { + use_osm = !use_osm; + move(lon_, lat_); // to re calculate the center for each map type + if (use_osm) set_osm_max_zoom(); + redraw_map = true; + set_dirty(); + return false; // false, because with true this hits 2 times + } + + if ((event.type == TouchEvent::Type::Start) && (mode_ == PROMPT)) { + Point p; + set_highlighted(true); + if (on_move) { + if (!use_osm) { + p = event.point - screen_rect().center(); + on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio, false); + } else { + p = event.point - screen_rect().location(); + on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_zoom), true); + } + return true; + } + } + return false; +} + +void GeoMap::move(const float lon, const float lat) { + const auto r = screen_rect(); + bool is_changed = (lon_ != lon || lat_ != lat); + lon_ = lon; + lat_ = lat; + if (!use_osm) { + // Calculate x_pos/y_pos in map file corresponding to CENTER pixel of screen rect + // (Note there is a 1:1 correspondence between map file pixels and screen pixels when map_zoom=1) + GeoPoint mapPoint = lat_lon_to_map_pixel(lat_, lon_); + x_pos = mapPoint.x; + y_pos = mapPoint.y; + // Cap position + if (x_pos > (map_width - r.width() / 2)) + x_pos = map_width - r.width() / 2; + if (y_pos > (map_height + r.height() / 2)) + y_pos = map_height - r.height() / 2; + + // Scale calculation + float km_per_deg_lon = cos(lat * pi / 180) * 111.321; // 111.321 km/deg longitude at equator, and 0 km at poles + pixels_per_km = (r.width() / 2) / km_per_deg_lon; + } else { + if (is_changed) { + set_osm_max_zoom(); + redraw_map = true; + } + } +} + +bool GeoMap::init() { + auto result = map_file.open(adsb_dir / u"world_map.bin"); + map_opened = !result.is_valid(); + + if (map_opened) { + map_file.read(&map_width, 2); + map_file.read(&map_height, 2); + } else { + map_width = 32768; + map_height = 32768; + } + + map_visible = map_opened || has_osm; + map_center_x = map_width >> 1; + map_center_y = map_height >> 1; + + lon_ratio = 180.0 / map_center_x; + lat_ratio = -90.0 / map_center_y; + + map_bottom = sin(-85.05 * pi / 180); // Map bitmap only goes from about -85 to 85 lat + map_world_lon = map_width / (2 * pi); + map_offset = (map_world_lon / 2 * log((1 + map_bottom) / (1 - map_bottom))); + return map_opened; +} + +void GeoMap::set_mode(GeoMapMode mode) { + mode_ = mode; +} + +void GeoMap::set_manual_panning(bool v) { + manual_panning_ = v; +} + +bool GeoMap::manual_panning() { + return manual_panning_; +} + +void GeoMap::draw_scale(Painter& painter) { + const auto r = screen_rect(); + uint32_t m = 800000; + uint32_t scale_width = (map_zoom > 0) ? m * map_zoom * pixels_per_km : m * pixels_per_km / (-map_zoom); + ui::Color scale_color = (map_visible) ? Color::black() : Color::white(); + std::string km_string; + + while (scale_width > (uint32_t)r.width() * (1000 / 2)) { + scale_width /= 2; + m /= 2; + } + scale_width /= 1000; + if (m < 1000) { + km_string = to_string_dec_uint(m) + "m"; + } else { + m += 50; // (add rounding factor for div by 100 below) + uint32_t km = m / 1000; + m -= km * 1000; + if (m == 0) { + km_string = to_string_dec_uint(km) + " km"; + } else { + km_string = to_string_dec_uint(km) + "." + to_string_dec_uint(m / 100, 1) + "km"; + } + } + + painter.fill_rectangle({{r.right() - 5 - (uint16_t)scale_width, r.bottom() - 4}, {(uint16_t)scale_width, 2}}, scale_color); + painter.fill_rectangle({{r.right() - 5, r.bottom() - 8}, {2, 6}}, scale_color); + painter.fill_rectangle({{r.right() - 5 - (uint16_t)scale_width, r.bottom() - 8}, {2, 6}}, scale_color); + std::string_view sw = km_string; + ui::Point pos = {(uint16_t)(r.right() - 25 - scale_width - sw.length() * 5 / 2), r.bottom() - 10}; + painter.draw_string(pos, *Theme::getInstance()->fg_light, sw); +} + +void GeoMap::draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color, Painter& painter) { + Point arrow_a, arrow_b, arrow_c; + + for (size_t thickness = 0; thickness < 3; thickness++) { + arrow_a = fast_polar_to_point((int)angle, size) + origin; + arrow_b = fast_polar_to_point((int)(angle + 180 - 35), size) + origin; + arrow_c = fast_polar_to_point((int)(angle + 180 + 35), size) + origin; + + painter.draw_line(arrow_a, arrow_b, color); + painter.draw_line(arrow_b, arrow_c, color); + painter.draw_line(arrow_c, arrow_a, color); + + size--; + } + + painter.draw_pixel(origin, color); // 1 pixel indicating center pivot point of bearing symbol +} + +void GeoMap::draw_marker(Painter& painter, const ui::Point itemPoint, const uint16_t itemAngle, const std::string itemTag, const Color color, const Color fontColor, const Color backColor) { + const auto r = screen_rect(); + + int tagOffset = 10; + if (mode_ == PROMPT) { + // Cross + painter.fill_rectangle({itemPoint - Point(16, 1), {32, 2}}, color); + painter.fill_rectangle({itemPoint - Point(1, 16), {2, 32}}, color); + tagOffset = 16; + } else if (angle_ < 360) { + // if we have a valid angle draw bearing + draw_bearing(itemPoint, itemAngle, 10, color, painter); + tagOffset = 10; + } else { + // draw a small cross + painter.fill_rectangle({itemPoint - Point(8, 1), {16, 2}}, color); + painter.fill_rectangle({itemPoint - Point(1, 8), {2, 16}}, color); + tagOffset = 8; + } + // center tag above point + if ((itemPoint.y() - r.top() >= 32) && (itemTag.find_first_not_of(' ') != itemTag.npos)) { // only draw tag if doesn't overlap top & not just spaces + painter.draw_string(itemPoint - Point(((int)itemTag.length() * 8 / 2), 14 + tagOffset), + style().font, fontColor, backColor, itemTag); + } +} + +void GeoMap::draw_mypos(Painter& painter) { + if ((my_pos.lat < INVALID_LAT_LON) && (my_pos.lon < INVALID_LAT_LON)) + draw_marker_item(painter, my_pos, Color::yellow()); +} + +void GeoMap::clear_markers() { + markerListLen = 0; +} + +MapMarkerStored GeoMap::store_marker(GeoMarker& marker) { + const auto r = screen_rect(); + MapMarkerStored ret; + + // Check if it could be on screen + // (Shows more distant planes when zoomed out) + GeoPoint mapPoint = lat_lon_to_map_pixel(marker.lat, marker.lon); + int x_dist = abs((int)mapPoint.x - (int)x_pos); + int y_dist = abs((int)mapPoint.y - (int)y_pos); + int zoom_out = (map_zoom < 0) ? -map_zoom : 1; + + if ((x_dist >= (zoom_out * r.width() / 2)) || (y_dist >= (zoom_out * r.height() / 2))) { + ret = MARKER_NOT_STORED; + } else if (markerListLen < NumMarkerListElements) { + markerList[markerListLen] = marker; + markerListLen++; + redraw_map = true; + ret = MARKER_STORED; + } else { + ret = MARKER_LIST_FULL; + } + return ret; +} + +void GeoMap::update_my_position(float lat, float lon, int32_t altitude) { + bool is_changed = (my_pos.lat != lat) || (my_pos.lon != lon); + my_pos.lat = lat; + my_pos.lon = lon; + my_altitude = altitude; + redraw_map = is_changed; + set_dirty(); +} + +void GeoMap::update_my_orientation(uint16_t angle, bool refresh) { + bool is_changed = (my_pos.angle != angle); + my_pos.angle = angle; + if (refresh && is_changed) { + redraw_map = true; + set_dirty(); + } +} + +MapType GeoMap::get_map_type() { + return use_osm ? MAP_TYPE_OSM : MAP_TYPE_BIN; +} + +void GeoMapView::focus() { + geopos.focus(); + if (!geomap.map_file_opened()) { + // nav_.display_modal("No map", "No world_map.bin file in\n/" + adsb_dir.string() + "/ directory", ABORT); + // TODO crate an error display + } +} + +void GeoMapView::update_my_position(float lat, float lon, int32_t altitude) { + geomap.update_my_position(lat, lon, altitude); +} +void GeoMapView::update_my_orientation(uint16_t angle, bool refresh) { + geomap.update_my_orientation(angle, refresh); +} + +void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t altitude, int32_t speed) { + if (geomap.manual_panning()) { + geomap.set_dirty(); + return; + } + bool is_changed = lat_ != lat || lon_ != lon || altitude_ != altitude || speed_ != speed || angle_ != angle; + lat_ = lat; + lon_ = lon; + altitude_ = altitude; + speed_ = speed; + + // Stupid hack to avoid an event loop + geopos.set_report_change(false); + geopos.set_lat(lat_); + geopos.set_lon(lon_); + geopos.set_altitude(altitude_); + geopos.set_speed(speed_); + geopos.set_report_change(true); + + geomap.set_angle(angle); + if (is_changed) geomap.move(lon_, lat_); + geomap.set_dirty(); +} + +void GeoMapView::update_tag(const std::string tag) { + geomap.set_tag(tag); +} + +void GeoMapView::setup() { + add_child(&geomap); + + geopos.set_altitude(altitude_); + geopos.set_lat(lat_); + geopos.set_lon(lon_); + + geopos.on_change = [this](int32_t altitude, float lat, float lon, int32_t speed) { + bool is_changed = (altitude_ != altitude) || (lat_ != lat) || (lon_ != lon) || (speed_ != speed); + altitude_ = altitude; + lat_ = lat; + lon_ = lon; + speed_ = speed; + geopos.hide_altandspeed(); + geomap.set_manual_panning(true); + if (is_changed) geomap.move(lon_, lat_); + geomap.set_dirty(); + }; + + geomap.on_move = [this](float move_x, float move_y, bool absolute) { + if (absolute) { + lon_ = move_x; + lat_ = move_y; + } else { + lon_ += move_x; + lat_ += move_y; + } + + // Stupid hack to avoid an event loop + geopos.set_report_change(false); + geopos.set_lon(lon_); + geopos.set_lat(lat_); + geopos.set_report_change(true); + + geomap.move(lon_, lat_); + geomap.set_dirty(); + }; +} + +GeoMapView::~GeoMapView() { + if (on_close_) + on_close_(); +} + +// Display mode +GeoMapView::GeoMapView( + const std::string& tag, + int32_t altitude, + GeoPos::alt_unit altitude_unit, + GeoPos::spd_unit speed_unit, + float lat, + float lon, + uint16_t angle, + const std::function on_close) + : altitude_(altitude), + altitude_unit_(altitude_unit), + speed_unit_(speed_unit), + lat_(lat), + lon_(lon), + angle_(angle), + on_close_(on_close) { + mode_ = DISPLAY; + + add_child(&geopos); + + geomap.init(); + + setup(); + + geomap.set_mode(mode_); + geomap.set_tag(tag); + geomap.set_angle(angle); + geomap.move(lon_, lat_); + geomap.set_focusable(true); + + geopos.set_read_only(true); +} + +// Prompt mode +GeoMapView::GeoMapView( + int32_t altitude, + GeoPos::alt_unit altitude_unit, + GeoPos::spd_unit speed_unit, + float lat, + float lon, + const std::function on_done) + : altitude_(altitude), + altitude_unit_(altitude_unit), + speed_unit_(speed_unit), + lat_(lat), + lon_(lon) { + mode_ = PROMPT; + + add_child(&geopos); + + geomap.init(); + + setup(); + add_child(&button_ok); + + geomap.set_mode(mode_); + geomap.move(lon_, lat_); + geomap.set_focusable(true); + + button_ok.on_select = [this, on_done](Button&) { + if (on_done) + on_done(altitude_, lat_, lon_, speed_); + // exit handled on caller side + }; +} + +void GeoMapView::clear_markers() { + geomap.clear_markers(); +} + +MapMarkerStored GeoMapView::store_marker(GeoMarker& marker) { + return geomap.store_marker(marker); +} + +MapType GeoMapView::get_map_type() { + return geomap.get_map_type(); +} + +} /* namespace ui */ diff --git a/firmware/standalone/digitalrain/ui/ui_geomap.hpp b/firmware/standalone/digitalrain/ui/ui_geomap.hpp new file mode 100644 index 000000000..9d46a7cc7 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_geomap.hpp @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2017 Furrtek + * Copyright (C) 2024 Mark Thompson + * + * 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 __GEOMAP_H__ +#define __GEOMAP_H__ + +#include "ui.hpp" +#include "ui_widget.hpp" +#include "file.hpp" +#include "bmpfile.hpp" +#include "mathdef.hpp" +#include + +namespace ui { + +#define MAX_MAP_ZOOM_IN 4000 +#define MAX_MAP_ZOOM_OUT 10 +#define MAP_ZOOM_RESOLUTION_LIMIT 5 // Max zoom-in to show map; rect height & width must divide into this evenly + +#define INVALID_LAT_LON 200 +#define INVALID_ANGLE 400 + +#define GEOMAP_BANNER_HEIGHT (3 * 16) +#define GEOMAP_RECT_WIDTH 240 +#define GEOMAP_RECT_HEIGHT (320 - 16 - GEOMAP_BANNER_HEIGHT) + +#define TILE_SIZE 256 + +enum GeoMapMode { + DISPLAY, + PROMPT +}; + +struct GeoPoint { + public: + float x{0}; + float y{0}; +}; + +struct GeoMarker { + public: + float lat{0}; + float lon{0}; + uint16_t angle{0}; + std::string tag{""}; + + GeoMarker& operator=(GeoMarker& rhs) { + lat = rhs.lat; + lon = rhs.lon; + angle = rhs.angle; + tag = rhs.tag; + + return *this; + } +}; + +class GeoPos : public View { + public: + enum alt_unit { + FEET = 0, + METERS + }; + enum spd_unit { + NONE = 0, + MPH, + KMPH, + KNOTS, + HIDDEN = 255 + }; + + std::function on_change{}; + + GeoPos(const Point pos, const alt_unit altitude_unit, const spd_unit speed_unit); + + void focus() override; + + void set_read_only(bool v); + void set_altitude(int32_t altitude); + void set_speed(int32_t speed); + void set_lat(float lat); + void set_lon(float lon); + int32_t altitude(); + int32_t speed(); + void hide_altandspeed(); + float lat(); + float lon(); + + void set_report_change(bool v); + + private: + bool read_only{false}; + bool report_change{true}; + alt_unit altitude_unit_{}; + spd_unit speed_unit_{}; + + Labels labels_position{ + {{1 * 8, 0 * 16}, "Alt:", Theme::getInstance()->fg_light->foreground}, + {{1 * 8, 1 * 16}, "Lat: \xB0 ' \"", Theme::getInstance()->fg_light->foreground}, // 0xB0 is degree ° symbol in our 8x16 font + {{1 * 8, 2 * 16}, "Lon: \xB0 ' \"", Theme::getInstance()->fg_light->foreground}, + }; + Labels label_spd_position{ + {{15 * 8, 0 * 16}, "Spd:", Theme::getInstance()->fg_light->foreground}, + }; + NumberField field_altitude{ + {6 * 8, 0 * 16}, + 5, + {-1000, 50000}, + 250, + ' '}; + + NumberField field_speed{ + {19 * 8, 0 * 16}, + 4, + {0, 5000}, + 1, + ' '}; + Text text_alt_unit{ + {12 * 8, 0 * 16, 2 * 8, 16}, + ""}; + Text text_speed_unit{ + {25 * 8, 0 * 16, 5 * 8, 16}, + ""}; + + NumberField field_lat_degrees{ + {5 * 8, 1 * 16}, + 4, + {-90, 90}, + 1, + ' '}; + NumberField field_lat_minutes{ + {10 * 8, 1 * 16}, + 2, + {0, 59}, + 1, + ' ', + true}; + NumberField field_lat_seconds{ + {13 * 8, 1 * 16}, + 2, + {0, 59}, + 1, + ' ', + true}; + Text text_lat_decimal{ + {17 * 8, 1 * 16, 13 * 8, 1 * 16}, + ""}; + + NumberField field_lon_degrees{ + {5 * 8, 2 * 16}, + 4, + {-180, 180}, + 1, + ' '}; + NumberField field_lon_minutes{ + {10 * 8, 2 * 16}, + 2, + {0, 59}, + 1, + ' ', + true}; + NumberField field_lon_seconds{ + {13 * 8, 2 * 16}, + 2, + {0, 59}, + 1, + ' ', + true}; + Text text_lon_decimal{ + {17 * 8, 2 * 16, 13 * 8, 1 * 16}, + ""}; +}; + +enum MapMarkerStored { + MARKER_NOT_STORED, + MARKER_STORED, + MARKER_LIST_FULL +}; + +enum MapType { + MAP_TYPE_OSM, + MAP_TYPE_BIN +}; + +class GeoMap : public Widget { + public: + std::function on_move{}; + + GeoMap(Rect parent_rect); + + void paint(Painter& painter) override; + + bool on_touch(const TouchEvent event) override; + bool on_encoder(const EncoderEvent delta) override; + bool on_keyboard(const KeyboardEvent event) override; + + void update_my_position(float lat, float lon, int32_t altitude); + void update_my_orientation(uint16_t angle, bool refresh = false); + + bool init(); + void set_mode(GeoMapMode mode); + void set_manual_panning(bool v); + bool manual_panning(); + void move(const float lon, const float lat); + void set_tag(std::string new_tag) { + tag_ = new_tag; + } + MapType get_map_type(); + + void set_angle(uint16_t new_angle) { + angle_ = new_angle; + } + + bool map_file_opened() { return map_opened; } + + void set_hide_center_marker(bool hide) { + hide_center_marker_ = hide; + } + bool hide_center_marker() { return hide_center_marker_; } + + static const int NumMarkerListElements = 30; + + void clear_markers(); + MapMarkerStored store_marker(GeoMarker& marker); + + static const Dim banner_height = GEOMAP_BANNER_HEIGHT; + static const Dim geomap_rect_width = GEOMAP_RECT_WIDTH; + static const Dim geomap_rect_height = GEOMAP_RECT_HEIGHT; + + private: + void draw_scale(Painter& painter); + ui::Point item_rect_pixel(GeoMarker& item); + GeoPoint lat_lon_to_map_pixel(float lat, float lon); + void draw_marker_item(Painter& painter, GeoMarker& item, const Color color, const Color fontColor = Color::white(), const Color backColor = Color::black()); + void draw_marker(Painter& painter, const ui::Point itemPoint, const uint16_t itemAngle, const std::string itemTag, const Color color = Color::red(), const Color fontColor = Color::white(), const Color backColor = Color::black()); + void draw_markers(Painter& painter); + void draw_mypos(Painter& painter); + void draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color, Painter& painter); + void draw_map_grid(ui::Rect r, Painter& painter); + void draw_switcher(Painter& painter); + void map_read_line_bin(ui::Color* buffer, uint16_t pixels); + // open street map related + uint8_t find_osm_file_tile(); + void set_osm_max_zoom(); + bool draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y, Painter& painter); + int lon2tile(double lon, int zoom); + int lat2tile(double lat, int zoom); + double lon_to_pixel_x_tile(double lon, int zoom); + double lat_to_pixel_y_tile(double lat, int zoom); + double tile_pixel_x_to_lon(int x, int zoom); + double tile_pixel_y_to_lat(int y, int zoom); + uint8_t map_osm_zoom{3}; + double viewport_top_left_px = 0; + double viewport_top_left_py = 0; + + bool manual_panning_{false}; + bool hide_center_marker_{false}; + GeoMapMode mode_{}; + File map_file{}; + bool map_opened{}; + bool map_visible{}; + uint16_t map_width{}, map_height{}; + int32_t map_center_x{}, map_center_y{}; + int16_t map_zoom{1}; + + float lon_ratio{}, lat_ratio{}; + double map_bottom{}; + double map_world_lon{}; + double map_offset{}; + float x_pos{}, y_pos{}; + float prev_x_pos{32767.0f}, prev_y_pos{32767.0f}; + float lat_{}; + float lon_{}; + float zoom_pixel_offset{0.0f}; + float pixels_per_km{}; + uint16_t angle_{}; + std::string tag_{}; + + // the portapack's position data ( for example injected from serial ) + GeoMarker my_pos{INVALID_LAT_LON, INVALID_LAT_LON, INVALID_ANGLE, ""}; // lat, lon, angle, tag + int32_t my_altitude{0}; + + int markerListLen{0}; + GeoMarker markerList[NumMarkerListElements]; + bool redraw_map{true}; + bool use_osm{false}; + bool has_osm{false}; +}; + +class GeoMapView : public View { + public: + GeoMapView( + const std::string& tag, + int32_t altitude, + GeoPos::alt_unit altitude_unit, + GeoPos::spd_unit speed_unit, + float lat, + float lon, + uint16_t angle, + const std::function on_close = nullptr); + GeoMapView( + int32_t altitude, + GeoPos::alt_unit altitude_unit, + GeoPos::spd_unit speed_unit, + float lat, + float lon, + const std::function on_done); + ~GeoMapView(); + + GeoMapView(const GeoMapView&) = delete; + GeoMapView(GeoMapView&&) = delete; + GeoMapView& operator=(const GeoMapView&) = delete; + GeoMapView& operator=(GeoMapView&&) = delete; + + void focus() override; + + void update_position(float lat, float lon, uint16_t angle, int32_t altitude, int32_t speed = 0); + void update_my_position(float lat, float lon, int32_t altitude); + void update_my_orientation(uint16_t angle, bool refresh = false); + + MapType get_map_type(); + + std::string title() const override { return "Map view"; }; + + void clear_markers(); + MapMarkerStored store_marker(GeoMarker& marker); + + void update_tag(const std::string tag); + + private: + void setup(); + + const std::function on_done{}; + GeoMapMode mode_{}; + int32_t altitude_{}; + int32_t speed_{}; + GeoPos::alt_unit altitude_unit_{}; + GeoPos::spd_unit speed_unit_{}; + float lat_{}; + float lon_{}; + uint16_t angle_{}; + std::function on_close_{nullptr}; + + GeoPos geopos{ + {0, 0}, + altitude_unit_, + speed_unit_}; + + GeoMap geomap{ + {0, GeoMap::banner_height, GeoMap::geomap_rect_width, GeoMap::geomap_rect_height}}; + + Button button_ok{ + {screen_width - 15 * 8, 0, 15 * 8, 1 * 16}, + "OK"}; +}; + +} /* namespace ui */ + +#endif diff --git a/firmware/standalone/digitalrain/ui/ui_painter.cpp b/firmware/standalone/digitalrain/ui/ui_painter.cpp new file mode 100644 index 000000000..892815c5e --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_painter.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_painter.hpp" +#include "ui_widget.hpp" +// #include "portapack.hpp" +#include "standalone_app.hpp" + +// using namespace portapack; + +namespace ui { + +Style Style::invert() const { + return { + .font = font, + .background = foreground, + .foreground = background}; +} + +int Painter::draw_char(Point p, const Style& style, char c) { + const auto glyph = style.font.glyph(c); + _api->draw_bitmap(p.x(), p.y(), glyph.size().width(), glyph.size().height(), glyph.pixels(), style.foreground.v, style.background.v); + return glyph.advance().x(); +} + +int Painter::draw_string(Point p, const Style& style, std::string_view text) { + return draw_string(p, style.font, style.foreground, style.background, text); +} + +int Painter::draw_string( + Point p, + const Font& font, + Color foreground, + Color background, + std::string_view text) { + bool escape = false; + size_t width = 0; + Color pen = foreground; + + for (auto c : text) { + if (escape) { + if (c < std::size(term_colors)) + pen = term_colors[(uint8_t)c]; + else + pen = foreground; + escape = false; + } else { + if (c == '\x1B') { + escape = true; + } else { + const auto glyph = font.glyph(c); + _api->draw_bitmap(p.x(), p.y(), glyph.size().width(), glyph.size().height(), glyph.pixels(), pen.v, background.v); + const auto advance = glyph.advance(); + p += advance; + width += advance.x(); + } + } + } + + return width; +} + +void Painter::draw_bitmap(Point p, const Bitmap& bitmap, Color foreground, Color background) { + // If bright foreground colors on white background, darken the foreground color to improve visibility + if ((background.v == ui::Color::white().v) && (foreground.to_greyscale() > 146)) + foreground = foreground.dark(); + + _api->draw_bitmap(p.x(), p.y(), bitmap.size.width(), bitmap.size.height(), bitmap.data, foreground.v, background.v); +} + +void Painter::draw_hline(Point p, int width, Color c) { + _api->fill_rectangle(p.x(), p.y(), width, 1, c.v); +} + +void Painter::draw_vline(Point p, int height, Color c) { + _api->fill_rectangle(p.x(), p.y(), 1, height, c.v); +} + +void Painter::draw_rectangle(Rect r, Color c) { + draw_hline(r.location(), r.width(), c); + draw_vline({r.left(), r.top() + 1}, r.height() - 2, c); + draw_vline({r.left() + r.width() - 1, r.top() + 1}, r.height() - 2, c); + draw_hline({r.left(), r.top() + r.height() - 1}, r.width(), c); +} + +void Painter::fill_rectangle(Rect r, Color c) { + _api->fill_rectangle(r.left(), r.top(), r.width(), r.height(), c.v); +} + +void Painter::fill_rectangle_unrolled8(Rect r, Color c) { + _api->fill_rectangle_unrolled8(r.left(), r.top(), r.width(), r.height(), c.v); +} + +void Painter::paint_widget_tree(Widget* w) { + if (ui::is_dirty()) { + paint_widget(w); + ui::dirty_clear(); + } +} + +void Painter::paint_widget(Widget* w) { + if (w->hidden()) { + // Mark widget (and all children) as invisible. + w->visible(false); + } else { + // Mark this widget as visible and recurse. + w->visible(true); + + if (w->dirty()) { + w->paint(*this); + // Force-paint all children. + for (const auto child : w->children()) { + child->set_dirty(); + paint_widget(child); + } + w->set_clean(); + } else { + // Selectively paint all children. + for (const auto child : w->children()) { + paint_widget(child); + } + } + } +} + +void Painter::draw_pixel(const ui::Point p, const ui::Color color) { + _api->draw_pixel(p, color); +} + +void Painter::draw_pixels(const ui::Rect r, const ui::Color* const colors, const size_t count) { + _api->draw_pixels(r, colors, count); +} + +void Painter::draw_line(const ui::Point start, const ui::Point end, const ui::Color color) { + int x0 = start.x(); + int y0 = start.y(); + int x1 = end.x(); + int y1 = end.y(); + + int dx = std::abs(x1 - x0), sx = x0 < x1 ? 1 : -1; + int dy = std::abs(y1 - y0), sy = y0 < y1 ? 1 : -1; + int err = (dx > dy ? dx : -dy) / 2, e2; + + for (;;) { + draw_pixel({static_cast(x0), static_cast(y0)}, color); + if (x0 == x1 && y0 == y1) break; + e2 = err; + if (e2 > -dx) { + err -= dy; + x0 += sx; + } + if (e2 < dy) { + err += dx; + y0 += sy; + } + } +} + +} /* namespace ui */ diff --git a/firmware/standalone/digitalrain/ui/ui_painter.hpp b/firmware/standalone/digitalrain/ui/ui_painter.hpp new file mode 100644 index 000000000..7c95f92c0 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_painter.hpp @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_PAINTER_H__ +#define __UI_PAINTER_H__ + +#include "ui.hpp" +#include "ui_text.hpp" + +#include +#include +#include + +namespace ui { + +struct Style { + const Font& font; + const Color background; + const Color foreground; + + Style invert() const; +}; + +/* Sometimes mutation is just the more readable thing... */ +struct MutableStyle { + const Font* font; + Color background; + Color foreground; + + MutableStyle(const Style& s) + : font{&s.font}, + background{s.background}, + foreground{s.foreground} {} + + void invert() { + std::swap(background, foreground); + } + + operator Style() const { + return { + .font = *font, + .background = background, + .foreground = foreground}; + } +}; + +class Widget; + +class Painter { + public: + Painter() {}; + + Painter(const Painter&) = delete; + Painter(Painter&&) = delete; + + int draw_char(Point p, const Style& style, char c); + + int draw_string(Point p, const Style& style, std::string_view text); + int draw_string(Point p, const Font& font, Color foreground, Color background, std::string_view text); + + void draw_bitmap(Point p, const Bitmap& bitmap, Color background, Color foreground); + + void draw_rectangle(Rect r, Color c); + void fill_rectangle(Rect r, Color c); + void fill_rectangle_unrolled8(Rect r, Color c); + + void paint_widget_tree(Widget* w); + + void draw_hline(Point p, int width, Color c); + void draw_vline(Point p, int height, Color c); + + template + void draw_pixels( + const ui::Rect r, + const std::array& colors) { + draw_pixels(r, colors.data(), colors.size()); + } + + void draw_pixels( + const ui::Rect r, + const std::vector& colors) { + draw_pixels(r, colors.data(), colors.size()); + } + + void draw_pixels(const ui::Rect r, const ui::Color* const colors, const size_t count); + void draw_line(const ui::Point start, const ui::Point end, const ui::Color color); + void draw_pixel(const ui::Point p, const ui::Color color); + + private: + void paint_widget(Widget* w); +}; + +} /* namespace ui */ + +#endif /*__UI_PAINTER_H__*/ diff --git a/firmware/standalone/digitalrain/ui/ui_text.cpp b/firmware/standalone/digitalrain/ui/ui_text.cpp new file mode 100644 index 000000000..77428f52c --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_text.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_text.hpp" + +namespace ui { + +Glyph Font::glyph(const char c) const { + size_t index; + + if (c < c_start) { + // Non-display C0 Control characters - map to blank (index 0) + return {w, h, data}; + } + + // Handle gap in glyphs table for C1 Control characters 0x80-0x9F + if (c < C1_CONTROL_CHARS_START) { + // ASCII chars <0x80: + index = c - c_start; + } else if (c >= C1_CONTROL_CHARS_START + C1_CONTROL_CHARS_COUNT) { + // Latin 1 chars 0xA0-0xFF + index = c - c_start - C1_CONTROL_CHARS_COUNT; + } else { + // C1 Control characters - map to blank + return {w, h, data}; + } + + if (index >= c_count) { // Latin Extended characters > 0xFF - not supported + return {w, h, data}; + } else { + return {w, h, &data[index * data_stride]}; + } +} + +Dim Font::line_height() const { + return h; +} + +Dim Font::char_width() const { + return w; +} + +Size Font::size_of(const std::string s) const { + Size size; + + for (const auto c : s) { + const auto glyph_data = glyph(c); + size = { + size.width() + glyph_data.w(), + std::max(size.height(), glyph_data.h())}; + } + + return size; +} + +} /* namespace ui */ diff --git a/firmware/standalone/digitalrain/ui/ui_text.hpp b/firmware/standalone/digitalrain/ui/ui_text.hpp new file mode 100644 index 000000000..acf4ad201 --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_text.hpp @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_TEXT_H__ +#define __UI_TEXT_H__ + +#include +#include +#include + +#include "ui.hpp" +#include "standalone_app.hpp" +extern const standalone_application_api_t* _api; + +// C1 Control Characters are an unprintable range of glyphs missing from the font files +#define C1_CONTROL_CHARS_START 0x80 +#define C1_CONTROL_CHARS_COUNT 32 + +namespace ui { + +class Glyph { + public: + constexpr Glyph( + Dim w, + Dim h, + const uint8_t* const pixels) + : w_{static_cast(w)}, + h_{static_cast(h)}, + pixels_{pixels} { + } + + int w() const { + return w_; + } + + int h() const { + return h_; + } + + Size size() const { + return {w_, h_}; + } + + Point advance() const { + return {w_, 0}; + } + + const uint8_t* pixels() const { + return pixels_; + } + + private: + const uint8_t w_; + const uint8_t h_; + const uint8_t* const pixels_; +}; + +class Font { + public: + constexpr Font( + Dim w, + Dim h, + const uint8_t* data, + char c_start, + size_t c_count) + : w{w}, + h{h}, + data{data}, + c_start{c_start}, + c_count{c_count}, + data_stride{(w * h + 7U) >> 3} { + } + + Glyph glyph(const char c) const; + + Dim line_height() const; + Dim char_width() const; + + Size size_of(const std::string s) const; + + private: + const Dim w; + const Dim h; + const uint8_t* const data; + const char c_start; + const size_t c_count; + const size_t data_stride; +}; + +} /* namespace ui */ + +#endif /*__UI_TEXT_H__*/ diff --git a/firmware/standalone/digitalrain/ui/ui_widget.cpp b/firmware/standalone/digitalrain/ui/ui_widget.cpp new file mode 100644 index 000000000..1c8966faa --- /dev/null +++ b/firmware/standalone/digitalrain/ui/ui_widget.cpp @@ -0,0 +1,3310 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * 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_widget.hpp" +#include "ui_painter.hpp" +// #include "portapack.hpp" +#include "standalone_application.hpp" + +#include +#include +#include + +// #include "chprintf.h" +// #include "irq_controls.hpp" +#include "string_format.hpp" +// #include "usb_serial_device_to_host.h" +// #include "rtc_time.hpp" +// #include "battery.hpp" + +// using namespace portapack; +// using namespace rtc_time; + +namespace ui +{ + + static bool ui_dirty = true; + + void dirty_set() + { + _api->set_dirty(); + ui_dirty = true; + } + + void dirty_clear() + { + ui_dirty = false; + } + + bool is_dirty() + { + return ui_dirty; + } + + /* Widget ****************************************************************/ + + const std::vector Widget::no_children{}; + + Point Widget::screen_pos() + { + return screen_rect().location(); + } + + Size Widget::size() const + { + return _parent_rect.size(); + } + + Rect Widget::screen_rect() const + { + return parent() ? (parent_rect() + parent()->screen_pos()) : parent_rect(); + } + + Rect Widget::parent_rect() const + { + return _parent_rect; + } + + void Widget::set_parent_rect(const Rect new_parent_rect) + { + _parent_rect = new_parent_rect; + set_dirty(); + } + + Widget *Widget::parent() const + { + return parent_; + } + + void Widget::set_parent(Widget *const widget) + { + if (widget == parent_) + { + return; + } + + if (parent_ && !widget) + { + // We have a parent, but are losing it. Update visible status. + dirty_overlapping_children_in_rect(screen_rect()); + visible(false); + } + + parent_ = widget; + + set_dirty(); + } + + void Widget::set_dirty() + { + flags.dirty = true; + dirty_set(); + } + + bool Widget::dirty() const + { + return flags.dirty; + } + + void Widget::set_clean() + { + flags.dirty = false; + } + + void Widget::hidden(bool hide) + { + if (hide != flags.hidden) + { + flags.hidden = hide; + + // If parent is hidden, either of these is a no-op. + if (hide) + { + // TODO: Instead of dirtying parent entirely, dirty only children + // that overlap with this widget. + + // parent()->dirty_overlapping_children_in_rect(parent_rect()); + + /* TODO: Notify self and all non-hidden children that they're + * now effectively hidden? + */ + } + else + { + set_dirty(); + /* TODO: Notify self and all non-hidden children that they're + * now effectively shown? + */ + } + } + } + + void Widget::focus() + { + context().focus_manager().set_focus_widget(this); + } + + void Widget::on_focus() + { + } + + void Widget::blur() + { + context().focus_manager().set_focus_widget(nullptr); + } + + void Widget::on_blur() + { + } + + bool Widget::focusable() const + { + return flags.focusable; + } + + void Widget::set_focusable(const bool value) + { + flags.focusable = value; + } + + bool Widget::has_focus() + { + return (context().focus_manager().focus_widget() == this); + } + + bool Widget::on_key(const KeyEvent event) + { + (void)event; + return false; + } + + bool Widget::on_encoder(const EncoderEvent event) + { + (void)event; + return false; + } + + bool Widget::on_touch(const TouchEvent event) + { + (void)event; + return false; + } + bool Widget::on_keyboard(const KeyboardEvent event) + { + (void)event; + return false; + } + + const std::vector &Widget::children() const + { + return no_children; + } + + Context &Widget::context() const + { + if (parent_ == nullptr) + _api->panic("parent__ is null"); + + return parent()->context(); + } + + void Widget::set_style(const Style *new_style) + { + if (new_style != style_) + { + style_ = new_style; + set_dirty(); + } + } + + const Style &Widget::style() const + { + if (style_ != nullptr) + return *style_; + else + { + auto p = parent(); + if (p == nullptr) + // TODO: debug + while (true) + ; + return p->style(); + } + } + + void Widget::visible(bool v) + { + if (v != flags.visible) + { + flags.visible = v; + + /* TODO: This on_show/on_hide implementation seems inelegant. + * But I need *some* way to take/configure resources when + * a widget becomes visible, and reverse the process when the + * widget becomes invisible, whether the widget (or parent) is + * hidden, or the widget (or parent) is removed from the tree. + */ + if (v) + { + on_show(); + } + else + { + on_hide(); + + // Set all children invisible too. + for (const auto child : children()) + { + child->visible(false); + } + } + } + } + + bool Widget::highlighted() const + { + return flags.highlighted; + } + + void Widget::set_highlighted(const bool value) + { + flags.highlighted = value; + } + + void Widget::dirty_overlapping_children_in_rect(const Rect &child_rect) + { + for (auto child : children()) + { + if (!child_rect.intersect(child->parent_rect()).is_empty()) + { + child->set_dirty(); + } + } + } + + void Widget::getAccessibilityText(std::string &result) + { + result = ""; + } + void Widget::getWidgetName(std::string &result) + { + result = ""; + } + /* View ******************************************************************/ + + void View::paint(Painter &painter) + { + painter.fill_rectangle( + screen_rect(), + style().background); + } + + void View::add_child(Widget *const widget) + { + if (widget) + { + if (widget->parent() == nullptr) + { + widget->set_parent(this); + children_.push_back(widget); + } + } + } + + void View::add_children(const std::initializer_list children) + { + children_.insert(std::end(children_), children); + for (auto child : children) + { + child->set_parent(this); + } + } + + void View::remove_child(Widget *const widget) + { + if (widget) + { + children_.erase(std::remove(children_.begin(), children_.end(), widget), children_.end()); + widget->set_parent(nullptr); + } + } + + void View::remove_children(const std::vector &children) + { + for (auto child : children) + { + remove_child(child); + } + } + + const std::vector &View::children() const + { + return children_; + } + + std::string View::title() const + { + return ""; + }; + + /* OptionTabView *********************************************************/ + + OptionTabView::OptionTabView(Rect parent_rect) + { + set_parent_rect(parent_rect); + + add_child(&check_enable); + hidden(true); + + check_enable.on_select = [this](Checkbox &, bool value) + { + enabled = value; + }; + } + + void OptionTabView::set_enabled(bool value) + { + check_enable.set_value(value); + } + + bool OptionTabView::is_enabled() + { + return check_enable.value(); + } + + void OptionTabView::set_type(std::string type) + { + check_enable.set_text("Transmit " + type); + } + + void OptionTabView::focus() + { + check_enable.focus(); + } + + /* Rectangle *************************************************************/ + + Rectangle::Rectangle( + Color c) + : Widget{}, + color{c} + { + } + + Rectangle::Rectangle( + Rect parent_rect, + Color c) + : Widget{parent_rect}, + color{c} + { + } + + void Rectangle::set_color(const Color c) + { + color = c; + set_dirty(); + } + + void Rectangle::set_outline(const bool outline) + { + _outline = outline; + set_dirty(); + } + + void Rectangle::paint(Painter &painter) + { + if (!_outline) + { + painter.fill_rectangle( + screen_rect(), + color); + } + else + { + painter.draw_rectangle( + screen_rect(), + color); + } + } + + /* Text ******************************************************************/ + + Text::Text( + Rect parent_rect, + std::string text) + : Widget{parent_rect}, + text{std::move(text)} + { + } + + Text::Text( + Rect parent_rect) + : Text{parent_rect, {}} + { + } + + void Text::set(std::string_view value) + { + text = std::string{value}; + set_dirty(); + } + + void Text::getAccessibilityText(std::string &result) + { + result = text; + } + void Text::getWidgetName(std::string &result) + { + result = "Text"; + } + void Text::paint(Painter &painter) + { + const auto rect = screen_rect(); + auto s = has_focus() ? style().invert() : style(); + auto max_len = (unsigned)rect.width() / s.font.char_width(); + auto text_view = std::string_view{text}; + + painter.fill_rectangle(rect, s.background); + + if (text_view.length() > max_len) + text_view = text_view.substr(0, max_len); + + painter.draw_string( + rect.location(), + s, + text_view); + } + + /* Labels ****************************************************************/ + + Labels::Labels( + std::initializer_list