diff --git a/.github/workflows/LibraryBuild.yml b/.github/workflows/LibraryBuild.yml index 3745c46d..971a6471 100644 --- a/.github/workflows/LibraryBuild.yml +++ b/.github/workflows/LibraryBuild.yml @@ -49,24 +49,28 @@ jobs: required-libraries: "LovyanGFX" - matrix-context: M5Stack-SD-Menu arduino-boards-fqbn: esp32:esp32:m5stack-core-esp32 - sketch-names: M5Stack-SD-Menu.ino - bin-name: M5stack-Launcher.bin - required-libraries: "ESP32-Chimera-Core,LovyanGFX,ArduinoJson" + sketch-names: M5Stack-SD-Menu.ino,AppStore.ino + launcher-name: M5stack-Launcher.bin + appstore-name: M5stack-AppStore.bin + required-libraries: "ESP32-Chimera-Core,LovyanGFX,ArduinoJson,ESP32-targz" - matrix-context: M5Core2-SD-Menu arduino-boards-fqbn: esp32:esp32:m5stack-core2 - sketch-names: M5Stack-SD-Menu.ino - bin-name: M5Core2-Launcher.bin - required-libraries: "ESP32-Chimera-Core,LovyanGFX,ArduinoJson" + sketch-names: M5Stack-SD-Menu.ino,AppStore.ino + launcher-name: M5Core2-Launcher.bin + appstore-name: M5Core2-AppStore.bin + required-libraries: "ESP32-Chimera-Core,LovyanGFX,ArduinoJson,ESP32-targz" - matrix-context: M5Fire-SD-Menu arduino-boards-fqbn: esp32:esp32:m5stack-fire - sketch-names: M5Stack-SD-Menu.ino - bin-name: M5Fire-Launcher.bin - required-libraries: "ESP32-Chimera-Core,LovyanGFX,ArduinoJson" + sketch-names: M5Stack-SD-Menu.ino,AppStore.ino + launcher-name: M5Fire-Launcher.bin + appstore-name: M5Fire-AppStore.bin + required-libraries: "ESP32-Chimera-Core,LovyanGFX,ArduinoJson,ESP32-targz" - matrix-context: OdroidGo-SD-Menu arduino-boards-fqbn: esp32:esp32:odroid_esp32 - sketch-names: M5Stack-SD-Menu.ino - bin-name: OdroidGo-Launcher.bin - required-libraries: "ESP32-Chimera-Core,LovyanGFX,ArduinoJson" + sketch-names: M5Stack-SD-Menu.ino,AppStore.ino + launcher-name: OdroidGo-Launcher.bin + appstore-name: OdroidGo-AppStore.bin + required-libraries: "ESP32-Chimera-Core,LovyanGFX,ArduinoJson,ESP32-targz" #- matrix-context: TTGO-LoRa32-V2-SDLoader-Snippet #arduino-boards-fqbn: esp32:esp32:ttgo-lora32-v2 #sketch-names: TTGO-SDLoader-Snippet.ino @@ -87,6 +91,7 @@ jobs: arduino-board-fqbn: ${{ matrix.arduino-boards-fqbn }} required-libraries: ${{ matrix.required-libraries }} extra-arduino-lib-install-args: --no-deps + # extra-arduino-cli-args: sketch-names: ${{ matrix.sketch-names }} set-build-path: true #build-properties: ${{ toJson(matrix.build-properties) }} @@ -94,13 +99,16 @@ jobs: - name: Copy compiled binary if: startsWith(matrix.sketch-names, 'M5Stack-SD-Menu') run: | - cp examples/M5Stack-SD-Menu/build/M5Stack-SD-Menu.ino.bin examples/M5Stack-SD-Menu/build/${{ matrix.bin-name }} - - name: Upload artifact ${{ matrix.bin-name }} + cp examples/M5Stack-SD-Menu/build/M5Stack-SD-Menu.ino.bin examples/M5Stack-SD-Menu/build/${{ matrix.launcher-name }} + cp examples/AppStore/build/AppStore.ino.bin examples/M5Stack-SD-Menu/build/${{ matrix.appstore-name }} + - name: Upload artifact ${{ matrix.matrix-context }} uses: actions/upload-artifact@v2 if: startsWith(matrix.sketch-names, 'M5Stack-SD-Menu') with: - name: ${{ matrix.bin-name }} - path: examples/M5Stack-SD-Menu/build/${{ matrix.bin-name }} + name: ${{ matrix.matrix-context }} + path: | + examples/M5Stack-SD-Menu/build/${{ matrix.launcher-name }} + examples/M5Stack-SD-Menu/build/${{ matrix.appstore-name }} post_build: name: Gather Artefacts diff --git a/README.md b/README.md index 69430c44..ae778713 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Build Status](https://travis-ci.com/tobozo/M5Stack-SD-Updater.svg?branch=master)](https://travis-ci.com/github/tobozo/M5Stack-SD-Updater) [![Gitter](https://badges.gitter.im/M5Stack-SD-Updater/community.svg)](https://gitter.im/M5Stack-SD-Updater/community) [![arduino-library-badge](https://www.ardu-badge.com/badge/M5Stack-SD-Updater.svg?)](https://www.ardu-badge.com/M5Stack-SD-Updater) +[![PlatformIO Registry](https://badges.registry.platformio.org/packages/tobozo/library/M5Stack-SD-Updater.svg)](https://registry.platformio.org/packages/libraries/tobozo/M5Stack-SD-Updater) + # M5Stack-SD-Updater diff --git a/examples/AppStore/AppStore.ino b/examples/AppStore/AppStore.ino new file mode 100644 index 00000000..71139ea1 --- /dev/null +++ b/examples/AppStore/AppStore.ino @@ -0,0 +1,2 @@ + +#include "main/main.cpp" diff --git a/examples/AppStore/img/.directory b/examples/AppStore/img/.directory new file mode 100644 index 00000000..ac785c42 --- /dev/null +++ b/examples/AppStore/img/.directory @@ -0,0 +1,11 @@ +[Dolphin] +HeaderColumnWidths=493,72,118,134 +SortOrder=1 +SortRole=modificationtime +Timestamp=2021,11,26,16,18,14 +Version=4 +ViewMode=2 +VisibleRoles=Details_text,Details_size,Details_modificationtime,Details_type,CustomizedDetails + +[Settings] +HiddenFilesShown=true diff --git a/examples/AppStore/img/appstore-brokenimage.png b/examples/AppStore/img/appstore-brokenimage.png new file mode 100644 index 00000000..500ff354 Binary files /dev/null and b/examples/AppStore/img/appstore-brokenimage.png differ diff --git a/examples/AppStore/img/appstore.png b/examples/AppStore/img/appstore.png new file mode 100644 index 00000000..06f51873 Binary files /dev/null and b/examples/AppStore/img/appstore.png differ diff --git a/examples/AppStore/img/capture.png b/examples/AppStore/img/capture.png new file mode 100644 index 00000000..95355723 Binary files /dev/null and b/examples/AppStore/img/capture.png differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h02m09s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h02m09s.jpg new file mode 100644 index 00000000..7f3447b9 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h02m09s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h09m15s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h09m15s.jpg new file mode 100644 index 00000000..a62227c9 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h09m15s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h10m23s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h10m23s.jpg new file mode 100644 index 00000000..345de916 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h10m23s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h10m31s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h10m31s.jpg new file mode 100644 index 00000000..ef0d2cc8 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h10m31s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h10m40s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h10m40s.jpg new file mode 100644 index 00000000..74868878 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h10m40s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h10m49s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h10m49s.jpg new file mode 100644 index 00000000..2d053550 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h10m49s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h10m58s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h10m58s.jpg new file mode 100644 index 00000000..01747eab Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h10m58s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h11m07s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h11m07s.jpg new file mode 100644 index 00000000..684b192f Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h11m07s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h11m53s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h11m53s.jpg new file mode 100644 index 00000000..392b80e2 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h11m53s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h12m00s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h12m00s.jpg new file mode 100644 index 00000000..672b9086 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h12m00s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h12m09s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h12m09s.jpg new file mode 100644 index 00000000..abb4e0d3 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h12m09s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h12m29s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h12m29s.jpg new file mode 100644 index 00000000..1912dc38 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h12m29s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h13m02s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h13m02s.jpg new file mode 100644 index 00000000..3b9d76a2 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h13m02s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_17h13m49s.jpg b/examples/AppStore/img/screenshot-2021-12-05_17h13m49s.jpg new file mode 100644 index 00000000..d2438cf2 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_17h13m49s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-05_19h02m12s.jpg b/examples/AppStore/img/screenshot-2021-12-05_19h02m12s.jpg new file mode 100644 index 00000000..2fea8d9b Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-05_19h02m12s.jpg differ diff --git a/examples/AppStore/img/screenshot-2021-12-06_00h25m47s.jpg b/examples/AppStore/img/screenshot-2021-12-06_00h25m47s.jpg new file mode 100644 index 00000000..f2c03344 Binary files /dev/null and b/examples/AppStore/img/screenshot-2021-12-06_00h25m47s.jpg differ diff --git a/examples/AppStore/main/main.cpp b/examples/AppStore/main/main.cpp new file mode 100644 index 00000000..1da64d2b --- /dev/null +++ b/examples/AppStore/main/main.cpp @@ -0,0 +1,30 @@ + +#include "../modules/AppStoreMain/AppStoreMain.cpp" + + +void setup() +{ + AppStore::setup(); +} + + +void loop() +{ + AppStore::loop(); +} + +#if !defined ARDUINO +extern "C" { + void loopTask(void*) + { + setup(); + for(;;) { + loop(); + } + } + void app_main() + { + xTaskCreatePinnedToCore( loopTask, "loopTask", 8192, NULL, 1, NULL, 1 ); + } +} +#endif diff --git a/examples/AppStore/modules/.directory b/examples/AppStore/modules/.directory new file mode 100644 index 00000000..4c4d3470 --- /dev/null +++ b/examples/AppStore/modules/.directory @@ -0,0 +1,10 @@ +[Dolphin] +HeaderColumnWidths=493,72,118,134 +PreviewsShown=false +Timestamp=2021,11,23,16,26,29 +Version=4 +ViewMode=1 +VisibleRoles=Details_text,Details_size,Details_modificationtime,Details_type,CustomizedDetails + +[Settings] +HiddenFilesShown=true diff --git a/examples/AppStore/modules/AppStoreActions/AppStoreActions.cpp b/examples/AppStore/modules/AppStoreActions/AppStoreActions.cpp new file mode 100644 index 00000000..03424f96 --- /dev/null +++ b/examples/AppStore/modules/AppStoreActions/AppStoreActions.cpp @@ -0,0 +1,940 @@ +#pragma once + +#include +#include "AppStoreActions.hpp" +#include "../AppStoreUI/AppStoreUI.hpp" +#include "../Registry/Registry.hpp" +#include "../FSUtils/FSUtils.hpp" +#include "../Downloader/Downloader.hpp" + +extern AppRegistry Registry; +extern LogWindow *Console; + + +namespace UIDo +{ + + using namespace FSUtils; + using namespace UILists; + using namespace UIUtils; + using namespace RegistryUtils; + + void BtnA() + { + // Action button + UI->execBtnA(); + } + + void BtnB() + { + // Navigation button + //UI->pageDown(); + UI->menuUp(); + } + + void BtnC() + { + // Navigation button + UI->menuDown(); + } + + + void checkSleepTimer() + { + if( lastpush + MsBeforeSleep < millis() ) { // go to sleep if nothing happens for a while + if( brightness > 1 ) { // slowly dim the screen first + brightness--; + if( brightness %10 == 0 ) { + Serial.print("(\".¬.\") "); + } + if( brightness %30 == 0 ) { + Serial.print(" Yawn... "); + } + if( brightness %7 == 0 ) { + Serial.println(" .zzZzzz. "); + } + UI->getGfx()->setBrightness( brightness ); + lastpush = millis() - (MsBeforeSleep - brightness*10); // exponential dimming effect + return; + } + gotoSleep(); + } + } + + + void gotoSleep() + { + Serial.println( GOTOSLEEP_MESSAGE ); + #ifdef ARDUINO_M5STACK_Core2 + esp_sleep_enable_ext0_wakeup(GPIO_NUM_39, 0); // gpio39 == touch INT + #else + #if defined HAS_POWER || defined HAS_IP5306 + // M5Fire / M5Stack / Odroid-Go + M5.setWakeupButton( BUTTON_B_PIN ); + //M5.powerOFF(); + #endif + #endif + delay(100); + M5.Lcd.fillScreen( UI->getTheme()->BgColor ); + M5.Lcd.sleep(); + M5.Lcd.waitDisplay(); + esp_deep_sleep_start(); + } + + + void clearTLS() + { + // cleanup /cert/ and /.registry/ folders + Console = new LogWindow(); + cleanDir( SD_CERT_PATH ); + cleanDir( appRegistryFolder.c_str() ); + delete Console; + buildRootMenu(); + } + + + void clearApps() + { + int resp = modalConfirm( MODAL_DELETEALL_TITLE, nullptr, MODAL_DELETEALL_MSG, MENUACTION_APPDELETE, MENU_BTN_CANCEL, MENU_BTN_NO ); + if( resp == HID_BTN_A ) { + #if !defined HAS_RTC + setTimeFromLastFSAccess(); // set the time before cleaning up the folder + #endif + Console = new LogWindow(); + cleanDir( CATALOG_DIR ); + // cleanDir( ROOT_DIR ); // <<< TODO: filter from app list + // cleanDir( DIR_jpg ); // <<< TODO: filter from app list + // cleanDir( DIR_json ); // <<< TODO: filter from app list + cleanDir( "/tmp/" ); + delete Console; + } + buildRootMenu(); + } + + + void setNtpServer() + { + uint8_t menuId = UI->getListID(); + if( menuId > 0 ) { // first element is "back to root menu" + NTP::setServer( menuId-1 ); + } + buildRootMenu(); + } + + + void installApp( const char* appName ) + { + // TODO: confirmation dialog + if( !Downloader::downloadApp( String(appName) ) ) { + log_e("Download of %s app failed"); + } + } + + void installApp() + { + MenuActionLabels* tempLabels = UI->getMenuActionLabels(); + installApp( UI->getListItemTitle() ); + UI->setMenuActionLabels( tempLabels ); + } + + void deleteApp( const char* appName ) + { + drawInfoWindow( String( "Deleting " + String(appName) ).c_str() ); + removeInstalledApp( String( appName ) ); + } + + void deleteApp() + { + deleteApp( UI->getListItemTitle() ); + } + + void removeHiddenApp() + { + // TODO: open appinfowindow + hide button + String appName = String( UI->getListItemTitle() ); + drawInfoWindow( String( "Unhiding " + appName).c_str() ); + toggleHiddenApp( UI->getListItemTitle(), false ); + } + + void addHiddenApp() + { + // TODO: open appinfowindow + hide button + String appName = String( UI->getListItemTitle() ); + drawInfoWindow( String( "Hiding " + appName).c_str() ); + toggleHiddenApp( UI->getListItemTitle(), true ); + } + + + void downloadCatalog() + { + bool loop = true; + do { + loop = Downloader::downloadGzCatalog() + ? false + : modalConfirm( MODAL_DOWNLOADFAIL_TITLE, nullptr, MODAL_DOWNLOADFAIL_MSG, MENU_BTN_RETRY, MENU_BTN_CANCEL, MENU_BTN_NO ) == HID_BTN_A + ; + } while( loop == true ); + buildRootMenu(); + } + + + void idle() + { + + } + + void modal() + { + cycleAppAssets(); + } + + + #if !defined FS_CAN_CREATE_PATH + void doFSChecks() + { + #if !defined HAS_RTC + setTimeFromLastFSAccess(); + #endif + scanDataFolder(); // do SD health checks, create folders + } + #endif + +}; + + +namespace UIDraw +{ + + using namespace MenuItems; + using namespace UIUtils; + using namespace Downloader; + using namespace RegistryUtils; + + void drawDownloaderMenu( const char* title, const char* body ) + { + UI->windowClr( UI->getTheme()->MenuColor ); + UI->setMenuActionLabels( &RefreshAppsActionButtons ); + UI->drawMenu( false ); + drawStatusBar(); + if( title != nullptr ) { + drawInfoWindow( title, body ); + } + } + + + void drawList( bool renderButtons ) + { + if( renderButtons ) { + UI->drawMenu( false ); + drawStatusBar(); + } + UI->showList(); + } + + + void drawStatusBar() + { + UITheme* theme = UI->getTheme(); + LGFX* gfx = UI->getGfx(); + ForkIcon.draw( gfx, 4, (TITLEBAR_HEIGHT-2)/2-ForkIcon.height/2 ); + drawTextShadow(gfx, UI->channel_name, ForkIcon.width+8, (TITLEBAR_HEIGHT-2)/2, theme->TextColor, theme->TextShadowColor, &TomThumb, ML_DATUM ); + + if( wifisetup ) { + int8_t rssiminmaxed = min( (int8_t)-70, max( (int8_t)-50, (int8_t)WiFi.RSSI() ) ); + int16_t rssimapped = map( rssiminmaxed, -50, -70, 1, 5 ); + drawRSSIBar( 290, 4, rssimapped, theme->MenuColor, 2.0 ); + } else { + SDUpdaterIcon.draw( gfx, 298, 6 ); + } + if( ntpsetup ) { + // TODO: draw some ntp icon + } + } + + + void drawRegistryMenu() + { + + String _mc = ""; // macro separator + String modalMessage = _mc + CHANNEL_TOOL_TEXT + "\n" + + "\nCurrent: " + Registry.defaultChannel.name + + "\nAlternate: " + (Registry.defaultChannel.name == REGISTRY_MASTER ? Registry.unstableChannel.name : Registry.masterChannel.name) + ; + + int resp = modalConfirm( CHANNEL_TOOL, ROOTACTION_SWITCH, modalMessage.c_str(), DOWNLOADER_MODAL_CHANGE, MENU_BTN_UPDATE, MENU_BTN_CANCEL ); + // choose between updating the JSON or changing the default channel + switch( resp ) { + case HID_BTN_A: // pick a channel + resp = modalConfirm( CHANNEL_CHOOSER, CHANNEL_CHOOSER_PROMPT, CHANNEL_CHOOSER_TEXT, DOWNLOADER_MODAL_CHANGE, MENU_BTN_CANCEL, MENU_BTN_BACK ); + if( resp == HID_BTN_A ) { + if( Registry.pref_default_channel == REGISTRY_MASTER ) { + Registry.pref_default_channel = REGISTRY_UNSTABLE; + } else { + Registry.pref_default_channel = REGISTRY_MASTER; + } + registrySave( Registry, appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName ); + // TODO: download registry JSON + //Serial.println("Will reload in 5 sec"); + delay(5000); + ESP.restart(); + } + break; + case HID_BTN_B: // update channel + resp = modalConfirm( CHANNEL_DOWNLOADER, CHANNEL_DOWNLOADER_PROMPT, CHANNEL_DOWNLOADER_TEXT, MENU_BTN_UPDATE, MENU_BTN_CANCEL, MENU_BTN_BACK ); + if( resp == HID_BTN_A ) { + registryFetch( Registry, appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName ); + } + break; + default: // run wifi manager ? + + break; + } + drawList( true ); + } + +}; + + + + +namespace AppRenderer +{ + using namespace UILists; + using namespace FSUtils; + //LGFX* gfx; + + void AppInfo::clear() + { + appNameStr = ""; + authorNameStr = NOT_IN_REGISTRY; + projectURLStr = ""; + descriptionStr = ""; + creditsStr = ""; + type = REG_UNKNOWN; + assets_folder = nullptr; + assets.clear(); + binSize = packageSize = assetsCount = rawtime = 0; + log_v("Cleared"); + } + + void AppInfo::parseAssets( JsonObject root ) + { + if( assetsCount > 0 ) { + String appImageShaSum = ""; + String authorImageShaSum = ""; + assets.clear(); + for (JsonVariant value : root["json_meta"]["assets"].as() ) { + String assetName = value["name"].as();//: "9axis_data_publisher.jpg", + String assetPath = value["path"].as();//: "/jpg/", + String assetSha256Sum = value["sha256_sum"].as(); + size_t assetSize = value["size"].as();//: 7215, + rawtime = value["created_at"].as(); + log_d("Collected rawtime: %d for file %s%s", rawtime, assetPath.c_str(), assetName.c_str() ); + String assetFullPath = assetPath+assetName; + packageSize += assetSize; + if( isBinFile( assetFullPath.c_str() ) ) { + if( M5_FS.exists( assetFullPath ) ) { + type = REG_LOCAL; + } + binSize = assetSize; + rawtime = rawtime;//: 1573146468, + } else { + if( assetName == appNameStr + EXT_jpg ) { + // is app image + appImageShaSum = assetSha256Sum; + } else if ( assetName == appNameStr + "_gh" + EXT_jpg ) { + // is author image + authorImageShaSum = assetSha256Sum; + } + } + assets.push_back({ assetFullPath, assetName, assetPath, assetSha256Sum, assetSize, rawtime }); + } + has_app_image = false; + if( appImageShaSum != "" && authorImageShaSum != "" ) { + if( appImageShaSum != authorImageShaSum ) { + log_w("Author image and app image differ!"); + has_app_image = true; + } else { + log_w("Author image and app image are similar!"); + } + } else { + log_w("Author image and app image shasums are missing!"); + } + + } + } + + void AppInfo::draw() + { + log_v("rendering"); + UI->windowClr(); + UITheme* theme = UI->getTheme(); + AppInfoPosY = 64; + + if( appInfo.appNameStr ) { + drawTextShadow( _gfx, appInfo.appNameStr.c_str(), _gfx->width()/2, AppInfoPosY-20, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, MC_DATUM ); + } + + if( authorNameStr ) { + String authorString = authorNameStr; + if( strcmp( NOT_IN_REGISTRY, authorString.c_str() ) !=0 ) { + showAppImage( assets_folder, "_gh"); + authorString = "By: "+authorString; + } else { + UnknownAppIcon.draw( _gfx, theme->assetPosX, theme->assetPosY ); + } + drawTextShadow( _gfx, authorString.c_str(), AppInfoPosX, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM ); + AppInfoPosY += 21; + } else log_v("NO AUTHOR"); + if( assetsCount > 0 ) { + String assetsString = "Assets: " + String( assetsCount ); + drawTextShadow( _gfx, assetsString.c_str(), AppInfoPosX, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM ); + AppInfoPosY += 21; + } else log_v("NO ASSETS"); + if( binSize > 0 ) { + String sizeString = "Bin.: " + String(formatBytes( binSize, formatBuffer )); + drawTextShadow( _gfx, sizeString.c_str(), AppInfoPosX, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM ); + AppInfoPosY += 21; + } else log_v("NO BIN SIZE"); + if( packageSize > 0 ) { + String totalSize = "Tot.: " + String(formatBytes( packageSize, formatBuffer )); + drawTextShadow( _gfx, totalSize.c_str(), AppInfoPosX, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM ); + AppInfoPosY += 21; + } else log_v("NO PACK SIZE"); + if( rawtime > 0 ) { + struct tm * timeinfo; + //time (&rawtime); + timeinfo = localtime (&rawtime); + drawTextShadow( _gfx, "Creation date:", AppInfoPosX, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM ); + AppInfoPosY += 21; + + strftime( formatBuffer, 64, "%F", timeinfo ); // to ISO date format + String binDateStr = String( formatBuffer ); + drawTextShadow( _gfx, binDateStr.c_str(), AppInfoPosX+12, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM ); + AppInfoPosY += 21; + + strftime( formatBuffer, 64, "%T", timeinfo ); // to ISO time format + String binTimeStr = String( formatBuffer ); + drawTextShadow( _gfx, binTimeStr.c_str(), AppInfoPosX+12, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM ); + } else log_v("NO DATETIME"); + lastrender = millis(); + } + +}; + + + +namespace UIShow +{ + using namespace FSUtils; + using namespace UILists; + using namespace UIDo; + using namespace MenuItems; + using namespace AppRenderer; + + std::vector callbacks; + + + void showAppQrCode() + { + UITheme* th=UI->getTheme(); + qrRender(UI->getGfx(), appInfo.projectURLStr, th->assetPosX, th->assetPosY, th->assetWidth, th->assetHeight ); + } + + + void showAppAssetImage() + { + showAppImage( appInfo.assets_folder, "" ); + } + + + void showAppAssetAuthor() + { + showAppImage( appInfo.assets_folder, "_gh" ); + } + + + void showNTPImage() + { + const char* serverNameCstr = UI->getListItemTitle(); + String serverNameStr = String( serverNameCstr ); + String _ms = ""; // macro separator + String iconPath = CATALOG_DIR + _ms + DIR_png + "NTP-" + serverNameStr + EXT_png; + UITheme* theme = UI->getTheme(); + RemoteAsset appIcon = { iconPath.c_str(), theme->assetWidth, theme->assetHeight, serverNameCstr }; + drawAssetReveal( &appIcon, UI->getGfx(), theme->assetPosX, theme->assetPosY ); + } + + + + void updateCheckShowAppImage() + { + AppInfo tmpInfo; + getAppInfo( &tmpInfo, JSON_LOCAL ); + getAppInfo( &appInfo, JSON_REMOTE ); + // "IN REGISTRY" => compare binary size with meta, propose sha256_sum check or update + delete + showAppImage( "", ""); + UITheme* theme = UI->getTheme(); + LGFX* gfx = UI->getGfx(); + // add icon status as an overlay to the image + if( appInfo.binSize != tmpInfo.binSize ) { + UpdateIcon.draw( gfx, theme->assetPosX, theme->assetPosY ); + log_v("Bin sizes differ (local=%d, remote=%d)", tmpInfo.binSize, appInfo.binSize); + } else { + log_v("Bin sizes match (%d)", appInfo.binSize); + CheckIcon.draw( gfx, theme->assetPosX, theme->assetPosY ); + } + } + + void updateMetaShowAppImage() + { + // "BINARY INSTALLED" && "IN REGISTRY" => missing local meta ? => suggest copy meta + delete + log_d("TODO: check if missing local meta"); + getAppInfo( &appInfo, JSON_REMOTE ); + showAppImage( CATALOG_DIR, ""); + UITheme* theme = UI->getTheme(); + UpdateIcon.draw( UI->getGfx(), theme->assetPosX, theme->assetPosY ); + } + + void deleteShowAppImage() + { + String _ms = ""; // for macro separation + String imageLocal = DIR_jpg + String(UI->getListItemTitle()) + EXT_jpg; + String imageRemote = CATALOG_DIR + _ms + DIR_jpg + String(UI->getListItemTitle()) + EXT_jpg; + getAppInfo( &appInfo, JSON_LOCAL ); + UITheme* theme = UI->getTheme(); + RemoteAsset appIcon = { nullptr, theme->assetWidth, theme->assetHeight, UI->getListItemTitle() }; + String imageRender = ""; + if( M5_FS.exists( imageLocal ) ) { + appIcon.path = imageLocal.c_str(); + log_v("local image"); + } else if( M5_FS.exists( imageRemote ) ) { + appIcon.path = imageRemote.c_str(); + log_v("remote image"); + } else { + appIcon = UnknownAppIcon; + log_v("default image"); + } + drawAssetReveal( &appIcon, UI->getGfx(), theme->assetPosX, theme->assetPosY ); + } + + + void showAppImage() + { + showAppImage( UI->getList()->assets_folder, ""); + if( strcmp( UI->getListItemTitle(), appInfo.appNameStr.c_str() ) != 0 ) { + log_v("AppInfo expired (expecting:%s, got:%s)", UI->getListItemTitle(), appInfo.appNameStr.c_str() ); + getAppInfo( &appInfo, UI->getList()->assets_folder[0] == '0' ? JSON_LOCAL : JSON_REMOTE ); + } else { + log_v("re-using existing appinfo"); + } + appInfo.lastrender = millis(); + } + + + void showAppImage( const char* prefix, const char* suffix ) + { + const char* appNameStr = UI->getListItemTitle(); + const char* menuNameStr = UI->getListTitle(); + char appImageFile[255] = {0}; + memset( appImageFile, 0, 255 ); + snprintf( appImageFile, 255, "%s" DIR_jpg "%s%s" EXT_jpg, prefix, appNameStr, suffix ); + log_v("Extrapolated Icon Path: '%s' (%d bytes) for '%s' menu item", appImageFile, strlen(appImageFile), menuNameStr ); + UITheme* theme = UI->getTheme(); + RemoteAsset appIcon = { appImageFile, theme->assetWidth, theme->assetHeight, appNameStr }; + drawAssetReveal( &appIcon, UI->getGfx(), theme->assetPosX, theme->assetPosY ); + appInfo.lastrender = millis(); + } + + + void getAppInfo( AppInfo *appInfo, AppJSONType jsonType ) + { + appInfo->clear(); + appInfo->appNameStr = String( UI->getListItemTitle() ); + appInfo->type = isHiddenApp( appInfo->appNameStr ) ? REG_HIDDEN : M5_FS.exists( ROOT_DIR+appInfo->appNameStr+EXT_bin ) ? REG_LOCAL : REG_REMOTE; + appInfo->assets_folder = UI->getList()->assets_folder; + String assetsFolderStr = jsonType == JSON_LOCAL ? "" : CATALOG_DIR; + String jsonFile = assetsFolderStr + DIR_json + appInfo->appNameStr + EXT_json; + String appImageFile = assetsFolderStr + DIR_jpg + appInfo->appNameStr + EXT_jpg; + String binFile = ROOT_DIR + appInfo->appNameStr + EXT_bin; + if( M5_FS.exists( binFile ) ) getFileAttrs( binFile.c_str(), &appInfo->binSize, &appInfo->rawtime ); + JsonObject root; + DynamicJsonDocument jsonBuffer( 8192 ); + if( !getJson( jsonFile.c_str(), root, jsonBuffer ) ) { + appInfo->type = NOREG; + log_v("NOREG: '%s' has no JSON", jsonFile.c_str() ); + return; + } + if ( root.isNull() ) { + appInfo->type = NOREG; + log_e("No parsable JSON in %s file", jsonFile.c_str() ); + return; + } + + size_t jsonMetaPropsCount; + fs::File file; + switch( jsonType ) { + case JSON_LOCAL: + if( M5_FS.exists( appImageFile ) ) appInfo->assetsCount++; + if( M5_FS.exists( jsonFile ) ) appInfo->assetsCount++; + appInfo->descriptionStr = root["description"].isNull() ? "" : root["description"].as(); + appInfo->authorNameStr = root["authorName"].isNull() ? "" : root["authorName"].as(); + appInfo->projectURLStr = root["projectURL"].isNull() ? "" : root["projectURL"].as(); + appInfo->creditsStr = root["credits"].isNull() ? "" : root["credits"].as(); + log_v("JSON_LOCAL: %s (%d bytes)", appInfo->appNameStr.c_str(), appInfo->binSize ); + break; + case JSON_REMOTE: + if( !root["name"].as().equals( appInfo->appNameStr ) ) { + log_e("AppName mismatch (expecting:'%s', got:'%s'", appInfo->appNameStr.c_str(), root["name"].as().c_str() ); + return; + } + jsonMetaPropsCount = root["json_meta"].size(); // only present on remote appinfo + appInfo->binSize = root["size"].as(); + appInfo->descriptionStr = jsonMetaPropsCount > 0 ? root["json_meta"]["description"].isNull() ? "" : root["json_meta"]["description"].as() : ""; + appInfo->assetsCount = jsonMetaPropsCount > 0 ? root["json_meta"]["assets"].size() : 0; + appInfo->authorNameStr = jsonMetaPropsCount > 0 ? root["json_meta"]["authorName"].as() : ""; + appInfo->projectURLStr = jsonMetaPropsCount > 0 ? root["json_meta"]["projectURL"].as() : ""; + appInfo->creditsStr = jsonMetaPropsCount > 0 ? root["json_meta"]["credits"].as() : ""; + if( appInfo->appNameStr == "" || appInfo->binSize <= 0 || jsonMetaPropsCount == 0 ) { + log_e("Invalid AppInfo in JSON (path: %s)", jsonFile.c_str() ); + return; + } + appInfo->parseAssets( root ); + log_v("JSON_REMOTE: %s (%d bytes)", appInfo->appNameStr.c_str(), appInfo->binSize ); + break; + default: + log_e("INVALID TYPE"); + break; + } + + cycleid = 0; + cycleanimation = false; + callbacks.clear(); + + if( appInfo->projectURLStr !="" ) { + callbacks.push_back( &showAppQrCode ); + cycleanimation = true; + } + + callbacks.push_back( &showAppAssetAuthor ); + + if( appInfo->has_app_image ) { + callbacks.push_back( &showAppAssetImage ); + cycleanimation = true; + } + + } + + + void handleModalAction( AppInfo * appInfo ) + { + onselect_cb_t aftermodal = [](){}; + MenuActionLabels *confirmLabels = nullptr; + int hidState = HID_BTN_C; + + switch( appInfo->type ) { + case REG_HIDDEN: + log_v("REG_HIDDEN"); + confirmLabels = &UnhideAppsActionButtons; + aftermodal = &buildHiddenAppList; + break; + case NOREG: + log_v("NOREG"); + confirmLabels = &DeleteAppsActionButtons; + aftermodal = &buildMyAppsMenu; + break; + case REG_LOCAL: + log_v("REG_LOCAL"); + confirmLabels = &DeleteAppsActionButtons; + aftermodal = &buildMyAppsMenu; + break; + case REG_REMOTE: + log_v("REG_REMOTE"); + confirmLabels = &InstallHideAppsActionButtons; + aftermodal = &buildStoreMenu; + break; + case REG_UNKNOWN: + default: + log_e("REG_UNKNOWN, No valid AppInfo->type (#%d) provided for menu element '%s'", appInfo->type, UI->getListTitle() ); + break; + } + + if( confirmLabels ) { + confirmLabels->title = appInfo->appNameStr.c_str(); + hidState = modalConfirm( confirmLabels, true ); + } + + switch( hidState ) { + case HID_BTN_A: confirmLabels->Buttons[0]->onClick(); aftermodal(); break; + case HID_BTN_B: confirmLabels->Buttons[1]->onClick(); aftermodal(); break; + case HID_BTN_C: UI->drawMenu( true ); UI->showList(); break; + } + } + + + void showAppInfo() + { + appInfo.draw(); + handleModalAction( &appInfo ); + } + + + void scrollAppInfo() + { + LGFX* gfx = UI->getGfx(); + String scrollStr = appInfo.creditsStr; + if( appInfo.projectURLStr != "" ) scrollStr = scrollStr+ SCROLL_SEPARATOR + appInfo.projectURLStr; + if( appInfo.descriptionStr != "" ) scrollStr = scrollStr+ SCROLL_SEPARATOR + appInfo.descriptionStr; + scrollStr = scrollStr+SCROLL_SEPARATOR; + HeaderScroll.render( gfx, scrollStr, 10, 2, nullptr, 0, 0, gfx->width(), TITLEBAR_HEIGHT ); + } + + + void cycleAppAssets() + { + if( !cycleanimation ) return; + uint32_t elapsed = millis()-appInfo.lastrender; + if( elapsed > cbdelay ) { + uint32_t cbid = cycleid%callbacks.size(); + callbacks[cbid](); + appInfo.lastrender = millis(); + cycleid++; + } else { + if( elapsed > 0 ) { + UITheme* th=UI->getTheme(); + float progress = float(float(elapsed)/float(cbdelay))*th->assetWidth; + UI->getGfx()->fillRect( th->assetPosX, th->assetPosY+th->assetHeight-2, progress, 2, TFT_RED ); + } + delay(10); // kill that buzz sound coming out of the speaker :D + } + } + + +}; + + + + + + + + +namespace UILists +{ + using namespace MenuItems; + using namespace FSUtils; + using namespace UIDo; + using namespace UIShow; + using namespace Downloader; + + void buildNtpMenu() + { + size_t before = ESP.getFreeHeap(); + NtpMenuGroup.clear(); + NtpMenuGroup.push( &BackToRootMenu ); + size_t servers_count = sizeof( NTP::Servers ) / sizeof( NTP::Server ); + + for( int i=0; isetList( &NtpMenuGroup ); + UIDraw::drawList( true ); // render the menu + log_v("Servers count: %d (bytes free: before=%d, after=%d)", UI->getListSize(), before, ESP.getFreeHeap() ); + } + + + void buildRootMenu() + { + size_t before = ESP.getFreeHeap(); + countApps(); + String jsonFile = CATALOG_DIR + Registry.defaultChannel.catalog_endpoint /*"/catalog.json"*/; + + bool has_catalog = M5_FS.exists( jsonFile ); + bool has_certs = M5_FS.exists( SD_CERT_PATH ); + + log_v("Root menu: has_catalog:%s, has_certs:%s", has_catalog?"true":"false", has_certs?"true":"false" ); + + RootMenuGroup.clear(); + + if( has_catalog ) { + RootActionRefresh.setTitle( ROOTACTION_REFRESH ); + } else { + RootActionRefresh.setTitle( ROOTACTION_DOWNLOAD ); + } + + RootActionBrowse.setTitle( MENUTITLE_MANAGEAPPS ); + + if( has_catalog || appsCount > 0 ) { + RootMenuGroup.push( &RootActionBrowse ); + } + RootMenuGroup.push( &RootActionRefresh ); + RootMenuGroup.push( &RootActionSwitch ); + RootMenuGroup.push( &RootActionNtp ); + if( has_certs ) { + RootMenuGroup.push( &RootActionClearTls ); + } + if( has_catalog ) { + RootMenuGroup.push( &RootActionClearAll ); + } + RootMenuGroup.push( &RootActionSleep ); + + UI->setList( &RootMenuGroup ); + UI->setMenuActionLabels( &RootListActionButtons ); + UIDraw::drawList( true ); // render the menu + log_v("Rootmenu items count: %d (bytes free: before=%d, after=%d)", UI->getListSize(), before, ESP.getFreeHeap() ); + } + + + void buildBrowseAppsMenu() + { + countApps(); + ManageAppsGroup.clear(); + ManageAppsGroup.push( &BackToRootMenu ); + String jsonFile = CATALOG_DIR + Registry.defaultChannel.catalog_endpoint /*"/catalog.json"*/; + if( M5_FS.exists( jsonFile ) ) { + ManageAppsGroup.push( &BrowseAppStore ); // [Browse/Install/Hide] Available Apps + } else { // no catalog available + ManageAppsGroup.push( &RootActionRefresh ); // Download Catalog + } + if( appsCount > 0 ) { + ManageAppsGroup.push( &ManageMyApps ); // [Browse/Delete] Installed Apps + } + if( M5_FS.exists( HIDDEN_APPS_FILE ) ) { + ManageAppsGroup.push( &ManageAppStore ); // [Unhide] Available Apps + } + UI->setList( &ManageAppsGroup ); + UI->setMenuActionLabels( &MyAppsActionButtons ); + UIDraw::drawList( true ); // render the menu + } + + + + void buildHiddenAppList() + { + getHiddenApps(); + if( HiddenFiles.size() == 0 ) { + return; + } + HiddenAppsMenuGroup.clear(); + HiddenAppsMenuGroup.push( &BackToManageApps ); + + for( int i=0; isetList( &HiddenAppsMenuGroup ); + UIDraw::drawList( true ); // render the menu + } + + + + void buildMyAppsMenu() + { + countApps(); + if( appsCount == 0 ) { + log_v("No apps on SD Card, falling back to root menu"); + buildRootMenu(); + } + + std::vector files; + getInstalledApps( files ); + + if( files.size() <= 0 ) { + log_v("No apps found, falling back to root menu"); + buildRootMenu(); + return; + } + + std::sort( files.begin(), files.end() ); + + MyAppsMenuGroup.clear(); + + for( int i=0; i compare size with meta, propose sha256_sum check or update + delete + log_v("[#%d] mb=updateCheckShowAppImage(%s)", i, files[i].c_str() ); + MyAppsMenuGroup.push( files[i].c_str(), &UpdateCheckCallbacks, nullptr, isLauncher(files[i].c_str())?LAUNCHER_COLOR:TEXT_COLOR ); + } else if( M5_FS.exists( jsonRemoteFile ) ) { // "BINARY INSTALLED" && "IN REGISTRY" => missing local meta ? => suggest copy meta + delete + log_v("[#%d] mb=updateMetaShowAppImage(%s)", i, files[i].c_str() ); + MyAppsMenuGroup.push( files[i].c_str(), &UpdateMetaCallbacks, nullptr, isLauncher(files[i].c_str())?LAUNCHER_COLOR:TEXT_COLOR ); + } else { // "BINARY INSTALLED" && "NOT IN REGISTRY" => only suggest delete + log_v("[#%d] mb=deleteShowAppImage(%s)", i, files[i].c_str() ); + MyAppsMenuGroup.push( files[i].c_str(), &DeleteAppCallbacks, nullptr, isLauncher(files[i].c_str())?LAUNCHER_COLOR:TEXT_COLOR ); + } + } + + UI->setList( &MyAppsMenuGroup ); + // TODO: focus selected item + UIDraw::drawList( true ); // render the menu + log_v("Added %d items to menu", MyAppsMenuGroup.actions_count ); + } + + + + void buildStoreMenu() + { + size_t before = ESP.getFreeHeap(); + + getHiddenApps(); // Fill 'HiddenFiles' vector + + String jsonFile = CATALOG_DIR + Registry.defaultChannel.catalog_endpoint; // "/catalog.json" + + JsonObject root; + DynamicJsonDocument jsonBuffer( 8192 ); + if( !getJson( jsonFile.c_str(), root, jsonBuffer ) ) { + log_e("Failed to get json from %s", jsonFile.c_str() ); + return; + } + if ( root.isNull() ) { + log_e("No parsable JSON in %s file", jsonFile.c_str() ); + return; + } + if( root["apps"].size() <= 0 ) { + log_e("No apps in catalog"); + return; + } + if( root["apps_count"].as() == 0 ) { // TODO: root["generated_at"] + log_e("Empty catalog"); + return; + } + if( root["base_url"].as() == "" ) { + log_e("No base url"); + return; + } + if( root["gz_url"].as() == "" ) { + log_e("No gz url"); + return; + } + + baseCatalogURL = root["base_url"].as(); + gzCatalogURL = root["gz_url"].as(); + + AppStoreMenuGroup.clear(); + std::vector files; + + for( JsonVariant appEntry : root["apps"].as() ) { + String fName = appEntry["name"].as(); + fName.trim(); + if( fName != "" && !isHiddenApp(fName) && !M5_FS.exists( ROOT_DIR + fName + EXT_bin ) ) + files.push_back( fName ); // don't list installed or hidden apps + } + + std::sort( files.begin(), files.end() ); + + for( int i=0; isetList( &AppStoreMenuGroup ); + UIDraw::drawList( true ); // render the menu + log_v("Added %d apps (bytes free: before=%d, after=%d)", AppStoreMenuGroup.actions_count-1, before, ESP.getFreeHeap() ); + } + +}; diff --git a/examples/AppStore/modules/AppStoreActions/AppStoreActions.hpp b/examples/AppStore/modules/AppStoreActions/AppStoreActions.hpp new file mode 100644 index 00000000..fa66f8ac --- /dev/null +++ b/examples/AppStore/modules/AppStoreActions/AppStoreActions.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include "../misc/core.h" +#include // https://github.com/bblanchon/ArduinoJson/ + +namespace UIDo +{ + unsigned long MsBeforeSleep = MS_BEFORE_SLEEP; + unsigned long lastcheck = millis(); // timer check + unsigned long lastpush = millis(); // keypad/keyboard activity + uint8_t brightness = MAX_BRIGHTNESS; // used for fadeout before sleep + + void gotoSleep(); + void clearTLS(); + void clearApps(); + void setRootMenu(); + void setMyAppsMenu(); + void setStoreMenu(); + void setNtpServer(); + void setTimezone( float tz ); + void setDst( bool set ); + void downloadCatalog(); + #if !defined FS_CAN_CREATE_PATH + void doFSChecks(); + #endif + void checkSleepTimer(); + void deleteApp(); + void deleteApp( const char* appName ); + void removeHiddenApp(); + void addHiddenApp(); + void installApp(); + void installApp( const char* appName ); + void downloadCatalog(); + void BtnA(); + void BtnB(); + void BtnC(); + void idle(); + void modal(); +}; + +namespace UIDraw +{ + void drawBrowseAppsMenu(); + void drawStatusBar(); + void drawDownloaderMenu( const char* title = nullptr, const char* body = nullptr ); + void drawList( bool renderButtons = false ); + void drawRegistryMenu(); +}; + +namespace UILists +{ + void buildBrowseAppsMenu(); + void buildMyAppsMenu(); + void buildStoreMenu(); + void buildRootMenu(); + void buildNtpMenu(); + void buildHiddenAppList(); +}; + +enum AppJSONType +{ + JSON_LOCAL, // local json type, short version with authorName/width/height/projectURL/credits + JSON_REMOTE // remote json type, long version with meta assets +}; + +enum AppType +{ + REG_UNKNOWN, // default when unscanned + REG_LOCAL, // in registry and installed + REG_REMOTE, // in registry but not installed + REG_HIDDEN, // in registry and hidden + NOREG // not in resistry but present +}; + +struct AppAsset +{ + String assetFullPath; //: "/jpg/9axis_data_publisher.jpg" + String name; //: "9axis_data_publisher.jpg" + String path; //: "/jpg/" + String assetSha256Sum; + size_t size; + time_t created_at; +}; + +namespace AppRenderer +{ + uint16_t AppInfoPosY = 46; + uint16_t AppInfoPosX = LISTITEM_OFFSETX; + uint32_t cycleid = 0; + uint32_t cbdelay = 5000; // msec cycle per callback + bool cycleanimation = false; + + struct AppInfo + { + LGFX* _gfx; + String appNameStr; + String authorNameStr = NOT_IN_REGISTRY; + String projectURLStr; + String creditsStr; + String descriptionStr; + AppType type; + std::vector assets; + bool has_app_image; + const char* assets_folder; + size_t binSize; + size_t packageSize; + size_t assetsCount; + time_t rawtime; + void clear(); + void parseAssets( JsonObject root ); + void draw(); + uint32_t lastrender; + }; + AppInfo appInfo; +}; + + +namespace UIShow +{ + using namespace AppRenderer; + + void updateCheckShowAppImage(); + void updateMetaShowAppImage(); + void deleteShowAppImage(); + void handleModalAction( AppInfo * appInfo ); + + void showAppInfo(); + void scrollAppInfo(); + void getAppInfo( AppInfo *appInfo, AppJSONType jsonType ); + void showNTPImage(); + void showAppImage(); + void showAppImage( const char* prefix, const char* suffix ); + void cycleAppAssets(); +}; diff --git a/examples/AppStore/modules/AppStoreMain/AppStoreMain.cpp b/examples/AppStore/modules/AppStoreMain/AppStoreMain.cpp new file mode 100644 index 00000000..1cfacb96 --- /dev/null +++ b/examples/AppStore/modules/AppStoreMain/AppStoreMain.cpp @@ -0,0 +1,141 @@ +/* + * + * M5Stack Application Store + * Project Page: https://github.com/tobozo/M5Stack-SD-Updater + * + * Copyright 2021 tobozo http://github.com/tobozo + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files ("M5Stack SD Updater"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#pragma once + +#include "AppStoreMain.hpp" +#include "../Console/Console.cpp" +#include "../Assets/Assets.cpp" +#include "../MenuItems/MenuItems.cpp" +#include "../MenuUtils/MenuUtils.cpp" +#include "../AppStoreUI/AppStoreUI.cpp" +#include "../AppStoreActions/AppStoreActions.cpp" +#include "../FSUtils/FSUtils.cpp" +#include "../Downloader/Downloader.cpp" +#include "../Registry/Registry.cpp" +#include "../CertsManager/CertsManager.cpp" + +AppStoreUI *UI = nullptr; +LogWindow *Console = nullptr; +AppRegistry Registry; + +namespace AppStore +{ + + void setup() + { + M5.begin( true, true, true, false, ScreenShotEnable ); // bool LCDEnable, bool SDEnable, bool SerialEnable, bool I2CEnable, bool ScreenShotEnable + + #if !defined FS_CAN_CREATE_PATH + UIDo::doFSChecks(); + #endif + + UI = new AppStoreUI( &tft ); + + #if !defined HAS_RTC + FSUtils::setTimeFromLastFSAccess(); + #endif + + checkSDUpdater( M5_FS, MENU_BIN, 5000, TFCARD_CS_PIN ); + + Registry = RegistryUtils::init(); // load registry profile + UI->channel_name = Registry.defaultChannel.name.c_str(); + + DummyAsset::setup( &UIUtils::drawCaption, &MenuItems::BrokenImage, UI->getTheme()->TextColor, UI->getTheme()->BgColor ); + + TLS::wget = &Downloader::wget; // attach downloader to TLS + TLS::modalConfirm = &UIUtils::modalConfirm; + TLS::certProvider = Registry.defaultChannel.api_cert_provider_url_https; // set TLS cert provider + NTP::loadPrefServer(); + + BackToRootMenu.textcolor = DIMMED_COLOR; + BackToManageApps.textcolor = DIMMED_COLOR; + + Serial.println( WELCOME_MESSAGE ); + Serial.println( INIT_MESSAGE ); + Serial.printf( MENU_SETTINGS, LINES_PER_PAGE, LIST_MAX_COUNT); + Serial.printf("Has PSRam: %s\n", psramInit() ? "true" : "false"); + Serial.println("Build DateTime: "+ Downloader::ISODateTime ); + + log_i("\nRAM SIZE:\t%s\nFREE RAM:\t%s\nMAX ALLOC:\t%s", + String( formatBytes(ESP.getHeapSize(), formatBuffer) ).c_str(), + String( formatBytes(ESP.getFreeHeap(), formatBuffer) ).c_str(), + String( formatBytes(ESP.getMaxAllocHeap(), formatBuffer) ).c_str() + ); + + tft.setBrightness(100); + lastcheck = millis(); + + FSUtils::countApps(); + UILists::buildRootMenu(); + + } + + + void loop() + { + HIDSignal hidState = getControls(); + + if( hidState!=HID_INERT && UIDo::brightness != MAX_BRIGHTNESS ) { + // some activity occured, restore brightness + Serial.println(".. !!! Waking up !!"); + UIDo::brightness = MAX_BRIGHTNESS; + tft.setBrightness( UIDo::brightness ); + } + + UIDo::lastcheck = millis(); + + switch( hidState ) { + case HID_BTN_C: + UIDo::BtnC(); + break; + case HID_BTN_A: + UI->getList()->selectedindex = UI->getListID(); + UIDo::BtnA(); + break; + case HID_BTN_B: + UI->getList()->selectedindex = UI->getListID(); + UIDo::BtnB(); + break; + default: + case HID_INERT: + UIDo::lastcheck = UIDo::lastpush; + break; + case HID_SCREENSHOT: + #if defined USE_SCREENSHOT + M5.ScreenShot->snap( "screenshot" ); + #endif + break; + } + UIDo::lastpush = UIDo::lastcheck; + UI->idle(); + } + +}; diff --git a/examples/AppStore/modules/AppStoreMain/AppStoreMain.hpp b/examples/AppStore/modules/AppStoreMain/AppStoreMain.hpp new file mode 100644 index 00000000..8f46af4c --- /dev/null +++ b/examples/AppStore/modules/AppStoreMain/AppStoreMain.hpp @@ -0,0 +1,41 @@ +/* + * + * M5Stack Application Store + * Project Page: https://github.com/tobozo/M5Stack-SD-Updater + * + * Copyright 2021 tobozo http://github.com/tobozo + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files ("M5Stack SD Updater"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#pragma once + +#include "../misc/core.h" // base software stack (ESP32-Chimera-Core + SD, etc ) +#include "../misc/config.h" // config settings +#include "../misc/controls.h" // keypad / joypad / keyboard controls + +namespace AppStore +{ + void setup(); + void loop(); +}; diff --git a/examples/AppStore/modules/AppStoreUI/AppStoreUI.cpp b/examples/AppStore/modules/AppStoreUI/AppStoreUI.cpp new file mode 100644 index 00000000..6e228141 --- /dev/null +++ b/examples/AppStore/modules/AppStoreUI/AppStoreUI.cpp @@ -0,0 +1,1016 @@ +#pragma once + +#include "AppStoreUI.hpp" +#include "lgfx/utility/lgfx_qrcode.h" + +using namespace MenuItems; +using namespace UIDraw; +using namespace UIDo; +using namespace UIUtils; + +// destructor +AppStoreUI::~AppStoreUI() +{ + clearList(); +} + +// constructor +AppStoreUI::AppStoreUI( LGFX* _gfx, UITheme * theme ) +{ + gfx = _gfx; + Theme = theme; + Theme->setupCanvas( gfx ); + initSprites( gfx ); +} + +// some public getters +size_t AppStoreUI::getListPages() { return ListTotalPages; } +size_t AppStoreUI::getListSize() { return ListCount; } +uint16_t AppStoreUI::getListID() { return MenuID; } +uint16_t AppStoreUI::getPageID() { return PageID; } +const char* AppStoreUI::getListTitle() { return Menu->ActionLabels->title; } +const char* AppStoreUI::getListItemTitle() { return Menu->Actions[MenuID]->title; } +MenuActionLabels* AppStoreUI::getMenuActionLabels() { return Menu->ActionLabels; } +UITheme* AppStoreUI::getTheme() { return Theme; } +LGFX* AppStoreUI::getGfx() { return gfx; } + + +bool AppStoreUI::empty( const char* str ) +{ + return str ? (str[0] == '\0') : true; +} + + +void AppStoreUI::clearList() +{ + PageID = 0; + MenuID = 0; + ListCount = 0; + ListTotalPages = 0; + ListLinesInLastPage = 0; +} + + +void AppStoreUI::setPageID( uint16_t idx ) +{ + if( idx < ListCount/LinesPerPage ) { + PageID = idx; + } +} + + +void AppStoreUI::setListID( uint16_t idx ) +{ + uint16_t was = PageID; + if( idx < ListCount ) { + MenuID = idx; + } else { + MenuID = ListCount -1; + } + PageID = MenuID / LinesPerPage; + log_v("Setting list id as %d (was %d)", idx, was ); +} + + +void AppStoreUI::nextList( bool renderAfter ) +{ + if( MenuID < ( PageID * LinesPerPage + LinesInCurrentPage - 1 ) ) { + MenuID++; + } else { + if( PageID 0 ) { + PageID--; + MenuID -= LinesPerPage; + log_v("Paging up to %d/%d", PageID, MenuID); + if( renderAfter ) showList(); + } +} + + +void AppStoreUI::menuDown( bool renderAfter ) +{ + int16_t lastId = MenuID; + int16_t oldPageID = PageID; + + if( MenuID == ListCount-1 ) { + setListID( 0 ); + } else { + setListID( MenuID+1 ); + } + + if( PageID != oldPageID ) { + if( renderAfter ) showList(); + } else { + if( renderAfter ) updateList( lastId ); + } +} + + +void AppStoreUI::menuUp( bool renderAfter ) +{ + int16_t oldPageID = PageID; + int16_t lastId = MenuID; + + if( MenuID == 0 ) { + // jump to end + setListID( ListCount-1 ); + } else { + setListID( MenuID-1 ); + } + + if( PageID != oldPageID ) { + if( renderAfter ) showList(); + } else { + if( renderAfter ) updateList( lastId ); + } + log_v("Menu up to %d/%d", PageID, MenuID); + +} + + +void AppStoreUI::setList( MenuGroup* menu ) +{ + if( menu->actions_count == 0 ) { + log_e("Cowardly refusing to insert an empty menu list for collection %s, aborting", menu->Title ); + return; + } + clearList(); + + ListCount = menu->actions_count; + + if( ListCount>0 ) { + if( ListCount > LinesPerPage ) { + ListLinesInLastPage = ListCount % LinesPerPage; + if( ListLinesInLastPage>0 ) { + ListTotalPages = ( ListCount - ListLinesInLastPage ) / LinesPerPage; + ListTotalPages++; + } else { + ListTotalPages = ListCount / LinesPerPage; + } + } else { + ListTotalPages = 1; + } + } + // if( Menu && menu->Parent != nullptr ) menu->Parent = Menu; + Menu = menu; + if( Menu->selectedindex > 0 ) { + setListID( Menu->selectedindex ); + } + log_v("Added list '%s' with %d elements", menu->Title, menu->actions_count ); +} + + +void AppStoreUI::execBtn( uint8_t bnum ) +{ + if( Menu->ActionLabels && Menu->ActionLabels->Buttons && Menu->ActionLabels->Buttons[bnum] && Menu->ActionLabels->Buttons[bnum]->onClick ) { + log_v("Button #%d Inherited Callback for Item '%s' in menu '%s' (#%d / %d)", bnum, getListItemTitle(), getListTitle(), MenuID, Menu->actions_count ); + Menu->ActionLabels->Buttons[bnum]->onClick(); + } else { + log_e("Button #%d MISSED Callback for Item #%d / %d", bnum, MenuID, Menu->actions_count ); + } +} + + +void AppStoreUI::execBtnA() +{ + if( Menu && MenuID < Menu->actions_count ) { + if( Menu->Actions[MenuID]->callbacks && Menu->Actions[MenuID]->callbacks->onSelect ) { + log_v("ButtonA Callback for Item '%s' (#%d / %d)", getListItemTitle(), MenuID, Menu->actions_count ); + Menu->Actions[MenuID]->callbacks->onSelect(); + } else { + execBtn( 0 ); + } + } else { + log_e("No menu or bad menu range( %d ) to exec from", MenuID ); + } +} + + +void AppStoreUI::execBtnB() +{ + execBtn( 1 ); +} + + +void AppStoreUI::execBtnC() +{ + execBtn( 2 ); +} + + +void AppStoreUI::idle() +{ + checkSleepTimer(); + if( Menu->Actions[MenuID]->callbacks && Menu->Actions[MenuID]->callbacks->onIdle ) { + Menu->Actions[MenuID]->callbacks->onIdle(); + } +} + +void AppStoreUI::modal() +{ + checkSleepTimer(); + if( Menu->Actions[MenuID]->callbacks && Menu->Actions[MenuID]->callbacks->onModal ) { + Menu->Actions[MenuID]->callbacks->onModal(); + } +} + + +void AppStoreUI::buttonsClr() +{ + gfx->fillRect( Theme->WinPosX, Theme->ButtonsPosY, Theme->WinWidth, TITLEBAR_HEIGHT, Theme->BgColor ); +} + + +void AppStoreUI::windowClr( uint32_t fillcolor ) +{ + gfx->fillRect( Theme->WinPosX, Theme->WinPosY, Theme->WinWidth, Theme->WinHeight, fillcolor ); +} + + +void AppStoreUI::windowClr() +{ + uint32_t begin = millis(); + + if( !getGradientLine() ) { + log_d("Failed to create sprite (%d x %d ), will fill with single color", Theme->WinWidth, 1 ); + gfx->fillScreen( Theme->MenuColor ); + return; + } + gfx->startWrite(); + gfx->pushImageRotateZoom( gfx->width()/2, gfx->height()/2, Theme->WinWidth/2, 0, 0.0, 1, Theme->WinHeight, GradienSprite->width(), GradienSprite->height(), (uint16_t*)GradienSprite->getBuffer() ); + GradienSprite->deleteSprite(); + cropRoundRect( gfx, Theme->WinPosX, Theme->WinPosY, Theme->WinWidth, Theme->WinHeight, 5, Theme->BgColor ); + gfx->endWrite(); + + log_v("clear took %d ms", millis()-begin ); // avg 179ms +} + + +void AppStoreUI::drawListItem( uint16_t inIDX, uint16_t posY ) +{ + if( inIDX==MenuID ) { + drawTextShadow( gfx, Menu->Actions[inIDX]->title, Theme->LIOffsetX, Theme->LIOffsetY+(posY*Theme->LIHeight), Menu->Actions[inIDX]->textcolor, Theme->TextShadowColor, LIFont, TL_DATUM ); + drawCheckMark( gfx, 4, Theme->LIOffsetY+(posY*Theme->LIHeight)+3, Theme->arrowWidth, LIFontHeight/2, Menu->Actions[inIDX]->textcolor, Theme->TextShadowColor ); + } else { + drawTextShadow( gfx, Menu->Actions[inIDX]->title, Theme->LIOffsetX, Theme->LIOffsetY+(posY*Theme->LIHeight), Menu->Actions[inIDX]->textcolor, Theme->TextShadowColor, LIFont, TL_DATUM ); + } +} + + +void AppStoreUI::updateList( uint16_t clearid ) +{ + if( !getGradientLine() ) { // can't blit + showList(); + return; + } + uint16_t prevPosY = Theme->LIOffsetY+((clearid%LinesPerPage)*Theme->LIHeight)+3; // last arrow position + uint16_t currPosY = Theme->LIOffsetY+((MenuID%LinesPerPage)*Theme->LIHeight)+3; // current arrow position + uint16_t clipPosX = 4; + uint16_t clipWidth = 1+Theme->arrowWidth; + uint16_t clipHeight = 1+LIFontHeight/2; + uint16_t *sprPtr = (uint16_t*)GradienSprite->getBuffer(); + uint16_t *clipPtr = &sprPtr[clipPosX]; + bool clear_asset = false; + // delete old checkmark + gfx->pushImageRotateZoom( clipPosX+clipWidth/2, prevPosY+clipHeight/2, clipWidth/2, 0, 0.0, 1, clipHeight+1, clipWidth+1, GradienSprite->height()+1, clipPtr ); + // draw new checkmark + drawCheckMark( gfx, 4, currPosY, Theme->arrowWidth, LIFontHeight/2, Menu->Actions[MenuID]->textcolor, Theme->TextShadowColor ); + + GradienSprite->deleteSprite(); + + callShowListHooks(); +} + + +void AppStoreUI::showList() +{ + windowClr(); + uint16_t i, items = 0; + gfx->startWrite(); + if( ListTotalPages > 1 ) { + snprintf(paginationStr, 16, paginationTpl, PageID+1, ListTotalPages ); + drawTextShadow( gfx, paginationStr, Theme->LICaptionPosX, Theme->LICaptionPosY, Theme->TextColor, Theme->TextShadowColor, &Font2, TR_DATUM ); + snprintf(paginationStr, 16, totalCountTpl, ListCount ); + drawTextShadow( gfx, paginationStr, Theme->LICaptionPosX, gfx->height()-(Theme->LICaptionPosY-3), Theme->TextColor, Theme->TextShadowColor, &Font2, BR_DATUM ); + } + if( (PageID + 1) == ListTotalPages ) { // in last page + if( ListLinesInLastPage == 0 and ListCount >= LinesPerPage ) { + LinesInCurrentPage = LinesPerPage; + items = LinesPerPage; + } else { + if( ListTotalPages>1 ) { + LinesInCurrentPage = ListLinesInLastPage; + items = ListLinesInLastPage; + } else { + LinesInCurrentPage = ListCount; + items = ListCount; + } + } + } else { // in first page or paginaged + LinesInCurrentPage = LinesPerPage; + items = LinesPerPage; + } + for( i = 0; iendWrite(); + callShowListHooks(); +} + + +void AppStoreUI::callShowListHooks() +{ + if( Menu->Actions[MenuID]->callbacks && Menu->Actions[MenuID]->callbacks->onRender ) { + log_v("Side Callback for MenuItem '%s' (item #%d)", Menu->Actions[MenuID]->title, MenuID ); + Menu->Actions[MenuID]->callbacks->onRender(); + } + if( Menu->Actions[MenuID]->icon ) { + log_v("Icon attached: %s", Menu->Actions[MenuID]->icon->path ); + RemoteAsset* asset = (RemoteAsset*)Menu->Actions[MenuID]->icon; + drawAssetReveal( asset, gfx, Theme->assetPosX, Theme->assetPosY ); + } +} + + +void AppStoreUI::setMenuActionLabels( MenuActionLabels* _ActionLabels ) +{ + log_v("Overwriting Menu ActionLabels '%s' with '%s'", Menu->ActionLabels->title, _ActionLabels->title ); + Menu->ActionLabels = _ActionLabels; +} + + +void AppStoreUI::drawButton( uint8_t bnum ) +{ + if( Menu && Menu->ActionLabels && Menu->ActionLabels->Buttons && Menu->ActionLabels->Buttons[bnum] ) { + ButtonAction* btn = Menu->ActionLabels->Buttons[bnum]; + ButtonSprite->pushSprite( buttonsXOffset[bnum], Theme->ButtonsPosY ); + if( btn->asset && M5_FS.exists( btn->asset->path ) ) { + RemoteAsset *btnIcon = btn->asset; + btnIcon->draw( gfx, buttonsXOffset[bnum]+BUTTON_HWIDTH - btnIcon->width/2, Theme->ButtonsPosY+BUTTON_HEIGHT/2 - btnIcon->height/2 ); + } else if( btn->title && !empty( btn->title ) ) { + drawTextShadow( gfx, btn->title, buttonsXOffset[bnum]+BUTTON_HWIDTH, Theme->ButtonsPosY+BUTTON_HEIGHT/2, Theme->TextColor, Theme->TextShadowColor, ButtonFont, MC_DATUM ); + } else { + // empty button wut ? + } + } else { + gfx->fillRect( buttonsXOffset[bnum], Theme->ButtonsPosY, BUTTON_WIDTH, BUTTON_HEIGHT, Theme->BgColor ); + } +} + + +void AppStoreUI::drawButtons() +{ + createButtonMask(); + for( int i=0;istartWrite(); + if( !getGradientLine( true ) ) { + gfx->fillRect( 0, 0, Theme->WinWidth, TITLEBAR_HEIGHT-2, Theme->MenuColor ); + } else { + for( int i=0;ipushSprite( 0, i ); + } + GradienSprite->deleteSprite(); + } + // header text + drawTextShadow( gfx, Menu->Title, Theme->WinWidth/2, (TITLEBAR_HEIGHT-2)/2, Theme->TextColor, Theme->TextShadowColor, HeaderFont, MC_DATUM ); + cropRoundRect( gfx, 0, 0, Theme->WinWidth, TITLEBAR_HEIGHT-2, 3, Theme->BgColor ); + gfx->endWrite(); + // window + if( clearWindow ) windowClr(); + // buttons + drawButtons(); +} + + + + + +void UITheme::setupCanvas( LGFX* gfx ) +{ + fontHeightFM9p7 = gfx->fontHeight(InfoWindowFont); + LIFontHeight = gfx->fontHeight(LIFont); + fontHeight0 = gfx->fontHeight(&Font0); + arrowWidth = gfx->textWidth(">"); + ButtonsPosY = gfx->height()-BUTTON_HEIGHT; + WinHeight = gfx->height()-TITLEBAR_HEIGHT*2; + WinWidth = gfx->width(); + WinPosY = TITLEBAR_HEIGHT+1; + WinPosX = 0; + WinMargin = WINDOW_MARGINX; + LIHeight = LISTITEM_HEIGHT; + LIOffsetY = LISTITEM_OFFSETY; // pixels offset from top for list items + LIOffsetX = LISTITEM_OFFSETX; // pixels offset from left for list items + LICaptionPosX = WinWidth-WinMargin; // text cursor position-x for list caption + LICaptionPosY = LISTCAPTION_POSY; // text cursor position-Y for list caption + assetPosY = ASSET_POSY; // assets position + assetPosX = WinWidth-(assetWidth+WinMargin); // assets positionx + pgW = gfx->width()-LISTITEM_OFFSETX*2; // progress bar width + pgX = gfx->width()/2 - pgW/2; // pogress bar posx, based on width + BgColor = BG_COLOR; + MenuColor = MENU_COLOR; + TextColor = TEXT_COLOR; + TextShadowColor = SHADOW_COLOR; + gzProgressBar.setup( gfx, pgX, 106, pgW, 10, GZ_PROGRESS_COLOR, MenuColor, true ); // gzip + tarProgressBar.setup( gfx, pgX, 152, pgW, 5, TAR_PROGRESS_COLOR, MenuColor, true ); // tar + dlProgressBar.setup( gfx, pgX+1, 188, pgW-2, 4, DL_PROGRESS_COLOR, MenuColor, false ); // download/sha sum + createGradients(); +} + + +void UITheme::createGradients() +{ + uint8_t r = (MenuColor >> 16) & 0xff; // red + uint8_t g = (MenuColor >> 8) & 0xff; // green + uint8_t b = MenuColor & 0xff; // blue + float sf = 0.2; // shade factor + float tf = 0.2; // tint factor + TintedMenuColor = ( uint8_t(r + (255 - r) * tf) << 16 ) + ( uint8_t(g + (255 - g) * tf) << 8 ) + ( uint8_t(b + (255 - b) * tf) ) ; + ShadedMenuColor = ( uint8_t(r * (1 - sf)) << 16 ) + ( uint8_t(g * (1 - sf)) << 8 ) + ( uint8_t(b * (1 - sf)) ) ; +} + + +void ProgressBarTheme::clear() +{ + gfx->fillRect( x, y, w, caption?h+fontHeight0+2:h, bg ); +} + + +void ProgressBarTheme::progress( uint8_t progress ) +{ + uint16_t th = fontHeight0+2; + uint16_t ty = y+h+2; + drawProgressBar( gfx, x, y, w, h, progress, fg, bg ); + if( caption ) { + String progressStr = " " + String(progress)+"% "; + drawCaption( gfx, progressStr.c_str(), x, ty, w, th, &Font0, TC_DATUM, fg, bg ); + } +} + + +void ProgressBarTheme::setup(LGFX* _gfx, uint16_t _x, uint16_t _y, uint16_t _w, uint16_t _h, uint32_t _fg, uint32_t _bg, bool _cap ) +{ + gfx = _gfx; + x = _x; + y = _y; + w = _w; + h = _h; + fg = _fg; + bg = _bg; + caption = _cap; +} + + + + +namespace UIUtils +{ + + using namespace MenuItems; + using namespace AppRenderer; + + void initSprites( LGFX* gfx ) + { + appInfo._gfx = gfx; + + GradienSprite = new LGFX_Sprite( gfx ); + GradienSprite->setColorDepth(16); + + ButtonSprite = new LGFX_Sprite( gfx ); + ButtonSprite->setColorDepth(16); + + MaskSprite = new LGFX_Sprite( gfx ); + MaskSprite->setColorDepth(16); + + AssetSprite = new LGFX_Sprite( gfx ); + AssetSprite->setColorDepth(16); + + ScrollSprite = new LGFX_Sprite( gfx ); + ScrollSprite->setColorDepth(1); + + } + + void drawProgressBar( LGFX* gfx, int x, int y, int w, int h, uint8_t val, uint32_t color, uint32_t bgcolor ) + { + gfx->drawRect(x, y, w, h, color); + if( val>100) val = 100; + if( val==0 ) { + gfx->fillRect(x + 1, y + 1, w-2, h - 2, bgcolor); + } else { + int fillw = (w * (((float)val) / 100.0)) -2; + gfx->fillRect(x + 1, y + 1, fillw-2, h - 2, color); + gfx->fillRect(x + fillw + 1, y + 1, w-fillw-2, h - 2, bgcolor); + } + } + + + void gzProgressCallback( uint8_t progress ) + { + static int8_t gzLibLastProgress = -1; + if( gzLibLastProgress != progress ) { + gzLibLastProgress = progress; + UI->getTheme()->gzProgressBar.progress( progress ); + } + } + + + void tarProgressCallback( uint8_t progress ) + { + static int8_t tarLibLastProgress = -1; + if( tarLibLastProgress != progress ) { + tarLibLastProgress = progress; + UI->getTheme()->tarProgressBar.progress( progress ); + } + } + + + void tarStatusCallback( const char* name, size_t size, size_t total_unpacked ) + { + log_d("[TAR] %-64s %8d bytes - %8d Total bytes", name, size, total_unpacked ); + size_t th_small = fontHeight0*1.3; + size_t th_big = fontHeightFM9p7*1.3; + uint16_t posy = 162; // bottom text position (file size, overall size) + static bool clean; + UITheme* theme = UI->getTheme(); + LGFX* gfx = UI->getGfx(); + if( clean != true ) { + clean = true; + UI->windowClr( theme->MenuColor ); + drawInfoWindow( TAR_PROGRESS_TITLE ); + } + // TODO: don't redraw OVERALL_PROGRESS_TITLE on every callback + drawCaption( gfx, OVERALL_PROGRESS_TITLE, theme->gzProgressBar.x, theme->gzProgressBar.y - (th_big+2), theme->gzProgressBar.w, th_big, InfoWindowFont, MC_DATUM ); + + drawCaption( gfx, name, WINDOW_MARGINX, theme->tarProgressBar.y - (th_small+2), gfx->width()-WINDOW_MARGINX*2, th_small, &Font0, MC_DATUM ); + drawCaption( gfx, String( formatBytes( size, formatBuffer ) ).c_str(), WINDOW_MARGINX, posy+th_big, gfx->width()/3-WINDOW_MARGINX, th_big, InfoWindowFont, TL_DATUM ); + const char* caption = String( "Total: " + String( formatBytes( total_unpacked, formatBuffer ) ) ).c_str(); + drawCaption( gfx, caption, gfx->width()/3, posy+th_big, gfx->width()*2/3-WINDOW_MARGINX, th_big, InfoWindowFont, TR_DATUM ); + } + + + bool getGradientLine( bool invert ) + { + uint32_t width = UI->getGfx()->width(); + if( !GradienSprite->createSprite( width, 1 ) ) { + log_d("Failed to create sprite (%d x %d ), will fill with single color", width, 1 ); + return false; + } + UITheme* theme = UI->getTheme(); + GradienSprite->drawGradientHLine( 0, 0, width, invert?theme->TintedMenuColor:theme->ShadedMenuColor, invert?theme->ShadedMenuColor:theme->TintedMenuColor ); + return true; + } + + + void createButtonMask() + { + if( !ButtonSprite->createSprite( BUTTON_WIDTH, BUTTON_HEIGHT ) ) { + ButtonSprite->setColorDepth( ButtonSprite->getColorDepth()/2 || 1 ); + log_d("Failed to create Button Sprite (%d x %d )", BUTTON_WIDTH, 1 ); + return; + } + UITheme* theme = UI->getTheme(); + // fill with gradient + for( int i=0; idrawGradientHLine( 0, i, BUTTON_WIDTH, theme->TintedMenuColor, theme->ShadedMenuColor ); + } + // make a button skin + MaskSprite->createSprite( BUTTON_WIDTH, BUTTON_HEIGHT ); + // colors will be cut diagonally, prepare coord for triangles + uint16_t x1 = 0 , y1 = 0; + uint16_t x2 = 0 , y2 = BUTTON_HEIGHT-1; + uint16_t x3 = BUTTON_WIDTH-1, y3 = 0; + uint16_t x4 = BUTTON_WIDTH-1, y4 = BUTTON_HEIGHT-1; + // apply both colors + for( int i=-1; i<1; i++ ) { + drawButtonMask( (LGFX*)MaskSprite, 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, 3, 3, theme->BgColor, i?theme->ShadedMenuColor:theme->TintedMenuColor ); + MaskSprite->fillTriangle( x2, y2, i?x4:x1, i?y4:y1, x3, y3, theme->BgColor ); + MaskSprite->pushSprite( ButtonSprite, 0, 0, theme->BgColor ); + } + MaskSprite->deleteSprite(); + // apply rounded borders + cropRoundRect( (LGFX*)ButtonSprite, 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, 5, theme->BgColor ); + } + + + void drawButtonMask( LGFX* gfx, int32_t x, int32_t y, uint32_t w, uint32_t h, uint8_t radius, uint8_t thickness, uint32_t fillcolor, uint32_t bgcolor ) + { + gfx->fillRect( x, y, w, h, fillcolor ); + for( int i=0; idrawRoundRect( x+i, y+i, w-i*2, h-i*2, radius+i, bgcolor ); + } + } + + + void clearButtonMask() + { + ButtonSprite->deleteSprite(); + } + + + void drawCheckMark( LGFX* gfx, uint16_t gx, uint16_t gy, uint16_t gw, uint16_t gh, uint32_t textcolor, uint32_t shadowcolor, int8_t dirx, int8_t diry ) + { + gfx->fillTriangle( dirx+gx, diry+gy, dirx+gx, diry+gy+gh, dirx+gx+gw, diry+gy+gh/2, shadowcolor ); + gfx->fillTriangle( gx, gy, gx, gy+gh, gx+gw, gy+gh/2, textcolor ); + } + + + void drawTextShadow( LGFX* gfx, const char*str, int32_t x, int32_t y, uint32_t textcolor, uint32_t shadowcolor, const lgfx::v1::IFont* font, textdatum_t datum, int8_t dirx, int8_t diry ) + { + gfx->setTextDatum( datum ); + gfx->setFont( font ); + gfx->setTextColor( shadowcolor ); + gfx->drawString( str, dirx+x, diry+y ); + gfx->setTextColor( textcolor ); + gfx->drawString( str, x, y ); + } + + + template void cropRoundRect( T* gfx, int32_t x, int32_t y, uint32_t w, uint32_t h, uint8_t radius, uint32_t bgcolor ) + { + if( cropSprite1bit == nullptr ) { + cropSprite1bit = new LGFX_Sprite( gfx ); + cropSprite1bit->setColorDepth(1); + cropSprite1bit->createSprite( 1, 1 ); + } + if( w != cropSprite1bit->width() || h != cropSprite1bit->width() ) { + cropSprite1bit->deleteSprite(); + if( ! cropSprite1bit->createSprite( w, h ) ) { + log_e("Could not create sprite (%d x %x)", w, h); + return; + } + log_v("Using 1BitMask@[%d:%d]->[%dx%d] radius(%d), color(0x%06x)", x, y, w, h, radius, bgcolor ); + cropSprite1bit->setPaletteColor( 0, 0x123456U ); + cropSprite1bit->setPaletteColor( 1, bgcolor ); + cropSprite1bit->fillSprite( 1 ); + cropSprite1bit->fillRoundRect( 0, 0, w, h, radius, 0 ); + } + cropSprite1bit->pushSprite( gfx, x, y, 0 ); + } + + + void drawDownloadIcon( uint32_t color, int16_t x, int16_t y, float size ) + { + float halfsize = size/2; + LGFX* gfx = UI->getGfx(); + gfx->fillTriangle( x, y+2*size, x+4*size, y+2*size, x+2*size, y+5*size, color ); + gfx->fillTriangle( x+size, y, x+3*size, y, x+2*size, y+5*size, color ); + gfx->fillRect( x, -halfsize+y+6*size, 1+4*size, size, color ); + } + + void drawRSSIBar( int16_t x, int16_t y, int16_t rssi, uint32_t bgcolor, float size ) + { + RGBColor heatMapColors[4] = { {0xff, 0, 0}, {0xff, 0xa5, 0x00}, {0xff, 0xff, 0}, {0, 0xff, 0} }; // # [RED, ORANGE, YELLOW, GREEN ] + uint32_t heatColor32 = getHeatMapColor( rssi%6, 0, 5, heatMapColors, sizeof(heatMapColors)/sizeof(RGBColor) ); + uint8_t bars = 0; + uint32_t barColors[4] = { bgcolor, bgcolor, bgcolor, bgcolor }; + LGFX* gfx = UI->getGfx(); + switch(rssi%6) { + case 5: bars = 4; break; + case 4: bars = 3; break; + case 3: bars = 3; break; + case 2: bars = 2; break; + case 1: bars = 1; break; + default: + case 0: bars = 1; break; + } + for( int i=0; ifillRect(x + (i*3*size), y + (4-i)*size, 2*size, (4+i)*size, barColors[i]); + } + } + + + void drawCaption( LGFX* gfx, const char* caption, int32_t x, int32_t y, uint32_t w, uint32_t h, const void* font, int datum, uint32_t fgcolor, uint32_t bgcolor ) + { + if( captionSprite == nullptr ) { + captionSprite = new LGFX_Sprite( gfx ); + captionSprite->setColorDepth(1); + } + captionSprite->createSprite( w, h ); + captionSprite->setPaletteColor(0, bgcolor ); + captionSprite->setPaletteColor(1, fgcolor ); + captionSprite->setFont((lgfx::v1::IFont*)font ); + captionSprite->setTextDatum( TL_DATUM ); // we manage our own datum here + captionSprite->setTextWrap(false); + int32_t tx, ty; + uint16_t tw = captionSprite->textWidth( caption ); + uint16_t th = captionSprite->fontHeight(); + bool wrap = true; + switch( datum ) { + case TL_DATUM: tx=0; ty=0; wrap = true; break; + case TC_DATUM: tx=w/2-tw/2; ty=0; wrap = false; break; + case TR_DATUM: tx=w-tw-1; ty=0; wrap = true; break; + case ML_DATUM: tx=0; ty=h/2-th/2; wrap = true; break; + case MC_DATUM: tx=w/2-tw/2; ty=h/2-th/2; wrap = false; break; + case MR_DATUM: tx=w-tw-1; ty=h/2-th/2; wrap = true; break; + case BL_DATUM: tx=0; ty=h-1; wrap = true; break; + case BC_DATUM: tx=w/2-tw/2; ty=h-1; wrap = false; break; + case BR_DATUM: tx=w-tw-1; ty=h-1; wrap = true; break; + } + captionSprite->setTextWrap(wrap); + captionSprite->setCursor( tx, ty ); + captionSprite->println( caption ); + captionSprite->pushSprite( gfx, x, y ); + captionSprite->deleteSprite(); + } + + + void drawInfoWindow( const char* title, const char* body, unsigned long waitdelay ) + { + UITheme* theme = UI->getTheme(); + LGFX* gfx = UI->getGfx(); + UI->windowClr( theme->MenuColor ); + if( title ) { + drawCaption( gfx, title, LISTITEM_OFFSETX, LISTITEM_OFFSETY, gfx->width()-LISTITEM_OFFSETX*2, TITLEBAR_HEIGHT, &FreeMonoBold12pt7b, MC_DATUM, theme->TextColor, theme->MenuColor ); + } + if( body ) { + drawCaption( gfx, body, LISTITEM_OFFSETX, 80, gfx->width()-LISTITEM_OFFSETX*2, TITLEBAR_HEIGHT*3, InfoWindowFont, TL_DATUM ); + } + if( waitdelay > 0 ) { + delay( waitdelay ); + } + } + + + int modalConfirm( MenuActionLabels* labels, bool clearAfter ) + { + // backup UI labels + MenuActionLabels* tempLabels = UI->getMenuActionLabels(); + const char* tempTitle = UI->getList()->Title; + // set temporary labels and redraw + UI->setMenuActionLabels( labels ); + UI->getList()->Title = labels->title; + UI->drawMenu( false ); + // wait for button input + int hidState = HID_INERT; + while( hidState == HID_INERT ) { + hidState = getControls(); + UI->modal(); + } + // deinit scroll if necessary + if( HeaderScroll.scrollInited ) HeaderScroll.scrollEnd(); + // restore labels to lask known values and redraw UI + UI->setMenuActionLabels( tempLabels ); + UI->getList()->Title = tempTitle; + if( clearAfter ) { + UI->drawMenu( true ); + drawStatusBar(); + } + return hidState; + } + + int modalConfirm( const char* question, const char* title, const char* body, const char* labelA, const char* labelB, const char* labelC ) + { + ButtonAction btnA = { labelA, nullptr }; + ButtonAction btnB = { labelB, nullptr }; + ButtonAction btnC = { labelC, nullptr }; + ButtonAction *buttons[BUTTONS_COUNT] = { &btnA, &btnB, &btnC }; + MenuActionLabels ActionLabels = { question, buttons }; + LGFX* gfx = UI->getGfx(); + drawInfoWindow( title?title:UI->getListItemTitle(), body ); + CautionModalIcon.draw( gfx, gfx->width()-CautionModalIcon.width-LISTITEM_OFFSETX, gfx->height()-CautionModalIcon.height-LISTITEM_OFFSETY ); + HIDSignal hidState = HID_INERT; + return modalConfirm( &ActionLabels ); + } + + // like map( value, min, max, COLOR_MIN, COLOR_MAX ) but with RGBColor Palette + uint32_t getHeatMapColor( int value, int minimum, int maximum, RGBColor *p, size_t psize ) + { + // They float, they all float ​🤡 + float f = float(value-minimum) / float(maximum-minimum) * float(psize-1); // convert to palette index + int i = int(f/1); // closest color index in palette + float dst = f - float(i); // distance to next color in palette + if( dst < std::numeric_limits::epsilon() ) { // exact color match + return ( p[i].r << 16 ) + ( p[i].g << 8 ) + p[i].b; + } else { // apply distance to matched color + return (uint8_t( p[i].r + dst * float( p[i+1].r - p[i].r ) ) << 16) +(uint8_t( p[i].g + dst * float( p[i+1].g - p[i].g ) ) << 8)+ +(uint8_t( p[i].b + dst * float( p[i+1].b - p[i].b ) ) ) ; + } + } + + + /* give up on redundancy and ECC to produce less and bigger squares */ + uint8_t getLowestQRVersionFromString( String text, uint8_t ecc ) + { + #define QR_MAX_VERSION 9 + if(ecc>3) return QR_MAX_VERSION; // fail fast + uint16_t len = text.length(); + uint8_t QRMaxLenByECCLevel[4][QR_MAX_VERSION] = { + // https://www.qrcode.com/en/about/version.html + // Handling version 1-9 only since there's no point with M5Stack's 320x240 display (next version is at 271) + { 17, 32, 53, 78, 106, 134, 154, 192, 230 }, // L + { 14, 26, 45, 62, 84, 106, 122, 152, 180 }, // M + { 11, 20, 32, 46, 60, 74, 86, 108, 130 }, // Q + { 7, 14, 24, 34, 44, 58, 64, 84, 98 } // H + }; + for( uint8_t i=0; i width || realHeight > height ) { + log_e("Can't fit QR with gridsize(%d),dotSize(%d) =>[%dx%d] => [%dx%d]", gridSize, dotWidth, realWidth, realHeight, width, height ); + return; + } else { + log_d("Rendering QR Code '%s' (%d bytes) on version #%d on [%dx%d] => [%dx%d] grid", text.c_str(), text.length(), version, qrcode.size, qrcode.size, realWidth, realHeight ); + } + + uint8_t marginX = (width - qrcode.size*dotWidth)/2; + uint8_t marginY = (height - qrcode.size*dotHeight)/2; + + gfx->fillRect( posX, posY, width, height, TFT_WHITE ); + + for ( uint8_t y = 0; y < qrcode.size; y++ ) { + // Each horizontal module + for ( uint8_t x = 0; x < qrcode.size; x++ ) { + bool q = lgfx_qrcode_getModule( &qrcode, x, y ); + if (q) { + gfx->fillRect( x*dotWidth +posX+marginX, y*dotHeight +posY+marginY, dotWidth, dotHeight, TFT_BLACK ); + } + } + } + } + + + void fillAssetSprite( LGFX_Sprite* dst, int32_t offsetX, int32_t offsetY=0 ) + { + if( !getGradientLine() ) { // can't blit + uint16_t transcolor = 0x1234; + dst->fillSprite( UI->getTheme()->MenuColor ); + return; + } + uint16_t *sprPtr = (uint16_t*)GradienSprite->getBuffer(); + uint16_t *clipPtr = &sprPtr[offsetX]; + int32_t width = dst->width(), height = dst->height(); + dst->pushImageRotateZoom( width/2, height/2, width/2, 0, 0.0, 1, height, width, 1, clipPtr ); + GradienSprite->deleteSprite(); + } + + + template void drawAssetReveal( T *asset, LGFX*_gfx, int32_t posX, int32_t posY ) + { + if( ! AssetSprite->createSprite( UI->getTheme()->assetWidth, UI->getTheme()->assetHeight ) ) { + log_w("Can't create %dx%d sprite, drawing raw", UI->getTheme()->assetWidth, UI->getTheme()->assetHeight ); + asset->draw( _gfx, posX, posY ); + return; + } + fillAssetSprite( AssetSprite, posX ); + asset->draw( (LGFX*)AssetSprite, 0, 0 ); + uint16_t *sprPtr = (uint16_t*)AssetSprite->getBuffer(); + for( int y=0; yheight*2; y++ ) { + int scanY = y%2==0? y/4 : (-1+asset->height-y/4); + _gfx->pushImageRotateZoom( posX+asset->width/2, posY+scanY, asset->width/2, 0, 0.0, 1, 1, asset->width, 1, &sprPtr[scanY*asset->width] ); + delayMicroseconds(300); + } + AssetSprite->deleteSprite(); + } + +}; + + + + +template bool UIHScroll::init( T* gfx, String _scrollText, uint32_t width, uint32_t height, size_t textSize, const void*font ) +{ + if( scrollInited ) return true; + if( _scrollText=="" ) { + log_e("No text to scroll"); + return false; + } + scrollText = _scrollText; + ScrollSprite->setColorDepth( 16 ); + if( width==0 || height==0 || !ScrollSprite->createSprite( width, height ) ) { + log_e("Can't create scrollsprite"); + return false; + } + if( !getGradientLine() ) { + log_d("Failed to create gradient sprite (%d x %d )", width, 1 ); + return false; + } + + ScrollSprite->setTextWrap( false ); // lazy way to solve a wrap bug + ScrollSprite->setFont((lgfx::v1::IFont*)font ); + ScrollSprite->setTextSize( textSize ); // setup text size before it's measured + ScrollSprite->setTextDatum( ML_DATUM ); + + scrollTextColor1 = TFT_WHITE; + scrollTextColor2 = ScrollSprite->color565(0x40,0xaa,0x40); + + if( !scrollText.endsWith( " " )) { + scrollText += SCROLL_SEPARATOR; // append a space since scrolling text *will* repeat + } + while( ScrollSprite->textWidth( scrollText ) < width ) { + scrollText += scrollText; // grow text to desired width + } + + scrollWidth1 = ScrollSprite->textWidth( scrollText ); + ScrollSprite->setTextSize( textSize+1 ); // setup text size before it's measured + scrollWidth2 = ScrollSprite->textWidth( scrollText ); + + log_w("Inited scroll to [%dx%d] virtual [%d / %d]", width, height, scrollWidth1, scrollWidth2 ); + + scrollInited = true; + return true; +} + + +template void UIHScroll::render( T* gfx, String _scrollText, uint32_t delay, size_t textSize, const void*font, uint8_t x, uint8_t y, uint32_t width, uint32_t height ) +{ + if( millis() - lastScrollRender < delay ) return; // debouncing + if( !init( gfx, _scrollText, width, height, textSize, font ) ) return; // first call + if( scrollText=="" ) { + log_e("No text to scroll"); + return; + } + // push background + ScrollSprite->pushImageRotateZoom( ScrollSprite->width()/2, ScrollSprite->height()/2, width/2, 0, 0.0, 1, height, GradienSprite->width(), 1, (uint16_t*)GradienSprite->getBuffer() ); + // draw text to scroll + printText( ScrollSprite, scrollText, 0.75, scrollWidth2, scrollTextColor2, delay, textSize+1, width, height ); + printText( ScrollSprite, scrollText, 1.5, scrollWidth1, scrollTextColor1, delay, textSize, width, height ); + cropRoundRect( ScrollSprite, 0, 0, width, height, 5, 0x000000U ); + ScrollSprite->pushSprite( gfx, x, y ); + lastScrollRender = millis(); +} + +template void UIHScroll::printText( T* gfx, String scrollText, float speed, size_t textWidth, uint16_t textColor, uint32_t delay, size_t textSize, uint32_t width, uint32_t height ) +{ + gfx->setTextSize( textSize ); // setup text size before it's measured + int32_t tick = int(millis()/(delay*speed)) % textWidth*2; + gfx->setTextColor( textColor ); + if( -tick > -textWidth ) { + gfx->drawString( scrollText, -tick, height/2 ); + } + gfx->drawString( scrollText, textWidth - tick, height/2 ); + if( textWidth*2 - tick < width ) { + gfx->drawString( scrollText, textWidth*2 - tick, height/2 ); + } +} + + +void UIHScroll::scrollEnd() +{ + GradienSprite->deleteSprite(); + ScrollSprite->deleteSprite(); + scrollInited = false; + log_e("Deinited scroll"); +} diff --git a/examples/AppStore/modules/AppStoreUI/AppStoreUI.hpp b/examples/AppStore/modules/AppStoreUI/AppStoreUI.hpp new file mode 100644 index 00000000..009b0b55 --- /dev/null +++ b/examples/AppStore/modules/AppStoreUI/AppStoreUI.hpp @@ -0,0 +1,212 @@ +#pragma once + +#include "../MenuItems/MenuItems.hpp" +#include "../MenuUtils/MenuUtils.hpp" +#include "../AppStoreActions/AppStoreActions.hpp" + +static size_t fontHeight0; +static size_t LIFontHeight; +static size_t fontHeightFM9p7; + +// Progressbar theme +struct ProgressBarTheme +{ + LGFX* gfx; + uint16_t x; + uint16_t y; + uint16_t w; + uint16_t h; + uint32_t fg; + uint32_t bg; + bool caption; + void progress( uint8_t progress ); + void setup(LGFX* _gfx, uint16_t _x, uint16_t _y, uint16_t _w, uint16_t _h, uint32_t _fg, uint32_t _bg, bool _cap ); + void clear(); +}; + + +// UI Colors +struct UITheme +{ + uint32_t BgColor; // dark + uint32_t MenuColor; // light + ProgressBarTheme gzProgressBar; + ProgressBarTheme tarProgressBar; + ProgressBarTheme dlProgressBar; + uint32_t TextColor; // light + uint32_t TextShadowColor; // dark + uint32_t TintedMenuColor; // generated + uint32_t ShadedMenuColor; // generated + + uint16_t pgW; // Window width minus the margins ( gfx->width()-LISTITEM_OFFSETX*2) + uint16_t pgX; // pogress bar posx, based on width + + uint16_t WinHeight; + uint16_t WinWidth; + uint16_t WinPosY; + uint16_t WinPosX; + uint16_t WinMargin; + + uint16_t ButtonsPosY; + uint16_t arrowWidth; + + uint16_t LIOffsetY; + uint16_t LIOffsetX; + uint16_t LIHeight; + uint16_t LICaptionPosX; + uint16_t LICaptionPosY; + + // base dimensions for assets placeholder + uint16_t assetPosX; + uint16_t assetPosY; + uint16_t assetWidth = 120; + uint16_t assetHeight = 120; + + // const lgfx::v1::IFont* ButtonFont = _ButtonFont; + // const lgfx::v1::IFont* HeaderFont = _HeaderFont; + // const lgfx::v1::IFont* LIFont = _LIFont; + // const lgfx::v1::IFont* InfoWindowFont = _InfoWindowFont; + + void setupCanvas( LGFX* gfx ); + void createGradients(); + +}; + + +struct UIHScroll +{ + int16_t scrollPointer = 0; // pointer to the scrollText position + unsigned long lastScrollRender = millis(); // timer for scrolling + String lastScrollMessage; // last scrolling string state + int16_t lastScrollOffset; // last scrolling string position + bool scrollInited = false; + uint16_t scrollTextColor1;// TFT_WHITE + uint16_t scrollTextColor2;// = gfx->color565(0x40,0xaa,0x40); + size_t scrollWidth1; + size_t scrollWidth2; + String scrollText; + + void scrollEnd(); + template bool init( T* gfx, String _scrollText, uint32_t width, uint32_t height, size_t textSize, const void*font ); + template void render( T* gfx, String text, uint32_t delay=15, size_t size=2, const void*font=nullptr, uint8_t x=0, uint8_t y=0, uint32_t width=0, uint32_t height=0 ); + template void printText( T* gfx, String scrollText, float speed, size_t textWidth, uint16_t textColor, uint32_t delay, size_t textSize, uint32_t width, uint32_t height ); +}; + + + +namespace UIUtils +{ + // buttons, masks, gradients, effects, etc + LGFX_Sprite* captionSprite = nullptr; + LGFX_Sprite* cropSprite1bit = nullptr; + LGFX_Sprite* GradienSprite = nullptr; + LGFX_Sprite* ButtonSprite = nullptr; + LGFX_Sprite* MaskSprite = nullptr; + LGFX_Sprite* AssetSprite = nullptr; + LGFX_Sprite* ScrollSprite = nullptr; + UITheme Theme; + + // all draw primitives + void initSprites( LGFX*gfx ); + int modalConfirm( MenuActionLabels* labels, bool clearMenu = false ); + int modalConfirm(const char*question, const char*title, const char*body, const char*labelA=MENU_BTN_YES, const char*labelB=MENU_BTN_NO, const char*labelC=MENU_BTN_CANCEL); + void drawProgressBar(LGFX*gfx, int x, int y, int w, int h, uint8_t val, uint32_t color=MENU_COLOR, uint32_t bgcolor=BG_COLOR ); + void drawCaption(LGFX*gfx, const char*caption, int32_t x, int32_t y, uint32_t w, uint32_t h, const void*font=nullptr, int dt=MC_DATUM, uint32_t fgcol=TEXT_COLOR, uint32_t bgcol=MENU_COLOR ); + void drawInfoWindow( const char* title = nullptr, const char* body = nullptr, unsigned long waitdelay = 0 ); + void drawTextShadow(LGFX*gfx, const char*str, int32_t x, int32_t y, uint32_t txtcol=TEXT_COLOR, uint32_t shcol=SHADOW_COLOR, + const lgfx::v1::IFont* font=&Font2, textdatum_t dt=MC_DATUM, int8_t dirx=1, int8_t diry=1 ); + void drawCheckMark( LGFX* _gfx, uint16_t gx, uint16_t gy, uint16_t gw, uint16_t gh, uint32_t textcolor=TEXT_COLOR, uint32_t shadowcolor=SHADOW_COLOR, int8_t dirx=1, int8_t diry=1 ); + uint32_t getHeatMapColor( int value, int minimum, int maximum, RGBColor *p, size_t psize ); // like map( value, min, max, COLOR_MIN, COLOR_MAX ) but with RGBColor Palette + void drawDownloadIcon( uint32_t color=0x00ff00U, int16_t x=272, int16_t y=7, float size=2.0 ); + void drawRSSIBar( int16_t x, int16_t y, int16_t rssi, uint32_t bgcolor, float size=1.0 ); + void gzProgressCallback( uint8_t progress ); + void tarProgressCallback( uint8_t progress ); + void tarStatusCallback( const char* name, size_t size, size_t total_unpacked ); + bool getGradientLine( bool invert = false ); + void createButtonMask(); + void clearButtonMask(); + void drawButtonMask( LGFX*_gfx, int32_t x, int32_t y, uint32_t w, uint32_t h, uint8_t radius, uint8_t thickness, uint32_t fillcolor, uint32_t bgcolor ); + + UIHScroll HeaderScroll; + + template void cropRoundRect( T *gfx, int32_t x, int32_t y, uint32_t w, uint32_t h, uint8_t radius, uint32_t bgcolor ); + template void drawAssetReveal( T *asset, LGFX*_gfx, int32_t posX, int32_t posY ); + +}; + + +class AppStoreUI +{ + public: + AppStoreUI( LGFX*_gfx, UITheme * theme = &UIUtils::Theme ); + ~AppStoreUI(); + void windowClr(); + void windowClr( uint32_t fillcolor ); + void buttonsClr(); + void drawMenu( bool clearWindow = true ); + void drawButtons(); + void drawButton( uint8_t bnum ); + void showList(); + void clearList(); + uint16_t getListID(); + uint16_t getPageID(); + size_t getListSize(); + size_t getListPages(); + void setListID( uint16_t idx ); + void setPageID( uint16_t idx ); + void nextList( bool renderAfter = true ); + void pageUp( bool renderAfter = true ); + void pageDown( bool renderAfter = true ); + void menuUp( bool renderAfter = true ); + void menuDown( bool renderAfter = true ); + void execBtn( uint8_t bnum ); + void execBtnA(); + void execBtnB(); + void execBtnC(); + void idle(); + void modal(); + + void setList( MenuGroup *actions ); + MenuGroup* getList() { return Menu; } + + void setMenuActionLabels( MenuActionLabels *labels ); + MenuActionLabels* getMenuActionLabels(); + + void setTheme( UITheme* theme ); + UITheme *getTheme(); + + LGFX* getGfx(); + + const char* getListItemTitle(); + const char* getListTitle(); + const char* channel_name; + + private: + + bool empty( const char* str ); + void drawListItem( uint16_t inIDX, uint16_t postIDX ); + void updateList( uint16_t clearid ); + void callShowListHooks(); + + LGFX* gfx = nullptr; // tft instance + MenuGroup* Menu = nullptr; + UITheme* Theme = nullptr; + + const char* emptyString = ""; + char paginationStr[16]; + const char* paginationTpl = "%d/%d"; + const char* totalCountTpl = "Total: %d"; + + uint16_t LinesPerPage = LINES_PER_PAGE; + uint16_t ListLinesInLastPage; + uint16_t ListCount; + uint16_t ListTotalPages; + int16_t PageID; + int16_t MenuID; + uint16_t LinesInCurrentPage; + + bool CyclePageList = false; // affects menuDown()/menuUp() behaviour when hitting end/bottom of page +}; + + +extern AppStoreUI* UI; diff --git a/examples/AppStore/modules/Assets/Assets.cpp b/examples/AppStore/modules/Assets/Assets.cpp new file mode 100644 index 00000000..c7de382a --- /dev/null +++ b/examples/AppStore/modules/Assets/Assets.cpp @@ -0,0 +1,56 @@ +#pragma once + +#include "Assets.hpp" + +void LocalAsset::draw( LGFX* gfx, int32_t x, int32_t y, int32_t w, int32_t h ) +{ + log_v("Image '%s'", alt_text); + switch( type ) { + case IMG_JPG: gfx->drawJpg( data, data_len, x, y, w>0?w:width, h>0?h:height ); break; + case IMG_PNG: gfx->drawPng( data, data_len, x, y, w>0?w:width, h>0?h:height ); break; + case IMG_RAW: gfx->pushImage( x, y, width, height, data ); break; + } +} + + +void RemoteAsset::draw( LGFX* gfx, int32_t x, int32_t y, int32_t w, int32_t h ) +{ + log_v("Image File: '%s'", path ); + fs::File iconFile = M5_FS.open( path ); + if( !iconFile ) { + log_v("File not found: %s, will draw caption", path ); + DummyAsset::drawAltText( gfx, alt_text, x, y, w>0?w:width, h>0?h:height ); + return; + } + + if( String(path).endsWith(EXT_png) ) { + gfx->drawPng( &iconFile, x, y, w>0?w:width, h>0?h:height ); + } else if( String(path).endsWith(EXT_jpg) ) { + gfx->drawJpg( &iconFile, x, y, w>0?w:width, h>0?h:height ); + } else { + log_w("Unsupported image format: %s, will draw caption", path ); + DummyAsset::drawAltText( gfx, alt_text, x, y, w>0?w:width, h>0?h:height ); + } + iconFile.close(); +} + + +namespace DummyAsset +{ + void drawAltText( LGFX* gfx, const char* caption, int32_t x, int32_t y, uint32_t w, uint32_t h ) + { + if( drawCaption ) { + drawCaption( gfx, caption, x, y, w, h, &Font0, MC_DATUM, fgcolor, bgcolor ); + } + BrokenImage->draw( gfx, x, y ); + } + + void setup( captionDrawer_t drawCb, LocalAsset * brokenImage, uint32_t _fgcolor, uint32_t _bgcolor ) + { + BrokenImage = brokenImage; + drawCaption = drawCb; + bgcolor = _bgcolor; + fgcolor = _fgcolor; + } + +}; diff --git a/examples/AppStore/modules/Assets/Assets.h b/examples/AppStore/modules/Assets/Assets.h new file mode 100644 index 00000000..035b4196 --- /dev/null +++ b/examples/AppStore/modules/Assets/Assets.h @@ -0,0 +1,214 @@ +/* + * Image Source: https://i.imgur.com/llemDHF.gif + * + * - ImageMagick: + * #> convert -strip disk.gif disk%02d.jpg + * + * - Crop visual to 30x30 in an image editor + * + * - Export to C uchar format: + * #> xxd -i disk00.jpg >> assets.h + * #> xxd -i disk01.jpg >> assets.h + * + * + */ + +#pragma once + + +static const unsigned char disk01_jpg[1486] = { + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, + 0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, + 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x1e, 0x00, 0x1e, 0x03, + 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, + 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, + 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, + 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, + 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, + 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, + 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, + 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, + 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, + 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, + 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, + 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, + 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, + 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, + 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, + 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, + 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, + 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, + 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, + 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, + 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, + 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfe, + 0xc7, 0x7f, 0x6f, 0xc0, 0x7f, 0xe1, 0x45, 0x78, 0x04, 0x16, 0x04, 0xaf, + 0xed, 0xaf, 0xff, 0x00, 0x04, 0xd4, 0x0d, 0xb7, 0x20, 0x93, 0xff, 0x00, + 0x0f, 0x16, 0xfd, 0x95, 0xf2, 0x06, 0x64, 0x24, 0x65, 0x8a, 0xb9, 0x0e, + 0x64, 0x25, 0x41, 0x03, 0x2c, 0x55, 0xc7, 0xa9, 0xfe, 0xd7, 0x7a, 0xff, + 0x00, 0xc5, 0x4f, 0x09, 0xfe, 0xc9, 0xff, 0x00, 0xb4, 0xff, 0x00, 0x8a, + 0xbe, 0x05, 0x43, 0xad, 0xdc, 0xfc, 0x6f, 0xf0, 0xcf, 0xec, 0xf1, 0xf1, + 0xab, 0xc4, 0x1f, 0x07, 0x2d, 0xfc, 0x37, 0xe1, 0xa8, 0xfc, 0x6d, 0xe2, + 0x29, 0xfe, 0x2a, 0xe8, 0xdf, 0x0d, 0xbc, 0x4b, 0xa8, 0xfc, 0x3d, 0x87, + 0x40, 0xf0, 0x64, 0xfa, 0x66, 0xb7, 0x07, 0x8b, 0xb5, 0xa9, 0x7c, 0x5d, + 0x6d, 0xa4, 0x26, 0x97, 0xe1, 0x89, 0xb4, 0x5d, 0x5e, 0x2d, 0x7e, 0xf9, + 0xa0, 0xd2, 0xa4, 0xd2, 0xef, 0xd2, 0xec, 0xda, 0x4b, 0xe4, 0xff, 0x00, + 0xb4, 0xaf, 0xc5, 0x3f, 0x0c, 0xeb, 0xba, 0xb6, 0xbb, 0xfb, 0x33, 0xc7, + 0xfb, 0x34, 0xfc, 0x6c, 0xfd, 0xaa, 0x35, 0xcf, 0xf8, 0x42, 0xbc, 0x0f, + 0xf1, 0x2f, 0xe2, 0x27, 0x86, 0x3e, 0x0c, 0x6b, 0xff, 0x00, 0x07, 0xbc, + 0x0d, 0xaa, 0x7c, 0x2a, 0xd0, 0xbc, 0x4b, 0xe3, 0x2f, 0x14, 0xc5, 0xf0, + 0x4b, 0xc7, 0xb0, 0xf8, 0xf3, 0xe3, 0x37, 0xc7, 0xdf, 0xd9, 0xda, 0xeb, + 0x43, 0xf1, 0x9d, 0xd7, 0xc4, 0x0f, 0x83, 0x1e, 0x39, 0xf1, 0x17, 0xc2, + 0xef, 0x15, 0xfc, 0x12, 0xf1, 0x4e, 0xa7, 0xf1, 0x1b, 0xe1, 0x6f, 0x8d, + 0x7e, 0x13, 0x58, 0xf8, 0xde, 0xe2, 0xf3, 0xe1, 0xb6, 0xb3, 0x27, 0xc3, + 0x6d, 0x6f, 0x5d, 0xf8, 0xc0, 0xfc, 0x2b, 0x38, 0x38, 0xfd, 0x84, 0x3f, + 0xe0, 0xb5, 0xd9, 0xc1, 0xc6, 0x7f, 0xe0, 0xac, 0x8a, 0xc3, 0x3d, 0xbe, + 0x57, 0xff, 0x00, 0x82, 0xd2, 0x3a, 0x37, 0xd1, 0xd1, 0x94, 0xf4, 0x65, + 0x61, 0x90, 0x40, 0x3c, 0xa3, 0xc4, 0x9e, 0x2e, 0xf0, 0x06, 0xb7, 0x69, + 0xf1, 0x2b, 0xc3, 0xff, 0x00, 0xb3, 0x97, 0xc6, 0xbd, 0x6f, 0xe3, 0xef, + 0xec, 0xa3, 0xa0, 0xfe, 0xd0, 0xff, 0x00, 0xf0, 0x44, 0xad, 0x67, 0xc2, + 0x9e, 0x39, 0x9b, 0xf6, 0x89, 0xf1, 0xb7, 0xed, 0x67, 0xe1, 0xcb, 0x2f, + 0xda, 0x17, 0xc4, 0xbf, 0xf0, 0x54, 0x2d, 0x2a, 0xcb, 0xe3, 0x8f, 0x83, + 0xb4, 0xaf, 0x8f, 0xde, 0x3d, 0xf1, 0xef, 0xc5, 0x8f, 0x12, 0x1d, 0x63, + 0x47, 0xf8, 0x6d, 0xe1, 0x7f, 0xd9, 0x9b, 0x5a, 0xd7, 0xbe, 0x10, 0x43, + 0xf1, 0x02, 0x5d, 0x23, 0xc0, 0x36, 0x3e, 0x22, 0xf0, 0xcf, 0x8c, 0x34, + 0xff, 0x00, 0x07, 0xf8, 0x7a, 0xe3, 0xe2, 0xd6, 0xab, 0xad, 0x78, 0xd7, + 0xeb, 0xcf, 0xf8, 0x28, 0xa7, 0xfc, 0x12, 0x1f, 0xf6, 0x42, 0xff, 0x00, + 0x82, 0x9b, 0xb7, 0x81, 0x35, 0x5f, 0x8f, 0x5a, 0x6f, 0x8c, 0xfc, 0x27, + 0xf1, 0x07, 0xe1, 0xd8, 0x97, 0x4e, 0xd0, 0xbe, 0x2c, 0xfc, 0x21, 0xd4, + 0x7c, 0x2b, 0xe1, 0xbf, 0x88, 0x77, 0x9e, 0x0d, 0x95, 0xb5, 0x3b, 0x89, + 0xbe, 0x1c, 0xf8, 0x8f, 0x50, 0xf1, 0x67, 0x82, 0xfc, 0x79, 0xa1, 0xf8, + 0x8b, 0xc1, 0xb6, 0xfa, 0xe6, 0xa5, 0x27, 0x89, 0x34, 0x8b, 0x4d, 0x5b, + 0xc3, 0xf7, 0x3a, 0xaf, 0x85, 0xf5, 0xc9, 0x35, 0x5b, 0x9f, 0x05, 0xeb, + 0x5e, 0x1e, 0xb2, 0xf1, 0x8f, 0x8f, 0xec, 0x3c, 0x55, 0xe6, 0x3e, 0x1c, + 0xf0, 0x1c, 0x5e, 0x18, 0xf1, 0x26, 0x81, 0xe2, 0x67, 0xff, 0x00, 0x82, + 0x70, 0xff, 0x00, 0xc1, 0x57, 0x3c, 0x75, 0x71, 0xe1, 0xad, 0x67, 0x4b, + 0xf1, 0x26, 0x8f, 0xa0, 0xfc, 0x62, 0xff, 0x00, 0x82, 0x82, 0x7c, 0x2b, + 0xf8, 0xeb, 0xf0, 0xfe, 0x3f, 0x11, 0xe8, 0x17, 0xf1, 0x6b, 0x7e, 0x18, + 0xd7, 0xf5, 0x0f, 0x86, 0x1f, 0x1a, 0xff, 0x00, 0xe0, 0xae, 0x7e, 0x3d, + 0xf8, 0x6f, 0xab, 0x6b, 0xbe, 0x10, 0xf1, 0x1d, 0x8e, 0x97, 0xe2, 0xdf, + 0x05, 0xeb, 0x7a, 0xbf, 0x85, 0x35, 0x1d, 0x4f, 0xc1, 0xbe, 0x34, 0xd1, + 0xbc, 0x3d, 0xe3, 0x4f, 0x0c, 0x5c, 0x69, 0x3e, 0x2d, 0xf0, 0xee, 0x85, + 0xad, 0x69, 0xff, 0x00, 0xa4, 0x9f, 0x03, 0x7e, 0x36, 0xe9, 0x9f, 0x1a, + 0xf4, 0xcf, 0x1a, 0xff, 0x00, 0xc5, 0x19, 0xe3, 0x8f, 0x86, 0x9e, 0x33, + 0xf8, 0x63, 0xe3, 0x86, 0xf8, 0x75, 0xf1, 0x47, 0xe1, 0x77, 0xc4, 0x41, + 0xe0, 0xf9, 0xbc, 0x5f, 0xf0, 0xf7, 0xc6, 0x73, 0x78, 0x3f, 0xc1, 0xff, + 0x00, 0x12, 0x34, 0x8d, 0x1f, 0x54, 0xd5, 0xbe, 0x1b, 0x78, 0xd3, 0xe2, + 0x37, 0xc3, 0x7d, 0x69, 0x35, 0xef, 0x86, 0x3f, 0x11, 0x3e, 0x1e, 0x7c, + 0x40, 0xd3, 0xaf, 0x3c, 0x0d, 0xf1, 0x03, 0xc5, 0xfa, 0x55, 0x9e, 0x93, + 0xe3, 0x4d, 0x3f, 0x43, 0xd6, 0x75, 0x0d, 0x23, 0xc6, 0xda, 0x3f, 0x8b, + 0x3c, 0x25, 0xe1, 0xb0, 0x0f, 0x1b, 0xf0, 0x18, 0x3f, 0xf0, 0xf1, 0xaf, + 0xda, 0xa8, 0xe3, 0x8f, 0xf8, 0x62, 0x8f, 0xd8, 0x09, 0x73, 0xdb, 0x70, + 0xf8, 0xe9, 0xff, 0x00, 0x05, 0x2a, 0x24, 0x67, 0xd4, 0x06, 0x52, 0x47, + 0x50, 0x19, 0x4f, 0x42, 0x2b, 0xed, 0x8a, 0xf8, 0x8f, 0xc0, 0x60, 0xff, + 0x00, 0xc3, 0xc6, 0x3f, 0x6a, 0x77, 0xda, 0xa4, 0xb7, 0xec, 0x55, 0xfb, + 0x02, 0x23, 0x64, 0xf3, 0x94, 0xf8, 0xe7, 0xff, 0x00, 0x05, 0x27, 0x3b, + 0x4b, 0x6c, 0x25, 0xd1, 0x09, 0x92, 0x48, 0xd8, 0xed, 0x7d, 0xee, 0xc1, + 0xbe, 0x56, 0x1e, 0x5f, 0xdb, 0x7c, 0xfa, 0x0f, 0xcc, 0xff, 0x00, 0x85, + 0x00, 0x2d, 0x7c, 0x4f, 0xfb, 0x2b, 0x02, 0x3e, 0x3a, 0xff, 0x00, 0xc1, + 0x4a, 0xf2, 0x31, 0x9f, 0xdb, 0x5f, 0xc0, 0x6c, 0x33, 0xdd, 0x7f, 0xe1, + 0xdc, 0xdf, 0xb0, 0x12, 0xe4, 0x7a, 0x8d, 0xca, 0xc3, 0x3d, 0x32, 0xa4, + 0x75, 0x06, 0xbe, 0xd6, 0x3b, 0xb0, 0x70, 0xaa, 0x4e, 0x0f, 0x05, 0x88, + 0x07, 0xd8, 0x9d, 0xa7, 0x00, 0xf7, 0xe0, 0xfd, 0x0d, 0x7c, 0x49, 0xfb, + 0x2b, 0xab, 0x7f, 0xc2, 0xf3, 0xff, 0x00, 0x82, 0x94, 0x15, 0x0b, 0xba, + 0x4f, 0xdb, 0x57, 0xc0, 0x92, 0x7d, 0xf2, 0x99, 0x03, 0xfe, 0x09, 0xd3, + 0xfb, 0x02, 0xa8, 0x77, 0x75, 0x8c, 0x93, 0x21, 0x50, 0x8a, 0x54, 0x2e, + 0xc0, 0x88, 0x80, 0x12, 0x54, 0xb3, 0x80, 0x7f, 0xff, 0xd9 +}; +static const unsigned int disk01_jpg_len = 1486; + + +// 16 x 18 +static const unsigned char broken_png[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x12, + 0x08, 0x06, 0x00, 0x00, 0x00, 0x52, 0x3b, 0x5e, 0x6a, 0x00, 0x00, 0x00, + 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0e, 0xc4, 0x00, 0x00, 0x0e, + 0xc4, 0x01, 0x95, 0x2b, 0x0e, 0x1b, 0x00, 0x00, 0x02, 0x9f, 0x49, 0x44, + 0x41, 0x54, 0x38, 0x8d, 0x85, 0x91, 0x5f, 0x48, 0x53, 0x61, 0x1c, 0x86, + 0x9f, 0x63, 0xe7, 0xb0, 0x69, 0x63, 0xb8, 0x94, 0x0d, 0x74, 0x61, 0x24, + 0xa9, 0x24, 0x76, 0x91, 0x19, 0x06, 0x21, 0x08, 0x11, 0x9c, 0x42, 0x21, + 0x42, 0x22, 0x08, 0xba, 0x49, 0x54, 0x90, 0xae, 0x22, 0x2c, 0xa2, 0xae, + 0xba, 0xe8, 0x0f, 0x24, 0x08, 0x82, 0x90, 0x09, 0x26, 0x06, 0xa6, 0x84, + 0x41, 0x64, 0x9a, 0x90, 0x56, 0x0a, 0xa9, 0x9b, 0x0c, 0x5d, 0x2c, 0x19, + 0x1e, 0x54, 0x2a, 0x25, 0xb5, 0xb6, 0xf4, 0x6c, 0x3b, 0xf3, 0x9c, 0x2e, + 0x64, 0xb1, 0x75, 0x24, 0xbf, 0xcb, 0xf7, 0x7b, 0x7f, 0xcf, 0xef, 0xcf, + 0x2b, 0x60, 0xc5, 0x10, 0xad, 0x22, 0x02, 0x02, 0xbb, 0xbd, 0xc6, 0x2b, + 0x8d, 0x14, 0x1d, 0x2a, 0xaa, 0xad, 0xab, 0xab, 0x7b, 0x9c, 0xd0, 0x04, + 0x51, 0x14, 0x8d, 0xd1, 0xd1, 0x51, 0x5c, 0x2e, 0xd7, 0xae, 0x00, 0x8f, + 0xc7, 0xc3, 0xda, 0xda, 0xda, 0x9a, 0xae, 0xeb, 0xf7, 0x1a, 0x1a, 0x1a, + 0xee, 0x03, 0x08, 0x92, 0x24, 0x19, 0x8a, 0xa2, 0x10, 0x8f, 0xc7, 0xd9, + 0xdc, 0xdc, 0xfc, 0x2f, 0xc0, 0xe7, 0xf3, 0x61, 0xb7, 0xdb, 0x71, 0x38, + 0x1c, 0x4b, 0xd3, 0xd3, 0xd3, 0x9f, 0xea, 0xeb, 0xeb, 0xcf, 0x8b, 0x89, + 0x4f, 0x55, 0x55, 0xd9, 0xd8, 0xd8, 0x00, 0x60, 0x36, 0x3c, 0x4b, 0xe7, + 0x4a, 0x27, 0x36, 0x6c, 0xdc, 0xc8, 0xbb, 0x81, 0x28, 0x6e, 0xdb, 0xb6, + 0xb6, 0xb6, 0xb0, 0x5a, 0xad, 0x14, 0x14, 0x14, 0xb8, 0x6d, 0x76, 0x9b, + 0xa3, 0xad, 0xad, 0xad, 0x47, 0xfc, 0xb7, 0xcb, 0x54, 0x68, 0x8a, 0x5e, + 0xbd, 0x97, 0xe1, 0xdc, 0x61, 0xac, 0x5e, 0x2b, 0xd9, 0xc3, 0xd9, 0x54, + 0x9f, 0xad, 0x26, 0x37, 0x37, 0x17, 0xbf, 0xdf, 0xcf, 0xe0, 0xe0, 0x20, + 0x6e, 0xb7, 0x9b, 0x48, 0x24, 0xb2, 0x77, 0x6e, 0x7e, 0xae, 0x26, 0xcd, + 0xb4, 0xe7, 0x6f, 0x0f, 0x43, 0xbf, 0x86, 0x00, 0x88, 0xc4, 0x22, 0xb4, + 0x77, 0xb4, 0xb3, 0xb2, 0xb2, 0x02, 0x80, 0xc5, 0x62, 0x61, 0x7d, 0x7d, + 0x9d, 0x40, 0x20, 0xc0, 0xfc, 0xfc, 0x3c, 0xfd, 0x03, 0xfd, 0x98, 0x26, + 0x70, 0xa7, 0xbb, 0x29, 0x5c, 0x2a, 0x24, 0xf0, 0x21, 0x00, 0x2a, 0x64, + 0xef, 0xcb, 0x46, 0x92, 0x24, 0x00, 0x64, 0x59, 0x46, 0x96, 0x65, 0x6c, + 0x36, 0x1b, 0x9a, 0xa6, 0xd1, 0x37, 0xd0, 0x67, 0x06, 0xc8, 0x0e, 0x19, + 0xcb, 0x6f, 0x0b, 0xd7, 0x1f, 0x5d, 0x47, 0x92, 0x24, 0xba, 0x5f, 0x75, + 0x13, 0x0e, 0xef, 0x27, 0x18, 0x4c, 0x38, 0x96, 0xc9, 0xc9, 0x51, 0x97, + 0xb2, 0x32, 0xb3, 0x16, 0xd9, 0x4a, 0x4a, 0x21, 0x1c, 0x0e, 0xa3, 0x2e, + 0x2f, 0x63, 0xa4, 0xa5, 0xa1, 0xa7, 0xa7, 0xa7, 0x40, 0xab, 0xaa, 0x8e, + 0xf2, 0x4d, 0x15, 0x20, 0x0a, 0xa8, 0x2d, 0xc0, 0xd5, 0xbb, 0xc0, 0x2d, + 0x80, 0x94, 0x1b, 0xb8, 0x9b, 0x9b, 0x71, 0x75, 0x75, 0xed, 0x18, 0x21, + 0x2f, 0x80, 0x4b, 0x66, 0x39, 0xf5, 0x88, 0xf1, 0x16, 0x72, 0x9e, 0xe4, + 0x70, 0xe0, 0xe6, 0x4d, 0xb3, 0xf1, 0x1c, 0xf0, 0xf4, 0x3d, 0xe0, 0xa8, + 0x4d, 0x74, 0x37, 0x03, 0xb0, 0x80, 0x56, 0x47, 0xe6, 0xc7, 0x0b, 0x1c, + 0xbc, 0x76, 0x2d, 0x49, 0x3f, 0x8d, 0xfe, 0xb3, 0x14, 0x22, 0x0f, 0x2f, + 0xc2, 0xf7, 0x67, 0x86, 0x61, 0x14, 0xfb, 0x7c, 0xbe, 0x1e, 0x32, 0xe8, + 0x31, 0xc5, 0x08, 0x2e, 0xf6, 0xa8, 0x67, 0xb0, 0x4f, 0x5c, 0x26, 0xbf, + 0xa9, 0x09, 0x34, 0x0d, 0x49, 0xfa, 0x72, 0x12, 0x3c, 0x27, 0xe0, 0xe5, + 0x6b, 0xaf, 0xf7, 0xd4, 0x61, 0x45, 0x51, 0xee, 0x08, 0x82, 0x50, 0x83, + 0x40, 0x4d, 0x4a, 0x0a, 0x1d, 0x42, 0x07, 0x7e, 0xfc, 0xa0, 0x43, 0xe6, + 0x46, 0xda, 0xe7, 0x87, 0x13, 0x13, 0xb7, 0x0d, 0x41, 0x60, 0x61, 0x61, + 0x61, 0x0c, 0x30, 0x00, 0x74, 0x5d, 0x2f, 0x8e, 0x46, 0xa3, 0x35, 0x89, + 0x9a, 0x14, 0x40, 0xec, 0x78, 0x8c, 0x99, 0xaf, 0x33, 0x23, 0x93, 0x93, + 0x93, 0x53, 0xc0, 0x5c, 0xf3, 0x2f, 0x7a, 0x29, 0x2f, 0x07, 0xc0, 0xeb, + 0xf5, 0x96, 0xc5, 0xe3, 0xf1, 0x3c, 0xc3, 0x30, 0xca, 0x92, 0x6b, 0xfe, + 0xc6, 0x18, 0x8b, 0xc5, 0x9e, 0xaf, 0xae, 0xae, 0x2e, 0x0a, 0x82, 0x30, + 0x58, 0x5a, 0x5a, 0xfa, 0x26, 0xd9, 0x64, 0x18, 0x46, 0x71, 0x30, 0x18, + 0x7c, 0xa0, 0x69, 0x9a, 0x0c, 0x90, 0x91, 0x91, 0x41, 0x28, 0x14, 0xa2, + 0xa4, 0xbc, 0x04, 0x51, 0xd7, 0x75, 0xc6, 0xc7, 0xc7, 0xc9, 0xca, 0xcf, + 0x6a, 0xad, 0x3c, 0x56, 0xf9, 0xce, 0x7c, 0x13, 0x18, 0x1b, 0x1d, 0x6b, + 0xd5, 0x0c, 0xad, 0x22, 0x59, 0x53, 0x14, 0x65, 0x7b, 0x02, 0xa7, 0xd3, + 0xb9, 0xbd, 0xdb, 0x11, 0xbd, 0xf2, 0xc7, 0xdb, 0x1f, 0x3b, 0x02, 0x9c, + 0x4e, 0xe7, 0x08, 0x50, 0x61, 0xfa, 0xb0, 0xc1, 0x1f, 0xc0, 0xbf, 0x14, + 0x66, 0xb5, 0xad, 0x80, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, + 0x44, 0xae, 0x42, 0x60, 0x82 +}; +static const unsigned int broken_png_len = 749; diff --git a/examples/AppStore/modules/Assets/Assets.hpp b/examples/AppStore/modules/Assets/Assets.hpp new file mode 100644 index 00000000..29ecf812 --- /dev/null +++ b/examples/AppStore/modules/Assets/Assets.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "../misc/core.h" +#include "Assets.h" + + +class RemoteAsset +{ +public: + RemoteAsset( const char* p, uint32_t w, uint32_t h, const char* a ) : path(p), width(w), height(h), alt_text(a) { } + const char* path; // jpg or png file + uint32_t width; + uint32_t height; + const char* alt_text; + void draw( LGFX* gfx, int32_t x=0, int32_t y=0, int32_t w=0, int32_t h=0 ); +}; + + +enum imageType_t +{ + IMG_JPG, + IMG_PNG, + IMG_RAW +}; + +class LocalAsset +{ +public: + LocalAsset( const unsigned char* d, size_t l, imageType_t t, uint32_t w, uint32_t h, const char* a ) : data(d), data_len(l), type(t), width(w), height(h), alt_text(a) { } + const unsigned char* data; // MUST BE bytes array of png, jpg, or raw colors + size_t data_len; + imageType_t type; + uint32_t width; + uint32_t height; + const char* alt_text; + void draw( LGFX* gfx, int32_t x=0, int32_t y=0, int32_t w=0, int32_t h=0 ); +}; + + +namespace DummyAsset +{ + typedef void(*captionDrawer_t)( LGFX* gfx, const char* caption, int32_t x, int32_t y, uint32_t w, uint32_t h, const void* ffont, int datum, uint32_t fgcolor, uint32_t bgcolor ); + uint32_t fgcolor; + uint32_t bgcolor; + captionDrawer_t drawCaption = nullptr; + LocalAsset *BrokenImage = nullptr; + void setup( captionDrawer_t drawCb, LocalAsset *brokenImage, uint32_t _fgcolor, uint32_t _bgcolor ); + void drawAltText( LGFX* gfx, const char* caption, int32_t x, int32_t y, uint32_t w, uint32_t h ); +}; diff --git a/examples/AppStore/modules/CertsManager/CertsManager.cpp b/examples/AppStore/modules/CertsManager/CertsManager.cpp new file mode 100644 index 00000000..9e49e49d --- /dev/null +++ b/examples/AppStore/modules/CertsManager/CertsManager.cpp @@ -0,0 +1,154 @@ +#pragma once + +#include "CertsManager.hpp" + +namespace TLS +{ + + const char* updateWallet( String host, const char* ca) + { + int8_t idx = -1; + uint8_t sizeOfWallet = sizeof( TLSWallet ) / sizeof( TLSWallet[0] ); + for(uint8_t i=0; i -1 ) { + int hostlen = host.length() + 1; + int certlen = String(ca).length() + 1; + char *newhost = (char*)malloc( hostlen ); + char *newcert = (char*)malloc( certlen ); + memcpy( newhost, host.c_str(), hostlen ); + memcpy( newcert, ca, certlen); + TLSWallet[idx] = { (const char*)newhost , (const char*)newcert }; + log_v("[WALLET UPDATE] Wallet #%d loaded ( %s )\n", idx, TLSWallet[idx].host ); + return TLSWallet[idx].ca; + } + return ca; + } + + + const char* fetchLocalCert( String host ) + { + String certPath = SD_CERT_PATH PATH_SEPARATOR + host; + File certFile = M5_FS.open( certPath ); + if(! certFile ) { // failed to open the cert file + log_w("[WARNING] Failed to open the cert file %s, TLS cert checking therefore disabled", certPath.c_str() ); + return NULL; + } + String certStr = ""; + while( certFile.available() ) { + certStr += certFile.readStringUntil('\n') + "\n"; + } + certFile.close(); + log_v("\n%s\n", certStr.c_str() ); + const char* certChar = updateWallet( host, certStr.c_str() ); + return certChar; + } + + + const char* getWalletCert( String host ) + { + uint8_t sizeOfWallet = sizeof( TLSWallet ) / sizeof( TLSWallet[0] ); + log_v("\nChecking wallet (%d items)", sizeOfWallet ); + for(uint8_t i=0; i %s", host.c_str() ); + return walletCert; + } else { + // + } + } + String certPath = SD_CERT_PATH PATH_SEPARATOR + host; + String certURL = certProvider + host; + if( !checkFS || !M5_FS.exists( certPath ) ) { + //log_d("[FETCHING REMOTE CERT] -> "); + //wget(certURL , certPath ); + return fetchLocalCert( host ); + } else { + log_v("[FETCHING LOCAL (SD) CERT] -> %s", certPath.c_str() ); + } + return fetchLocalCert( host ); + } + + + bool isInWallet( String host ) + { + uint8_t sizeOfWallet = sizeof( TLSWallet ) / sizeof( TLSWallet[0] ); + for(uint8_t i=0; iwidth()-LISTITEM_OFFSETX*2; + if( h == 0 ) h = TITLEBAR_HEIGHT*3.5; + if( x == -1 ) x = gfx->width()/2 - w/2; + if( y == -1 ) y = 75; + + sprite = new LGFX_Sprite( gfx ); + sprite->setColorDepth(1); + sprite->createSprite( w, h ); + sprite->setPaletteColor(0, bgcolor ); + sprite->setPaletteColor(1, fgcolor ); + sprite->setTextDatum( TL_DATUM ); + sprite->setTextWrap( false, true ); + sprite->setFont( &Font0 ); + th = sprite->fontHeight(); + tw = sprite->textWidth("@"); + sprite->setCursor( tw, th ); +} + +LogWindow::~LogWindow() +{ + sprite->deleteSprite(); +} + +void LogWindow::log( const char* str ) +{ + if( str ) sprite->println( str ); + else sprite->println(); + render(); +} + +void LogWindow::clear() +{ + sprite->fillSprite( bgcolor ); + sprite->setCursor( tw, th ); + render(); +} + +void LogWindow::render() +{ + checkScroll(); + sprite->fillRect( 0, 0, sprite->width()-(tw+1), th, bgcolor ); + sprite->fillRect( sprite->width()-(tw+1), 0, tw, sprite->height(), bgcolor ); + sprite->pushSprite( x, y ); +} + +void LogWindow::checkScroll() +{ + int32_t posy = sprite->getCursorY(); + if( posy + th > sprite->height() ) { + sprite->scroll( 0, -th ); + posy -= th; + } + sprite->setCursor( tw, posy ); +} diff --git a/examples/AppStore/modules/Console/Console.hpp b/examples/AppStore/modules/Console/Console.hpp new file mode 100644 index 00000000..02dbe65d --- /dev/null +++ b/examples/AppStore/modules/Console/Console.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "Console.hpp" + +// Scrollable text window +class LogWindow +{ + public: + LogWindow( LGFX* gfx=nullptr, int32_t _x=-1, int32_t _y=-1, uint32_t _w=0, uint32_t _h=0, uint32_t fgcolor=0x00ff00U, uint32_t bgcolor=0x000000U ); + ~LogWindow(); + void log( const char* str = nullptr ); + void clear(); + void render(); + private: + int32_t x; + int32_t y; + uint32_t w; + uint32_t h; + uint32_t fgcolor; + uint32_t bgcolor; + LGFX_Sprite* sprite; + uint16_t th; // text height + uint16_t tw; // text width for "@" sign + void checkScroll(); +}; diff --git a/examples/AppStore/modules/Downloader/Downloader.cpp b/examples/AppStore/modules/Downloader/Downloader.cpp new file mode 100644 index 00000000..8694fafc --- /dev/null +++ b/examples/AppStore/modules/Downloader/Downloader.cpp @@ -0,0 +1,754 @@ +#pragma once + +#include "Downloader.hpp" +#include "../AppStoreUI/AppStoreUI.hpp" +#include "../FSUtils/FSUtils.hpp" // filesystem bin formats, functions, helpers +#include "../Registry/Registry.hpp" // registry this launcher is tied to +#include "../CertsManager/CertsManager.hpp" + +//#define USE_SODIUM // as of 2.0.1-rc1 this produces a bigger binary (+4Kb) + occasional crashes +#define USE_MBEDTLS // old mbedtls still more stable and produces a smaller binary + +#ifdef USE_SODIUM + #include "sodium/crypto_hash_sha256.h" + crypto_hash_sha256_state ctx; + #define SHA_START() [](){} + #define SHA_INIT crypto_hash_sha256_init + #define SHA_UPDATE crypto_hash_sha256_update + #define SHA_FINAL crypto_hash_sha256_final +#elif defined USE_MBEDTLS + #define MBEDTLS_SHA256_ALT + #define MBEDTLS_ERROR_C + #include "mbedtls/sha256.h" + mbedtls_sha256_context ctx; + #define SHA_START() mbedtls_sha256_starts(&ctx,0) + #define SHA_INIT mbedtls_sha256_init + #define SHA_UPDATE mbedtls_sha256_update + #define SHA_FINAL mbedtls_sha256_finish +#endif + +// inherit progress bar from SD-Updater library +//#define M5SDMenuProgress SDUCfg.onProgress + +namespace NTP +{ + + const char* NVS_NAMESPACE = "NTP"; + const char* NVS_KEY = "Server"; + uint32_t nvs_handle = 0; + + void setTimezone( float tz ) + { + timezone = tz; + } + + void setDst( bool set ) + { + daysavetime = set ? 1 : 0; + } + + void setServer( uint8_t id ) + { + size_t servers_count = sizeof( Servers ) / sizeof( Server ); + + if( id < servers_count ) { + if( id != currentServer ) { + currentServer = id; + log_v("Setting NTP server to #%d ( %s / %s )", currentServer, Servers[currentServer].name, Servers[currentServer].addr ); + if (ESP_OK == nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle)) { + if( ESP_OK == nvs_set_u8(nvs_handle, NVS_KEY, currentServer) ) { + log_i("[NTP] saved to nvs::%s.%s=%d", NVS_NAMESPACE, NVS_KEY, currentServer); + } else { + log_e("[NTP] saving failed for nvs::%s.%s=%d", NVS_NAMESPACE, NVS_KEY, currentServer); + } + nvs_close(nvs_handle); + } else { + log_e("Can't open nvs::%s for writing", NVS_NAMESPACE ); + } + } + return; + } + log_e("Invalid NTP requested: #%d", id ); + } + + + void loadPrefServer() + { + if (ESP_OK == nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle)) { + uint8_t nvs_ntpserver = 0; + if(ESP_OK == nvs_get_u8(nvs_handle, NVS_KEY, static_cast(&nvs_ntpserver)) ) { + currentServer = nvs_ntpserver; + log_i("[NTP] load from NVS: server=%d", nvs_ntpserver); + } else { + log_w("Can't access nvs::%s.%s", NVS_NAMESPACE, NVS_KEY ); + } + nvs_close(nvs_handle); + } else { + log_e("Can't open nvs::%s for reading", NVS_NAMESPACE ); + } + } + + +} + + +namespace Downloader +{ + + using namespace MenuItems; + using namespace UIDraw; + using namespace UIUtils; + using namespace RegistryUtils; + using namespace FSUtils; + + void httpSetup() + { + http.setUserAgent( UserAgent ); + http.setConnectTimeout( 10000 ); // 10s timeout = 10000 + http.setReuse(true); // handle 301 redirects gracefully + } + + + URLParts parseURL( String url ) // logic stolen from HTTPClient::beginInternal() + { + URLParts urlParts; + int index = url.indexOf(':'); + if(index < 0) { + log_e("failed to parse protocol"); + return urlParts; + } + urlParts.url = "" + url; + urlParts.protocol = url.substring(0, index); + url.remove(0, (index + 3)); // remove http:// or https:// + index = url.indexOf('/'); + String host = url.substring(0, index); + url.remove(0, index); // remove host part + index = host.indexOf('@'); // get Authorization + if(index >= 0) { // auth info + urlParts.auth = host.substring(0, index); + host.remove(0, index + 1); // remove auth part including @ + } + index = host.indexOf(':'); // get port + if(index >= 0) { + urlParts.host = host.substring(0, index); // hostname + host.remove(0, (index + 1)); // remove hostname + : + urlParts.port = host.toInt(); // get port + } else { + urlParts.host = host; + } + urlParts.uri = url; + return urlParts; + } + + + URLParts parseURL( const char* url ) + { + return parseURL( String( url ) ); + } + + + bool tinyBuffInit() + { + if( tinyBuff == nullptr ) { + tinyBuff = (uint8_t *)heap_caps_malloc(sizeOfTinyBuff, MALLOC_CAP_8BIT); + if( tinyBuff == NULL ) { + return false; + } else { + log_v("Allocated %d bytes for wget buffer", sizeOfTinyBuff ); + } + } else { + log_v("Reusing wget buffer"); + } + return true; + } + + + void sha_sum_to_str() + { + shaResultStr = ""; + char str[3]; + for(int i= 0; i< sizeof(shaResult); i++) { + sprintf(str, "%02x", (int)shaResult[i]); + shaResultStr += String( str ); + } + } + + + void sha256_sum(const char* fileName) + { + log_d("SHA256: checking file %s\n", fileName); + File checkFile = M5_FS.open( fileName ); + size_t fileSize = checkFile.size(); + size_t len = fileSize; + if( !checkFile || fileSize==0 ) { + downloadererrors++; + log_e(" [ERROR] Can't open %s file for reading, aborting\n", fileName); + return; + } + //CheckSumIcon.draw( &tft, 288, 125 ); + + tinyBuffInit(); + *shaResult = {0}; + + SHA_INIT(&ctx); + SHA_START(); + + size_t n; + while ((n = checkFile.read(tinyBuff, sizeOfTinyBuff)) > 0) { + SHA_UPDATE(&ctx, (const unsigned char *) tinyBuff, n); + if( fileSize/10 > sizeOfTinyBuff && fileSize != len ) { + //shaProgressBar.draw( 100*len / fileSize ); + } + len -= n; + delay(1); + } + //shaProgressBar.clear(); + checkFile.close(); + + SHA_FINAL(&ctx, shaResult); + sha_sum_to_str(); + } + + + void sha256_sum( String fileName ) + { + return sha256_sum( fileName.c_str() ); + } + + + + bool wget( const char* url, const char* path ) + { + WiFiClientSecure *client = new WiFiClientSecure; + UITheme* theme = UI->getTheme(); + + if( !tinyBuffInit() ) { + log_e("Failed to allocate memory for download buffer, aborting"); + delete client; + return false; + } + + URLParts urlParts = parseURL( url ); + + const char* cert = TLS::fetchCert( urlParts.host ); + if( cert == NULL ) client->setInsecure(); + else client->setCACert( cert ); + + httpSetup(); + + if( ! http.begin(*client, url ) ) { + log_e("Can't open url %s", url ); + delete client; + return false; + } + + http.collectHeaders(headerKeys, numberOfHeaders); + + log_v("URL = %s", url); + + int httpCode = http.GET(); + + // file found at server + if (httpCode == HTTP_CODE_FOUND || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { + String newlocation = ""; + for(int i = 0; i< http.headers(); i++) { + String headerContent = http.header(i); + if( headerContent !="" ) { + newlocation = headerContent; + Serial.printf("%s: %s\n", headerKeys[i], headerContent.c_str()); + } + } + + http.end(); + if( newlocation != "" ) { + log_i("Found 302/301 location header: %s", newlocation.c_str() ); + delete client; + return wget( newlocation.c_str(), path ); + } else { + log_e("Empty redirect !!"); + delete client; + return false; + } + } + + WiFiClient *stream = http.getStreamPtr(); + + if( stream == nullptr ) { + http.end(); + log_e("Connection failed!"); + delete client; + return false; + } + + #if defined FS_CAN_CREATE_PATH + File outFile = M5_FS.open( path, FILE_WRITE, true ); + #else + File outFile = M5_FS.open( path, FILE_WRITE ); + #endif + + if( ! outFile ) { + log_e("Can't open %s file to save url %s", path, url ); + delete client; + return false; + } + + int len = http.getSize(); + int bytesLeftToDownload = len; + int bytesDownloaded = 0; + + *tinyBuff = {0}; + *shaResult = {0}; + + SHA_INIT( &ctx ); + SHA_START(); + + //CheckSumIcon.draw( &tft, 288, 125 ); + //DownloadIcon.draw( &tft, 252, 125 ); + + while(http.connected() && (len > 0 || len == -1)) { + size_t size = stream->available(); + if(size) { + // read up to 512 byte + int c = stream->readBytes(tinyBuff, sizeOfTinyBuff); + if( c > 0 ) { + SHA_UPDATE(&ctx, (const unsigned char *)tinyBuff, c); + outFile.write( tinyBuff, c ); + bytesLeftToDownload -= c; + bytesDownloaded += c; + //Serial.printf("%d bytes left\n", bytesLeftToDownload ); + float progress = (((float)bytesDownloaded / (float)len) * 100.00); + theme->dlProgressBar.progress( progress ); + } + } + if( bytesLeftToDownload == 0 ) break; + } + + SHA_FINAL(&ctx, shaResult); + outFile.close(); + sha_sum_to_str(); + + // clear progress bar + theme->dlProgressBar.clear(); + delete client; + return true; + } + + + // aliases + bool wget( String bin_url, String outputFile ) + { + return wget( bin_url.c_str(), outputFile.c_str() ); + } + bool wget( String bin_url, const char* outputFile ) + { + return wget( bin_url.c_str(), outputFile ); + } + bool wget( const char* bin_url, String outputFile ) + { + return wget( bin_url, outputFile.c_str() ); + } + + + WiFiClient *wgetptr( WiFiClientSecure *client, const char* url, const char *cert ) + { + if( cert == NULL ) client->setInsecure(); + else client->setCACert( cert ); + + httpSetup(); + + if( ! http.begin(*client, url ) ) { + log_e("Can't open url %s", url ); + return nullptr; + } + + http.collectHeaders(headerKeys, numberOfHeaders); + int httpCode = http.GET(); + // file found at server + if (httpCode == HTTP_CODE_FOUND || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { + String newlocation = ""; + String headerLocation = http.header("location"); + String headerRedirect = http.header("redirect"); + if( headerLocation !="" ) { + newlocation = headerLocation; + log_i("302 (location): %s => %s", url, headerLocation.c_str()); + } else if ( headerRedirect != "" ) { + log_i("301 (redirect): %s => %s", url, headerLocation.c_str()); + newlocation = headerRedirect; + } + http.end(); + if( newlocation != "" ) { + log_i("Found 302/301 location header: %s", newlocation.c_str() ); + // delete client; + return wgetptr( client, newlocation.c_str(), cert ); + } else { + log_e("Empty redirect !!"); + return nullptr; + } + } + if( httpCode != 200 ) return nullptr; + + log_v("Got response with Content:Type: %s / Content-Length: %s", http.header("Content-Type").c_str(), http.header("Content-Length").c_str() ); + + return http.getStreamPtr(); + } + + #if ARDUHAL_LOG_LEVEL < ARDUHAL_LOG_LEVEL_DEBUG + void WiFiEvent(WiFiEvent_t event) + { + const char * arduino_event_names[] = { + "WIFI_READY", + "SCAN_DONE", + "STA_START", "STA_STOP", "STA_CONNECTED", "STA_DISCONNECTED", "STA_AUTHMODE_CHANGE", "STA_GOT_IP", "STA_GOT_IP6", "STA_LOST_IP", + "AP_START", "AP_STOP", "AP_STACONNECTED", "AP_STADISCONNECTED", "AP_STAIPASSIGNED", "AP_PROBEREQRECVED", "AP_GOT_IP6", + "FTM_REPORT", + "ETH_START", "ETH_STOP", "ETH_CONNECTED", "ETH_DISCONNECTED", "ETH_GOT_IP", "ETH_GOT_IP6", + "WPS_ER_SUCCESS", "WPS_ER_FAILED", "WPS_ER_TIMEOUT", "WPS_ER_PIN", "WPS_ER_PBC_OVERLAP", + "SC_SCAN_DONE", "SC_FOUND_CHANNEL", "SC_GOT_SSID_PSWD", "SC_SEND_ACK_DONE", + "PROV_INIT", "PROV_DEINIT", "PROV_START", "PROV_END", "PROV_CRED_RECV", "PROV_CRED_FAIL", "PROV_CRED_SUCCESS" + }; + log_i("[WiFi-event]: #%d (%s)", event, arduino_event_names[event]); + } + #endif + + void disableWiFi() + { + WiFi.mode(WIFI_OFF); + delay(500); + wifisetup = false; + } + + + void enableWiFi() + { + //WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector + WiFi.mode(WIFI_OFF); + delay(500); + WiFi.mode(WIFI_STA); + String mac = WiFi.macAddress(); + Serial.println( mac ); // print mac address to serial so it can eventually be copy/pasted + #if ARDUHAL_LOG_LEVEL < ARDUHAL_LOG_LEVEL_DEBUG + // raise DEBUG messages to INFO level + WiFi.onEvent(WiFiEvent); + #endif + WiFi.begin(); // set SSID/PASS from another app (i.e. WiFi Manager) and reload this app + unsigned long startup = millis(); + + const String nnnn = "\n\n\n\n"; + drawDownloaderMenu( "WiFi Setup", String( WIFI_MSG_WAITING+nnnn+mac ).c_str()); + size_t rssi = 0; + + while ( WiFi.status() != WL_CONNECTED ) { + drawRSSIBar( 122, 100, rssi++, UI->getTheme()->MenuColor, 4.0 ); + delay(500); + if(startup + 30000 < millis()) { + drawInfoWindow( WIFI_TITLE_TIMEOUT, WIFI_MSG_TIMEOUT, 1000 ); + return; + } + } + drawInfoWindow( WIFI_TITLE_CONNECTED, WIFI_MSG_CONNECTED ); + wifisetup = true; + } + + + void enableNTP() + { + drawDownloaderMenu(NTP_TITLE_SETUP, NTP_MSG_SETUP ); + LGFX* gfx = UI->getGfx(); + NtpIcon.draw( gfx, gfx->width()/2-NtpIcon.width/2, gfx->height()/2-NtpIcon.height/2 ); + configTime(NTP::timezone*3600, NTP::daysavetime*3600, NTP::Servers[NTP::currentServer].addr, NTP::Servers[3].addr, NTP::Servers[4].addr ); + struct tm tmstruct ; + tmstruct.tm_year = 0; + + int max_attempts = 5; + while( !ntpsetup && max_attempts-->0 ) { + if( getLocalTime(&tmstruct, 5000) ) { + // TODO: draw ntp-success icon + Serial.printf("\nNow is : %d-%02d-%02d %02d:%02d:%02d\n",(tmstruct.tm_year)+1900,( tmstruct.tm_mon)+1, tmstruct.tm_mday,tmstruct.tm_hour , tmstruct.tm_min, tmstruct.tm_sec); + Serial.println(""); + ntpsetup = true; + } + delay(500); + } + + if( !ntpsetup ) { // all attempts consumed + // TODO: modal-confirm retry/restart-wifi/restart-esp + drawInfoWindow( NTP_TITLE_FAIL, NTP_MSG_FAIL, 1500 ); + } else { + drawInfoWindow( NTP_TITLE_SETUP, NTP_MSG_SETUP, 500 ); + } + } + + + bool wifiSetupWorked() + { + int16_t maxAttempts = 5; + + while( !wifisetup ) { + enableWiFi(); + maxAttempts--; + if( maxAttempts < 0 ) { + WiFi.mode(WIFI_OFF); + break; + } + } + if( wifisetup ) { + if( !ntpsetup ) { + enableNTP(); + } + drawDownloaderMenu(); + } + return wifisetup; + } + + + + void registryFetch( AppRegistry registry, String appRegistryLocalFile ) + { + if( !wifiSetupWorked() ) { + modalConfirm( MODAL_CANCELED_TITLE, MODAL_WIFI_NOCONN_MSG, MODAL_SAME_PLAYER_SHOOT_AGAIN, MENU_BTN_REBOOT, MENU_BTN_RESTART, MENU_BTN_CANCEL ); + ESP.restart(); + } + URLParts urlParts = parseURL( registry.url ); + + if( ! TLS::init( urlParts.host ) ) { + log_e("Unable to init tls, aborting"); + return; + } + + if( appRegistryLocalFile == "" ) { + appRegistryLocalFile = appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName; + } else { + appRegistryLocalFile = appRegistryFolder + PATH_SEPARATOR + urlParts.host + ".json"; + } + if( !wget( registry.url , appRegistryLocalFile ) ) { + modalConfirm( MODAL_CANCELED_TITLE, MODAL_REGISTRY_DAMAGED, MODAL_SAME_PLAYER_SHOOT_AGAIN, MENU_BTN_REBOOT, MENU_BTN_RESTART, MENU_BTN_CANCEL ); + } else { + String appRegistryDefaultFile = appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName; + File sourceFile = M5_FS.open( appRegistryLocalFile ); + if( M5_FS.exists( appRegistryDefaultFile ) ) { + M5_FS.remove( appRegistryDefaultFile ); + } + #if defined FS_CAN_CREATE_PATH + File destFile = M5_FS.open( appRegistryDefaultFile, FILE_WRITE, true ); + #else + File destFile = M5_FS.open( appRegistryDefaultFile, FILE_WRITE ); + #endif + static uint8_t buf[512]; + size_t packets = 0; + while( (packets = sourceFile.read( buf, sizeof(buf))) > 0 ) { + destFile.write( buf, packets ); + } + destFile.close(); + sourceFile.close(); + modalConfirm( UPDATE_SUCCESS, MODAL_REGISTRY_UPDATED, MODAL_REBOOT_REGISTRY_UPDATED, MENU_BTN_REBOOT, MENU_BTN_RESTART, MENU_BTN_CANCEL ); + } + ESP.restart(); + } + + + + bool downloadGzCatalog() + { + if( !wifiSetupWorked() ) { + modalConfirm( MODAL_CANCELED_TITLE, MODAL_WIFI_NOCONN_MSG, MODAL_SAME_PLAYER_SHOOT_AGAIN, MENU_BTN_REBOOT, MENU_BTN_RESTART, MENU_BTN_CANCEL ); + ESP.restart(); + } + + Console = new LogWindow(); + + bool has_backup = false; + if( M5_FS.exists( CATALOG_DIR ) ) { + if( M5_FS.exists( CATALOG_DIR_BKP ) ) { + drawInfoWindow( DL_FSCLEANUP_TITLE, DL_FSCLEANUP_MSG, 500 ); + log_d("Removing previous backup"); + cleanDir( CATALOG_DIR_BKP ); + } + log_i("Backing up registry"); + drawInfoWindow( DL_FSBACKUP_TITLE, DL_FSBACKUP_MSG, 500 ); + M5_FS.rename( CATALOG_DIR, CATALOG_DIR_BKP ); + has_backup = true; + } + + TarGzUnpacker *TARGZUnpacker = new TarGzUnpacker(); + //TARGZUnpacker->haltOnError( true ); // stop on fail (manual restart/reset required) + //TARGZUnpacker->setTarVerify( true ); // true = enables health checks but slows down the overall process + TARGZUnpacker->setupFSCallbacks( targzTotalBytesFn, targzFreeBytesFn ); // prevent the partition from exploding, recommended + TARGZUnpacker->setGzProgressCallback( gzProgressCallback /*BaseUnpacker::defaultProgressCallback*/ ); // targzNullProgressCallback or defaultProgressCallback + TARGZUnpacker->setTarProgressCallback( tarProgressCallback /*BaseUnpacker::defaultProgressCallback*/ ); // prints the untarring progress for each individual file + TARGZUnpacker->setTarStatusProgressCallback( tarStatusCallback /*BaseUnpacker::defaultTarStatusProgressCallback*/ ); // print the filenames as they're expanded + //TARGZUnpacker->setPsram( true ); + + gzCatalogURL = String( Registry.defaultChannel.api_url_https ) + "/catalog.tar.gz"; + + URLParts urlParts = parseURL( gzCatalogURL ); + + drawInfoWindow( DL_TLSFETCH_TITLE, DL_TLSFETCH_MSG, 500 ); + + const char* cert = TLS::fetchCert( urlParts.host ); + + if( cert == nullptr ) { + if( ! TLS::init( urlParts.host ) ) { + log_e("Unable to init TLS, aborting"); + drawInfoWindow( DL_TLSFAIL_TITLE, DL_TLSFAIL_MSG, 1000 ); + delete TARGZUnpacker; + disableWiFi(); + return false; + } + cert = TLS::fetchCert( urlParts.host ); + } + + drawInfoWindow( DL_HTTPINIT_TITLE, DL_HTTPINIT_MSG ); + + WiFiClientSecure *client = new WiFiClientSecure; + Stream* streamptr = wgetptr( client, gzCatalogURL.c_str(), cert ); + bool ret = false; + + if( streamptr != nullptr ) { + log_i("Fetching %s", gzCatalogURL.c_str() ); + drawInfoWindow( DL_AWAITING_TITLE, nullptr ); + // see if content-length was provided, and enable download progress + String contentLengthStr = http.header("Content-Length"); + contentLengthStr.trim(); + int64_t streamSize = -1; + if( contentLengthStr != "" ) { + streamSize = atoi( contentLengthStr.c_str() ); + } + // wait for data to happen, for some reason this is necessary + unsigned long start = millis(); + unsigned long timeout = 10000; // wait 10 secs max + float blah = -PI; + + while( !streamptr->available() && millis()-start < timeout ) { + blah += .1; + uint8_t r = abs(sin(blah))*128 + 128; + uint8_t g = abs(cos(blah))*128 + 128; + uint8_t b = abs(cos(blah))*256 - 128; + //uint16_t color = tft.color565( r, g, b ); + uint32_t color = (r << 16) + (g << 8) + b; + drawDownloadIcon( color ); + vTaskDelay(1); + } + + drawDownloadIcon(); + + if( !TARGZUnpacker->tarGzStreamExpander( streamptr, SD, CATALOG_DIR, streamSize ) ) { + drawDownloadIcon( 0x800000U ); + log_e("tarGzStreamExpander failed with return code #%d", TARGZUnpacker->tarGzGetError() ); + drawInfoWindow( "Unpacking failed", "GZ archive could\nnot be unpacked.", 1000 ); + if( has_backup ) { // restore backup + log_d("Restoring backup"); + drawInfoWindow( DL_FSBACKUP_TITLE, DL_FSRESTORE_MSG, 500 ); + M5_FS.rename( CATALOG_DIR_BKP, CATALOG_DIR ); + } + } else { + drawInfoWindow( DL_SUCCESS_TITLE, DL_SUCCESS_MSG, 500 ); + drawDownloadIcon( UI->getTheme()->MenuColor ); + ret = true; // success ! + if( has_backup ) { + log_d("Removing old backup"); + drawInfoWindow( DL_FSCLEANUP_TITLE, DL_FSCLEANUP_MSG, 500 ); + cleanDir( CATALOG_DIR_BKP ); // no need to keep the backup + } + } + } else { + drawInfoWindow( DL_HTTPFAIL_TITLE, DL_FAIL_MSG, 1000 ); + } + delete client; + delete TARGZUnpacker; + delete Console; + disableWiFi(); + log_v(" Leaving registry fetcher with %d bytes free", ESP.getFreeHeap() ); + return ret; + } + + + bool downloadApp( String appName ) + { + + if( !wifisetup ) { + if( !wifiSetupWorked() ) { + modalConfirm( MODAL_CANCELED_TITLE, MODAL_WIFI_NOCONN_MSG, MODAL_SAME_PLAYER_SHOOT_AGAIN, MENU_BTN_REBOOT, MENU_BTN_RESTART, MENU_BTN_CANCEL ); + ESP.restart(); + } + } + + drawInfoWindow( "Downloading", "..." ); + + if( baseCatalogURL == "" ) { + log_e("No base catalog url set, download catalog first "); + return false; + } + + String macroseparator = ""; + String jsonFile = CATALOG_DIR + macroseparator + DIR_json + appName + EXT_json; + + JsonObject root; + DynamicJsonDocument jsonBuffer( 8192 ); + if( !getJson( jsonFile.c_str(), root, jsonBuffer ) ) { + log_e("Failed to get json from %s", jsonFile.c_str() ); + return false; + } + + progress_modulo = 100; // progress_modulo = 100/appsCount; + size_t assets_count = root["json_meta"]["assets"].size(); + + if( assets_count == 0 ) return false; + int i=0; + + Console = new LogWindow(); + + for (JsonVariant value : root["json_meta"]["assets"].as() ) { + String finalName = value["path"].as() + value["name"].as(); + String tempFileName = finalName + String(".tmp"); + Console->log( value["name"].as() ); + if(M5_FS.exists( finalName.c_str() )) { + // local file already exists, calculate sha shum and compare to registry + sha256_sum( finalName ); + if( value["sha256_sum"].as().equals(shaResultStr) ) { + // doesn't need to be updated + Console->log( WGET_SKIPPING ); + i++; + continue; // no need to download + } + Console->log( WGET_UPDATING ); + } else { + // file does not exist locally, see if it is present in the catalog folder + if(M5_FS.exists( CATALOG_DIR+finalName )) { + // file exists in the catalog folder as it should, no need to download => copy it locally + if( copyFile( String( CATALOG_DIR+finalName ).c_str(), finalName.c_str() ) ) { + Console->log( WGET_SKIPPING ); + i++; + continue; + } + } + Console->log( WGET_CREATING ); + } + String appURL = baseCatalogURL + finalName; + drawDownloadIcon(); + if( !wget( appURL, tempFileName ) ) { // uh-oh + log_e("\n%s\n", "[ERROR] could not download %s to %s", appURL.c_str(), tempFileName.c_str() ); + drawDownloadIcon( 0x800000U ); + drawInfoWindow( DL_HTTPFAIL_TITLE, tempFileName.c_str(), 500 ); + i++; + continue; + } + drawDownloadIcon( UI->getTheme()->MenuColor ); + if( value["sha256_sum"].as().equals(shaResultStr) ) { + if( M5_FS.exists( tempFileName ) ) { + if( M5_FS.exists( finalName ) ) M5_FS.remove( finalName ); // remove existing as it'll be replaced + M5_FS.rename( tempFileName, finalName ); + } else { // download failed, error was previously disclosed + drawInfoWindow( DL_FSFAIL_TITLE, DOWNLOAD_FAIL, 500 ); + } + } else { + log_e(" [SHA256 SUM ERROR] Remote hash: %s, Local hash: %s ### keeping local file and removing temp file ###", value["sha256_sum"].as(), shaResultStr.c_str() ); + drawInfoWindow( DL_SHAFAIL_TITLE, SHASHUM_FAIL, 500 ); + M5_FS.remove( tempFileName ); + } + uint16_t myprogress = progress + (i* float(progress_modulo/assets_count)); + i++; + } + return true; + } + +}; diff --git a/examples/AppStore/modules/Downloader/Downloader.hpp b/examples/AppStore/modules/Downloader/Downloader.hpp new file mode 100644 index 00000000..a9041579 --- /dev/null +++ b/examples/AppStore/modules/Downloader/Downloader.hpp @@ -0,0 +1,157 @@ +/* + * + * M5Stack SD Menu + * Project Page: https://github.com/tobozo/M5Stack-SD-Updater + * + * Copyright 2019 tobozo http://github.com/tobozo + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files ("M5Stack SD Updater"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +// + +#pragma once + +#include +#include +#include "../misc/compile_time.h" // for app watermarking & user-agent customization +#include "../misc/config.h" + +namespace NTP +{ + struct Server + { + const char* name; + const char* addr; + }; + + // Timezone is using a float because Newfoundland, India, Iran, Afghanistan, Myanmar, Sri Lanka, the Marquesas, + // as well as parts of Australia use half-hour deviations from standard time, also some nations such as Nepal + // and some provinces such as the Chatham Islands of New Zealand, use quarter-hour deviations. + float timezone = 0; // UTC + uint8_t daysavetime = 1; // UTC + 1 + const char* defaultServer = "pool.ntp.org"; + uint8_t currentServer = 0; + + void setTimezone( float tz ); + void setDst( bool set ); + void setServer( uint8_t id ); + void loadPrefServer(); + + const Server Servers[] = + { + { "Global", "pool.ntp.org" }, + { "Africa", "africa.pool.ntp.org" }, + { "Asia", "asia.pool.ntp.org" }, + { "Europe", "europe.pool.ntp.org" }, + { "North America", "north-america.pool.ntp.org" }, + { "Oceania", "oceania.pool.ntp.org" }, + { "South America", "south-america.pool.ntp.org" }, + }; + +}; + + +namespace Downloader +{ + + HTTPClient http; + // interesting http headers to watch for this module + const char * headerKeys[] = {"location", "redirect", "Content-Type", "Content-Length", "Content-Disposition" }; + const size_t numberOfHeaders = 5; + + + struct URLParts + { + String url; + String protocol; + String host; + String port; + String auth; + String uri; + }; + + + URLParts parseURL( String url ); + URLParts parseURL( const char* url ); + + const String _ds = "-", _is = " ", _ts = ":"; // for iso datetime + const String _sdp = "HTTPClient (SDU-", _sdc = "+Chimera-Core-", _sds = ", ", _sde = ")"; // for user agent + + // This sketch build date/time in iso format + const String ISODateTime = __TIME_YEARS__+_ds+__TIME_MONTH__+_ds+__TIME_DAYS__+_is+__TIME_HOURS__+_ts+__TIME_MINUTES__+_ts+__TIME_SECONDS__; + // A comprehensive user agent to provide some hardware/software identity to the remote registry + const String UserAgent = PLATFORM_NAME+_sdp+M5_SD_UPDATER_VERSION+_sdc+CHIMERA_CORE_VERSION+_sds+ISODateTime+_sde; + + String gzCatalogURL = ""; + String baseCatalogURL = ""; + + // Tiny buffer shared by HTTP and sha256 sum + size_t sizeOfTinyBuff = 512; // smaller is better because sha256 hashing happens between reads + uint8_t *tinyBuff = nullptr; + + bool wifisetup = false; + bool ntpsetup = false; + bool done = false; + + uint8_t progress = 0; + float progress_modulo = 0; + + uint8_t shaResult[32]; + static String shaResultStr = "****************************************************************"; // cheap malloc: any string is good as long as it's 64 chars + + int tlserrors = 0; + int jsonerrors = 0; + int downloadererrors = 0; + int updatedfiles = 0; + int newfiles = 0; + int checkedfiles = 0; + + void httpSetup(); + bool tinyBuffInit(); + + void sha_sum_to_str(); + void sha256_sum(const char* fileName); + void sha256_sum( String fileName ); + + bool wget( const char* url, const char* path ); + bool wget( String bin_url, String outputFile ); + bool wget( String bin_url, const char* outputFile ); + bool wget( const char* bin_url, String outputFile ); + + WiFiClient *wgetptr( WiFiClientSecure *client, const char* url, const char *cert = nullptr ); + #if ARDUHAL_LOG_LEVEL < ARDUHAL_LOG_LEVEL_DEBUG + void WiFiEvent(WiFiEvent_t event); + #endif + void disableWiFi(); + void enableWiFi(); + void enableNTP(); + bool wifiSetupWorked(); + + void registryFetch( AppRegistry registry, String appRegistryLocalFile = "" ); + + bool downloadGzCatalog(); + + bool downloadApp( String appName ); + +}; diff --git a/examples/AppStore/modules/FSUtils/FSUtils.cpp b/examples/AppStore/modules/FSUtils/FSUtils.cpp new file mode 100644 index 00000000..43f1221a --- /dev/null +++ b/examples/AppStore/modules/FSUtils/FSUtils.cpp @@ -0,0 +1,462 @@ +#pragma once + +#include "FSUtils.hpp" + +extern LogWindow *Console; + +namespace FSUtils +{ + + using namespace RegistryUtils; + + void setTimeFromLastFSAccess() + { + // Try to get a timestamp from filesystem in order to set system time + // to a value closer to "now" than the defaults + File root; + if( M5_FS.exists( SDU_APP_PATH ) ) { // try self + root = M5_FS.open( SDU_APP_PATH ); + } else if ( M5_FS.exists( MENU_BIN ) ) { // try /menu.bin + root = M5_FS.open( MENU_BIN ); + } else { // try rootdir + root = M5_FS.open( ROOT_DIR ); + } + time_t lastWrite; + String lastWriteSource; + if( root ) { + lastWrite = root.getLastWrite(); + lastWriteSource = root.name(); + root.close(); + } else { + lastWrite = __TIME_UNIX__; + lastWriteSource = "__TIME_UNIX__"; + } + // RTC-less devices: + // Set a pseudo realistic internal clock time when no NTP sync occured, + // and before writing to the SD Card. Timestamps are still inacurate but + // better than ESP32's [1980-01-01 00:00:00] default boot datetime. + int epoch_time = __TIME_UNIX__; // this macro is populated at compilation time + struct tm * tmstruct = localtime(&lastWrite); + String timeSource; + String timeStatus; + + // TODO: manually change this limit every century + if( (tmstruct->tm_year)+1900 < 2000 || (tmstruct->tm_year)+1900 > 2100 ) { + timeStatus = "an unreliable"; + timeSource = "sketch build time"; + } else { + int tmptime = mktime(tmstruct); // epoch time ( seconds since 1st jan 1969 ) + if( tmptime > epoch_time ) { + timeStatus = "a realistic"; + timeSource = lastWriteSource+" last write"; + epoch_time = tmptime; + } else { + timeStatus = "an obsolete"; + timeSource = "sketch build time"; + } + } + + log_w( DEBUG_TIMESTAMP_GUESS, + lastWriteSource.c_str(), + timeStatus.c_str(), + (tmstruct->tm_year)+1900, + ( tmstruct->tm_mon)+1, + tmstruct->tm_mday, + tmstruct->tm_hour, + tmstruct->tm_min, + tmstruct->tm_sec, + timeSource.c_str() + ); + + timeval epoch = {epoch_time, 0}; + const timeval *tv = &epoch; + settimeofday(tv, NULL); + + struct tm now; + getLocalTime(&now,0); + + Serial.printf("[Hobo style] Clock set to %s source (%s): ", timeStatus.c_str(), timeSource.c_str()); + Serial.println(&now,"%B %d %Y %H:%M:%S (%A)"); + } + + + bool getFileAttrs( const char* name, size_t *_size, time_t *_time ) + { + fs::File file = M5_FS.open( name ); + if( !file ) { + log_v("Can't reach file: %s", name ); + return false; + } + *_size = file.size(); + *_time = file.getLastWrite(); + log_v("Extracted size/time (%d/%d) for file %s", *_size, *_time, name ); + file.close(); + return true; + } + + + + void countApps() + { + std::vector files; + getInstalledApps( files ); + appsCount = files.size(); + } + + + bool getInstalledApps( std::vector &files ) { + files.clear(); + File root = M5_FS.open( ROOT_DIR ); + if( !root ) { + log_e( "%s", DEBUG_DIROPEN_FAILED ); + return false; + } + if( !root.isDirectory() ) { + log_e( "%s", DEBUG_NOTADIR ); + return false; + } + File file = root.openNextFile(); + if( !file ) { + // empty root + root.close(); + log_w( DEBUG_EMPTY_FS ); + //buildRootMenu(); + return false; + } + + size_t files_count = 0; + + while( file ) { + files_count++; + String fPath = String( fs_file_path(&file ) ); + if( fPath == MENU_BIN || fPath == SDU_APP_PATH ) { + file = root.openNextFile(); + continue; // ignore self and menu.bin + } + + if( isValidAppName( fPath.c_str() ) ) { + + fPath = gnu_basename( fPath ); + fPath = fPath.substring( 0, fPath.length()-4 ); // remove extension + + files.push_back( fPath ); + log_v("Found app %s", fPath.c_str() ); + } else { + log_v("Ignoring '%s' (not a valid appname)", fPath.c_str() ); + } + file = root.openNextFile(); + } + root.close(); + return true; + } + + + void removeInstalledApp( String appName ) + { + log_d("Deleting App '%s' + its assets", appName.c_str() ); + // TODO: confirmation dialog + trashFile( ROOT_DIR + appName + EXT_bin ); + trashFile( DIR_jpg + appName + EXT_jpg ); + trashFile( DIR_jpg + appName + "_gh" + EXT_jpg ); + // TODO: read json first and delete assets + trashFile( DIR_json + appName + EXT_json ); + } + + + bool isBinFile( const char* fileName ) + { + String fName = String( fileName ); + fName.toLowerCase(); + return fName.endsWith( EXT_bin ); + } + + + bool isLauncher( const char* binFileName ) + { + String fName = String( binFileName ); + fName.toLowerCase(); + return fName.indexOf( "launcher" )>=0 || fName.indexOf( "menu" )>=0; + } + + + bool isJsonFile( const char* fileName ) + { + String fName = String( fileName ); + fName.toLowerCase(); + return fName.endsWith( EXT_json ); + } + + + bool isValidAppName( const char* fileName ) + { + if( ( isBinFile( fileName ) ) // ignore files not ending in ".bin" + && !String( fileName ).startsWith( "/." ) ) { // ignore dotfiles (thanks to https://twitter.com/micutil) + return true; + } + return false; + } + + + bool iFile_exists( fs::FS *fs, String &fname ) + { + if( fs->exists( fname.c_str() ) ) { + return true; + } + String locasename = fname; + String hicasename = fname; + locasename.toLowerCase(); + hicasename.toUpperCase(); + if( fs->exists( locasename.c_str() ) ) { + fname = locasename; + return true; + } + if( fs->exists( hicasename.c_str() ) ) { + fname = hicasename; + return true; + } + return false; + } + + + void cleanDir( const char* dir ) + { + String dirToOpen = String( dir ); + bool selfdeletable = false; + + if( Console ) Console->clear(); + + // trim last slash if any, except for rootdir + if( dirToOpen != ROOT_DIR ) { + selfdeletable = true; + if( dirToOpen.endsWith( PATH_SEPARATOR ) ) { + dirToOpen = dirToOpen.substring(0, dirToOpen.length()-1); + } + } + + File root = M5_FS.open( dirToOpen ); + if(!root){ + log_e("%s", DEBUG_DIROPEN_FAILED ); + return; + } + if(!root.isDirectory()){ + log_e("%s", DEBUG_NOTADIR ); + return; + } + + File file = root.openNextFile(); + while( file ) { + if(file.isDirectory()){ + // go recursive + cleanDir( file.path() ); + M5_FS.rmdir( file.path() ); + Serial.printf( CLEANDIR_REMOVED, file.path() ); + file = root.openNextFile(); + continue; + } + Serial.printf( CLEANDIR_REMOVED, file.path() ); + if( Console ) Console->log( file.path() ); + M5_FS.remove( file.path() ); + file = root.openNextFile(); + } + root.close(); + if( selfdeletable ) { + if( M5_FS.exists( dirToOpen ) ) { + M5_FS.rmdir( dirToOpen ); + Serial.printf( CLEANDIR_REMOVED, dir ); + } + } + if( Console ) Console->clear(); + } + + + + bool copyFile( const char* src, const char* dst ) + { + fs::File sourceFile = M5_FS.open(src); + if( !sourceFile ) { + log_e("Can't open source file %s, aborting", src); + return false; + } + #if !defined FS_CAN_CREATE_PATH + // WARNING: not creating traversing folders unless sdk version >= 2.0.0 + // TODO: mkdir -p dirname( dst ) + fs::File destFile = M5_FS.open(dst, FILE_WRITE); + #else + fs::File destFile = M5_FS.open(dst, FILE_WRITE, true); + #endif + if( !destFile ) { + log_e("Can't open dest file %s, aborting", dst); + sourceFile.close(); + return false; + } + while( sourceFile.available() ) destFile.write( sourceFile.read() ); + destFile.close(); + return true; + } + + + bool trashFile( String path ) + { + if( !M5_FS.exists( path ) ) { + log_e("Can't trash unexistent file %s", path.c_str() ); + return false; + } + + String newPath = trashFolderPathStr + PATH_SEPARATOR + gnu_basename( path ); + + if( M5_FS.exists( newPath ) ) M5_FS.remove( newPath ); + else if( !M5_FS.exists( trashFolderPathStr ) ) M5_FS.mkdir( trashFolderPathStr ); + + if( ! M5_FS.rename( path.c_str(), newPath.c_str() ) ) { + log_e("Can't trash '%s', renaming to '%s' failed :(", path.c_str(), newPath.c_str() ); + return false; + } + return true; + } + + + + + bool getJson( const char* path, JsonObject &root, DynamicJsonDocument &jsonBuffer ) + { + if( jsonBuffer.capacity() == 0 ) { + log_e("JSON Buffer allocation failed" ); + return false; + } + if( ! M5_FS.exists( path ) ) { + log_v("JSON File %s does not exist", path ); + return false; + } + fs::File file = M5_FS.open( path ); + if( !file ) { + log_e("JSON File %s can't be opened"); + return false; + } + log_v("Opened JSON File %s for reading (%d bytes)", path, file.size() ); + DeserializationError error = deserializeJson( jsonBuffer, file ); + if (error) { + log_e("JSON deserialization error #%d in %s", error, path ); + file.close(); + return false; + } + root = jsonBuffer.as(); + file.close(); + return true; + } + + + void getHiddenApps() + { + //String jsonFile = "/.hidden-apps.json"; + if( !M5_FS.exists( HIDDEN_APPS_FILE ) ) return; + HiddenFiles.clear(); + JsonObject root; + DynamicJsonDocument jsonBuffer( 8192 ); + if( !getJson( HIDDEN_APPS_FILE , root, jsonBuffer ) ) { + log_v("No hidden apps (file %s not created)", HIDDEN_APPS_FILE ); + return; + } + if ( root.isNull() ) { + log_e("No parsable JSON in %s file", HIDDEN_APPS_FILE ); + return; + } + if( root["apps"].size() <= 0 ) { + log_e("No apps in catalog"); + return; + } + for( int i=0; i() ); + } + std::sort( HiddenFiles.begin(), HiddenFiles.end() ); + } + + + void toggleHiddenApp( String appName, bool add ) + { + getHiddenApps(); + + if( add ) { + if ( std::find(HiddenFiles.begin(), HiddenFiles.end(), appName) != HiddenFiles.end() ) { + log_w("App %s is already hidden!", appName.c_str() ); + return; + } + HiddenFiles.push_back( appName ); + } + + std::sort( HiddenFiles.begin(), HiddenFiles.end() ); + + DynamicJsonDocument doc(8192); + if( doc.capacity() == 0 ) { + log_e("ArduinoJSON failed to allocate 2kb"); + return; + } + + // Open file for writing + #if defined FS_CAN_CREATE_PATH + File file = M5_FS.open( HIDDEN_APPS_FILE , FILE_WRITE, true ); + #else + File file = M5_FS.open( HIDDEN_APPS_FILE , FILE_WRITE ); + #endif + if (!file) { + log_e("Failed to open file %s", HIDDEN_APPS_FILE ); + return; + } + + JsonObject root = doc.to(); + JsonArray array = root.createNestedArray("apps"); + + for( int i=0; i 0 ) { + log_i("Created json:"); + serializeJsonPretty(doc, Serial); + if (serializeJson(doc, file) == 0) { + log_e( "Failed to write to file %s", HIDDEN_APPS_FILE ); + } else { + log_i ("Successfully created %s", HIDDEN_APPS_FILE ); + } + file.close(); + } else { + HiddenFiles.clear(); + file.close(); + M5_FS.remove( HIDDEN_APPS_FILE ); + } + + } + + + bool isHiddenApp( String appName ) + { + if( HiddenFiles.size() == 0 ) getHiddenApps(); + if( HiddenFiles.size() == 0 ) return false; + return std::find( HiddenFiles.begin(), HiddenFiles.end(), appName) != HiddenFiles.end(); + } + + + #if !defined FS_CAN_CREATE_PATH + void scanDataFolder() + { + // check if mandatory folders exists and create if necessary + if( !M5_FS.exists( appRegistryFolder ) ) { + M5_FS.mkdir( appRegistryFolder ); + } + for( uint8_t i=0; i +#include // https://github.com/bblanchon/ArduinoJson/ +#include "../misc/compile_time.h" // for app watermarking & user-agent customization +#include "../misc/core.h" +#include "../misc/config.h" + + + +namespace FSUtils + +{ + + uint16_t appsCount = 0; + + const uint8_t extensionsCount = 7; // change this if you add / remove an extension + const String trashFolderPathStr = "/.trash"; + + String allowedExtensions[extensionsCount] = + { + // do NOT remove jpg and json or the menu will crash !!! + "jpg", "png", "bmp", "json", "mod", "mp3", "cert" + }; + + static std::vector HiddenFiles; + + bool getInstalledApps( std::vector &files ); + void removeInstalledApp( String appName ); + + void setTimeFromLastFSAccess(); + void countApps(); + bool getFileAttrs( const char* name, size_t *_size, time_t *_time ); + bool isBinFile( const char* fileName ); + bool isLauncher( const char* binFileName ); + bool isJsonFile( const char* fileName ); + bool isValidAppName( const char* fileName ); + bool iFile_exists( fs::FS *fs, String &fname ); + bool isHiddenApp( String appName ); + void cleanDir( const char* dir ); + bool getJson( const char* path, JsonObject &root, DynamicJsonDocument &jsonBuffer ); + void getHiddenApps(); + void toggleHiddenApp( String appName, bool add = true ); + + #if !defined FS_CAN_CREATE_PATH + void scanDataFolder(); + #endif + + bool copyFile( const char* src, const char* dst ); + bool trashFile( String path ); + + // inherit espressif32-2.x.x file->name() to file->path() migration handler from M5StackUpdater + const char* (*fs_file_path)( fs::File *file ) = SDUpdater::fs_file_path; + + static String gnu_basename( String path ) + { + char *base = strrchr(path.c_str(), '/'); + return base ? String( base+1) : path; + } + +}; + diff --git a/examples/AppStore/modules/MenuItems/MenuItems.cpp b/examples/AppStore/modules/MenuItems/MenuItems.cpp new file mode 100644 index 00000000..ec54074f --- /dev/null +++ b/examples/AppStore/modules/MenuItems/MenuItems.cpp @@ -0,0 +1,9 @@ +#pragma once + +#include "MenuItems.hpp" + +namespace MenuItems +{ + + +}; diff --git a/examples/AppStore/modules/MenuItems/MenuItems.hpp b/examples/AppStore/modules/MenuItems/MenuItems.hpp new file mode 100644 index 00000000..24e5e0a9 --- /dev/null +++ b/examples/AppStore/modules/MenuItems/MenuItems.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include "../misc/i18n.h" +#include "../Assets/Assets.hpp" +#include "../AppStoreActions/AppStoreActions.hpp" +#include "../MenuUtils/MenuUtils.hpp" + + + +namespace MenuItems +{ + + using namespace UIDo; + using namespace UIDraw; + using namespace UILists; + using namespace UIShow; + + // JPG assets (bytes array) + LocalAsset DiskIcon = { disk01_jpg, disk01_jpg_len, IMG_JPG, 0, 0, "Insert SD" }; + LocalAsset BrokenImage = { broken_png, broken_png_len, IMG_PNG, 16, 18, "Broken asset" }; + + // JPG assets (filesystem) + RemoteAsset SDUpdaterIcon = { "/catalog/jpg/sd-updater15x16.jpg" , 15, 16, "SDUpdater" }; + RemoteAsset CautionModalIcon = { "/catalog/jpg/caution.jpg" , 64, 46, "Warning" }; + + // PNG assets (filesystem) + RemoteAsset UnknownAppIcon = { "/catalog/png/unknown-app.png" , 120, 120, "Unknown App" }; + RemoteAsset CheckIcon = { "/catalog/png/missing-meta.png" , 32, 32, "Meta" }; // used to overlay app icon + RemoteAsset UpdateIcon = { "/catalog/png/update-icon.png" , 32, 32, "Update" }; // used to overlay app icon + RemoteAsset ForkIcon = { "/catalog/png/fork.png" , 12, 16, "Channel" }; + RemoteAsset NtpIcon = { "/catalog/png/ntp-connect.png" , 32, 32, "Syncing to NTP" }; + + RemoteAsset SelectBtnIcon = { "/catalog/png/select.png" , 16, 16, "Select" }; + RemoteAsset ArrowDownBtnIcon = { "/catalog/png/arrowdown.png" , 16, 16, "Arrow down" }; + RemoteAsset ArrowUpBtnIcon = { "/catalog/png/arrowup.png" , 16, 16, "Arrow up" }; + //RemoteAsset *defaultBtnIcons[3] = { &SelectBtnIcon, &ArrowUpBtnIcon, &ArrowDownBtnIcon }; + + RemoteAsset ManageAppsImage = { "/catalog/png/manage-apps.png" , 120, 120, "App Manager" }; + RemoteAsset RefreshCatalogImage = { "/catalog/png/refresh-catalog.png" , 120, 120, "Catalog Updater" }; + RemoteAsset SwitchChannelImage = { "/catalog/png/switch-channel.png" , 120, 120, "Channel Switcher" }; + RemoteAsset ChangeNTPServerImage = { "/catalog/png/ntp.png" , 120, 120, "Region Selector" }; + RemoteAsset ClearTLSImage = { "/catalog/png/clear-TLS.png" , 120, 120, "SD Cleaner" }; + RemoteAsset ClearAllImage = { "/catalog/png/clear-ALL.png" , 120, 120, "Certs Cleaner" }; + RemoteAsset SleepImage = { "/catalog/png/sleep.png" , 120, 120, "Turn Off" }; + RemoteAsset EditDeleteImage = { "/catalog/png/edit-delete.png" , 120, 120, "Edit/Delete" }; + RemoteAsset InstallAppsImage = { "/catalog/png/add-apps.png" , 120, 120, "Install/Hide" }; + RemoteAsset HideAppsImage = { "/catalog/png/toggle.png" , 120, 120, "Unhide" }; + RemoteAsset Cpanel1Image = { "/catalog/png/cpanel2.png" , 120, 120, "Settings" }; + RemoteAsset Cpanel2Image = { "/catalog/png/cpanel.png" , 120, 120, "Back to Settings" }; + + + // buttons + ButtonAction defaultListSelect = { MENU_BTN_INFO, &BtnA, &SelectBtnIcon }; // "SELECT" (exec selected item cb) + ButtonAction defaultListPrevItem = { MENU_BTN_PREV, &BtnB, &ArrowUpBtnIcon }; // ">>" (prev item) + ButtonAction defaultListNextItem = { MENU_BTN_NEXT, &BtnC, &ArrowDownBtnIcon }; // ">" (next item) + ButtonAction BackToBrowseAppsButton = { MENUACTION_BACKTOAPPS, &buildBrowseAppsMenu, nullptr }; + ButtonAction BackButton = { MENU_BTN_BACK, &BtnC, nullptr }; + ButtonAction CancelButton = { MENU_BTN_CANCEL, &BtnA, nullptr }; + ButtonAction GoButton = { MENU_BTN_GO, &BtnA, nullptr }; + ButtonAction InstallButton = { MENUACTION_APPINSTALL, &installApp, nullptr }; + ButtonAction DeleteButton = { MENUACTION_APPDELETE, &deleteApp, nullptr }; + ButtonAction HideButton = { MENUACTION_APPHIDE, &addHiddenApp, nullptr }; + ButtonAction UnhideButton = { MENUACTION_UNHIDE, &removeHiddenApp, nullptr }; + ButtonAction RefreshButton = { MENUACTION_REFRESH, &downloadCatalog, nullptr }; + ButtonAction EmptyButton = { "", [](){}, nullptr}; + + // buttons-sets + ButtonAction *defaultListButtons[3] = { &defaultListSelect, &defaultListPrevItem, &defaultListNextItem }; // "select", "next page", "next item" + ButtonAction *emptyButtons[3] = { &EmptyButton, &EmptyButton, &EmptyButton }; + ButtonAction *manageAppsButtons[3] = { &GoButton, &defaultListPrevItem, &defaultListNextItem }; // "go", "next page", "next item" + ButtonAction *myAppsButtons[3] = { &DeleteButton, &EmptyButton, &BackButton }; // "delete", [empty], "back" + ButtonAction *InstallHideActionButtons[3] = { &InstallButton, &HideButton, &BackButton }; + ButtonAction *unhideAppsButtons[3] = { &UnhideButton, &EmptyButton, &BackButton }; + + // buttons-sets/title for static lists + MenuActionLabels RootListActionButtons = { MENUTITLE_DEFAULT, defaultListButtons }; + MenuActionLabels ManageAppsActionButtons = { MENUTITLE_MANAGEAPPS, manageAppsButtons }; + MenuActionLabels RefreshAppsActionButtons = { MENUTITLE_DOWNLOADER, emptyButtons }; + + // buttons-sets/title for dynamic lists + MenuActionLabels MyAppsActionButtons = { MENUTITLE_MYAPPS, defaultListButtons }; + MenuActionLabels AppStoreActionButtons = { MENUTITLE_APPSTORE, defaultListButtons }; + MenuActionLabels HiddenAppsActionButtons = { MENUTITLE_TOGGLEAPPS, defaultListButtons }; + MenuActionLabels NTPServersActionButtons = { MENUACTION_NTP, defaultListButtons }; + + // buttons-sets/title for dynamic list items + MenuActionLabels InstallHideAppsActionButtons = { MENUTITLE_DOWNLOADER, InstallHideActionButtons }; + MenuActionLabels UnhideAppsActionButtons = { MENUTITLE_TOGGLEAPPS, unhideAppsButtons }; + MenuActionLabels DeleteAppsActionButtons = { MENUTITLE_MYAPPS, myAppsButtons }; + + // callbacks for menu events: select / render / idle / modal + MenuItemCallBacks BackToRootMenuCallbacks = { &buildRootMenu, nullptr, nullptr, nullptr }; + MenuItemCallBacks BackToManageAppsCallbacks = { &buildBrowseAppsMenu, nullptr, nullptr, nullptr }; + MenuItemCallBacks RootActionRefreshCallbacks = { &downloadCatalog, nullptr, nullptr, nullptr }; + MenuItemCallBacks RootActionBrowseCallbacks = { &buildBrowseAppsMenu, nullptr, nullptr, nullptr }; + MenuItemCallBacks ManageMyAppsCallbacks = { &buildMyAppsMenu, nullptr, nullptr, nullptr }; + MenuItemCallBacks BrowseAppStoreCallbacks = { &buildStoreMenu, nullptr, nullptr, nullptr }; + MenuItemCallBacks ManageAppStoreCallbacks = { &buildHiddenAppList, nullptr, nullptr, nullptr }; + MenuItemCallBacks RootActionSwitchCallbacks = { &drawRegistryMenu, nullptr, nullptr, nullptr }; + MenuItemCallBacks RootActionNtpCallbacks = { &buildNtpMenu, nullptr, nullptr, nullptr }; + MenuItemCallBacks RootActionClearAllCallbacks = { &clearApps, nullptr, nullptr, nullptr }; + MenuItemCallBacks RootActionClearTlsCallbacks = { &clearTLS, nullptr, nullptr, nullptr }; + MenuItemCallBacks RootActionSleepCallbacks = { &gotoSleep, nullptr, nullptr, nullptr }; + // callbacks for menuitem events: select / render / idle / modal + MenuItemCallBacks UpdateCheckCallbacks = { &showAppInfo, &updateCheckShowAppImage, &cycleAppAssets, &scrollAppInfo }; + MenuItemCallBacks UpdateMetaCallbacks = { &showAppInfo, &updateMetaShowAppImage, &cycleAppAssets, &scrollAppInfo }; + MenuItemCallBacks DeleteAppCallbacks = { &showAppInfo, &deleteShowAppImage, &cycleAppAssets, &scrollAppInfo }; + MenuItemCallBacks AppStoreCallbacks = { &showAppInfo, &showAppImage, &cycleAppAssets, &scrollAppInfo }; + MenuItemCallBacks HiddenAppsCallbacks = { &showAppInfo, &showAppImage, &cycleAppAssets, &scrollAppInfo }; + MenuItemCallBacks NtpItemCallbacks = { &setNtpServer, &showNTPImage, nullptr, nullptr }; + + + + MenuAction BackToRootMenu = MenuAction( MENUACTION_BACK " to " MENUTITLE_MAINMENU, &BackToRootMenuCallbacks, &ManageAppsImage ); + MenuAction BackToManageApps = MenuAction( MENUACTION_BACK " to " MENUTITLE_MANAGEAPPS, &BackToManageAppsCallbacks, &ManageAppsImage ); + + /* Main Menu level 0 */ + /* */MenuGroup RootMenuGroup = MenuGroup( &RootListActionButtons ); + /* | MenuItem 1 level 0 */ // Apps Management action: Refresh Catalog + /* +--*/MenuAction RootActionRefresh = MenuAction( ROOTACTION_REFRESH, &RootActionRefreshCallbacks, &RefreshCatalogImage ); + /* | MenuItem 2 level 0 */ // General Action: Open Apps Management Menu + /* +--*/MenuAction RootActionBrowse = MenuAction( MENUTITLE_MANAGEAPPS, &RootActionBrowseCallbacks, &ManageAppsImage ); + /* | MenuItem 3 level 0 */ // Apps Management Menu + /* +--*/MenuGroup ManageAppsGroup = MenuGroup( &ManageAppsActionButtons ); + /* | | MenuItem 9 level 1 */ // Apps Management: SDCard applications list + /* | +--*/MenuGroup MyAppsMenuGroup = MenuGroup( &MyAppsActionButtons ); + /* | | | MenuItem 13 level 2 */ // SDCard applications list actions: delete/update + /* | | +--*/MenuAction ManageMyApps = MenuAction( MENUACTION_DEL_APPS, &ManageMyAppsCallbacks, &EditDeleteImage ); + /* | | MenuItem 10 level 1 */ // Apps Management: Installable applications list + /* | +--*/MenuGroup AppStoreMenuGroup = MenuGroup( &AppStoreActionButtons, CATALOG_DIR ); + /* | | | MenuItem 14 level 2 */ // Installable applications list actions: install/hide + /* | | +--*/MenuAction BrowseAppStore = MenuAction( MENUACTION_BROWSEAPP, &BrowseAppStoreCallbacks, &InstallAppsImage ); + /* | | MenuItem 11 level 1 */ // Apps Management: Hidden applications list + /* | +--*/MenuGroup HiddenAppsMenuGroup = MenuGroup( &HiddenAppsActionButtons, CATALOG_DIR ); + /* | | MenuItem 15 level 2 */ // Hidden applications list actions: unhide + /* | +--*/MenuAction ManageAppStore = MenuAction( MENUTITLE_TOGGLEAPPS, &ManageAppStoreCallbacks, &HideAppsImage ); + /* | MenuItem 4 level 0 */ // Registry switcher + /* +--*/MenuAction RootActionSwitch = MenuAction( ROOTACTION_SWITCH, &RootActionSwitchCallbacks, &SwitchChannelImage ); + /* | MenuItem 5 level 0 */ // NTP Management: NTP Servers list + /* +--*/MenuAction RootActionNtp = MenuAction( ROOTACTION_NTP_PICK, &RootActionNtpCallbacks, &ChangeNTPServerImage ); + /* | | MenuItem 12 level 1 */ // NTP Servers list actions: Pick server + /* | +--*/MenuGroup NtpMenuGroup = MenuGroup( &NTPServersActionButtons ); + /* | MenuItem 6 level 0 */ // Full filesystem wipe + /* +--*/MenuAction RootActionClearAll = MenuAction( ROOTACTION_CLEAR_ALL, &RootActionClearAllCallbacks, &ClearAllImage ); + /* | MenuItem 7 level 0 */ // TLS wipe + /* +--*/MenuAction RootActionClearTls = MenuAction( ROOTACTION_CLEAR_TLS, &RootActionClearTlsCallbacks, &ClearTLSImage ); + /* | MenuItem 8 level 0 */ // Sleep mode + /* +--*/MenuAction RootActionSleep = MenuAction( ROOTACTION_SLEEP, &RootActionSleepCallbacks, &SleepImage ); + + +}; diff --git a/examples/AppStore/modules/MenuUtils/MenuUtils.cpp b/examples/AppStore/modules/MenuUtils/MenuUtils.cpp new file mode 100644 index 00000000..cd79f4a2 --- /dev/null +++ b/examples/AppStore/modules/MenuUtils/MenuUtils.cpp @@ -0,0 +1,115 @@ +#pragma once + +#include "MenuUtils.hpp" + + +MenuAction::MenuAction( const char* _title, MenuItemCallBacks* _callbacks, const RemoteAsset* _icon ) +{ + //setTitle( _title ); + title = (char*)_title; + icon = _icon; + callbacks = _callbacks; + needs_free = false; +} + + +MenuAction::~MenuAction() +{ + clear(); +} + + +void MenuAction::setTitle( const char* _title ) +{ + if( !_title || _title[0]=='\0' ) return; + size_t title_len = strlen( _title ); + title = (char*)calloc( title_len+1, sizeof(char)); + if( title == NULL ) { + log_e("Failed to alloc %d bytes for string %s", title_len+1, _title ); + return; + } + memcpy( title, _title, title_len ); + log_v("Allocated %d bytes for title %s", title_len+1, _title ); + needs_free = true; +} + +void MenuAction::clear() +{ + if( needs_free ) { + log_v("Clearing %s", title ); + free( title ); + title = nullptr; + } + needs_free = false; +} + + + + +MenuGroup::MenuGroup( MenuActionLabels* actionLabels, const char* _assets_folder ) +{ + ActionLabels = actionLabels; + Title = ActionLabels->title; + assets_folder = _assets_folder; +} + + +MenuGroup::~MenuGroup() +{ + clear(); +} + +void MenuGroup::clear() +{ + if( actions_count == 0 || !Actions || needs_free == false ) { + log_v("Actions for menu %s do not need clearing", Title ); + return; + } + size_t before_free = ESP.getFreeHeap(); + log_v("Clearing actions for menu %s", Title ); + for( int i=0; ititle ); + Actions[i]->clear(); + free( Actions[i] ); + } + if( Actions ) { + free( Actions ); + } + log_v("%d bytes freed", ESP.getFreeHeap() - before_free ); + actions_count = 0; + Actions = nullptr; + needs_free = false; +} + + +void MenuGroup::push( MenuAction *action ) +{ + push( action->title, action->callbacks, action->icon, action->textcolor ); +} + + +void MenuGroup::push( const char* _title, MenuItemCallBacks* _callbacks, const RemoteAsset* _icon, uint32_t _textcolor ) +{ + if( actions_count == 0 || Actions == nullptr ) { + Actions = (MenuAction**)calloc( 1, sizeof( MenuAction* ) ); + } else { + Actions = (MenuAction**)realloc( Actions, (actions_count+1) * (sizeof( MenuAction* )) ); + } + log_v("Pushing action #%d %s", actions_count, _title?_title:"" ); + if( Actions == NULL ) { + log_e("Failed to allocate %d bytes for menu Actions", (actions_count+1) * sizeof( MenuAction* ) ); + clear(); + return; + } + needs_free = true; + Actions[actions_count] = (MenuAction*)calloc( 1, sizeof( MenuAction ) ); + Actions[actions_count]->setTitle( _title ); + Actions[actions_count]->icon = _icon; + Actions[actions_count]->callbacks = _callbacks; + Actions[actions_count]->textcolor = _textcolor; + actions_count++; +} + + + + diff --git a/examples/AppStore/modules/MenuUtils/MenuUtils.hpp b/examples/AppStore/modules/MenuUtils/MenuUtils.hpp new file mode 100644 index 00000000..f4203ebb --- /dev/null +++ b/examples/AppStore/modules/MenuUtils/MenuUtils.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "../misc/core.h" +#include "../misc/config.h" +#include "../Assets/Assets.hpp" + +typedef void(*onselect_cb_t)(void); +typedef void(*onrender_cb_t)(void); +typedef void(*onidle_cb_t)(void); +typedef void(*onmodal_cb_t)(void); + +struct MenuItemCallBacks +{ + onselect_cb_t onSelect; // on BtnA Click + onrender_cb_t onRender; // on menuitem render + onrender_cb_t onIdle; // on idle (animations) + onmodal_cb_t onModal; // on modal window (animations) +}; + + +struct ButtonAction +{ + const char* title; + onselect_cb_t onClick; + RemoteAsset *asset; +}; + + +struct MenuActionLabels +{ + const char *title; + ButtonAction **Buttons; +}; + + +class MenuGroup; + +// UI Menu Item +class MenuAction +{ + public: + MenuAction( const char* _title, MenuItemCallBacks* _callbacks, const RemoteAsset* _icon ); + ~MenuAction(); + void setTitle( const char* _title ); + void clear(); + char* title; + MenuItemCallBacks* callbacks; + const RemoteAsset* icon = nullptr; + uint32_t textcolor = TEXT_COLOR; + private: + bool needs_free = false; +}; + +// UI Menu ItemCollection +class MenuGroup +{ + public: + MenuGroup( MenuActionLabels* actionLabels, const char* _assets_folder = "" ); // empty menu group to be filled with push() + ~MenuGroup(); + void push( const char* _title, MenuItemCallBacks* _callbacks, const RemoteAsset* _icon = nullptr, uint32_t _textcolor = TEXT_COLOR ); + void push( MenuAction *action ); + void setTitle( const char* title ) { Title = title; } + void clear(); + size_t actions_count = 0; + const char* Title = nullptr; + MenuAction** Actions = nullptr; + MenuActionLabels* ActionLabels = nullptr; + const char* assets_folder = ""; + uint16_t selectedindex; + private: + bool needs_free = false; +}; + diff --git a/examples/AppStore/modules/Registry/Registry.cpp b/examples/AppStore/modules/Registry/Registry.cpp new file mode 100644 index 00000000..9e9f0868 --- /dev/null +++ b/examples/AppStore/modules/Registry/Registry.cpp @@ -0,0 +1,239 @@ +#pragma once + +#include "Registry.hpp" +#include "../Downloader/Downloader.hpp" + + +void AppRegistry::init() +{ + masterChannel.init(); + unstableChannel.init(); + if( pref_default_channel == REGISTRY_MASTER ) { + defaultChannel = masterChannel; + } else { + defaultChannel = unstableChannel; + } + print(); +} + + +void AppRegistry::print() +{ + log_i("\nRegistry infos:\n\tname: %s\n\tdescription: %s\n\turl: %s\n\tpref_default_channel: %s", + name.c_str(), + description.c_str(), + url.c_str(), + pref_default_channel.c_str() + ); + masterChannel.print(); + unstableChannel.print(); + defaultChannel.print(); +} + + + +void AppRegistryItem::init() +{ + api_cert_provider_url_http = "http://" + api_host + api_path + api_cert_path; + api_cert_provider_url_https = "https://" + api_host + api_path + api_cert_path; + api_url_https = "https://" + api_host + api_path + updater_path; + api_url_http = "http://" + api_host + api_path + updater_path; +} + +void AppRegistryItem::print() +{ + log_i("\nChannel '%s' infos:\n\tdescription: %s\n\turl: %s\n\tapi_host: %s\n\tapi_path: %s\n\tapi_cert_path: %s\n\tupdater_path: %s\n\tcatalog_endpoint: %s\n\tapi_cert_provider_url_http: %s\n\tapi_url_https: %s\n\tapi_url_http: %s", + name.c_str(), + description.c_str(), + url.c_str(), + api_host.c_str(), + api_path.c_str(), + api_cert_path.c_str(), + updater_path.c_str(), + catalog_endpoint.c_str(), + api_cert_provider_url_https.c_str(), + api_url_https.c_str(), + api_url_http.c_str() + ); +} + + + +namespace RegistryUtils +{ + using namespace Downloader; + + + void setJsonChannelItem( JsonObject json, AppRegistryItem reg ) + { + json["name"] = reg.name; + json["description"] = reg.description; + json["url"] = reg.url; + json["api_host"] = reg.api_host; + json["api_path"] = reg.api_path; + json["cert_path"] = reg.api_cert_path; + json["updater_path"] = reg.updater_path; + json["endpoint"] = reg.catalog_endpoint; + } + + + void registrySave( AppRegistry registry, String appRegistryLocalFile ) + { + URLParts urlParts = parseURL( registry.url ); + if( appRegistryLocalFile == "" ) { + log_d("Will attempt to create/save %s", appRegistryLocalFile.c_str() ); + appRegistryLocalFile = appRegistryFolder + PATH_SEPARATOR + urlParts.host + EXT_json; + } + + DynamicJsonDocument jsonRegistryBuffer(2048); + if( jsonRegistryBuffer.capacity() == 0 ) { + log_e("ArduinoJSON failed to allocate 2kb"); + return; + } + + if( M5_FS.exists( appRegistryLocalFile ) ) { + log_d("Removing %s before writing", appRegistryLocalFile.c_str()); + M5_FS.remove( appRegistryLocalFile ); + } + // Open file for writing + #if defined FS_CAN_CREATE_PATH + File file = M5_FS.open( appRegistryLocalFile, FILE_WRITE, true ); + #else + File file = M5_FS.open( appRegistryLocalFile, FILE_WRITE ); + #endif + if (!file) { + log_e("Failed to create file %s", appRegistryLocalFile.c_str()); + return; + } + + JsonObject channels = jsonRegistryBuffer.createNestedObject("channels"); + JsonObject masterChannelJson = channels.createNestedObject(REGISTRY_MASTER); + JsonObject unstableChannelJson = channels.createNestedObject(REGISTRY_UNSTABLE); + + setJsonChannelItem( masterChannelJson, registry.masterChannel ); + setJsonChannelItem( unstableChannelJson, registry.unstableChannel ); + + jsonRegistryBuffer["name"] = registry.name; + jsonRegistryBuffer["description"] = registry.description; + jsonRegistryBuffer["url"] = registry.url; + jsonRegistryBuffer["pref_default_channel"] = registry.pref_default_channel; + + log_i("Created json:"); + // serializeJsonPretty(jsonRegistryBuffer, Serial); + + if (serializeJson(jsonRegistryBuffer, file) == 0) { + log_e( "Failed to write to file %s", appRegistryLocalFile.c_str() ); + } else { + log_i ("Successfully created %s", appRegistryLocalFile.c_str() ); + } + file.close(); + } + + + bool isValidRegistryChannel( JsonVariant json ) + { + return( json["name"].as() !="" + && json["description"].as() !="" + && json["url"].as().startsWith("http") + && json["api_host"].as() !="" + && json["api_path"].as() !="" + && json["cert_path"].as() !="" + && json["updater_path"].as() !="" + && json["endpoint"].as() !="" + ); + } + + + AppRegistryItem getJsonChannel( JsonVariant jsonChannels, const char* channel ) + { + return { + String(channel), + jsonChannels[channel]["description"].as(), + jsonChannels[channel]["url"].as(), + jsonChannels[channel]["api_host"].as(), + jsonChannels[channel]["api_path"].as(), + jsonChannels[channel]["cert_path"].as(), + jsonChannels[channel]["updater_path"].as(), + jsonChannels[channel]["endpoint"].as() + }; + } + + + AppRegistry init( String appRegistryLocalFile ) + { + if( appRegistryLocalFile == "" ) { + appRegistryLocalFile = appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName; + } + log_i("Opening channel file: %s", appRegistryLocalFile.c_str()); + + if( !M5_FS.exists( appRegistryLocalFile ) ) { + // create file from registry default template, return template + log_i("Registry file %s does not exist, creating from firmware defaults", appRegistryLocalFile.c_str() ); + registrySave( defaultAppRegistry, appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName ); + defaultAppRegistry.init(); + return defaultAppRegistry; + } + + // load registry profiles from file + File file = M5_FS.open( appRegistryLocalFile ); + + DynamicJsonDocument jsonRegistryBuffer(2048); + DeserializationError error = deserializeJson( jsonRegistryBuffer, file ); + file.close(); + if (error) { + log_e("JSON Error while reading registry file %s", appRegistryLocalFile.c_str() ); + defaultAppRegistry.init(); + return defaultAppRegistry; + } + + JsonObject root = jsonRegistryBuffer.as(); + if ( root.isNull() ) { + log_w("Registry file %s has empty JSON", appRegistryLocalFile.c_str() ); + defaultAppRegistry.init(); + return defaultAppRegistry; + } + + if( !isValidRegistryChannel( root["channels"][REGISTRY_MASTER] ) ) { + // bad master item + log_w("%s", "Bad master channel in JSON file"); + defaultAppRegistry.init(); + return defaultAppRegistry; + } + + if( !isValidRegistryChannel( root["channels"][REGISTRY_UNSTABLE] ) ) { + // bad master item + log_w("%s", "Bad unstable channel in JSON file"); + defaultAppRegistry.init(); + return defaultAppRegistry; + } + + if( root["name"].as() == "" + || root["description"].as() == "" + || root["url"].as() == "" ) { + log_w("%s", "Bad channel meta in JSON file"); + defaultAppRegistry.init(); + return defaultAppRegistry; + } + + String SDUpdaterChannelNameStr = ""; + if( !root["pref_default_channel"].isNull() && root["pref_default_channel"].as() != "" ) { + // inherit from json + SDUpdaterChannelNameStr = root["pref_default_channel"].as(); + } else { + // assign default + SDUpdaterChannelNameStr = REGISTRY_MASTER; + } + + AppRegistry appRegistry = { + root["name"].as(), + root["description"].as(), + root["url"].as(), + SDUpdaterChannelNameStr, // default channel + getJsonChannel( root["channels"], REGISTRY_MASTER ), + getJsonChannel( root["channels"], REGISTRY_UNSTABLE ) + }; + appRegistry.init(); + return appRegistry; + } + +}; diff --git a/examples/AppStore/modules/Registry/Registry.hpp b/examples/AppStore/modules/Registry/Registry.hpp new file mode 100644 index 00000000..fa62fe54 --- /dev/null +++ b/examples/AppStore/modules/Registry/Registry.hpp @@ -0,0 +1,115 @@ +/* + * + * M5Stack SD Menu + * Project Page: https://github.com/tobozo/M5Stack-SD-Updater + * + * Copyright 2019 tobozo http://github.com/tobozo + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files ("M5Stack SD Updater"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +// load the registry information this launcher is attached to +#pragma once + +#include +#include "../misc/config.h" + + +// registry +class AppRegistryItem +{ + public: + String name; + String description; + String url; + String api_host; + String api_path; + String api_cert_path; + String updater_path; + String catalog_endpoint; + String api_cert_provider_url_http; + String api_cert_provider_url_https; + String api_url_https; + String api_url_http; + void init() ; + void print(); +}; + +class AppRegistry +{ + public: + String name; + String description; + String url; + String pref_default_channel; // local option for SDUpdater use only + AppRegistryItem masterChannel; + AppRegistryItem unstableChannel; + AppRegistryItem defaultChannel; + void init(); + void print(); +}; + + +namespace RegistryUtils +{ + const String appRegistryFolder = "/.registry"; + const String appRegistryDefaultName = "default.json"; + void setJsonChannelItem( JsonObject json, AppRegistryItem reg ); + void registrySave( AppRegistry registry, String appRegistryLocalFile = "" ); + bool isValidRegistryChannel( JsonVariant json ); + AppRegistryItem getJsonChannel( JsonVariant jsonChannels, const char* channel ); + AppRegistry init( String appRegistryLocalFile = "" ); + + AppRegistryItem defaultMasterChannel = { + REGISTRY_MASTER, // "master" + DEFAULT_MASTER_DESC, + DEFAULT_MASTER_URL, + DEFAULT_MASTER_API_HOST, + DEFAULT_MASTER_API_PATH, + DEFAULT_MASTER_API_CERT_PATH, + DEFAULT_MASTER_UPDATER_PATH, + DEFAULT_MASTER_CATALOG_ENDPOINT + }; + + AppRegistryItem defaultUnstableChannel = { + REGISTRY_UNSTABLE, // "unstable" + DEFAULT_UNSTABLE_DESC, + DEFAULT_UNSTABLE_URL, + DEFAULT_UNSTABLE_API_HOST, + DEFAULT_UNSTABLE_API_PATH, + DEFAULT_UNSTABLE_API_CERT_PATH, + DEFAULT_UNSTABLE_UPDATER_PATH, + DEFAULT_UNSTABLE_CATALOG_ENDPOINT + }; + + AppRegistry defaultAppRegistry = { + DEFAULT_REGISTRY_NAME, + DEFAULT_REGISTRY_DESC, + DEFAULT_REGISTRY_URL, + DEFAULT_REGISTRY_CHANNEL, + defaultMasterChannel, + defaultUnstableChannel + }; + +}; + diff --git a/examples/AppStore/modules/misc/compile_time.h b/examples/AppStore/modules/misc/compile_time.h new file mode 100644 index 00000000..f3f53e5c --- /dev/null +++ b/examples/AppStore/modules/misc/compile_time.h @@ -0,0 +1,79 @@ +/* + * compile_time.h + * + * Created: 30.05.2017 20:57:58 + * Author: Dennis (instructable.com/member/nqtronix) + * + * This code provides the macro __TIME_UNIX__ which returns the current time in UNIX format. It can + * be used to identify a version of code on an embedded device, to initialize its RTC and much more. + * Along that several more constants for seconds, minutes, etc. are provided + * + * The macro is based on __TIME__ and __DATE__, which are assumed to be formatted "HH:MM:SS" and + * "MMM DD YYYY", respectively. The actual value can be calculated by the C compiler at compile time + * as all inputs are literals. MAKE SURE TO ENABLE OPTIMISATION! + */ + +#pragma once + +#include + + +// extracts 1..4 characters from a string and interprets it as a decimal value +#define CONV_STR2DEC_1(str, i) (str[i]>'0'?str[i]-'0':0) +#define CONV_STR2DEC_2(str, i) (CONV_STR2DEC_1(str, i)*10 + str[i+1]-'0') +#define CONV_STR2DEC_3(str, i) (CONV_STR2DEC_2(str, i)*10 + str[i+2]-'0') +#define CONV_STR2DEC_4(str, i) (CONV_STR2DEC_3(str, i)*10 + str[i+3]-'0') + +// Some definitions for calculation +#define SEC_PER_MIN 60UL +#define SEC_PER_HOUR 3600UL +#define SEC_PER_DAY 86400UL +#define SEC_PER_YEAR (SEC_PER_DAY*365) +#define UNIX_START_YEAR 1970UL + +// Custom "glue logic" to convert the month name to a usable number +#define GET_MONTH(str, i) (str[i]=='J' && str[i+1]=='a' && str[i+2]=='n' ? 1 : \ + str[i]=='F' && str[i+1]=='e' && str[i+2]=='b' ? 2 : \ + str[i]=='M' && str[i+1]=='a' && str[i+2]=='r' ? 3 : \ + str[i]=='A' && str[i+1]=='p' && str[i+2]=='r' ? 4 : \ + str[i]=='M' && str[i+1]=='a' && str[i+2]=='y' ? 5 : \ + str[i]=='J' && str[i+1]=='u' && str[i+2]=='n' ? 6 : \ + str[i]=='J' && str[i+1]=='u' && str[i+2]=='l' ? 7 : \ + str[i]=='A' && str[i+1]=='u' && str[i+2]=='g' ? 8 : \ + str[i]=='S' && str[i+1]=='e' && str[i+2]=='p' ? 9 : \ + str[i]=='O' && str[i+1]=='c' && str[i+2]=='t' ? 10 : \ + str[i]=='N' && str[i+1]=='o' && str[i+2]=='v' ? 11 : \ + str[i]=='D' && str[i+1]=='e' && str[i+2]=='c' ? 12 : 0) + +#define GET_MONTH2DAYS(month) ((month == 1 ? 0 : 31 + \ + (month == 2 ? 0 : 28 + \ + (month == 3 ? 0 : 31 + \ + (month == 4 ? 0 : 30 + \ + (month == 5 ? 0 : 31 + \ + (month == 6 ? 0 : 30 + \ + (month == 7 ? 0 : 31 + \ + (month == 8 ? 0 : 31 + \ + (month == 9 ? 0 : 30 + \ + (month == 10 ? 0 : 31 + \ + (month == 11 ? 0 : 30)))))))))))) \ + + +#define GET_LEAP_DAYS ((__TIME_YEARS__-1968)/4 - (__TIME_MONTH__ <=2 ? 1 : 0)) + + + +#define __TIME_SECONDS__ CONV_STR2DEC_2(__TIME__, 6) +#define __TIME_MINUTES__ CONV_STR2DEC_2(__TIME__, 3) +#define __TIME_HOURS__ CONV_STR2DEC_2(__TIME__, 0) +#define __TIME_DAYS__ CONV_STR2DEC_2(__DATE__, 4) +#define __TIME_MONTH__ GET_MONTH(__DATE__, 0) +#define __TIME_YEARS__ CONV_STR2DEC_4(__DATE__, 7) + +#define __TIME_UNIX__ ((__TIME_YEARS__-UNIX_START_YEAR)*SEC_PER_YEAR+ \ + GET_LEAP_DAYS*SEC_PER_DAY+ \ + GET_MONTH2DAYS(__TIME_MONTH__)*SEC_PER_DAY+ \ + __TIME_DAYS__*SEC_PER_DAY-SEC_PER_DAY+ \ + __TIME_HOURS__*SEC_PER_HOUR+ \ + __TIME_MINUTES__*SEC_PER_MIN+ \ + __TIME_SECONDS__) + diff --git a/examples/AppStore/modules/misc/config.h b/examples/AppStore/modules/misc/config.h new file mode 100644 index 00000000..70b98712 --- /dev/null +++ b/examples/AppStore/modules/misc/config.h @@ -0,0 +1,106 @@ +#pragma once + +#include "core.h" +#include +#include + +#define SD_CERT_PATH "/cert" // Filesystem (SD) temporary path where certificates are stored. without trailing slash +#define ROOT_DIR "/" +#define CATALOG_DIR "/catalog" +#define CATALOG_DIR_BKP "/catalog.old" +#define HIDDEN_APPS_FILE "/.hidden-apps.json" + +#define REGISTRY_MASTER "master" +#define REGISTRY_UNSTABLE "unstable" + +#define PATH_SEPARATOR "/" +#define EXT_bin ".bin" +#define EXT_json ".json" +#define EXT_jpg ".jpg" +#define EXT_png ".png" + +#define DIR_jpg "/jpg/" +#define DIR_png "/png/" +#define DIR_json "/json/" + +#define DEFAULT_REGISTRY_NAME "SDUpdater" +#define DEFAULT_REGISTRY_DESC "Tobozo's " PLATFORM_NAME " Application registry @ phpsecu.re" +#define DEFAULT_REGISTRY_URL "https://phpsecu.re/" DEFAULT_REGISTRY_BOARD "/registry/phpsecu.re.json" // should exist as "/.registry/default.json" on SD Card +#define DEFAULT_REGISTRY_CHANNEL "unstable" // "master" or "unstable" + +#define DEFAULT_MASTER_DESC "Master channel at phpsecu.re/" DEFAULT_REGISTRY_BOARD " registry" +#define DEFAULT_MASTER_URL "https://phpsecu.re/" DEFAULT_REGISTRY_BOARD "/sd-updater/" +#define DEFAULT_MASTER_API_HOST "phpsecu.re" +#define DEFAULT_MASTER_API_PATH PATH_SEPARATOR DEFAULT_REGISTRY_BOARD +#define DEFAULT_MASTER_API_CERT_PATH "/cert/" +#define DEFAULT_MASTER_UPDATER_PATH "/sd-updater" +#define DEFAULT_MASTER_CATALOG_ENDPOINT "/catalog.json" + +#define DEFAULT_UNSTABLE_DESC "Unstable channel at phpsecu.re/" DEFAULT_REGISTRY_BOARD " registry" +#define DEFAULT_UNSTABLE_URL "https://phpsecu.re/" DEFAULT_REGISTRY_BOARD "/sd-updater/unstable/" +#define DEFAULT_UNSTABLE_API_HOST "phpsecu.re" +#define DEFAULT_UNSTABLE_API_PATH PATH_SEPARATOR DEFAULT_REGISTRY_BOARD +#define DEFAULT_UNSTABLE_API_CERT_PATH "/cert/" +#define DEFAULT_UNSTABLE_UPDATER_PATH "/sd-updater/unstable" +#define DEFAULT_UNSTABLE_CATALOG_ENDPOINT "/catalog.json" + +#define LIST_MAX_COUNT 96 + +#define MENU_TITLE_MAX_SIZE 24 +#define BTN_TITLE_MAX_SIZE 6 + +#define LIST_MAX_LABEL_SIZE 36 // list labels will be trimmed +#define LINES_PER_PAGE 8 + +#if !defined BUTTON_HEIGHT + #define BUTTON_HEIGHT 28 +#endif + +#if !defined BUTTON_WIDTH + #define BUTTON_HEIGHT 60 +#endif + +#if !defined BUTTON_HWIDTH + #define BUTTON_HWIDTH BUTTON_WIDTH/2 +#endif + +#define TITLEBAR_HEIGHT 32 +#define WINDOW_MARGINX 10 +#define LISTITEM_OFFSETX 15 +#define LISTITEM_OFFSETY 42 +#define LISTITEM_HEIGHT 20 +#define LISTCAPTION_POSY 38 +#define ASSET_POSY 62 + +#define BUTTONS_COUNT 3 + + +#define MAX_BRIGHTNESS 100 + +#define MS_BEFORE_SLEEP 600000 // 600000 = 10mn + +const uint32_t MENU_COLOR = 0x008000U; // must be dark (0x00) on two colors +const uint32_t BG_COLOR = 0x000000U; // default bgcolor when nothing is drawn (e.g. behind the buttons) +const uint32_t TEXT_COLOR = 0xffffffU; // text color, must have high contrast compared to MENU_COLOR +const uint32_t DIMMED_COLOR = 0xaaaaaaU; // text color, must have high contrast compared to MENU_COLOR +const uint32_t LAUNCHER_COLOR = 0xcccc00U; // text color, must have high contrast compared to MENU_COLOR +const uint32_t SHADOW_COLOR = 0x202020U; // shadow color, must be a shaded version of TEXT_COLOR + +const uint32_t GZ_PROGRESS_COLOR = 0x40bb40U; // uint32_t ProgressBarColor1 (gzip) +const uint32_t TAR_PROGRESS_COLOR = 0xbbbb40U; // uint32_t ProgressBarColor2 (tar) +const uint32_t DL_PROGRESS_COLOR = 0xff0000U; // uint32_t ProgressBarColor3 (download/sha_sum) + +uint16_t buttonsXOffset[BUTTONS_COUNT] = +{ + 31, 126, 221 +}; + +#if defined USE_SCREENSHOT + static bool ScreenShotEnable = true; +#else + static bool ScreenShotEnable = false; +#endif + + + + diff --git a/examples/AppStore/modules/misc/controls.h b/examples/AppStore/modules/misc/controls.h new file mode 100644 index 00000000..66e5471d --- /dev/null +++ b/examples/AppStore/modules/misc/controls.h @@ -0,0 +1,98 @@ +#pragma once + +/* + * Mandatory and optional controls for the menu to be usable + */ +enum HIDSignal +{ + HID_INERT = 0, // when nothing happens + HID_UP = 1, // optional + HID_DOWN = 2, + HID_SELECT = 3, + HID_PAGE_DOWN = 4, + HID_PAGE_UP = 5, + HID_SCREENSHOT = 6 +}; + +#define HID_BTN_A HID_SELECT +#define HID_BTN_B HID_PAGE_DOWN +#define HID_BTN_C HID_DOWN + +#define FAST_REPEAT_DELAY 50 // ms, push delay +#define SLOW_REPEAT_DELAY 500 // ms, must be higher than FAST_REPEAT_DELAY and smaller than LONG_DELAY_BEFORE_REPEAT +#define LONG_DELAY_BEFORE_REPEAT 1000 // ms, delay before slow repeat enables +unsigned long fastRepeatDelay = FAST_REPEAT_DELAY; +unsigned long beforeRepeatDelay = LONG_DELAY_BEFORE_REPEAT; + + +#if defined ARDUINO_M5STACK_Core2 + // enable M5Core2's haptic feedback ! + static bool isVibrating = false; + + static void vibrateTask( void * param ) + { + if( !isVibrating ) { + isVibrating = true; + int ms = *((int*)param); // dafuq + M5.Axp.SetLDOEnable( 3,1 ); + delay( ms ); + M5.Axp.SetLDOEnable( 3,0 ); + isVibrating = false; + } + vTaskDelete( NULL ); + } + + static void HIDFeedback( int ms ) + { + // xTaskCreatePinnedToCore( vibrateTask, "vibrateTask", 2048, (void*)&ms, 1, NULL , 1 ); + } + +#else + + static void HIDFeedback( int ms ) { ; } + +#endif + + + +HIDSignal HIDFeedback( HIDSignal signal, int ms = 50 ) +{ + if( signal != HID_INERT ) { + HIDFeedback( ms ); + } + return signal; +} + + +HIDSignal getControls() +{ + // no buttons? no problemo! (c) Arnold S. + if( Serial.available() ) { + char command = Serial.read(); // read one char + Serial.flush(); + switch(command) { + case 'a':Serial.println("Sending HID_DOWN Signal"); return HIDFeedback( HID_DOWN ); + case 'b':Serial.println("Sending HID_UP Signal"); return HIDFeedback( HID_UP ); + case 'c':Serial.println("Sending HID_PAGE_DOWN Signal"); return HIDFeedback( HID_PAGE_DOWN ); + case 'd':Serial.println("Sending HID_PAGE_UP Signal"); return HIDFeedback( HID_PAGE_UP ); + case 'e':Serial.println("Sending HID_SCREENSHOT Signal");return HIDFeedback( HID_SCREENSHOT ); + case 'f':Serial.println("Sending HID_SELECT Signal"); return HIDFeedback( HID_SELECT ); + default: if( command != '\n' ) { Serial.print("Ignoring serial input: ");Serial.println( String(command) ); } + } + } + M5.update(); + + // legacy buttons support + bool a = M5.BtnA.wasPressed() || M5.BtnA.pressedFor( 500 ); + bool b = M5.BtnB.wasPressed() || M5.BtnB.pressedFor( 500 ); + bool c = M5.BtnC.wasPressed() || M5.BtnC.pressedFor( 500 ); + //bool d = ( M5.BtnB.wasPressed() && M5.BtnC.isPressed() ); + //bool e = ( M5.BtnB.isPressed() && M5.BtnC.wasPressed() ); + + //if( d || e ) return HIDFeedback( HID_PAGE_UP ); // multiple push, suggested by https://github.com/mongonta0716 + if( b ) return HIDFeedback( HID_PAGE_DOWN ); + if( c ) return HIDFeedback( HID_DOWN ); + if( a ) return HIDFeedback( HID_SELECT ); + //HIDSignal padValue = HID_INERT; + return HID_INERT; +} diff --git a/examples/AppStore/modules/misc/core.h b/examples/AppStore/modules/misc/core.h new file mode 100644 index 00000000..0e2e010b --- /dev/null +++ b/examples/AppStore/modules/misc/core.h @@ -0,0 +1,55 @@ +#pragma once + +#include // https://github.com/tobozo/ESP32-Chimera-Core + +#define SDU_APP_NAME "M5Stack App Store" // app title for the sd-updater lobby screen +#define SDU_APP_PATH "/AppStore.bin" // app binary file name on the SD Card (also displayed on the sd-updater lobby screen) +#define SDU_APP_AUTHOR "@tobozo" // app binary author name for the sd-updater lobby screen +#include // https://github.com/tobozo/M5Stack-SD-Updater + +#define DEST_FS_USES_SD // -> This instance will be ecompressing to the SDCard +#include // https://github.com/tobozo/ESP32-targz + +#include // use to store some prefs + +//#define USE_SCREENSHOT // keep this commented out unless you really need to take UI screenshots + +// auto-select board +#if defined( ARDUINO_M5STACK_Core2 ) + #pragma message "M5Stack Core2 detected" + #define PLATFORM_NAME "M5Core2" + #define DEFAULT_REGISTRY_BOARD "m5core2" +#elif defined( ARDUINO_M5Stack_Core_ESP32 ) + #pragma message "M5Stack Classic detected" + #define PLATFORM_NAME "M5Stack" + #define DEFAULT_REGISTRY_BOARD "m5stack" +#elif defined( ARDUINO_M5STACK_FIRE ) + #pragma message "M5Stack Fire detected" + #define PLATFORM_NAME "M5Fire" + #define DEFAULT_REGISTRY_BOARD "m5fire" +#elif defined( ARDUINO_ODROID_ESP32 ) + #pragma message "Odroid Go detected" + #define PLATFORM_NAME "Odroid-GO" + #define DEFAULT_REGISTRY_BOARD "odroid" +#else + #pragma message "Generic ESP32 detected" + #define DEFAULT_REGISTRY_BOARD "esp32" + #define PLATFORM_NAME "ESP32" +#endif + +static M5Display &tft( M5.Lcd ); +static fs::SDFS &M5_FS(SD); + +static char formatBuffer[64]; + +static const char *formatBytes(long long bytes, char *str) +{ + const char *sizes[5] = { "B", "KB", "MB", "GB", "TB" }; + int i; + double dblByte = bytes; + for (i = 0; i < 5 && bytes >= 1024; i++, bytes /= 1024) + dblByte = bytes / 1024.0; + sprintf(str, "%.2f", dblByte); + return strcat(strcat(str, " "), sizes[i]); +} + diff --git a/examples/AppStore/modules/misc/i18n.h b/examples/AppStore/modules/misc/i18n.h new file mode 100644 index 00000000..9381b174 --- /dev/null +++ b/examples/AppStore/modules/misc/i18n.h @@ -0,0 +1,44 @@ +#pragma once + +#define LANG_EN 0xff +#define LANG_JP 0x12 +#define LANG_CN 0x07 + +#define I18N_LANG LANG_EN + +#if I18N_LANG == LANG_CN + + #if !defined ARDUINO_M5STACK_Core2 && !defined ARDUINO_M5STACK_FIRE + #error "Chinese font require M5Core2 or M5Fire" + #endif + + #include "lang/i18n.cn.h" + #define ButtonFont &efontCN_10 + #define HeaderFont &efontCN_10 + #define LIFont &efontCN_10 + #define InfoWindowFont &efontCN_10 + +#elif I18N_LANG == LANG_EN + + #include "lang/i18n.en.h" + #define ButtonFont &Font2 + #define HeaderFont &FreeMono9pt7b + #define LIFont &Font2 + #define InfoWindowFont &FreeMono9pt7b + +#elif I18N_LANG == LANG_JP + + #include "lang/i18n.jp.h" + #define ButtonFont &lgfxJapanGothic_12 + #define HeaderFont &lgfxJapanGothic_12 + #define LIFont &lgfxJapanGothic_12 + #define InfoWindowFont &lgfxJapanGothic_12 + +#else + + #error "Unsupported language" + +#endif + + +#define SCROLL_SEPARATOR " *** " diff --git a/examples/AppStore/modules/misc/lang/i18n.cn.h b/examples/AppStore/modules/misc/lang/i18n.cn.h new file mode 100644 index 00000000..c5a3e66b --- /dev/null +++ b/examples/AppStore/modules/misc/lang/i18n.cn.h @@ -0,0 +1,172 @@ +#define WELCOME_MESSAGE "Welcome to the " PLATFORM_NAME " App Store!" +#define INIT_MESSAGE PLATFORM_NAME " App Store initializing..." +#define MENU_SETTINGS "AppStoreUI loaded with %d labels per page, max %d items\n" +#define GOTOSLEEP_MESSAGE "Will go to sleep" + +#define MENU_BTN_INFO "SELECT" +#define MENU_BTN_UPDATE "UPDATE" +#define MENU_BTN_BACK "Back" +#define MENU_BTN_PREV "<" +#define MENU_BTN_NEXT ">" +#define MENU_BTN_YES "YES" +#define MENU_BTN_NO "NO" +#define MENU_BTN_GO "GO" +#define MENU_BTN_CANCEL "Cancel" +#define MENU_BTN_REBOOT "Reboot" +#define MENU_BTN_RESTART "Restart" +#define MENU_BTN_CONTINUE "Continue" +#define MENU_BTN_RETRY "RETRY" + + +#define MENUTITLE_DEFAULT PLATFORM_NAME " App Store" +#define MENUTITLE_MYAPPS "My Applications" +#define MENUTITLE_APPSTORE "Applications Store" +#define MENUTITLE_TOGGLEAPPS "Unhide Apps" +#define MENUTITLE_DOWNLOADER "Apps Downloader" +#define MENUTITLE_MANAGEAPPS "Manage Applications" +#define MENUTITLE_MAINMENU "Main Menu" + +#define MENUACTION_BACKTOAPPS "Back to Browse Apps" +#define MENUACTION_APPINSTALL "Install" +#define MENUACTION_APPUPDATE "Update" +#define MENUACTION_APPVERIFY "Verify" +#define MENUACTION_VIEW "View" +#define MENUACTION_APPHIDE "Hide" +#define MENUACTION_APPDELETE "Delete" +#define MENUACTION_BACK "Back" +#define MENUACTION_UNHIDE "Unhide" +#define MENUACTION_REFRESH "Refresh" + +#define MENUACTION_NTP "NTP Server" +#define MENUACTION_MYAPPS "*My Applications" +#define MENUACTION_APPTOGGLE "Manage Hidden Apps" +#define MENUACTION_DOWNLOADER "*Apps Downloader" +#define MENUACTION_BROWSEAPP "Browse App Store" +#define MENUACTION_DEL_APPS "Manage My Apps" + +#define ROOTACTION_DOWNLOAD "Download Catalog" +#define ROOTACTION_REFRESH "Refresh Catalog" +#define ROOTACTION_GET "Install Applications" + +#define ROOTACTION_SWITCH "Switch Channel" +#define ROOTACTION_NTP_PICK "Change NTP Server" +#define ROOTACTION_CLEAR_ALL "Clear ALL" +#define ROOTACTION_CLEAR_TLS "Clear TLS cache" +#define ROOTACTION_SLEEP "Sleep" + +#define MODAL_DELETEALL_TITLE "DELETE EVERYTHING" +#define MODAL_DELETEALL_MSG "CAUTION! This will remove apps, assets, registries\nand databases, even those\noutside the scope of this\nApplication Manager!" + +#define MODAL_DOWNLOADFAIL_TITLE "HTTP FAILED" +#define MODAL_DOWNLOADFAIL_MSG "An HTTP error occured\nwhile downloading\nthe registry\narchive.\nTry again?" + +#define MODAL_WIFI_NOCONN_MSG "No connection" + +#define DOWNLOADER_MODAL_CHANGE "CHANGE" + +#define MODAL_CANCELED_TITLE "OPERATION CANCELED" +#define MODAL_SUCCESS_TITLE "OPERATION SUCCESSFUL" + + +#define CHANNEL_TOOL "CHANNEL TOOL" +#define CHANNEL_TOOL_TEXT "Do you want to change or\nupdate your SD Card\nchannel?" + +#define CHANNEL_CHOOSER "CHANNEL CHOOSER" +#define CHANNEL_CHOOSER_PROMPT "Change channel ?" +#define CHANNEL_CHOOSER_TEXT "You are about to change\nyour SD Card channel.\n\n Are you sure ?" + +#define CHANNEL_DOWNLOADER "CHANNEL DOWNLOADER" +#define CHANNEL_DOWNLOADER_PROMPT "Download channel ?" +#define CHANNEL_DOWNLOADER_TEXT "You are about to overwrite\nyour SD Card channel.\r\n Are you sure ?" + +#define DOWNLOADER_MODAL_NAME "Update binaries ?" +#define DOWNLOADER_MODAL_TITLE "This action will:" +#define DOWNLOADER_MODAL_ENDED "Synchronization complete" +#define DOWNLOADER_MODAL_TITLE_ERRORS_OCCURED "Some errors occured. " + +#define OVERALL_PROGRESS_TITLE "Overall progress" +#define TAR_PROGRESS_TITLE "Downloading Registry" +#define NOT_IN_REGISTRY "NOT IN REGISTRY" + +#define WGET_SKIPPING " [Checksum OK]" +#define WGET_UPDATING " [Outdated]" +#define WGET_CREATING " [New file]" +#define SYNC_FINISHED "Synch finished" +#define CLEANDIR_REMOVED "Removed %s\n" +#define DOWNLOAD_FAIL " [DOWNLOAD FAIL]" +#define SHASHUM_FAIL " [SHASUM FAIL]" +#define UPDATE_SUCCESS "UPDATE SUCCESS" + +#define WIFI_MSG_WAITING "Enabling WiFi..." +#define WIFI_MSG_CONNECTING "Connecting WiFi.." +#define WIFI_TITLE_TIMEOUT "WiFi Timeout" +#define WIFI_MSG_TIMEOUT "Timed out, will try again" +#define WIFI_TITLE_CONNECTED "WiFi OK" +#define WIFI_MSG_CONNECTED "Connected to wifi :-)" + +#define NTP_TITLE_SETUP "NTP Setup" +#define NTP_MSG_SETUP "Contacting NTP Server" +#define NTP_TITLE_FAIL "NTP Down" +#define NTP_MSG_FAIL "Can't enable NTP" + + +#define DL_FSCLEANUP_TITLE "Cleanup" +#define DL_FSCLEANUP_MSG "Removing previous\nbackup" +#define DL_FSBACKUP_TITLE "Backup" +#define DL_FSBACKUP_MSG "Backing up registry" +#define DL_FSRESTORE_MSG "Restoring backup" + +#define DL_TLSFETCH_TITLE "TLS" +#define DL_TLSFETCH_MSG "Fetching TLS Cert" + +#define DL_TLSFAIL_TITLE "TLS Error" +#define DL_TLSFAIL_MSG "Could not init TLS" + +#define DL_HTTPINIT_TITLE "HTTP Init" +#define DL_HTTPINIT_MSG "Contacting catalog\nendpoint" + +#define DL_HTTPFAIL_TITLE "HTTP Error" +#define DL_FSFAIL_TITLE "Filesystem Error" +#define DL_SHAFAIL_TITLE "SHA256 Error" + +#define DL_AWAITING_TITLE "Awaiting response" + +#define DL_SUCCESS_TITLE "Success" +#define DL_SUCCESS_MSG "Registry fetched!" +#define DL_FAIL_MSG "Registry could not\nbe reached." + + +#define WGET_MSG_FAIL "Please check the remote server" +#define FS_MSG_FAIL "Please check the filesystem" + +#define MODAL_TLSCERT_INSTALLFAILED_MSG "Certificate fetching\nOK but TLS Install\nfailed" +#define MODAL_TLSCERT_FETCHINGFAILED_MSG "Unable to wget()\ncertificate" +#define NEW_TLS_CERTIFICATE_TITLE "New TLS certificate installed" +#define NEW_TLS_CERTIFICATE_TEXT "A new certificate\nwas fetched and\ninstalled successfully." +#define MODAL_RESTART_REQUIRED "Restarting is\noptional.\n\nReboot anyway?" +#define MODAL_SAME_PLAYER_SHOOT_AGAIN "Please try again.\n\nReboot now?" +#define MODAL_REGISTRY_UPDATED "New Registry file\nhas been updated" +#define MODAL_REGISTRY_DAMAGED "New Registry file\nmay be damaged" +#define MODAL_REBOOT_REGISTRY_UPDATED "Please reboot and\nchoose a channel.\n\nReboot now?" + + +#define DEBUG_DIROPEN_FAILED "Failed to open directory" +#define DEBUG_EMPTY_FS "Empty SD Card, falling back to root menu" +#define DEBUG_NOTADIR "Not a directory" +#define DEBUG_DIRLABEL " DIR : " +#define DEBUG_IGNORED " IGNORED FILE: " +#define DEBUG_CLEANED " CLEANED FILE: " +#define DEBUG_ABORTLISTING " ***Max files reached for Menu, please adjust LIST_MAX_COUNT for more (maximum is 255, sorry :-)" +#define DEBUG_FILELABEL " FILE: " + +#define DEBUG_FILECOPY "Starting File Copy for " +#define DEBUG_FILECOPY_DONE "Transfer finished" +#define DEBUG_WILL_RESTART "Binary removed from SPIFFS, will now restart" +#define DEBUG_NOTHING_TODO "No binary to transfer" +#define DEBUG_KEYPAD_NOTFOUND "Keypad not installed" +#define DEBUG_KEYPAD_FOUND "Keypad detected!" +#define DEBUG_JOYPAD_NOTFOUND "No Joypad detected, disabling" +#define DEBUG_JOYPAD_FOUND "Joypad detected!" + +#define DEBUG_TIMESTAMP_GUESS "%s has %s time set (%04d-%02d-%02d %02d:%02d:%02d), will use %s date to set the clock" + diff --git a/examples/AppStore/modules/misc/lang/i18n.en.h b/examples/AppStore/modules/misc/lang/i18n.en.h new file mode 100644 index 00000000..279ffa1c --- /dev/null +++ b/examples/AppStore/modules/misc/lang/i18n.en.h @@ -0,0 +1,172 @@ +#define WELCOME_MESSAGE "Welcome to the " PLATFORM_NAME " App Store!" +#define INIT_MESSAGE PLATFORM_NAME " App Store initializing..." +#define MENU_SETTINGS "AppStoreUI loaded with %d labels per page, max %d items\n" +#define GOTOSLEEP_MESSAGE "Will go to sleep" + +#define MENU_BTN_INFO "SELECT" +#define MENU_BTN_UPDATE "UPDATE" +#define MENU_BTN_BACK "Back" +#define MENU_BTN_PREV "<" +#define MENU_BTN_NEXT ">" +#define MENU_BTN_YES "YES" +#define MENU_BTN_NO "NO" +#define MENU_BTN_GO "GO" +#define MENU_BTN_CANCEL "Cancel" +#define MENU_BTN_REBOOT "Reboot" +#define MENU_BTN_RESTART "Restart" +#define MENU_BTN_CONTINUE "Continue" +#define MENU_BTN_RETRY "RETRY" + + +#define MENUTITLE_DEFAULT PLATFORM_NAME " App Store" +#define MENUTITLE_MYAPPS "My Applications" +#define MENUTITLE_APPSTORE "Applications Store" +#define MENUTITLE_TOGGLEAPPS "Unhide Apps" +#define MENUTITLE_DOWNLOADER "Apps Downloader" +#define MENUTITLE_MANAGEAPPS "Manage Applications" +#define MENUTITLE_MAINMENU "Main Menu" + +#define MENUACTION_BACKTOAPPS "Back to Browse Apps" +#define MENUACTION_APPINSTALL "Install" +#define MENUACTION_APPUPDATE "Update" +#define MENUACTION_APPVERIFY "Verify" +#define MENUACTION_VIEW "View" +#define MENUACTION_APPHIDE "Hide" +#define MENUACTION_APPDELETE "Delete" +#define MENUACTION_BACK "Back" +#define MENUACTION_UNHIDE "Unhide" +#define MENUACTION_REFRESH "Refresh" + +#define MENUACTION_NTP "NTP Server" +#define MENUACTION_MYAPPS "*My Applications" +#define MENUACTION_APPTOGGLE "Manage Hidden Apps" +#define MENUACTION_DOWNLOADER "*Apps Downloader" +#define MENUACTION_BROWSEAPP "Browse App Store" +#define MENUACTION_DEL_APPS "Manage My Apps" + +#define ROOTACTION_DOWNLOAD "Download Catalog" +#define ROOTACTION_REFRESH "Refresh Catalog" +#define ROOTACTION_GET "Install Applications" + +#define ROOTACTION_SWITCH "Switch Channel" +#define ROOTACTION_NTP_PICK "Change NTP Server" +#define ROOTACTION_CLEAR_ALL "Clear ALL" +#define ROOTACTION_CLEAR_TLS "Clear TLS cache" +#define ROOTACTION_SLEEP "Sleep" + +#define MODAL_DELETEALL_TITLE "DELETE EVERYTHING" +#define MODAL_DELETEALL_MSG "CAUTION! This will remove apps, assets, registries\nand databases, even those\noutside the scope of this\nApplication Manager!" + +#define MODAL_DOWNLOADFAIL_TITLE "HTTP FAILED" +#define MODAL_DOWNLOADFAIL_MSG "An HTTP error occured\nwhile downloading\nthe registry\narchive.\nTry again?" + +#define MODAL_WIFI_NOCONN_MSG "No connection" + +#define DOWNLOADER_MODAL_CHANGE "CHANGE" + +#define MODAL_CANCELED_TITLE "OPERATION CANCELED" +#define MODAL_SUCCESS_TITLE "OPERATION SUCCESSFUL" + + +#define CHANNEL_TOOL "CHANNEL TOOL" +#define CHANNEL_TOOL_TEXT "Do you want to change or\nupdate your SD Card channel?" + +#define CHANNEL_CHOOSER "CHANNEL CHOOSER" +#define CHANNEL_CHOOSER_PROMPT "Change channel ?" +#define CHANNEL_CHOOSER_TEXT "You are about to change\nyour SD Card channel.\n\n Are you sure ?" + +#define CHANNEL_DOWNLOADER "CHANNEL DOWNLOADER" +#define CHANNEL_DOWNLOADER_PROMPT "Download channel ?" +#define CHANNEL_DOWNLOADER_TEXT "You are about to overwrite\nyour SD Card channel.\r\n Are you sure ?" + +#define DOWNLOADER_MODAL_NAME "Update binaries ?" +#define DOWNLOADER_MODAL_TITLE "This action will:" +#define DOWNLOADER_MODAL_ENDED "Synchronization complete" +#define DOWNLOADER_MODAL_TITLE_ERRORS_OCCURED "Some errors occured. " + +#define OVERALL_PROGRESS_TITLE "Overall progress" +#define TAR_PROGRESS_TITLE "Downloading Registry" +#define NOT_IN_REGISTRY "NOT IN REGISTRY" + +#define WGET_SKIPPING " [Checksum OK]" +#define WGET_UPDATING " [Outdated]" +#define WGET_CREATING " [New file]" +#define SYNC_FINISHED "Synch finished" +#define CLEANDIR_REMOVED "Removed %s\n" +#define DOWNLOAD_FAIL " [DOWNLOAD FAIL]" +#define SHASHUM_FAIL " [SHASUM FAIL]" +#define UPDATE_SUCCESS "UPDATE SUCCESS" + +#define WIFI_MSG_WAITING "Enabling WiFi..." +#define WIFI_MSG_CONNECTING "Connecting WiFi.." +#define WIFI_TITLE_TIMEOUT "WiFi Timeout" +#define WIFI_MSG_TIMEOUT "Timed out, will try again" +#define WIFI_TITLE_CONNECTED "WiFi OK" +#define WIFI_MSG_CONNECTED "Connected to wifi :-)" + +#define NTP_TITLE_SETUP "NTP Setup" +#define NTP_MSG_SETUP "Contacting NTP Server" +#define NTP_TITLE_FAIL "NTP Down" +#define NTP_MSG_FAIL "Can't enable NTP" + + +#define DL_FSCLEANUP_TITLE "Cleanup" +#define DL_FSCLEANUP_MSG "Removing previous\nbackup" +#define DL_FSBACKUP_TITLE "Backup" +#define DL_FSBACKUP_MSG "Backing up registry" +#define DL_FSRESTORE_MSG "Restoring backup" + +#define DL_TLSFETCH_TITLE "TLS" +#define DL_TLSFETCH_MSG "Fetching TLS Cert" + +#define DL_TLSFAIL_TITLE "TLS Error" +#define DL_TLSFAIL_MSG "Could not init TLS" + +#define DL_HTTPINIT_TITLE "HTTP Init" +#define DL_HTTPINIT_MSG "Contacting catalog\nendpoint" + +#define DL_HTTPFAIL_TITLE "HTTP Error" +#define DL_FSFAIL_TITLE "Filesystem Error" +#define DL_SHAFAIL_TITLE "SHA256 Error" + +#define DL_AWAITING_TITLE "Awaiting response" + +#define DL_SUCCESS_TITLE "Success" +#define DL_SUCCESS_MSG "Registry fetched!" +#define DL_FAIL_MSG "Registry could not\nbe reached." + + +#define WGET_MSG_FAIL "Please check the remote server" +#define FS_MSG_FAIL "Please check the filesystem" + +#define MODAL_TLSCERT_INSTALLFAILED_MSG "Certificate fetching\nOK but TLS Install\nfailed" +#define MODAL_TLSCERT_FETCHINGFAILED_MSG "Unable to wget()\ncertificate" +#define NEW_TLS_CERTIFICATE_TITLE "New TLS certificate installed" +#define NEW_TLS_CERTIFICATE_TEXT "A new certificate\nwas fetched and\ninstalled successfully." +#define MODAL_RESTART_REQUIRED "Restarting is\noptional.\n\nReboot anyway?" +#define MODAL_SAME_PLAYER_SHOOT_AGAIN "Please try again.\n\nReboot now?" +#define MODAL_REGISTRY_UPDATED "New Registry file\nhas been updated" +#define MODAL_REGISTRY_DAMAGED "New Registry file\nmay be damaged" +#define MODAL_REBOOT_REGISTRY_UPDATED "Please reboot and\nchoose a channel.\n\nReboot now?" + + +#define DEBUG_DIROPEN_FAILED "Failed to open directory" +#define DEBUG_EMPTY_FS "Empty SD Card, falling back to root menu" +#define DEBUG_NOTADIR "Not a directory" +#define DEBUG_DIRLABEL " DIR : " +#define DEBUG_IGNORED " IGNORED FILE: " +#define DEBUG_CLEANED " CLEANED FILE: " +#define DEBUG_ABORTLISTING " ***Max files reached for Menu, please adjust LIST_MAX_COUNT for more (maximum is 255, sorry :-)" +#define DEBUG_FILELABEL " FILE: " + +#define DEBUG_FILECOPY "Starting File Copy for " +#define DEBUG_FILECOPY_DONE "Transfer finished" +#define DEBUG_WILL_RESTART "Binary removed from SPIFFS, will now restart" +#define DEBUG_NOTHING_TODO "No binary to transfer" +#define DEBUG_KEYPAD_NOTFOUND "Keypad not installed" +#define DEBUG_KEYPAD_FOUND "Keypad detected!" +#define DEBUG_JOYPAD_NOTFOUND "No Joypad detected, disabling" +#define DEBUG_JOYPAD_FOUND "Joypad detected!" + +#define DEBUG_TIMESTAMP_GUESS "%s has %s time set (%04d-%02d-%02d %02d:%02d:%02d), will use %s date to set the clock" + diff --git a/examples/AppStore/modules/misc/lang/i18n.jp.h b/examples/AppStore/modules/misc/lang/i18n.jp.h new file mode 100644 index 00000000..ca4979f2 --- /dev/null +++ b/examples/AppStore/modules/misc/lang/i18n.jp.h @@ -0,0 +1,172 @@ +#define WELCOME_MESSAGE "Welcome to the " PLATFORM_NAME " App Store!" +#define INIT_MESSAGE PLATFORM_NAME " App Store initializing..." +#define MENU_SETTINGS "AppStoreUI loaded with %d labels per page, max %d items\n" +#define GOTOSLEEP_MESSAGE "Will go to sleep" + +#define MENU_BTN_INFO "撰ぶ" // "SELECT" +#define MENU_BTN_UPDATE "UPDATE" +#define MENU_BTN_BACK "Back" +#define MENU_BTN_PREV "<" +#define MENU_BTN_NEXT ">" +#define MENU_BTN_YES "YES" +#define MENU_BTN_NO "NO" +#define MENU_BTN_GO "GO" +#define MENU_BTN_CANCEL "Cancel" +#define MENU_BTN_REBOOT "Reboot" +#define MENU_BTN_RESTART "Restart" +#define MENU_BTN_CONTINUE "Continue" +#define MENU_BTN_RETRY "RETRY" + + +#define MENUTITLE_DEFAULT PLATFORM_NAME " アプリストア" // " App Store" +#define MENUTITLE_MYAPPS "My Applications" +#define MENUTITLE_APPSTORE "Applications Store" +#define MENUTITLE_TOGGLEAPPS "Unhide Apps" +#define MENUTITLE_DOWNLOADER "Apps Downloader" +#define MENUTITLE_MANAGEAPPS "アプリケーションの管理" // "Manage Applications" +#define MENUTITLE_MAINMENU "Main Menu" + +#define MENUACTION_BACKTOAPPS "Back to Browse Apps" +#define MENUACTION_APPINSTALL "Install" +#define MENUACTION_APPUPDATE "Update" +#define MENUACTION_APPVERIFY "Verify" +#define MENUACTION_VIEW "View" +#define MENUACTION_APPHIDE "Hide" +#define MENUACTION_APPDELETE "Delete" +#define MENUACTION_BACK "Back" +#define MENUACTION_UNHIDE "Unhide" +#define MENUACTION_REFRESH "Refresh" + +#define MENUACTION_NTP "NTP Server" +#define MENUACTION_MYAPPS "*My Applications" +#define MENUACTION_APPTOGGLE "Manage Hidden Apps" +#define MENUACTION_DOWNLOADER "*Apps Downloader" +#define MENUACTION_BROWSEAPP "Browse App Store" +#define MENUACTION_DEL_APPS "Manage My Apps" + +#define ROOTACTION_DOWNLOAD "Download Catalog" +#define ROOTACTION_REFRESH "カタログを更新" // "Refresh Catalog" +#define ROOTACTION_GET "Install Applications" + +#define ROOTACTION_SWITCH "チャンネルの切り替え" // "Switch Channel" +#define ROOTACTION_NTP_PICK "NTPサーバーを変更する" // "Change NTP Server" +#define ROOTACTION_CLEAR_ALL "すべてクリア" // "Clear ALL" +#define ROOTACTION_CLEAR_TLS "TLSキャッシュをクリアする" // "Clear TLS cache" +#define ROOTACTION_SLEEP "寝る" // "Sleep" + +#define MODAL_DELETEALL_TITLE "DELETE EVERYTHING" +#define MODAL_DELETEALL_MSG "CAUTION! This will remove apps, assets, registries\nand databases, even those\noutside the scope of this\nApplication Manager!" + +#define MODAL_DOWNLOADFAIL_TITLE "HTTP FAILED" +#define MODAL_DOWNLOADFAIL_MSG "An HTTP error occured\nwhile downloading\nthe registry\narchive.\nTry again?" + +#define MODAL_WIFI_NOCONN_MSG "No connection" + +#define DOWNLOADER_MODAL_CHANGE "CHANGE" + +#define MODAL_CANCELED_TITLE "OPERATION CANCELED" +#define MODAL_SUCCESS_TITLE "OPERATION SUCCESSFUL" + + +#define CHANNEL_TOOL "CHANNEL TOOL" +#define CHANNEL_TOOL_TEXT "Do you want to change or\nupdate your SD Card\nchannel?" + +#define CHANNEL_CHOOSER "CHANNEL CHOOSER" +#define CHANNEL_CHOOSER_PROMPT "Change channel ?" +#define CHANNEL_CHOOSER_TEXT "You are about to change\nyour SD Card channel.\n\n Are you sure ?" + +#define CHANNEL_DOWNLOADER "CHANNEL DOWNLOADER" +#define CHANNEL_DOWNLOADER_PROMPT "Download channel ?" +#define CHANNEL_DOWNLOADER_TEXT "You are about to overwrite\nyour SD Card channel.\n\n Are you sure ?" + +#define DOWNLOADER_MODAL_NAME "Update binaries ?" +#define DOWNLOADER_MODAL_TITLE "This action will:" +#define DOWNLOADER_MODAL_ENDED "Synchronization complete" +#define DOWNLOADER_MODAL_TITLE_ERRORS_OCCURED "Some errors occured. " + +#define OVERALL_PROGRESS_TITLE "Overall progress" +#define TAR_PROGRESS_TITLE "Downloading Registry" +#define NOT_IN_REGISTRY "NOT IN REGISTRY" + +#define WGET_SKIPPING " [Checksum OK]" +#define WGET_UPDATING " [Outdated]" +#define WGET_CREATING " [New file]" +#define SYNC_FINISHED "Synch finished" +#define CLEANDIR_REMOVED "Removed %s\n" +#define DOWNLOAD_FAIL " [DOWNLOAD FAIL]" +#define SHASHUM_FAIL " [SHASUM FAIL]" +#define UPDATE_SUCCESS "UPDATE SUCCESS" + +#define WIFI_MSG_WAITING "Enabling WiFi..." +#define WIFI_MSG_CONNECTING "Connecting WiFi.." +#define WIFI_TITLE_TIMEOUT "WiFi Timeout" +#define WIFI_MSG_TIMEOUT "Timed out, will try again" +#define WIFI_TITLE_CONNECTED "WiFi OK" +#define WIFI_MSG_CONNECTED "Connected to wifi :-)" + +#define NTP_TITLE_SETUP "NTP Setup" +#define NTP_MSG_SETUP "Contacting NTP Server" +#define NTP_TITLE_FAIL "NTP Down" +#define NTP_MSG_FAIL "Can't enable NTP" + + +#define DL_FSCLEANUP_TITLE "Cleanup" +#define DL_FSCLEANUP_MSG "Removing previous\nbackup" +#define DL_FSBACKUP_TITLE "Backup" +#define DL_FSBACKUP_MSG "Backing up registry" +#define DL_FSRESTORE_MSG "Restoring backup" + +#define DL_TLSFETCH_TITLE "TLS" +#define DL_TLSFETCH_MSG "Fetching TLS Cert" + +#define DL_TLSFAIL_TITLE "TLS Error" +#define DL_TLSFAIL_MSG "Could not init TLS" + +#define DL_HTTPINIT_TITLE "HTTP Init" +#define DL_HTTPINIT_MSG "Contacting catalog\nendpoint" + +#define DL_HTTPFAIL_TITLE "HTTP Error" +#define DL_FSFAIL_TITLE "Filesystem Error" +#define DL_SHAFAIL_TITLE "SHA256 Error" + +#define DL_AWAITING_TITLE "Awaiting response" + +#define DL_SUCCESS_TITLE "Success" +#define DL_SUCCESS_MSG "Registry fetched!" +#define DL_FAIL_MSG "Registry could not\nbe reached." + + +#define WGET_MSG_FAIL "Please check the remote server" +#define FS_MSG_FAIL "Please check the filesystem" + +#define MODAL_TLSCERT_INSTALLFAILED_MSG "Certificate fetching\nOK but TLS Install\nfailed" +#define MODAL_TLSCERT_FETCHINGFAILED_MSG "Unable to wget()\ncertificate" +#define NEW_TLS_CERTIFICATE_TITLE "New TLS certificate installed" +#define NEW_TLS_CERTIFICATE_TEXT "A new certificate\nwas fetched and\ninstalled successfully." +#define MODAL_RESTART_REQUIRED "Restarting is\noptional.\n\nReboot anyway?" +#define MODAL_SAME_PLAYER_SHOOT_AGAIN "Please try again.\n\nReboot now?" +#define MODAL_REGISTRY_UPDATED "New Registry file\nhas been updated" +#define MODAL_REGISTRY_DAMAGED "New Registry file\nmay be damaged" +#define MODAL_REBOOT_REGISTRY_UPDATED "Please reboot and\nchoose a channel.\n\nReboot now?" + + +#define DEBUG_DIROPEN_FAILED "Failed to open directory" +#define DEBUG_EMPTY_FS "Empty SD Card, falling back to root menu" +#define DEBUG_NOTADIR "Not a directory" +#define DEBUG_DIRLABEL " DIR : " +#define DEBUG_IGNORED " IGNORED FILE: " +#define DEBUG_CLEANED " CLEANED FILE: " +#define DEBUG_ABORTLISTING " ***Max files reached for Menu, please adjust LIST_MAX_COUNT for more (maximum is 255, sorry :-)" +#define DEBUG_FILELABEL " FILE: " + +#define DEBUG_FILECOPY "Starting File Copy for " +#define DEBUG_FILECOPY_DONE "Transfer finished" +#define DEBUG_WILL_RESTART "Binary removed from SPIFFS, will now restart" +#define DEBUG_NOTHING_TODO "No binary to transfer" +#define DEBUG_KEYPAD_NOTFOUND "Keypad not installed" +#define DEBUG_KEYPAD_FOUND "Keypad detected!" +#define DEBUG_JOYPAD_NOTFOUND "No Joypad detected, disabling" +#define DEBUG_JOYPAD_FOUND "Joypad detected!" + +#define DEBUG_TIMESTAMP_GUESS "%s has %s time set (%04d-%02d-%02d %02d:%02d:%02d), will use %s date to set the clock" + diff --git a/examples/AppStore/platformio.ini b/examples/AppStore/platformio.ini new file mode 100644 index 00000000..cb977e3e --- /dev/null +++ b/examples/AppStore/platformio.ini @@ -0,0 +1,67 @@ +[platformio] +src_dir = main +;default_envs = m5stack-fire +;default_envs = m5stack-core-esp32 +default_envs = m5stack-core-esp32 +;default_envs = odroid_esp32 + +[env] +;platform = espressif32@3.3.2 +;platform = espressif32 +;platform = https://github.com/platformio/platform-espressif32.git +platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream +;platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.0 +;platform = espressif32@3.4.0 +platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.1 +framework = arduino +upload_speed = 921600 +monitor_speed = 115200 +build_flags = + -DCORE_DEBUG_LEVEL=4 +lib_extra_dirs = ../../../M5Stack-SD-Updater +lib_deps = + FS + SPI + Wire + LovyanGFX + ESP32-Chimera-Core + ESP32-targz +; M5Stack-SD-Updater + WiFi + HTTPClient + WiFiClientSecure + Preferences + Update + bblanchon/ArduinoJson + + + +[env:m5stack-fire] +board = m5stack-fire +board_build.partitions = default_16MB.csv +lib_deps = + ${env.lib_deps} + fastled/FastLED@3.4.0 + +[env:m5stack-core-esp32] +board = m5stack-core-esp32 +build_flags = + -DCORE_DEBUG_LEVEL=0 +;debug_build_flags = -Os +;board_build.partitions = min_spiffs.csv +;board_build.partitions = default.csv +lib_deps = + ${env.lib_deps} + +[env:m5stack-core2] +board = m5stack-core2 +board_build.partitions = default_16MB.csv +lib_deps = + ${env.lib_deps} + +[env:odroid_esp32] +board = odroid_esp32 +board_build.partitions = min_spiffs.csv +lib_deps = + ${env.lib_deps} + diff --git a/examples/M5Stack-SD-Menu/SAM.h b/examples/M5Stack-SD-Menu/SAM.h index 127d67a7..099bf9cf 100644 --- a/examples/M5Stack-SD-Menu/SAM.h +++ b/examples/M5Stack-SD-Menu/SAM.h @@ -401,7 +401,7 @@ void M5SAM::keyboardIRQ(){ #ifdef ARDUINO_ODROID_ESP32 - #define BUTTON_WIDTH 60 + //#define BUTTON_WIDTH 60 #define BUTTON_HWIDTH BUTTON_WIDTH/2 // 30 #define BUTTON_HEIGHT 28 uint16_t buttonsXOffset[4] = { @@ -423,7 +423,7 @@ void M5SAM::keyboardIRQ(){ #else - #define BUTTON_WIDTH 60 + //#define BUTTON_WIDTH 60 #define BUTTON_HWIDTH BUTTON_WIDTH/2 // 30 #define BUTTON_HEIGHT 28 uint16_t buttonsXOffset[3] = diff --git a/examples/M5Stack-SD-Menu/fsformat.h b/examples/M5Stack-SD-Menu/fsformat.h index de3872cc..a541bdd5 100644 --- a/examples/M5Stack-SD-Menu/fsformat.h +++ b/examples/M5Stack-SD-Menu/fsformat.h @@ -170,30 +170,56 @@ struct FileInfo }; -void getMeta( fs::FS &fs, String metaFileName, JSONMeta &jsonMeta ) +void getMeta( FileInfo *fileInfo ) { - File file = fs.open( metaFileName ); - StaticJsonDocument<512> jsonMetaBuffer; - DeserializationError error = deserializeJson( jsonMetaBuffer, file ); - if (error) return; - JsonObject root = jsonMetaBuffer.as(); + + fs::File file = M5_FS.open( fileInfo->metaName ); + if( !file ) { + log_e("Can't open %s", fileInfo->metaName ); + return; + } + log_d("Fetching meta for %s (%d bytes)", fileInfo->metaName.c_str(), file.size() ); + DynamicJsonDocument root(2048); + if( root.capacity() == 0 ) { + log_e("ArduinoJSON failed to allocate 2kb"); + return; + } + + DeserializationError error = deserializeJson( root, file ); + + if (error) { + log_e("JSON ERROR #%d : %s", error, error.c_str() ); + file.close(); + return; + } + // serializeJsonPretty(root, Serial); if ( !root.isNull() ) { - jsonMeta.width = root["width"]; - jsonMeta.height = root["height"]; - jsonMeta.authorName = root["authorName"].as(); - jsonMeta.projectURL = root["projectURL"].as(); - jsonMeta.credits = root["credits"].as(); + JsonObject meta; + if( !root["json_meta"].isNull() ) { + meta = root["json_meta"].as(); // new format + } else { + meta = root.as(); + } + fileInfo->jsonMeta.width = meta["width"].as(); + fileInfo->jsonMeta.height = meta["height"].as(); + fileInfo->jsonMeta.authorName = meta["authorName"].as(); + fileInfo->jsonMeta.projectURL = meta["projectURL"].as(); + fileInfo->jsonMeta.credits = meta["credits"].as(); + log_d("Fetched values: w=%d, h=%d", fileInfo->jsonMeta.width, fileInfo->jsonMeta.height ); + } else { + log_e("Unparsable JSON"); } + file.close(); } -void getFileInfo( FileInfo &fileInfo, fs::FS &fs, File &file, const char* binext=".bin" ) +void getFileInfo( FileInfo &fileInfo, File *file, const char* binext=".bin" ) { String BINEXT = binext; BINEXT.toUpperCase(); - String fileName = sdUpdater->fs_file_path( &file ); //.name(); - uint32_t fileSize = file.size(); - time_t lastWrite = file.getLastWrite(); + String fileName = sdUpdater->fs_file_path( file ); //.name(); + uint32_t fileSize = file->size(); + time_t lastWrite = file->getLastWrite(); struct tm * tmstruct = localtime(&lastWrite); char fileDate[64] = "1980-01-01 00:07:20"; sprintf(fileDate, "%04d-%02d-%02d %02d:%02d:%02d",(tmstruct->tm_year)+1900,( tmstruct->tm_mon)+1, tmstruct->tm_mday,tmstruct->tm_hour , tmstruct->tm_min, tmstruct->tm_sec); @@ -215,12 +241,12 @@ void getFileInfo( FileInfo &fileInfo, fs::FS &fs, File &file, const char* binext String currentDataFolder = appDataFolder + fileName; currentDataFolder.replace( binext, "" ); currentDataFolder.replace( BINEXT, "" ); - if( fs.exists( currentDataFolder.c_str() ) ) { + if( M5_FS.exists( currentDataFolder.c_str() ) ) { fileInfo.hasData = true; // TODO: actually use this feature } if( fileInfo.hasMeta() == true ) { - getMeta( fs, fileInfo.metaName, fileInfo.jsonMeta ); + getMeta( &fileInfo ); } } diff --git a/examples/M5Stack-SD-Menu/menu.h b/examples/M5Stack-SD-Menu/menu.h index a05d58e4..c23a5727 100644 --- a/examples/M5Stack-SD-Menu/menu.h +++ b/examples/M5Stack-SD-Menu/menu.h @@ -38,7 +38,7 @@ #warning M5STACK Core2 DETECTED !! #define PLATFORM_NAME "M5Core2" #define DEFAULT_REGISTRY_BOARD "m5core2" - #define USE_DOWNLOADER + //#define USE_DOWNLOADER #elif defined( ARDUINO_M5Stack_Core_ESP32 ) #warning M5STACK CLASSIC DETECTED !! #define PLATFORM_NAME "M5Stack" @@ -48,7 +48,7 @@ #warning M5STACK FIRE DETECTED !! #define PLATFORM_NAME "M5Fire" #define DEFAULT_REGISTRY_BOARD "m5fire" - #define USE_DOWNLOADER + //#define USE_DOWNLOADER #elif defined( ARDUINO_ODROID_ESP32 ) #warning ODROID DETECTED !! #define PLATFORM_NAME "Odroid-GO" @@ -57,7 +57,7 @@ #warning WROVER OR LOLIN_D32_PRO DETECTED !! #define DEFAULT_REGISTRY_BOARD "esp32" #define PLATFORM_NAME "ESP32" - #define USE_DOWNLOADER + //#define USE_DOWNLOADER #else #warning NOTHING DETECTED !! #define DEFAULT_REGISTRY_BOARD "lambda" @@ -131,12 +131,12 @@ M5SAM M5Menu; /* vMicro compliance, see https://github.com/tobozo/M5Stack-SD-Updater/issues/5#issuecomment-386749435 */ -void getMeta( fs::FS &fs, String metaFileName, JSONMeta &jsonMeta ); +//void getMeta( fs::FS &fs, String metaFileName, JSONMeta &jsonMeta ); void freeAllMeta(); void freeMeta(); void renderIcon( FileInfo &fileInfo ); void renderMeta( JSONMeta &jsonMeta ); -void qrRender( String text, float sizeinpixels ); +void qrRender( String text, float sizeinpixels, int xOffset=-1, int yOffset=-1 ); static String heapState() { @@ -252,8 +252,12 @@ void renderIcon( FileInfo &fileInfo ) fs::File iconFile = M5_FS.open( fileInfo.iconName.c_str() ); if( !iconFile ) return; - tft.drawJpg( &iconFile, tft.width()-jsonMeta.width-10, (tft.height()/2)-(jsonMeta.height/2)+10/*, jsonMeta.width, jsonMeta.height, 0, 0, JPEG_DIV_NONE*/ ); + tft.drawJpg( &iconFile, 190, 60/*, jsonMeta.width, jsonMeta.height, 0, 0, JPEG_DIV_NONE*/ ); iconFile.close(); + + + qrRender( jsonMeta.projectURL, 50, 190, 60 ); + //tft.drawJpgFile( M5_FS, fileInfo.iconName.c_str(), tft.width()-jsonMeta.width-10, (tft.height()/2)-(jsonMeta.height/2)+10/*, jsonMeta.width, jsonMeta.height, 0, 0, JPEG_DIV_NONE*/ ); } @@ -288,17 +292,22 @@ void renderMeta( JSONMeta &jsonMeta ) sprite.println( fileInfo[MenuID].fileName ); sprite.println(); + log_d("Rendering meta"); + if( jsonMeta.authorName!="" && jsonMeta.projectURL!="" ) { // both values provided + log_d("Rendering QRCode+author"); sprite.print( AUTHOR_PREFIX ); sprite.print( jsonMeta.authorName ); sprite.println( AUTHOR_SUFFIX ); sprite.println(); qrRender( jsonMeta.projectURL, 160 ); } else if( jsonMeta.projectURL!="" ) { // only projectURL + log_d("Rendering QRCode"); sprite.println( jsonMeta.projectURL ); sprite.println(); qrRender( jsonMeta.projectURL, 160 ); } else { // only authorName + log_d("Rendering Authorname"); sprite.println( jsonMeta.authorName ); sprite.println(); } @@ -331,7 +340,7 @@ uint8_t getLowestQRVersionFromString( String text, uint8_t ecc ) } -void qrRender( String text, float sizeinpixels ) +void qrRender( String text, float sizeinpixels, int xOffset, int yOffset ) { // see https://github.com/Kongduino/M5_QR_Code/blob/master/M5_QRCode_Test.ino // Create the QR code @@ -339,13 +348,20 @@ void qrRender( String text, float sizeinpixels ) uint8_t ecc = 0; // QR on TFT can do with minimal ECC uint8_t version = getLowestQRVersionFromString( text, ecc ); + + log_d("Rendering QR Code on version #%d", version ); + uint8_t qrcodeData[qrcode_getBufferSize( version )]; qrcode_initText( &qrcode, qrcodeData, version, ecc, text.c_str() ); uint8_t thickness = sizeinpixels / qrcode.size; uint16_t lineLength = qrcode.size * thickness; - uint8_t xOffset = ( ( tft.width() - ( lineLength ) ) / 2 ) + 70; - uint8_t yOffset = ( tft.height() - ( lineLength ) ) / 2; + if( xOffset == -1 ) { + xOffset = ( ( tft.width() - ( lineLength ) ) / 2 ) + 70; + } + if( yOffset == -1 ) { + yOffset = ( tft.height() - ( lineLength ) ) / 2; + } tft.fillRect( xOffset-5, yOffset-5, lineLength+10, lineLength+10, TFT_WHITE ); @@ -383,7 +399,7 @@ void listDir( fs::FS &fs, const char * dirName, uint8_t levels, bool process ) } else { if( isValidAppName( sdUpdater->fs_file_path(&file) ) ) { if( process ) { - getFileInfo( fileInfo[appsCount], fs, file ); + getFileInfo( fileInfo[appsCount], &file ); if( appsCountProgress > 0 ) { float progressRatio = ((((float)appsCount+1.0) / (float)appsCountProgress) * 80.00)+20.00; tft.progressBar( 110, 112, 100, 20, progressRatio); @@ -411,7 +427,7 @@ void listDir( fs::FS &fs, const char * dirName, uint8_t levels, bool process ) if( fs.exists( MENU_BIN ) ) { file = fs.open( MENU_BIN ); if( process ) { - getFileInfo( fileInfo[appsCount], fs, file ); + getFileInfo( fileInfo[appsCount], &file ); if( appsCountProgress > 0 ) { float progressRatio = ((((float)appsCount+1.0) / (float)appsCountProgress) * 80.00)+20.00; tft.progressBar( 110, 112, 100, 20, progressRatio); diff --git a/library.json b/library.json index 85af9bf5..5c9d3fc0 100644 --- a/library.json +++ b/library.json @@ -10,7 +10,7 @@ "type": "git", "url": "https://github.com/tobozo/M5Stack-SD-Updater.git" }, - "version": "1.1.8", + "version": "1.1.9", "framework": "arduino", "headers": "M5StackUpdater.h", "platforms": "espressif32" diff --git a/library.properties b/library.properties index ef20a27a..41af4c31 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=M5Stack-SD-Updater -version=1.1.8 +version=1.1.9 author=tobozo maintainer=tobozo@noreply.github.com sentence=SD Card Loader for M5 Stack diff --git a/src/gitTagVersion.h b/src/gitTagVersion.h index 4a8630f7..1058138f 100644 --- a/src/gitTagVersion.h +++ b/src/gitTagVersion.h @@ -1,6 +1,6 @@ #define SDU_VERSION_MAJOR 1 #define SDU_VERSION_MINOR 1 -#define SDU_VERSION_PATCH 8 +#define SDU_VERSION_PATCH 9 #define _SDU_STR(x) #x #define SDU_STR(x) _SDU_STR(x) // Macro to convert library version number into an integer