[
  {
    "path": ".gitattributes",
    "content": ".github/tools/SD-Apps/* linguist-vendored\n.github/tools/* linguist-vendored \n.github/tools/scripts/* linguist-vendored\n#.github/tools/*.sh -linguist-detectable\n*.sh linguist-detectable=false\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [lovyan03, tobozo] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\ncustom: # Replace with a single custom sponsorship URL\n"
  },
  {
    "path": ".github/scripts/semver.sh",
    "content": "#!/bin/bash\n\n\nfunction patchpropertyfiles {\n\n  old_tag=$1\n  new_tag=$2\n\n  echo \"Patching property/json/tag files $old_tag => $new_tag\"\n\n  if [ -f \"$GITHUB_WORKSPACE/library.json\" ]; then\n    sed -i -e \"s/\\\"$old_tag\\\"/\\\"$new_tag\\\"/g\" $GITHUB_WORKSPACE/library.json\n    cat $GITHUB_WORKSPACE/library.json\n  fi\n\n  if [ -f \"$GITHUB_WORKSPACE/library.properties\" ]; then\n    sed -i -e \"s/version=$old_tag/version=$new_tag/g\" $GITHUB_WORKSPACE/library.properties\n    cat $GITHUB_WORKSPACE/library.properties\n  fi\n\n  if [ -f \"$GITHUB_WORKSPACE/src/gitTagVersion.h\" ]; then\n    sed -i -e \"s/\\\"$old_tag\\\"/\\\"$new_tag\\\"/g\" $GITHUB_WORKSPACE/src/gitTagVersion.h\n    cat $GITHUB_WORKSPACE/src/gitTagVersion.h\n  fi\n\n}\n\n\n\nif [ $GITHUB_EVENT_NAME == \"workflow_dispatch\" ]; then\n\n  echo \"Workflow dispatched event, guessing version from properties file\"\n  localtag=`cat $GITHUB_WORKSPACE/library.properties | grep version`\n  RELEASE_TAG=${localtag//version=/ }\n  minor=true\n\nelse\n\n  if [ ! $GITHUB_EVENT_NAME == \"release\" ]; then\n      echo \"Wrong event '$GITHUB_EVENT_NAME'!\"\n      exit 1\n  fi\n\n  EVENT_JSON=`cat $GITHUB_EVENT_PATH`\n\n  action=`echo $EVENT_JSON | jq -r '.action'`\n  if [ ! $action == \"published\" ]; then\n      echo \"Wrong action '$action'. Exiting now...\"\n      exit 0\n  fi\n\n  draft=`echo $EVENT_JSON | jq -r '.release.draft'`\n  if [ $draft == \"true\" ]; then\n      echo \"It's a draft release. Exiting now...\"\n      exit 0\n  fi\n\n  RELEASE_PRE=`echo $EVENT_JSON | jq -r '.release.prerelease'`\n  RELEASE_TAG=`echo $EVENT_JSON | jq -r '.release.tag_name'`\n  RELEASE_BRANCH=`echo $EVENT_JSON | jq -r '.release.target_commitish'`\n  RELEASE_ID=`echo $EVENT_JSON | jq -r '.release.id'`\n  RELEASE_BODY=`echo $EVENT_JSON | jq -r '.release.body'`\n\n  echo \"Event: $GITHUB_EVENT_NAME, Repo: $GITHUB_REPOSITORY, Path: $GITHUB_WORKSPACE, Ref: $GITHUB_REF\"\n  echo \"Action: $action, Branch: $RELEASE_BRANCH, ID: $RELEASE_ID\"\n  echo \"Tag: $RELEASE_TAG, Draft: $draft, Pre-Release: $RELEASE_PRE\"\n\nfi\n\n\n# Increment a version string using Semantic Versioning (SemVer) terminology.\nmajor=false;\nminor=false;\npatch=false;\n\nwhile getopts \":MmpF:\" Option\ndo\n  case $Option in\n    M ) major=true;;\n    m ) minor=true;;\n    p ) patch=true;;\n    F )\n      version=${OPTARG}\n      if [ \"$version\" == \"auto\" ]; then\n        # default to patch\n        patch=true\n      else\n        forcedversion=true\n        echo \"Forcing version to $version\"\n      fi\n    ;;\n    * ) echo \"NOTHING TO DO\";;\n  esac\ndone\n\nif [ $OPTIND -eq 1 ]; then\n  echo \"No options were passed, assuming patch level\"\n  patch=true\nfi\n\n\nif [ -z ${RELEASE_TAG} ]\nthen\n    echo \"Couldn't determine version\"\n    exit 1\nfi\n\n\nif [ \"$forcedversion\" != \"true\" ]; then\n\n  # Build array from version string.\n\n  a=( ${RELEASE_TAG//./ } )\n  major_version=0\n  # If version string is missing or has the wrong number of members, show usage message.\n\n  if [ ${#a[@]} -ne 3 ]; then\n    echo \"usage: $(basename $0) [-Mmp] major.minor.patch\"\n    exit 1\n  fi\n\n  # Increment version numbers as requested.\n\n  if [ $major == \"true\" ]; then\n    echo \"Raising MAJOR\"\n    # Check for v in version (e.g. v1.0 not just 1.0)\n    if [[ ${a[0]} =~ ([vV]?)([0-9]+) ]]; then\n      v=\"${BASH_REMATCH[1]}\"\n      major_version=${BASH_REMATCH[2]}\n      ((major_version++))\n      a[0]=${v}${major_version}\n    else\n      ((a[0]++))\n      major_version=a[0]\n    fi\n\n    a[1]=0\n    a[2]=0\n  fi\n\n  if [ $minor == \"true\" ]; then\n    echo \"Raising MINOR\"\n    ((a[1]++))\n    a[2]=0\n  fi\n\n  if [ $patch == \"true\"  ]; then\n    echo \"Raising PATCH\"\n    ((a[2]++))\n  fi\n\n  version=$(echo \"${a[0]}.${a[1]}.${a[2]}\")\n\nfi\n\npatchpropertyfiles $RELEASE_TAG $version\n\n\n"
  },
  {
    "path": ".github/tools/SD-Apps/after_deploy.sh",
    "content": "#!/bin/bash\n\ncurl -v -H \"Authorization: token $GH_TOKEN\" --retry 5 \"https://api.github.com/repos/tobozo/M5Stack-SD-Updater/releases\" | jq -r \".[] | select(.tag_name==\\\"$TRAVIS_TAG\\\")\" | jq \".assets[] | select(.name=\\\"$ARCHIVE_ZIP\\\")\" | jq \"select(.browser_download_url | contains(\\\"untagged\\\") == true ) .browser_download_url\"\n\n\n# curl -v `curl -v -H \"Authorization: token $GH_TOKEN\" --retry 5 \"https://api.github.com/repos/tobozo/M5Stack-SD-Updater/releases\" | jq -r \".[] | select(.tag_name | contains(\\\"untagged\\\"))\" | jq -r \".assets[]\" | jq \"select(.name==\\\"SD-Apps-Folder.zip\\\")\" | jq -r \"select(.browser_download_url | contains(\\\"untagged\\\") ) .browser_download_url\"`\n"
  },
  {
    "path": ".github/tools/SD-Apps/before_deploy.sh",
    "content": "#!/bin/bash\n\n\ncd $PWD\n\n#if ! [[ $TRAVIS_TAG ]]; then\n#  git config --global user.email \"travis@travis-ci.org\"\n#  git config --global user.name \"Travis CI\"\n#  git tag ${TRAVIS_TAG}\n#fi\n#cd $REPO_NAME\n\npwd\n\ncd $TRAVIS_BUILD_DIR\nexport git_version_last=\"$(git describe --abbrev=0 --tags --always)\"\nexport git_version_next=\"v$(echo $git_version_last | awk -F . '{ printf \"%d.%d.%d\", $1,$2,$3 + 1}')\"\n#cd ..\necho $TRAVIS_BRANCH | grep \"unstable\" && export prerelease=true || export prerelease=false\ngit tag ${git_version_next}\necho \"Before deploy Travis tag : $TRAVIS_TAG\"\necho \"Git version last         : $git_version_last\"\necho \"Git version next         : $git_version_next\"\necho \"Travis Branch            : $TRAVIS_BRANCH\"\necho \"Is pre-release           : $prerelease\"\ncd /home/travis/build/tobozo/\n\nif [ -f $REPO_NAME/src/gitTagVersion.h ]; then\n  echo \"#define M5_SD_UPDATER_VERSION F(\\\"${TRAVIS_TAG}\\\")\" > $REPO_NAME/src/gitTagVersion.h\nelse\n  echo \"Can't patch $REPO_NAME/src/gitTagVersion.h !!!\"\n  sleep 5\n  exit 1\nfi\n\nzip -r $TRAVIS_BUILD_DIR/$REPO_NAME.zip $REPO_NAME -x *.git* -x \"$REPO_NAME/examples/M5Stack-SD-Menu/SD-Apps\" -x \"$REPO_NAME/examples/M5Stack-SD-Menu/SD-Content\"\nif [ -f $TRAVIS_BUILD_DIR/$REPO_NAME.zip ]; then\n  echo \"Successfully Created $TRAVIS_BUILD_DIR/$REPO_NAME.zip :-)\";\nelse\n  echo \"Failed to create $TRAVIS_BUILD_DIR/$REPO_NAME.zip !!!\";\n  sleep 5\n  exit 1\nfi\n\ncd $M5_SD_BUILD_DIR\nzip -r $TRAVIS_BUILD_DIR/$ARCHIVE_ZIP ./\nif [ -f $TRAVIS_BUILD_DIR/$ARCHIVE_ZIP ]; then\n  echo \"Successfully created $TRAVIS_BUILD_DIR/$ARCHIVE_ZIP :-)\";\nelse\n  echo \"Failed to create $TRAVIS_BUILD_DIR/$ARCHIVE_ZIP !!!\";\n  sleep 5\n  exit 1\nfi\n\ncd $TRAVIS_BUILD_DIR\n\n\n\n# export BODY=$(cat CHANGELOG.md) # boo! Travis doesn't like multiline body\n"
  },
  {
    "path": ".github/tools/SD-Apps/before_install.sh",
    "content": "#!/bin/bash\n\n# REQUIRES: $IDE_VERSION, $M5_SD_BUILD_DIR\n\n#date -u\n#uname -a\n#git fetch -t\n#env | sort\n#git log `git describe --tags --abbrev=0 HEAD^ --always`..HEAD --oneline\n\n\n\nif [[ \"$IDE_VERSION\" != \"\" ]]; then\n\n  /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_1.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :1 -ac -screen 0 1280x1024x16\n  sleep 3\n  export DISPLAY=:1.0\n  export JAVA_ARGS=\"-Djavax.jmdns.level=OFF\"\n  wget --quiet \"http://downloads.arduino.cc/arduino-$IDE_VERSION-linux64.tar.xz\" # &>/dev/null 2>&1\n  if [ -f arduino-$IDE_VERSION-linux64.tar.xz ]; then\n    echo \"Downloaded arduino-$IDE_VERSION-linux64.tar.xz\"\n  else\n    echo \"Failed to download arduino-$IDE_VERSION-linux64.tar.xz\"\n    sleep 5\n    exit 1\n  fi\n  tar xf arduino-$IDE_VERSION-linux64.tar.xz  &>/dev/null\n  mv arduino-$IDE_VERSION ~/arduino-ide\n  rm arduino-$IDE_VERSION-linux64.tar.xz\n  export PATH=$PATH:~/arduino-ide\n  mkdir -p $M5_SD_BUILD_DIR\n\nelse\n\n  echo \"NO IDE VERSION !!\"\n  sleep 5\n  exit 1\n\nfi\n"
  },
  {
    "path": ".github/tools/SD-Apps/functions.sh",
    "content": "#!/bin/bash\n\nfunction movebin {\n find /tmp -name \\*.partitions.bin -exec rm {} \\; #<-- you need that backslash before and space after the semicolon\n #find /tmp -name \\*.ino.elf -exec rename 's/.ino.elf/.ino.bin/' {} \\; # sometimes arduino produces ELF, sometimes it's BIN\n find /tmp -name \\*.ino.bin -exec rename 's/.ino.bin/.bin/' {} \\; #\n find /tmp -name \\*.bin -exec rename 's/(_for)?(_|-)?(m5)_?(stack)?(-|_)?//ig' {} \\; #\n export DIRTY_BIN_FILE=`basename $( find /tmp -name \\*.bin )`\n export BIN_FILE=\"${DIRTY_BIN_FILE^}\"\n export DIRTY_FILE_BASENAME=${DIRTY_BIN_FILE%.bin}\n export FILE_BASENAME=${BIN_FILE%.bin}\n find /tmp -name \\*.bin -exec mv {} $M5_SD_BUILD_DIR/ \\; #<-- you need that backslash before and space after the semicolon\n if [ \"$BIN_FILE\" != \"$DIRTY_BIN_FILE\" ]; then\n   mv $M5_SD_BUILD_DIR/$DIRTY_BIN_FILE $M5_SD_BUILD_DIR/$BIN_FILE\n   echo \"[++++] UpperCasedFirst() $DIRTY_BIN_FILE to $BIN_FILE\"\n fi\n echo $BIN_FILE\n}\n\nfunction injectupdater {\n  export outfile=$PATH_TO_INO_FILE;\n  echo \"***** Injecting $1\"\n  awk '/#include <M5Stack.h>/{print;print \"#include <M5StackUpdater.h>\\nSDUpdater sdUpdater;\";next}1' $outfile > tmp && mv tmp $outfile;\n  awk '/M5.begin()/{print;print \"  if(digitalRead(BUTTON_A_PIN) == 0) { sdUpdater.updateFromFS(SD); ESP.restart(); } \";next}1' $outfile > tmp && mv tmp $outfile;\n  # the M5StackUpdater requires Wire.begin(), inject it if necessary\n  egrep -R \"Wire.begin()\" || (awk '/M5.begin()/{print;print \"  Wire.begin();\";next}1' $outfile > tmp && mv tmp $outfile);\n  # the display driver changed, get rid of default rotation in setup\n  sed -i -e 's/M5.Lcd.setRotation(0);/\\/\\//g' $outfile\n  # remove any hardcoded credentials so wifi auth can be done from another app (e.g. wifimanager)\n  sed -i -e 's/WiFi.begin(ssid, password);/WiFi.begin();/g' $outfile\n  sed -i -e 's/WiFi.begin(SSID, PASSWORD);/WiFi.begin();/g' $outfile\n  echo \"***** Injection successful\"\n}\n\nfunction populatemeta {\n  echo \"***** Populating meta\"\n  export IMG_NAME=${FILE_BASENAME}_gh.jpg\n  export REPO_URL=`git config remote.origin.url`\n  export REPO_OWNER_URL=`echo ${REPO_URL%/*}`\n  export REPO_USERNAME=$(echo \"$REPO_URL\" | cut -d \"/\" -f4)\n  export AVATAR_URL=$REPO_OWNER_URL.png?size=200\n  export JSONFILE=\"$M5_SD_BUILD_DIR/json/$FILE_BASENAME.json\"\n  export IMGFILE=\"$M5_SD_BUILD_DIR/jpg/$FILE_BASENAME.jpg\"\n  export AVATARFILE=\"$M5_SD_BUILD_DIR/jpg/${FILE_BASENAME}_gh.jpg\"\n\n  if [ -f $JSONFILE ]; then\n    echo \"JSON Meta file $JSONFILE exists, should check for contents or leave it be\"\n  else\n    if [ -f \"$M5_SD_BUILD_DIR/json/$DIRTY_FILE_BASENAME.json\" ]; then\n      echo \"[++++] UpperCasedFirst() $DIRTY_FILE_BASENAME.json, renaming other meta components\"\n      mv $M5_SD_BUILD_DIR/json/$DIRTY_FILE_BASENAME.json $JSONFILE\n      mv $M5_SD_BUILD_DIR/jpg/$DIRTY_FILE_BASENAME.jpg $IMGFILE &>/dev/null\n      mv $M5_SD_BUILD_DIR/jpg/${DIRTY_FILE_BASENAME}_gh.jpg $AVATARFILE &>/dev/null\n      sed -i -e \"s/$DIRTY_FILE_BASENAME/$FILE_BASENAME/g\" $JSONFILE &>/dev/null\n    else\n      echo \"[++++] No $JSONFILE JSON Meta file found, creating from the ether\"\n      export REPO_SHORTURL=`git.io $REPO_URL`\n      if [ \"\" != \"$REPO_SHORTURL\" ]; then\n        echo \"{\\\"width\\\":120,\\\"height\\\":120, \\\"authorName\\\":\\\"@$REPO_USERNAME\\\", \\\"projectURL\\\": \\\"$REPO_SHORTURL\\\",\\\"credits\\\":\\\"$REPO_OWNER_URL\\\"}\" > $JSONFILE\n      else\n        echo \"{\\\"width\\\":120,\\\"height\\\":120, \\\"authorName\\\":\\\"@$REPO_USERNAME\\\", \\\"projectURL\\\": \\\"$REPO_URL\\\",\\\"credits\\\":\\\"$REPO_OWNER_URL\\\"}\" > $JSONFILE\n      fi\n    fi\n  fi\n  cat $JSONFILE\n  # no gist in URL is valid to retrieve the profile pic\n  AVATAR_URL=`sed 's/gist.//g' <<< $AVATAR_URL`\n  if [ ! -f $AVATARFILE ]; then\n    echo \"**** Will download avatar from $AVATAR_URL and save it as $AVATARFILE\"\n    wget –-quiet $AVATAR_URL --output-document=temp\n    convert temp -resize 120x120 $AVATARFILE\n    identify $AVATARFILE\n    rm temp\n  fi\n  echo \"***** Populating successful\"\n}\n"
  },
  {
    "path": ".github/tools/SD-Apps/gen-apps.sh",
    "content": "#!/bin/bash\n\nmy_dir=\"$(dirname \"$0\")\"\nsource $my_dir/functions.sh\n\nreadonly ARDUINO_CI_SCRIPT_ARDUINO_OUTPUT_FILTER_REGEX='(^\\[SocketListener\\(travis-job-*|^  *[0-9][0-9]*: [0-9a-g][0-9a-g]*|^dns\\[query,[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*:[0-9][0-9]*, length=[0-9][0-9]*, id=|^dns\\[response,[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*:[0-9][0-9]*, length=[0-9][0-9]*, id=|^questions:$|\\[DNSQuestion@|type: TYPE_IGNORE|^\\.\\]$|^\\.\\]\\]$|^.\\.\\]$|^.\\.\\]\\]$)'\nreadonly ARDUINO_CI_SCRIPT_SUCCESS_EXIT_STATUS=0\nreadonly ARDUINO_CI_SCRIPT_FAILURE_EXIT_STATUS=1\n\ncp -R $TRAVIS_BUILD_DIR/examples/M5Stack-SD-Menu/SD-Content/jpg $M5_SD_BUILD_DIR/\ncp -R $TRAVIS_BUILD_DIR/examples/M5Stack-SD-Menu/SD-Content/json $M5_SD_BUILD_DIR/\ncp -R $TRAVIS_BUILD_DIR/examples/M5Stack-SD-Menu/SD-Content/mp3 $M5_SD_BUILD_DIR/\n\nexport hidecompilelogs=1\n\n\nfor D in *; do\n  if [ -d \"${D}\" ]; then\n    echo \"moving to ${D}\";\n    cd ${D};\n    # ls -la\n    egrep -R M5StackUpdater && egrep -R updateFromFS && export m5enabled=1 || export m5enabled=0;\n    if (( $m5enabled == 1 )); then\n\n      case \"$D\" in\n\n        'Pixel-Fun-M5Stack')\n          echo \"Renaming $D ino file\"\n          mv PixelFun.ino Pixel-Fun-M5Stack.ino\n          sed -i -e 's/ILI9341/M5Display/g' Mover.cpp # https://github.com/neoxharsh/Pixel-Fun-M5Stack/issues/1\n          #export hidecompilelogs=0\n          export hidecompilelogs=1\n        ;;\n\n        'M5Stack_LovyanToyBox')\n          export hidecompilelogs=1\n          cd LovyanToyBox\n        ;;\n\n        'M5Stack-SetWiFi_Mic')\n           echo \"Duplicating Meta\";\n           cp -Rf microSD/jpg $M5_SD_BUILD_DIR/\n           cp -Rf microSD/json $M5_SD_BUILD_DIR/\n        ;;\n\n        'M5Tube')\n          #export hidecompilelogs=0\n        ;;\n\n        #*)\n        #;;\n\n      esac\n      export PATH_TO_INO_FILE=\"$(find ${SDAPP_FOLDER}/${D} -type f -iname *.ino)\";\n\n    else\n\n      export hidecompilelogs=1\n      case \"$D\" in\n\n        'M5Stack_LovyanToyBox')\n          export hidecompilelogs=1\n          cd LovyanToyBox\n        ;;\n\n        'M5StackSandbox')\n           export hidecompilelogs=1\n           cd SWRasterizer\n           if [ -d \"Sd-Content\" ]; then\n             cp -Rf Sd-Content/* $M5_SD_BUILD_DIR/\n           fi\n        ;;\n\n        'd_invader')\n           echo \"Should replace esp_deep_sleep => esp_sleep\"\n           # esp_deep_sleep => esp_sleep\n        ;;\n        'M5Stack_CrackScreen')\n          echo \"Fixing path to crack.jpg\"\n          sed -i 's/\\/crack.jpg/\\/jpg\\/crack.jpg/g' M5Stack_CrackScreen.ino\n          cp crack.jpg $M5_SD_BUILD_DIR/jpg/crack.jpg\n        ;;\n\n        'M5Stack-CrazyAsteroids')\n          cat Crazy_Asteroid.ino EntrySection.ino ExitSection.ino printAsteroid.ino printSpaceShip.ino > out.blah\n          rm *.ino\n          mv out.blah M5Stack-CrazyAsteroids.ino\n        ;;\n\n        'M5Stack_Particle_demo')\n          # this is an Arduino compatible Platformio project\n          mv main.cpp M5Stack_Particle_demo.ino\n          sed -i -e 's/define LCD_WIDTH 320/define LCD_WIDTH M5.Lcd.width() \\/\\//g' M5Stack_Particle_demo.ino\n          sed -i -e 's/define LCD_HEIGHT 240/define LCD_HEIGHT M5.Lcd.height() \\/\\//g' M5Stack_Particle_demo.ino\n        ;;\n\n        'M5Stack_WebRadio_Avator')\n          echo \"Patching esp8266Audio with getLevel()\"\n          sed -i -e 's/bool SetOutputModeMono/int getLevel();\\nbool  SetOutputModeMono/g' ~/Arduino/libraries/ESP8266Audio-master/src/AudioOutputI2S.h\n          sed -i -e 's/include \"AudioOutputI2S.h\"/include \"AudioOutputI2S.h\"\\n\\n int aout_level = 0; int AudioOutputI2S::getLevel() { return aout_level; }/g' ~/Arduino/libraries/ESP8266Audio-master/src/AudioOutputI2S.cpp\n          sed -i -e 's/int16_t l/aout_level = (int)sample[RIGHTCHANNEL];\\nint16_t  l/g' ~/Arduino/libraries/ESP8266Audio-master/src/AudioOutputI2S.cpp\n        ;;\n\n        'M5Stack-WiFiScanner')\n          # remove unnecessary include causing an error\n          sed -i -e 's/#include <String.h>/\\/\\//g' M5Stack-WiFiScanner.ino\n        ;;\n\n        #'M5Stack-MegaChess')\n        #  # fix rotation problem caused by display driver changes (now applied globally)\n        #  sed -i -e 's/M5.Lcd.setRotation(0);/\\/\\//g' arduinomegachess_for_m5stack.ino\n        #;;\n        #'M5Stack-Pacman-JoyPSP')\n        #;;\n        #'M5Stack-SpaceShooter')\n        #;;\n        #'M5Stack-ESP32-Oscilloscope')\n        #;;\n        #'M5Stack-NyanCat')\n        #;;\n        #'M5Stack-Rickroll')\n        #;;\n\n        'M5Stack_NyanCat_Ext')\n          echo \"Renaming file to prevent namespace collision\"\n          mv M5Stack_NyanCat.ino M5Stack_NyanCat_Ext.ino\n          wget -–quiet https://github.com/jimpo/nyancat/raw/master/nyancat.mp3 --output-document=$M5_SD_BUILD_DIR/mp3/NyanCat.mp3\n          sed -i 's/\\/NyanCat.mp3/\\/mp3\\/NyanCat.mp3/g' M5Stack_NyanCat_Ext.ino\n        ;;\n\n        'M5Stack_lifegame')\n          echo \"Renaming file to .ino\"\n          mv M5Stack_lifegame M5Stack_lifegame.ino\n        ;;\n\n        'M5Stack-Tetris')\n          echo \"Renaming Tetris to M5Stack-Tetris + changing path to bg image\"\n          mv Tetris.ino M5Stack-Tetris.ino\n          sed -i 's/\\/tetris.jpg/\\/jpg\\/tetris_bg.jpg/g' M5Stack-Tetris.ino\n          cp tetris.jpg $M5_SD_BUILD_DIR/jpg/tetris_bg.jpg\n        ;;\n\n        #'SpaceDefense-m5stack')\n        #;;\n\n        'M5Stack_FlappyBird_game')\n          # put real comments to prevent syntax error\n          sed -i -e 's/#By Ponticelli Domenico/\\/\\/By Ponticelli Domenico/g' M5Stack_FlappyBird.ino\n        ;;\n\n        #'M5Stack-PacketMonitor')\n        #;;\n        #'M5Stack_Sokoban')\n        #;;\n\n        'M5Stack-Thermal-Camera')\n           echo \"Renaming to M5Stack-Thermal-Camera.ino\"\n           mv thermal_cam_interpolate.ino M5Stack-Thermal-Camera.ino\n        ;;\n\n        'mp3-player-m5stack')\n          echo \"Changing mp3 path in sketch\"\n          # TODO: fix this\n          sed -i 's/createTrackList(\"\\/\")/createTrackList(\"\\/mp3\")/g' mp3-player-m5stack.ino\n        ;;\n\n      esac\n      export PATH_TO_INO_FILE=\"$(find ${SDAPP_FOLDER}/${D} -type f -iname *.ino)\";\n      injectupdater # $PATH_TO_INO_FILE\n    fi\n\n    echo \"**** Compiling ${PATH_TO_INO_FILE}\";\n\n    #arduino --preserve-temp-files --verify --board $BOARD $PATH_TO_INO_FILE >> $SDAPP_FOLDER/out.log && movebin && populatemeta\n    set +o errexit\n    # shellcheck disable=SC2086\n    # eval \\\"arduino --preserve-temp-files --verify --board $BOARD $PATH_TO_INO_FILE\\\" &>/dev/null | tr --complement --delete '[:print:]\\n\\t' | tr --squeeze-repeats '\\n' | grep --extended-regexp --invert-match \"$ARDUINO_CI_SCRIPT_ARDUINO_OUTPUT_FILTER_REGEX\"\n    if (( $hidecompilelogs == 0 )); then\n      arduino --preserve-temp-files --verbose-build --verify --board $BOARD $PATH_TO_INO_FILE\n    else\n      arduino --preserve-temp-files --verbose-build --verify --board $BOARD $PATH_TO_INO_FILE &>/dev/null\n    fi\n    # local -r arduinoPreferenceSettingExitStatus=\"${PIPESTATUS[0]}\"\n    export arduinoPreferenceSettingExitStatus=\"${PIPESTATUS[0]}\"\n    set -o errexit\n    #arduino --preserve-temp-files --verify --board $BOARD $PATH_TO_INO_FILE | tr --complement --delete '[:print:]\\n\\t' | tr --squeeze-repeats '\\n' | grep --extended-regexp --invert-match \"$ARDUINO_CI_SCRIPT_ARDUINO_OUTPUT_FILTER_REGEX\"\n    #  local -r arduinoInstallPackageExitStatus=\"${PIPESTATUS[0]}\"\n    #if [[ \"$arduinoPreferenceSettingExitStatus\" != \"$ARDUINO_CI_SCRIPT_SUCCESS_EXIT_STATUS\" ]]; then\n      movebin && populatemeta\n    #else\n    #  echo \"**** Bad exit status\"\n    #fi\n    ls $M5_SD_BUILD_DIR -la;\n    cd $SDAPP_FOLDER\n  fi\ndone\n\nls $M5_SD_BUILD_DIR/jpg -la;\nls $M5_SD_BUILD_DIR/json -la;\n\n# egrep -R M5StackUpdater $SDAPP_FOLDER/*\n# egrep -R updateFromFS $SDAPP_FOLDER/*\n"
  },
  {
    "path": ".github/tools/SD-Apps/gen-menu.sh",
    "content": "#!/bin/bash\n\n# Generate TobozoLauncher.bin and BetaLauncher.bin\n\n\ncd $TRAVIS_BUILD_DIR;\narduino --pref \"compiler.warning_level=none\" --save-prefs   &>/dev/null\narduino --pref \"build.warn_data_percentage=75\" --save-prefs   &>/dev/null\narduino --pref \"boardsmanager.additional.urls=https://dl.espressif.com/dl/package_esp32_index.json\" --save-prefs   &>/dev/null\narduino --install-boards esp32:esp32 &>/dev/null\narduino --board $BOARD --save-prefs &>/dev/null\n\nexport inofile=$SDAPP_FOLDER/../$EXAMPLE.ino\nexport outfile=$SDAPP_FOLDER/../downloader.h\n\nif [ -f $inofile ]; then\n  echo \"Compiling $inofile\"\n  arduino --preserve-temp-files --verbose-build --verify $inofile &>/dev/null\n  find /tmp/arduino* -name \\*.partitions.bin -exec rm {} \\; #\n  find /tmp/arduino* -name \\*.bin -exec mv {} $M5_SD_BUILD_DIR/TobozoLauncher.bin \\; #\n  if [ -f $M5_SD_BUILD_DIR/TobozoLauncher.bin ]; then\n    cp $M5_SD_BUILD_DIR/TobozoLauncher.bin $M5_SD_BUILD_DIR/menu.bin\n  else\n    echo \"ERROR: Failed to compile $inofile, aborting\"\n    sleep 5\n    exit 1\n  fi\nelse\n  echo \"ERROR: cannot compile menu.bin\"\n  sleep 5\n  exit 1\nfi\n\nif [ -f $outfile ]; then\n  echo \"Attempting to enable unstable channel by patching $outfile\"\n  sed -i -e 's/\"\\/sd-updater\"/\"\\/sd-updater\\/unstable\"/g' $outfile\n  grep unstable $outfile && export patchok=1 || export patchok=0\n  if (( $patchok == 1 )); then\n    echo \"Compiling Beta $inofile\"\n    arduino --preserve-temp-files --verbose-build --verify $inofile &>/dev/null\n    find /tmp/arduino* -name \\*.partitions.bin -exec rm {} \\; #\n    find /tmp/arduino* -name \\*.bin -exec mv {} $M5_SD_BUILD_DIR/BetaLauncher.bin \\; #\n    if [ -f $M5_SD_BUILD_DIR/BetaLauncher.bin ]; then\n      # fine\n      echo \"SUCCESS: compiled BetaLauncher.bin from $inofile\"\n    else\n      echo \"ERROR: Failed to compile BetaLauncher.bin from $inofile, aborting\"\n      sleep 5\n      exit 1\n    fi\n  else\n    echo \"ERROR: Patching unstable channel failed !!\";\n    sleep 5\n    exit 1\n  fi\nelse\n  echo \"ERROR: cannot compile BetaLauncher.bin\"\n  sleep 5\n  exit 1\nfi\n\necho \"Fake Binary\" >> $M5_SD_BUILD_DIR/Downloader.bin\necho \"Launchers Compilation successful\"\n"
  },
  {
    "path": ".github/tools/SD-Apps/get-deps.sh",
    "content": "#!/bin/bash\n\n# TODO: foreach this from JSON\n\nwget --quiet https://github.com/adafruit/Adafruit_NeoPixel/archive/master.zip --output-document=Adafruit_NeoPixel.zip\nunzip -d ~/Arduino/libraries Adafruit_NeoPixel.zip\n\nwget --quiet https://github.com/adafruit/Adafruit_AMG88xx/archive/1.0.2.zip --output-document=Adafruit_AMG88xx.zip\nunzip -d ~/Arduino/libraries Adafruit_AMG88xx.zip\n\nwget --quiet https://github.com/bblanchon/ArduinoJson/archive/6.x.zip --output-document=Arduinojson-master.zip\nunzip -d ~/Arduino/libraries Arduinojson-master.zip\n\n# wget https://github.com/tomsuch/M5StackSAM/archive/master.zip --output-document=M5StackSAM-master.zip\nwget --quiet https://github.com/tobozo/M5StackSAM/archive/patch-1.zip --output-document=M5StackSAM-master.zip\nunzip -d ~/Arduino/libraries M5StackSAM-master.zip\n\n#wget https://github.com/m5stack/M5Stack/archive/master.zip --output-document=M5Stack.zip\n#curl -v --retry 5 \"https://api.github.com/repos/M5Stack/M5Stack/releases/latest?access_token=$GH_TOKEN\" | jq -r \".zipball_url\" | wget --output-document=M5Stack.zip -i -\n#unzip -d ~/Arduino/libraries M5Stack.zip\n\nwget --quiet https://github.com/tobozo/ESP32-Chimera-Core/archive/master.zip --output-document=ESP32-Chimera-Core.zip\nunzip -d ~/Arduino/libraries ESP32-Chimera-Core.zip\n\nwget --quiet https://github.com/earlephilhower/ESP8266Audio/archive/master.zip --output-document=ESP8266Audio.zip\nunzip -d ~/Arduino/libraries ESP8266Audio.zip\n\nwget --quiet https://github.com/Seeed-Studio/Grove_BMP280/archive/1.0.1.zip --output-document=Grove_BMP280.zip\nunzip -d ~/Arduino/libraries Grove_BMP280.zip\n\nwget --quiet https://github.com/Gianbacchio/ESP8266_Spiram/archive/master.zip --output-document=ESP8266_Spiram.zip\nunzip -d ~/Arduino/libraries ESP8266_Spiram.zip\n\nwget --quiet http://www.buildlog.net/blog/wp-content/uploads/2018/02/Game_Audio.zip --output-document=Game_Audio.zip\nmkdir -p ~/Arduino/libraries/Game_Audio\nunzip -d ~/Arduino/libraries/Game_Audio Game_Audio.zip\n\nwget --quiet https://github.com/lovyan03/M5Stack_TreeView/archive/master.zip --output-document=M5Stack_TreeView.zip\nunzip -d ~/Arduino/libraries M5Stack_TreeView.zip\n\nwget --quiet https://github.com/lovyan03/M5Stack_OnScreenKeyboard/archive/master.zip --output-document=M5Stack_OnScreenKeyboard.zip\nunzip -d ~/Arduino/libraries M5Stack_OnScreenKeyboard.zip\n\nwget --quiet https://github.com/kosme/arduinoFFT/archive/master.zip --output-document=arduinoFFT.zip\nunzip -d ~/Arduino/libraries arduinoFFT.zip\n\nrm -f *.zip\n"
  },
  {
    "path": ".github/tools/SD-Apps/get-precompiled.sh",
    "content": "#!/bin/bash\n\n\n#- curl -v --retry 5 \"https://api.github.com/repos/lovyan03/M5Stack_LovyanLauncher/releases/latest?access_token=$GH_TOKEN\" | jq -r \"\".assets[0].browser_download_url\"\" | wget --output-document=M5Burner.zip -i -\n#- unzip -d /tmp M5Burner.zip\n#- cp /tmp/M5Burner/firmwares/LovyanLauncher/LovyanLauncher.bin $M5_SD_BUILD_DIR/\n#- rm -Rf /tmp/M5Burner\n\n# TODO: foreach this from JSON source ( URL / channel )\n\ncd /tmp\n\nwget --quiet https://github.com/lovyan03/M5Stack_LovyanLauncher/archive/master.zip --output-document=M5Stack_LovyanLauncher.zip\nunzip -d /tmp M5Stack_LovyanLauncher.zip\ncp -Rf /tmp/M5Stack_LovyanLauncher-master/LovyanLauncher/build/* $M5_SD_BUILD_DIR/\nrm -Rf /tmp/M5Stack_LovyanLauncher*\n\nwget --quiet https://github.com/lovyan03/M5Stack_LovyanToyBox/archive/master.zip --output-document=M5Stack_LovyanToyBox.zip\nunzip -d /tmp M5Stack_LovyanToyBox.zip\ncp -Rf /tmp/M5Stack_LovyanToyBox-master/LovyanToyBox/build/* $M5_SD_BUILD_DIR/\nrm -Rf /tmp/M5Stack_LovyanToyBox*\n\nwget --quiet https://github.com/robo8080/SD_Updater_TestData/archive/master.zip --output-document=SD-Apps.zip\nunzip -d /tmp SD-Apps.zip\ncp -uf /tmp/SD_Updater_TestData-master/*.bin $M5_SD_BUILD_DIR/\ncp -Rf /tmp/SD_Updater_TestData-master/jpg $M5_SD_BUILD_DIR/\ncp -Rf /tmp/SD_Updater_TestData-master/json $M5_SD_BUILD_DIR/\nrm -Rf /tmp/SD_Updater_TestData*\n\nwget --quiet https://github.com/mongonta0716/M5Stack-Avatar-fugu1/archive/master.zip --output-document=M5Stack-Avatar-fugu1.zip\nunzip -d /tmp M5Stack-Avatar-fugu1.zip\ncp -Rf /tmp/M5Stack-Avatar-fugu1-master/Avatar_fugu/jpg $M5_SD_BUILD_DIR/\ncp -Rf /tmp/M5Stack-Avatar-fugu1-master/Avatar_fugu/json $M5_SD_BUILD_DIR/\ncp -Rf /tmp/M5Stack-Avatar-fugu1-master/Avatar_fugu/*.bin $M5_SD_BUILD_DIR/\nrm -Rf /tmp/M5Stack-Avatar-fugu1-master\n\nwget --quiet https://github.com/EiichiroIto/m5apple2/archive/master.zip --output-document=m5apple2.zip\nunzip -d /tmp m5apple2.zip\ncp -Rf /tmp/m5apple2-master/bin/* $M5_SD_BUILD_DIR/\nrm -Rf /tmp/m5apple2*\n\nwget --quiet https://github.com/phillowcompiler/2048_M5Stack/archive/master.zip --output-document=2048_M5Stack.zip\nunzip -d /tmp 2048_M5Stack.zip\ncp -Rf /tmp/2048_M5Stack-master/build/* $M5_SD_BUILD_DIR/\nrm -Rf /tmp/2048_M5Stack*\n\ncd $M5_SD_BUILD_DIR\n# force lowercase extensions\nfind . -name '*.*' -exec sh -c 'a=$(echo \"$0\" | sed -r \"s/([^.]*)\\$/\\L\\1/\"); [ \"$a\" != \"$0\" ] && mv \"$0\" \"$a\" ' {} \\;\n"
  },
  {
    "path": ".github/tools/SD-Apps/install.sh",
    "content": "#!/bin/bash\n\ngem install git.io\n\nif [ -f $SDAPP_FOLDER/install_$TRAVIS_BRANCH.sh ]; then\n  source $SDAPP_FOLDER/install_$TRAVIS_BRANCH.sh\n\n  cd $TRAVIS_BUILD_DIR;\n  mkdir -p ~/Arduino/libraries\n  # link the project's folder into the libraries folder\n  ln -s $PWD ~/Arduino/libraries/.\n\nelse\n  echo \"No install script for this branch\"\nfi\n\necho \"Installing extra libraries\"\nsource $SDAPP_FOLDER/get-deps.sh\n"
  },
  {
    "path": ".github/tools/SD-Apps/install_master.sh",
    "content": "#!/bin/bash\n\ngit submodule update --init --recursive\ncd $SDAPP_FOLDER\n# pull latest code from submodules\ngit submodule foreach --recursive git pull origin master\n"
  },
  {
    "path": ".github/tools/SD-Apps/install_unstable.sh",
    "content": "#!/bin/bash\n\n#git submodule update --init --recursive\n#cd $SDAPP_FOLDER\n## pull latest code from submodules\n#git submodule foreach --recursive git pull origin master\n"
  },
  {
    "path": ".github/tools/SD-Apps/script.sh",
    "content": "#!/bin/bash\n\nsource $SDAPP_FOLDER/gen-menu.sh\n\nif [ \"$TRAVIS_BRANCH\" != \"master\" ]; then\n  # UNSTABLE\n  export git_version_last=\"$(git describe --abbrev=0 --tags --always)\"\n  export git_version_next=\"v$(echo $git_version_last | awk -F . '{ printf \"%d.%d.%d\", $1,$2,$3 + 1}')\"\n\n  echo \"Computed last version : $git_version_last\"\n  echo \"Computed next version : $git_version_next\"\n\n  echo \"Will download last binaries\"\n  #export LAST_SDAPP_FILE=\"SD-Apps-Folder.zip\"\n  #curl --retry 5 \"https://api.github.com/repos/tobozo/M5Stack-SD-Updater/releases/latest?access_token=$GH_TOKEN\" | jq -r \".assets[0].browser_download_url\" | wget --output-document=$LAST_SDAPP_FILE -i -\n  curl -H \"Authorization: token $GH_TOKEN\" --retry 5 \"https://api.github.com/repos/tobozo/M5Stack-SD-Updater/releases\" | jq -r \".[] | select(.tag_name==\\\"$git_version_last\\\")\" | jq -r \".assets[] | select(.name==\\\"$ARCHIVE_ZIP\\\")  .browser_download_url\" | wget --output-document=$ARCHIVE_ZIP -q -i -\n  if [ -f $ARCHIVE_ZIP ]; then\n     echo \"$ARCHIVE_ZIP found\"\n     ls $ARCHIVE_ZIP -la\n  else\n    echo \"ERROR: Could not find a valid $ARCHIVE_ZIP from latest releases, time to tune up jq queries ?\"\n    sleep 5\n    exit 1\n  fi\n\n  unzip -d /tmp/$ARCHIVE_ZIP $ARCHIVE_ZIP\n  cp -Ruf /tmp/$ARCHIVE_ZIP/* $M5_SD_BUILD_DIR/\n\n  echo \"Fetching precompiled projects\"\n  source $SDAPP_FOLDER/get-precompiled.sh\n  ls $M5_SD_BUILD_DIR/ -la\n  sleep 15 # give some time to the logs to come up\n\nelse\n  # MASTER\n\n#if [ ! -z \"$TRAVIS_TAG\" ]; then\n#    # zip the package if tagged build\n#    tools/build-release.sh -a$ESP32_GITHUB_TOKEN\n#else\n#    # run cmake and sketch tests\n#    tools/check_cmakelists.sh && tools/build-tests.sh\n#fi\n\n  source $SDAPP_FOLDER/gen-apps.sh\nfi\n"
  },
  {
    "path": ".github/tools/common.sh",
    "content": "#!/bin/bash\n\n/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_1.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :1 -ac -screen 0 1280x1024x16\nsleep 3\n\necho \"[DEBUG] WORK_DIR=$WORK_DIR\"\necho \"[DEBUG] WORK_SPACE=$WORK_SPACE\"\necho \"[DEBUG] repo_branch=$REPO_BRANCH\"\necho \"[DEBUG] M5_SD_BUILD_DIR=$M5_SD_BUILD_DIR\"\necho \"[DEBUG] M5_BURNER_DIR=$M5_BURNER_DIR\"\necho \"[DEBUG] APPLICATION_FOLDER=$APPLICATION_FOLDER\"\necho \"[DEBUG] SKETCHBOOK_FOLDER=$SKETCHBOOK_FOLDER\"\n\nexport DISPLAY=:1.0\nexport PATH=$PATH:~/arduino-ide\nexport logDir=$WORK_DIR/artifacts\nexport arduino_installed=0\nexport platformio_installed=0\nexport needs_firmware=0\nexport repo_branch=$REPO_BRANCH\nexport varTagName=`$WORK_SPACE/tools/git-describe.awk $WORK_SPACE`\n\nprn() { printf \"$(date '+%Y/%m/%d %H:%M:%S') [$1] \"; printf \"$2\\n\"; }\nmsg() { prn MSG \"$1\"; }\nlog() { [ ${VERBOSE} -ge 1 ] || return 0; prn LOG \"$1\" >&2; }\ndbg() { [ ${VERBOSE} -ge 2 ] || return 0; prn DBG \"$1\" >&2; }\nerr() { prn ERR \"$1\" >&2; [ -n \"${2}\" ] && exit ${2}; }\n\n\nif [[ \"$M5_SD_BUILD_DIR\" == \"\" ]]; then\n  echo \"[ERROR] Workflow did not set M5_SD_BUILD_DIR variable\"\n  exit 1\nfi\n\nmkdir -p $M5_SD_BUILD_DIR\nif [[ \"$M5_BURNER_DIR\" != \"\" ]]; then\n  mkdir -p $M5_BURNER_DIR/firmware_4MB\nfi\nmkdir -p ~/Arduino/libraries\nmkdir -p $logDir\n\n# set -o xtrace\n# set -v\n\n\nfunction m5burner_json {\n  # $1: path to the JSON file (required, will be overwritten)\n  # create JSON file as in M5Burner standards\n  if [[ \"$1\" == \"\" ]]; then\n    echo \"[ERROR] No m5burner.json path provided\"\n    exit 1\n  fi\n\n  tee $1 << XXX\n{\n    \"name\": \"$SDCardAppNameSpace\",\n    \"description\": \"$repo_desc\",\n    \"keywords\": \"M5Stack-App-Registry generated firwmare\",\n    \"author\": \"$repo_author\",\n    \"repository\": \"$repo_url\",\n    \"firmware_category\": [\n        {\n            \"Stack-4MB\": {\n                \"path\": \"firmware_4MB\",\n                \"device\": [\n                    \"M5Stack 4MB Model (default partition)\"\n                ],\n                \"default_baud\": 921600\n            }\n        }\n    ],\n    \"version\": \"$varTagName\",\n    \"framework\": \"$platform\"\n}\n\nXXX\n\n}\n\n\nfunction get_arduino_app {\n  # $1: URL to the repo (required)\n  # $2: repo name (required)\n  if [ \"$1\" == \"\" ]; then\n    echo \"[FAIL] Can't install a repo without a URL!!\"\n    exit 1\n  else\n    appURL=$1\n    # TODO: find out if the URL points to a repo or to a zip file\n  fi\n  if [ \"$2\" == \"\" ]; then\n    echo \"[FAIL] Can't install a repo without a name!!\"\n    exit 1\n  else\n    repoName=$2\n  fi\n  cd $WORK_DIR\n  if [ \"$3\" == \"\" ]; then\n    echo \"[INFO] Cloning $repoName @ $appURL into $WORK_DIR\"\n    git clone --depth=1 $appURL $repoName\n  else\n    branchName=$3\n    git clone --depth=1 -b $branchName $appURL $repoName\n  fi\n  cd $repoName\n  if [[ \"$pre_hook\" != \"\" && \"$pre_hook\" != \"null\" ]]; then\n    echo \"[INFO] Running pre-hook : $pre_hook\"\n    eval $pre_hook\n  fi\n}\n\n\nfunction gen_arduino_app {\n  # requires : $REPO_URL, $REPO_NAME, $REPO_BRANCH, $inofile, $outfile, $M5_SD_BUILD_DIR, $WORK_DIR\n  cd $WORK_DIR\n  if [ ! -d \"$repo_name\" ]; then\n    get_arduino_app $repo_url $repo_name $repo_branch\n  else\n    cd $WORK_DIR\n    cd $repo_name\n  fi\n  #if [[ \"$pre_hook\" != \"\" && \"$pre_hook\" != \"null\" ]]; then\n  #  echo \"[INFO] Running pre-hook : $pre_hook\"\n  #  eval $pre_hook\n  #fi\n  egrep -R M5StackUpdater && egrep -R updateFromFS && export m5enabled=1 || export m5enabled=0;\n  if (( $m5enabled == 1 )); then\n    echo \"[INFO] This app is already using the Sd-Updater library\"\n  else\n    injectupdater\n  fi\n  copy_assets\n  if [ -f $inofile ]; then\n    echo \"[INFO] Compiling $inofile\"\n    arduino --preserve-temp-files --verbose-build --verify $inofile >> $logDir/compilation.log\n    #movebin \"/tmp/arduino_build*\"\n    newmovebin \"/tmp/arduino_build*\"\n    #if [ -f $M5_SD_BUILD_DIR/$outfile ]; then\n    #  echo \"[SUCCESS] $inofile has been compiled and saved into $outfile\"\n    #else\n    #  echo \"[ERROR] Failed to compile $inofile, aborting\"\n    #  sleep 5\n    #  exit 1\n    #fi\n    #populatemeta\n    newpopulatemeta\n  else\n    echo \"[ERROR] could not compile $outfile from $inofile\"\n    sleep 5\n    exit 1\n  fi\n  if [[ \"$post_hook\" != \"\" && \"$post_hook\" != \"null\" ]]; then\n    echo \"[INFO] Running post-hook : $post_hook\"\n    eval $post_hook\n  fi\n}\n\n\nfunction install_arduino_lib {\n  # $1: URL to the zip file (required)\n  # $2: archive name (optional)\n  if [ \"$1\" == \"\" ]; then\n    echo \"    [FAIL] Can't install a library without a URL!!\"\n    exit 1\n  else\n    repoURL=$1\n    # TODO: check that the url points to a zip file\n    if [[ \"$repoURL\" == *.zip ]]; then\n      echo \"    [INFO] library URL is a zip file\"\n    else\n      echo \"    [WARNING] library URL is not a zip file, will be extrapolated\"\n      if [[ \"$repoURL\" == *\"github\"* ]]; then\n        if [[ \"${repoURL: -1}\" == \"/\" ]]; then\n          repoURL=${repoURL}archive/master.zip\n        else\n          repoURL=$repoURL/archive/master.zip\n        fi\n      else\n        echo \"    [ERROR] Can't extrapolate from a non github address, URL needs to point to a zip file\"\n      fi\n    fi\n  fi\n  if [ \"$2\" == \"\" ]; then\n    if [[ \"$repoURL\" == *\"github\"* ]]; then\n      # repoOwner=`echo ${repoURL} | cut -d/ -f4`\n      repoName=`echo ${repoURL} | cut -d/ -f5`\n      # repoSlug=\"${repoOwner}_${repoName}\"\n      zipName=\"${repoName}.zip\"\n    else\n      #echo ${repoURL} | cut -d/ -f5\n      zipName=\"archive.zip\"\n    fi\n  else\n    zipName=$2\n  fi\n  if [ \"$3\" == \"\" ]; then\n    #  no dirname provided\n    libdir=~/Arduino/libraries\n    if [[ \"$repoURL\" == *\"github\"* && \"$zipName\" == \"archive.zip\" ]]; then\n      # TODO extract lib name from repoURL ?\n      libdir=~/Arduino/libraries/${zipName%.zip}\n    fi\n  else\n    libdir=~/Arduino/libraries/$3\n  fi\n  mkdir -p $libdir\n  ls -la\n  HTTP_RESPONSE=$(curl -L --silent --write-out \"HTTPSTATUS:%{http_code}\" $repoURL --output $zipName)\n  ls $zipName -la\n  if [ $? -ne 0 ]; then\n    echo \"    [FAIL] $? => aborting\";\n    exit 1;\n  fi\n  unzip -qq -o -d $libdir $zipName && export zip_worked=true\n  if [ \"$zip_worked\" == \"true\" ]; then\n    echo \"    [OK] $repoURL\"\n    rm $zipName\n    ls $libdir -la\n  else\n    echo \"    [FAIL] Bad Zip archive for $repoURL\"\n    sleep 5\n    exit 1\n  fi\n}\n\n\nfunction copy_assets {\n  # assets inventory\n  if [ \"$precompiled\" == \"1\" ]; then\n    echo \"Assets:\"\n    echo \"-------\"\n    assetFolders=`jq -r '.repo.precompiled | keys[] as $k | \"\\($k)\"' $appJSONPath`\n    for assetFolder in $(echo $assetFolders | sed 's/[[:space:]]/\\n/g'); do  # | sed s/:/\\\\n/\n      echo \"  [$assetFolder]\"\n      if [[ \"$assetFolder\" == \"bin\" ]]; then\n        destpath=$M5_SD_BUILD_DIR\n        if [[ \"$include_bin_assets\" == \"0\" ]]; then\n          continue\n        fi\n      else\n        destpath=$M5_SD_BUILD_DIR/$assetFolder\n        mkdir -p $destpath\n      fi\n      for asset in $(echo \"$jsonCode\" | jq -r .repo.precompiled.$assetFolder[]); do\n        echo \"    - $asset\"\n        # TODO: copy those files to the build folder\n        basename_asset=`basename $asset`\n        cp \"$asset\" \"$destpath/$basename_asset\"\n      done\n    done\n    echo\n  fi\n}\n\n\n\nfunction get_remote_app {\n\n  if [[ \"$1\" == \"\" ]]; then\n    echo \"[ERROR] No appJSONPath provided\"\n    exit 1\n  fi\n\n  appJSONPath=/tmp/app.json\n  echo \"[INFO] Fetching meta file $1 into $appJSONPath\"\n  HTTP_RESPONSE=$(curl -L --silent --write-out \"HTTPSTATUS:%{http_code}\" $1 --output $appJSONPath)\n  if [ $? -ne 0 ]; then\n    echo \"[FAIL] $? => aborting\";\n    sleep 5\n    exit 1;\n  fi\n  source=0\n  precompiled=0\n  has_deps=0\n  has_source=0\n  jsonCode=`cat $appJSONPath`\n  # echo \"Fetched json code:\\n${jsonCode}\\n\"\n  repo_url=`echo \"${jsonCode}\" | jq -r .repo.url`\n  repo_name=`echo \"${jsonCode}\" | jq -r .repo.name`\n  repo_desc=`echo \"${jsonCode}\" | jq -r .repo.description`\n  repo_author=`echo \"${jsonCode}\" | jq -r .author.name`\n  repo_updated_at=`echo \"${jsonCode}\" | jq -r .repo.updated_at`\n  libDepsResp=`echo \"$jsonCode\" | jq -r .repo.source.lib_deps`\n  sourceFilesResp=`echo \"$jsonCode\" | jq -r .repo.source.ino`\n  expectedOutFileName=`echo \"$jsonCode\" | jq -r .repo.source.bin_name`\n  SDCardAppNameSpace=`echo \"$jsonCode\" | jq -r .repo.source.sd_namespace`\n\n  platform=`echo \"$jsonCode\" | jq -r .repo.source.platform`\n  pre_hook=`echo \"$jsonCode\" | jq -r '.repo.source | .[\"pre-hook\"]'`\n  post_hook=`echo \"$jsonCode\" | jq -r '.repo.source | .[\"post-hook\"]'`\n  app_name=`echo \"$jsonCode\" | jq -r .repo.name`\n\n  # collect bundle types\n  for row in $(echo \"${jsonCode}\" | jq -r .repo.type[]); do\n    echo \"Found available bundle format: ${row}\"\n    if [ \"${row}\" == \"source\" ]; then\n      source=1 # json has source tree\n    fi\n    if [ \"${row}\" == \"precompiled\" ]; then\n      precompiled=1 # json has bin tree\n    fi\n  done\n  echo\n  # evaluate if the source version has library dependencies and is usable\n  if [[ \"$libDepsResp\" == \"null\" || \"$libDepsResp\" == \"\" || \"$libDepsResp\" == '[]' ]]; then\n    has_deps=0 # json has no library deps\n  else\n    has_deps=1 # json has library deps\n  fi\n  if [ \"$sourceFilesResp\" == \"null\" ]; then\n    echo \"[INFO] Json has no ino_file\"\n    has_source=0 # json has no ino_file\n  else\n    has_source=1 # json has ino_file\n    export ino_array=`echo \"$jsonCode\" | jq -r .repo.source.ino[]`\n    export array_size=`echo $ino_array | wc -l`\n  fi\n\n  if [ \"$has_source\" == \"1\" ]; then\n    if [[ \"$expectedOutFileName\" == \"null\"  || \"$expectedOutFileName\" == \"\"  ]]; then\n      if [[ \"$SDCardAppNameSpace\" == \"null\"  || \"$SDCardAppNameSpace\" == \"\"  ]]; then\n        echo \"[ERROR] Neither bin_name nor sd_namespace found in JSON\"\n        echo \"[ERROR] Please implement repo.source.bin_name or repo.source.sd_namespace in package $1\"\n        echo \"[INFO] TODO: hook delete and regen JSON\"\n        exit 1\n      else\n        expectedOutFileName=\"$SDCardAppNameSpace.bin\"\n        echo \"[INFO] Using namespace : $SDCardAppNameSpace\";\n        echo \"[WARNING] Forced bin_name from namespace : $SDCardAppNameSpace\"\n      fi\n    else\n      echo \"[INFO] Using bin_name = $expectedOutFileName\"\n      if [[ \"$SDCardAppNameSpace\" == \"null\"  || \"$SDCardAppNameSpace\" == \"\"  ]]; then\n        SDCardAppNameSpace=${expectedOutFileName%.bin}\n        echo \"[WARNING] Forced namespace from bin_name : $SDCardAppNameSpace\"\n      else\n        echo \"[INFO] Using namespace : $SDCardAppNameSpace\";\n      fi\n    fi\n  else\n    SDCardAppNameSpace=${repo_name}\n  fi\n\n  include_bin_assets=0\n\n  # build information\n  if [ \"$source\" == \"1\" ]; then\n    echo \"Source code:\"\n    echo \"------------\"\n    echo \"  Platform: $platform\"\n    if [[ $platform == *\"arduino\"* ]]; then\n      board=`echo \"$jsonCode\" | jq -r .repo.source.board`\n      install_arduino\n      get_arduino_app $repo_url $repo_name $repo_branch\n      # install_arduino_libraries\n      echo \"  Board: $board\"\n      if [ \"$has_deps\" == \"1\" ]; then\n        echo \"    Library requirements:\"\n        for libraryURL in $(echo \"${jsonCode}\" | jq -r .repo.source.lib_deps[]); do\n          install_arduino_lib $libraryURL\n        done\n      else\n        install_arduino_libraries\n      fi\n      if [ \"$has_source\" == \"1\" ]; then\n        if [[ \"$inofile_forced\" != \"\" ]]; then\n          # echo \"  [FORCED] Will conpile $inofile_forced\"\n          inofile=$inofile_forced\n          gen_arduino_app\n        else\n          if [ $array_size > 1 ]; then\n            for inofile in $(echo $ino_array | sed 's/\\n/\\n/g'); do\n              # echo \"  [ARRAY] Will compile $inofile\"\n              gen_arduino_app\n              break\n            done\n          else\n            inofile=$ino_array\n            # echo \"  [INFO] Will compile $ino_array\"\n            gen_arduino_app\n          fi\n        fi\n      else\n        # JSON says there's no ino/cpp source on this repo, use the binaries\n        # get_arduino_app $repo_url $repo_name\n        echo \"Can't use the source on this one, let's hope the binaries are fine\"\n        include_bin_assets=1\n        copy_assets\n      fi\n\n    else\n      # platformio ?\n      # get_arduino_app $repo_url $repo_name\n      if [ \"$source\" == \"1\" ]; then\n        get_arduino_app $repo_url $repo_name $repo_branch\n        install_platformio\n        if [ \"$has_deps\" == \"1\" ]; then\n          echo \"    Library requirements:\"\n          for libraryURL in $(echo \"${jsonCode}\" | jq -r .repo.source.lib_deps[]); do\n            echo \"    [INFO] installing library $libraryURL\"\n            python -m platformio  lib install $libraryURL\n          done\n        fi\n        gen_platformio_app\n      else\n        echo \"Can't use the source on this one, let's hope the binaries are fine\"\n        include_bin_assets=1\n        copy_assets\n      fi\n    fi\n\n    echo\n  fi\n\n  find $M5_SD_BUILD_DIR\n  cd $M5_SD_BUILD_DIR\n\n  if [[ \"${FILE_BASENAME}\" != \"\" ]]; then\n    zipFileName=${FILE_BASENAME}.zip\n  else\n    zipFileName=${SDCardAppNameSpace}.zip\n  fi\n\n  zip -r $zipFileName ./*\n  rm $M5_SD_BUILD_DIR/*.bin\n\n  if [[ \"$needs_firmware\" != \"0\" ]]; then\n\n    case $APP_BOARD in\n      m5stack)\n        echo \"[INFO] Project needs M5Burner zip file too\"\n        cd $M5_BURNER_DIR\n        m5burner_json m5burner.json\n        # m5BurnerDirName=${SDCardAppNameSpace}-v${varTagName}/${SDCardAppNameSpace}\n        # mkdir -p ${m5BurnerDirName} && mv firmware_4MB m5burner.json ${m5BurnerDirName}/\n        zipFileName=\"M5Burner-$zipFileName\"\n        # cd ${m5BurnerDirName}\n        # zip -r $zipFileName ./*\n        zip -r $zipFileName *\n        #zip -jrq $zipFileName ${m5BurnerDirName}\n        cp $zipFileName $M5_SD_BUILD_DIR/\n        echo \"[INFO] M5Burner zip file created: $zipFileName\"\n      ;;\n      odroid)\n        echo \"[INFO] Project needs OdroidFW zip file too\"\n        if [ ! -f $IMGFILE ]; then\n          echo \"[ERROR] can't make a firmware without a pic\"\n          exit 1\n        fi\n        cd $M5_BURNER_DIR\n\n        # git clone https://github.com/othercrashoverride/odroid-go-firmware.git -b factory\n        # cd odroid-go-firmware/tools/mkfw\n        # make\n        # chmod +x mkfw\n\n\n        # git clone https://github.com/lstux/OdroidGO/\n        # chmod +x OdroidGO/ino2fw/mkfw-build.sh OdroidGO/ino2fw/ino2fw.sh\n        $WORK_SPACE/tools/mkfw-build.sh\n        ls $WORK_DIR/$repo_name/$inofile -la\n        ls $IMGFILE -la\n        set -o xtrace\n        set -v\n        #echo \"[INFO] sending command: $WORK_SPACE/tools/ino2fw.sh -t $IMGFILE -l $SDCardAppNameSpace -d \\\"${repo_desc}\\\" $WORK_DIR/$repo_name/$inofile\"\n        #$WORK_SPACE/tools/ino2fw.sh -t $IMGFILE -l $SDCardAppNameSpace -d \"${repo_desc}\" $WORK_DIR/$repo_name/$inofile\n        # cp $M5_SD_BUILD_DIR/$expectedOutFileName firmware.bin\n        echo \"[INFO] sending command: $WORK_SPACE/tools/ino2fw.sh -t $IMGFILE -l $SDCardAppNameSpace -d \\\"${repo_desc}\\\" $M5_BURNER_DIR/$expectedOutFileName\"\n        $WORK_SPACE/tools/ino2fw.sh -t $IMGFILE -l $SDCardAppNameSpace -d \"${repo_desc}\" $M5_BURNER_DIR/$expectedOutFileName\n\n        pwd\n        ls -la\n\n        cd $M5_BURNER_DIR\n        # cp $M5_BURNER_DIR/$expectedOutFileName\n        echo \"$M5_BURNER_DIR :\"\n        ls $M5_BURNER_DIR -la\n\n        zipFileName=\"OdroidFW-$zipFileName\"\n        zip -r $zipFileName ${SDCardAppNameSpace}.fw\n        cp $zipFileName $M5_SD_BUILD_DIR/\n\n        #ls -la\n\n        #cp $zipFileName $M5_SD_BUILD_DIR/\n        #    ino2fw.sh [options] {sketch_dir|sketch_file.ino}\n        #      Create a .fw file for Odroid-Go from arduino sketch\n        #    options:\n        #      -b build_path  : build in build_path [/tmp/ino2fw]\n        #      -t tile.png    : use specified image as tile (resized to 86x48px)\n        #      -l label       : set application label\n        #      -d description : set application description\n        #      -v             : increase verbosity level\n        #      -h             : display help message\n\n\n\n        #ffmpeg -i $IMGFILE -f rawvideo -pix_fmt rgb565 tile.raw\n        #cp $M5_SD_BUILD_DIR/$expectedOutFileName firmware.bin\n        #ls -la\n        #filesize=$(stat -c%s \"$M5_BURNER_DIR/$expectedOutFileName\")\n        #blocks=$(($filesize/64))\n        #normsize=$(((1+$blocks)*64))\n        #echo \"[INFO] will mkfw '$expectedOutFileName' with hardkernel's tool\"\n        #echo \"[TODO] ./mkfw ${SDCardAppNameSpace} tile.raw 0 16 $normsize ${SDCardAppNameSpace} $M5_BURNER_DIR/$expectedOutFileName\"\n        ## ./mkfw test tile.raw 0 16 1048576 app $expectedOutFileName\n        #dbg \"./mkfw ${SDCardAppNameSpace} tile.raw 0 16 $normsize ${SDCardAppNameSpace} $expectedOutFileName\"\n        #mv firmware.fw $M5_BURNER_DIR/${SDCardAppNameSpace}.fw\n        #cd $M5_BURNER_DIR\n        #zipFileName=\"OdroidFW-$zipFileName\"\n        #zip -r $zipFileName $expectedOutFileName\n        #cp $zipFileName $M5_SD_BUILD_DIR/\n        echo \"[INFO] OdroidFW zip file created: $zipFileName\"\n\n      ;;\n      *)\n        echo \"[ERROR] bad APP_BOARD value: $APP_BOARD\"\n        exit 1\n      ;;\n    esac\n\n  fi\n\n  cd $M5_SD_BUILD_DIR\n\n}\n\n\n\nfunction install_platformio {\n  egrep -R M5StackUpdater && egrep -R updateFromFS && export m5enabled=1 || export m5enabled=0;\n  if (( $m5enabled == 1 )); then\n    echo \"[INFO] This app is already using the Sd-Updater library\"\n  else\n    if [[ \"$inofile\" == \"\" ]]; then\n      export ino_array=`echo \"$jsonCode\" | jq -r .repo.source.ino[]`\n      # export array_size=`echo $ino_array | wc -l`\n      inofile=$ino_array\n    fi\n    injectupdater\n  fi\n  if [[ \"$platformio_installed\" != \"0\" ]]; then\n    echo \"plaformio already installed\"\n    return\n  fi\n  pip install -U https://github.com/platformio/platformio/archive/develop.zip\n  python -m platformio platform install https://github.com/platformio/platform-espressif32.git#feature/stage\n  python -m platformio lib install m5stack-sd-updater\n  python -m platformio lib install https://github.com/tobozo/ESP32-Chimera-Core\n  platformio_installed=1\n}\n\n\nfunction gen_platformio_app {\n  if [[ \"$pre_hook\" != \"\" && \"$pre_hook\" != \"null\" ]]; then\n    echo \"[INFO] Running pre-hook : $pre_hook\"\n    eval $pre_hook\n  fi\n  copy_assets\n  echo \"[INFO] Compiling $app_name\"\n  python -m platformio run  # >> $logDir/compilation.log\n  #movebin \".pio/\"\n  #if [ -f $M5_SD_BUILD_DIR/$outfile ]; then\n  #  echo \"[SUCCESS] $inofile has been compiled and saved into $outfile\"\n  #else\n  #  echo \"[ERROR] Failed to compile $inofile, aborting\"\n  #  sleep 5\n  #  exit 1\n  #fi\n  #populatemeta\n  newmovebin \".pio/\"\n  newpopulatemeta\n  if [[ \"$post_hook\" != \"\" && \"$post_hook\" != \"null\" ]]; then\n    echo \"[INFO] Running pre-hook : $post_hook\"\n    eval $post_hook\n  fi\n}\n\n\n\n\nfunction install_arduino_libraries {\n  install_arduino_lib https://github.com/adafruit/Adafruit_NeoPixel/archive/master.zip Adafruit_NeoPixel.zip\n  install_arduino_lib https://github.com/adafruit/Adafruit_AMG88xx/archive/1.0.2.zip Adafruit_AMG88xx.zip\n  install_arduino_lib https://github.com/bblanchon/ArduinoJson/archive/6.x.zip Arduinojson-master.zip\n  install_arduino_lib https://github.com/tobozo/M5StackSAM/archive/patch-1.zip M5StackSAM-master.zip\n  install_arduino_lib https://github.com/earlephilhower/ESP8266Audio/archive/master.zip ESP8266Audio.zip\n  install_arduino_lib https://github.com/Seeed-Studio/Grove_BMP280/archive/1.0.1.zip Grove_BMP280.zip\n  install_arduino_lib https://github.com/Gianbacchio/ESP8266_Spiram/archive/master.zip ESP8266_Spiram.zip\n  install_arduino_lib http://www.buildlog.net/blog/wp-content/uploads/2018/02/Game_Audio.zip Game_Audio.zip Game_Audio\n  install_arduino_lib https://github.com/lovyan03/M5Stack_TreeView/archive/master.zip M5Stack_TreeView.zip\n  install_arduino_lib https://github.com/lovyan03/M5Stack_OnScreenKeyboard/archive/master.zip M5Stack_OnScreenKeyboard.zip\n  install_arduino_lib https://github.com/kosme/arduinoFFT/archive/master.zip arduinoFFT.zip\n}\n\n\nfunction install_arduino {\n  if [[ \"$arduino_installed\" != \"0\" ]]; then\n    echo \"arduino already installed\"\n    return\n  fi\n  cd $WORK_DIR\n  if [[ \"$platform\" != \"\" ]]; then\n    BOARD=\"esp32:esp32:$board:FlashFreq=80\"\n  else\n    echo \"[INFO] Using default board\"\n    BOARD=\"esp32:esp32:m5stack-core-esp32:FlashFreq=80\"\n  fi\n\n  if [[ \"$platform\" != \"\" ]]; then\n    export JAVA_ARGS=\"-Djavax.jmdns.level=OFF\"\n    wget --quiet \"http://downloads.arduino.cc/$platform-linux64.tar.xz\" # &>/dev/null 2>&1\n    if [ -f $platform-linux64.tar.xz ]; then\n      echo \"[OK] Downloaded $platform-linux64.tar.xz\"\n    else\n      echo \"[FAIL] Failed to download $platform-linux64.tar.xz\"\n      sleep 5\n      exit 1\n    fi\n    tar xf $platform-linux64.tar.xz  &>/dev/null\n    mv $platform ~/arduino-ide\n    rm $platform-linux64.tar.xz\n    # export PATH=$PATH:~/arduino-ide\n\n    releaseAddr=`sed \"s/\\/tag\\//\\/download\\//g\"<<<$(curl -s -D - https://github.com/espressif/arduino-esp32/releases/latest/ -o /dev/null | grep  -oP 'Location: \\K.*(?=\\r)')/package_esp32_index.json`\n\n    echo \"[OK] Successfully unpacked $platform-linux64.tar.xz and added ~/arduino-ide to PATH\"\n    arduino --pref \"compiler.warning_level=none\" --save-prefs   &>/dev/null\n    arduino --pref \"build.warn_data_percentage=75\" --save-prefs   &>/dev/null\n    arduino --pref \"boardsmanager.additional.urls=$releaseAddr\" --save-prefs   &>/dev/null\n    # arduino --pref \"boardsmanager.additional.urls=https://dl.espressif.com/dl/package_esp32_index.json\" --save-prefs   &>/dev/null\n    arduino --install-boards esp32:esp32 &>/dev/null\n    arduino --board $BOARD --save-prefs &>/dev/null\n\n    echo \"[OK] Successfully installed esp32 board in Arduino IDE\"\n\n    # mkdir -p ~/Arduino/libraries\n    wget --quiet https://github.com/tobozo/M5Stack-SD-Updater/archive/$SD_UPDATER_BRANCH.zip --output-document=SD-Updater.zip\n    unzip -qq -d ~/Arduino/libraries SD-Updater.zip\n\n    echo \"[OK] Successfully installed SD-Updater library from $SD_UPDATER_BRANCH branch\"\n\n    # TODO: move this somewhere else\n    cd ~/Arduino/libraries\n    git clone $M5_CORE_URL\n\n    echo \"[OK] Successfully installed M5 Core from $M5_CORE_URL\"\n    export arduino_installed=1\n  else\n\n    echo \"[FAIL] NO platform provided !!\"\n    sleep 5\n    exit 1\n\n  fi\n\n}\n\n\n\n#\n# function parse_yaml (lol, you wish)\n#\n# https://stackoverflow.com/questions/5014632/how-can-i-parse-a-yaml-file-from-a-linux-shell-script\n#\n# usage: parse_yaml sample.yml\n#\n# Data example:\n#\n# ## global definitions\n# global:\n#   debug: yes\n#   verbose: no\n#   debugging:\n#     detailed: no\n#     header: \"debugging started\"\n# output:\n#    file: \"yes\"\n#\n# Will output:\n#\n# global_debug=\"yes\"\n# global_verbose=\"no\"\n# global_debugging_detailed=\"no\"\n# global_debugging_header=\"debugging started\"\n# output_file=\"yes\"\n#\n#\n\nfunction parse_yaml {\n   local prefix=$2\n   local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\\034')\n   sed -ne \"s|^\\($s\\):|\\1|\" \\\n        -e \"s|^\\($s\\)\\($w\\)$s:$s[\\\"']\\(.*\\)[\\\"']$s\\$|\\1$fs\\2$fs\\3|p\" \\\n        -e \"s|^\\($s\\)\\($w\\)$s:$s\\(.*\\)$s\\$|\\1$fs\\2$fs\\3|p\"  $1 |\n   awk -F$fs '{\n      indent = length($1)/2;\n      vname[indent] = $2;\n      for (i in vname) {if (i > indent) {delete vname[i]}}\n      if (length($3) > 0) {\n         vn=\"\"; for (i=0; i<indent; i++) {vn=(vn)(vname[i])(\"_\")}\n         printf(\"%s%s%s=\\\"%s\\\"\\n\", \"'$prefix'\",vn, $2, $3);\n      }\n   }'\n}\n\n\nfunction newmovebin {\n  if [ \"$1\" == \"\" ]; then\n    binpath=\"/tmp/arduino_build*\"\n  else\n    binpath=$1\n  fi\n  echo \"[INFO] Searching $binpath\"\n\n  if [[ \"$needs_firmware\" != \"0\" ]]; then  # if [[ \"$M5_BURNER_DIR\" != \"\" ]]; then\n\n    case $APP_BOARD in\n      m5stack)\n        echo \"[INFO] Building M5Burner firmware package\"\n        find ~/.arduino15/packages/esp32/ -name \"boot_app0.bin\" -exec cp {} $M5_BURNER_DIR/firmware_4MB/boot_0xe000.bin \\; #\n        find ~/.arduino15/packages/esp32/ -name \"bootloader_qio_80m.bin\" -exec cp {} $M5_BURNER_DIR/firmware_4MB/bootloader_0x1000.bin \\; #\n        find $binpath -name \\*partitions.bin -exec mv {} $M5_BURNER_DIR/firmware_4MB/partitions_0x8000.bin \\; #\n        find $binpath -name \\*.bin -exec cp {} $M5_BURNER_DIR/firmware_4MB/${SDCardAppNameSpace}_0x10000.bin \\; #\n        echo \"[INFO] Resuming on SD package\"\n      ;;\n      odroid)\n        echo \"[INFO] Building Odroid-Go FW package\"\n        find $binpath -name \\*.bin -exec cp {} $M5_BURNER_DIR/$expectedOutFileName \\; #<-- you need that backslash before and space after the semicolon\n        # TODO:\n        # git clone https://github.com/othercrashoverride/odroid-go-firmware.git -b factory\n        # cd odroid-go-firmware/tools/mkfw\n        # make\n      ;;\n      *)\n        echo \"[ERROR] bad APP_BOARD value: $APP_BOARD\"\n        exit 1\n      ;;\n    esac\n\n  else\n    find $binpath -name \\*partitions.bin -exec rm {} \\; #<-- you need that backslash before and space after the semicolon\n  fi\n\n  find $binpath -name \\*.bin -exec mv {} $M5_SD_BUILD_DIR/$expectedOutFileName \\; #<-- you need that backslash before and space after the semicolon\n  echo \"[INFO] Kept bin file: $expectedOutFileName\"\n  if [ ! -f $M5_SD_BUILD_DIR/$expectedOutFileName ]; then\n    echo \"[ERROR] bin copy failed : $M5_SD_BUILD_DIR/$expectedOutFileName\"\n    echo \"[TODO]: escalate this\"\n    exit 1\n  fi\n}\n\n\n\nfunction movebin {\n  binpath=$1\n  if [ \"$binpath\" == \"\" ]; then\n    echo \"[ERROR] Can't move bin file without path\"\n    exit 1\n  fi\n  # remove the \"partitions.bin\" file first\n  find $binpath -name \\*partitions.bin -exec rm {} \\; #<-- you need that backslash before and space after the semicolon\n  # blind rename \".ino.bin\" to \".bin\" (Arduino IDE does that)\n  find $binpath -name \\*.ino.bin -exec rename -v 's/.ino.bin/.bin/' {} \\; # wut\n  if [[ \"$platform\" == \"platformio\" || \"$DIRTY_BIN_FILE\" == \"firmware.bin\" ]]; then\n    n=`find $binpath -name *.bin`\n    dn=$(dirname $n)\n    fn=$(basename $n)\n    cfn=`echo \"$fn\" | perl -pe 's/(_for)?(_|-)?(m5)_?(stack)?(-|_)?//ig'`\n    mv \"$n\" \"${dn}/${app_name^}.bin\"\n    export DIRTY_BIN_FILE=`basename $( find $binpath -name \\*.bin )`\n    export BIN_FILE=\"${app_name^}.bin\"\n    export DIRTY_FILE_BASENAME=${app_name%.bin}\n    # echo ${hello//[0-9]/}\n  else\n\n    if [ \"$expectedOutFileName\" == \"null\" ]; then\n      # use sketch default bin filename\n      find $binpath -name \\*.bin -exec rename -v 's/(_for)?(_|-)?(m5)_?(stack)?(-|_)?//ig' {} \\; #\n      export DIRTY_BIN_FILE=`basename $( find $binpath -name \\*.bin )`\n      export BIN_FILE=\"${DIRTY_BIN_FILE^}\"\n      export DIRTY_FILE_BASENAME=${DIRTY_BIN_FILE%.bin}\n    else\n      # use inherited bin filename\n      n=`find $binpath -name *.bin`\n      dn=$(dirname $n)\n      mv \"$n\" \"${dn}/${expectedOutFileName}\"\n      export DIRTY_BIN_FILE=`basename $( find $binpath -name \\*.bin )`\n      export BIN_FILE=\"$expectedOutFileName\"\n      export DIRTY_FILE_BASENAME=${expectedOutFileName%.bin}\n    fi\n  fi\n  export FILE_BASENAME=${BIN_FILE%.bin}\n  find $binpath -name \\*.bin -exec mv {} $M5_SD_BUILD_DIR/$BIN_FILE \\; #<-- you need that backslash before and space after the semicolon\n  #if [ \"$BIN_FILE\" != \"$DIRTY_BIN_FILE\" ]; then\n  #  mv $M5_SD_BUILD_DIR/$DIRTY_BIN_FILE $M5_SD_BUILD_DIR/$BIN_FILE\n  #  echo \"[++++] UpperCasedFirst() $DIRTY_BIN_FILE to $BIN_FILE\"\n  #fi\n  echo \"[INFO] Keeping bin file: $BIN_FILE\"\n  export outfile=$BIN_FILE\n}\n\n\nfunction injectupdater {\n  echo \"[INFO] Injecting target $inofile\"\n  awk '/#include <M5Stack.h>/{print;print \"#include <M5StackUpdater.h>\\nSDUpdater sdUpdater;\";next}1' $inofile > tmp && mv tmp $inofile;\n  awk '/M5.begin()/{print;print \"  if(digitalRead(BUTTON_A_PIN) == 0) { sdUpdater.updateFromFS(SD); ESP.restart(); } \";next}1' $inofile > tmp && mv tmp $inofile;\n  # the M5StackUpdater requires Wire.begin(), inject it if necessary\n  egrep -R \"Wire.begin()\" || (awk '/M5.begin()/{print;print \"  Wire.begin();\";next}1' $inofile > tmp && mv tmp $inofile);\n  # the display driver changed, get rid of default rotation in setup\n  sed -i -e 's/M5.Lcd.setRotation(0);/\\/\\//g' $inofile\n  # remove any hardcoded credentials so wifi auth can be done from another app (e.g. wifimanager)\n  sed -i -e 's/WiFi.begin(ssid, password);/WiFi.begin();/g' $inofile\n  sed -i -e 's/WiFi.begin(SSID, PASSWORD);/WiFi.begin();/g' $inofile\n  echo \"[OK] Injection successful\"\n}\n\n\nfunction newpopulatemeta {\n  # $SDCardAppNameSpace\n  echo \"***** Populating meta\"\n  IMG_NAME=${SDCardAppNameSpace}_gh.jpg\n  REPO_URL=`git config remote.origin.url`\n  REPO_OWNER_URL=`echo ${REPO_URL%/*}`\n  REPO_USERNAME=$(echo \"$REPO_URL\" | cut -d \"/\" -f4)\n  AVATAR_URL=$REPO_OWNER_URL.png?size=200\n  JSONFILE=\"$M5_SD_BUILD_DIR/json/$SDCardAppNameSpace.json\"\n  IMGFILE=\"$M5_SD_BUILD_DIR/jpg/$SDCardAppNameSpace.jpg\"\n  AVATARFILE=\"$M5_SD_BUILD_DIR/jpg/${SDCardAppNameSpace}_gh.jpg\"\n  if [ ! -f $JSONFILE ]; then\n    echo \"[WARNING] JSON Meta file $JSONFILE does not exists, will be created from the ether\"\n    mkdir -p $M5_SD_BUILD_DIR/json\n    REPO_SHORTURL=`git.io $REPO_URL`\n    if [ \"\" != \"$REPO_SHORTURL\" ]; then\n      echo \"{\\\"width\\\":120,\\\"height\\\":120, \\\"authorName\\\":\\\"@$REPO_USERNAME\\\", \\\"projectURL\\\": \\\"$REPO_SHORTURL\\\",\\\"credits\\\":\\\"$REPO_OWNER_URL\\\"}\" > $JSONFILE\n    else\n      echo \"{\\\"width\\\":120,\\\"height\\\":120, \\\"authorName\\\":\\\"@$REPO_USERNAME\\\", \\\"projectURL\\\": \\\"$REPO_URL\\\",\\\"credits\\\":\\\"$REPO_OWNER_URL\\\"}\" > $JSONFILE\n    fi\n  fi\n  if [ ! -f $AVATARFILE ]; then\n    mkdir -p $M5_SD_BUILD_DIR/jpg\n    AVATAR_URL=`sed 's/gist.//g' <<< $AVATAR_URL` # no gist in URL is valid to retrieve the profile pic\n    echo \"[WARNING] No avatar file found, will download from $AVATAR_URL and save it as $AVATARFILE\"\n    #wget --quiet $AVATAR_URL --output-document=temp\n    wget $AVATAR_URL --output-document=temp\n    convert temp -resize 120x120 -type TrueColor $AVATARFILE\n    identify $AVATARFILE\n    rm temp\n\n  fi\n  if [ ! -f $IMGFILE ]; then\n    mkdir -p $M5_SD_BUILD_DIR/jpg\n    # TODO: search in SD-Apps-folder.zip\n    echo \"[TODO] search in SD-Apps-folder.zip\"\n    echo \"[WARNING] No img file found, will use a copy of avatar file $AVATARFILE\"\n    cp $AVATARFILE $IMGFILE\n  fi\n  echo \"***** Populating successful\"\n}\n\n\n\n\nfunction populatemeta {\n  echo \"***** Populating meta\"\n  export IMG_NAME=${FILE_BASENAME}_gh.jpg\n  export REPO_URL=`git config remote.origin.url`\n  export REPO_OWNER_URL=`echo ${REPO_URL%/*}`\n  export REPO_USERNAME=$(echo \"$REPO_URL\" | cut -d \"/\" -f4)\n  export AVATAR_URL=$REPO_OWNER_URL.png?size=200\n  export JSONFILE=\"$M5_SD_BUILD_DIR/json/$FILE_BASENAME.json\"\n  export IMGFILE=\"$M5_SD_BUILD_DIR/jpg/$FILE_BASENAME.jpg\"\n  export AVATARFILE=\"$M5_SD_BUILD_DIR/jpg/${FILE_BASENAME}_gh.jpg\"\n\n  if [ -f $JSONFILE ]; then\n    echo \"JSON Meta file $JSONFILE exists, should check for contents or leave it be\"\n  else\n    if [ -f \"$M5_SD_BUILD_DIR/json/$DIRTY_FILE_BASENAME.json\" ]; then\n      echo \"[++++] UpperCasedFirst() $DIRTY_FILE_BASENAME.json, renaming other meta components\"\n      mv $M5_SD_BUILD_DIR/json/$DIRTY_FILE_BASENAME.json $JSONFILE\n      mv $M5_SD_BUILD_DIR/jpg/$DIRTY_FILE_BASENAME.jpg $IMGFILE &>/dev/null\n      mv $M5_SD_BUILD_DIR/jpg/${DIRTY_FILE_BASENAME}_gh.jpg $AVATARFILE &>/dev/null\n      sed -i -e \"s/$DIRTY_FILE_BASENAME/$FILE_BASENAME/g\" $JSONFILE &>/dev/null\n    else\n      echo \"[++++] No $JSONFILE JSON Meta file found, creating from the ether\"\n      mkdir -p $M5_SD_BUILD_DIR/json\n      mkdir -p $M5_SD_BUILD_DIR/jpg\n      export REPO_SHORTURL=`git.io $REPO_URL`\n      if [ \"\" != \"$REPO_SHORTURL\" ]; then\n        echo \"{\\\"width\\\":120,\\\"height\\\":120, \\\"authorName\\\":\\\"@$REPO_USERNAME\\\", \\\"projectURL\\\": \\\"$REPO_SHORTURL\\\",\\\"credits\\\":\\\"$REPO_OWNER_URL\\\"}\" > $JSONFILE\n      else\n        echo \"{\\\"width\\\":120,\\\"height\\\":120, \\\"authorName\\\":\\\"@$REPO_USERNAME\\\", \\\"projectURL\\\": \\\"$REPO_URL\\\",\\\"credits\\\":\\\"$REPO_OWNER_URL\\\"}\" > $JSONFILE\n      fi\n    fi\n  fi\n  cat $JSONFILE\n  # no gist in URL is valid to retrieve the profile pic\n  AVATAR_URL=`sed 's/gist.//g' <<< $AVATAR_URL`\n  if [ ! -f $AVATARFILE ]; then\n    echo \"**** Will download avatar from $AVATAR_URL and save it as $AVATARFILE\"\n    wget --quiet $AVATAR_URL --output-document=temp\n    convert temp -resize 120x120 $AVATARFILE\n    identify $AVATARFILE\n    rm temp\n  fi\n  echo \"***** Populating successful\"\n}\n"
  },
  {
    "path": ".github/tools/cron.sh",
    "content": "#!/bin/bash\n\n# set -o xtrace\n\ngit remote rm origin\ngit remote add origin https://$GITHUB_ACTOR:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY.git\ngit config --global user.name $GITHUB_ACTOR\ngit config --global user.email $GITHUB_ACTOR@users.noreply.github.com\n\nnextAW=`curl -L --silent $WORKFLOW_URL | jq -r .need_workflow[0]`\nif [[ \"$nextAW\" == \"\" || \"$nextAW\" == \"null\" ]]; then\n  echo \"No workflow in queue\"\n  exit 0;\nfi\nnextURL=`echo \"$nextAW\" | jq  -r .workflow_url`;\nif [[ \"$nextURL\" == \"\" || \"$nextURL\" == \"null\" ]]; then\n  echo \"No workflow_url in first queued item, something wrong on remote? JSON chunk: $nextAW\"\n  exit 0;\nfi\n\nworkflowFilename=\"${nextURL##*/}\"\n\ncd .github/workflows\n\nif [ -f $workflowFilename ]; then\n  echo \"Deleting $workflowFilename\"\n  git rm --cached $workflowFilename\n  git commit -m \"Auto-removed $workflowFilename from cron workflow\"\nfi\n\nwget \"$nextURL\" --output-document=$workflowFilename\ntouch $workflowFilename\ngit add $workflowFilename\n# TODO: also remove unwanted/obsoleted workflow files\ngit commit -m \"Auto-committed from cron workflow\"\ngit push -u origin master\n\n# now generate a comment if necessary\ncomments_url=`echo \"$nextAW\" | jq  -r .comments_url`;\nif [[ \"$comments_url\" == \"\" || \"$comments_url\" == \"null\" ]]; then\n  echo \"This workflow has no comments_url\"\n  exit 0;\nfi\n\n# and post the comment\ncommentMessage='The app has been scheduled for rebuild'\n\n# errorless jq call\nallComments=`curl -L --silent $comments_url | jq -r \".[]|. as \\\\$in|try (try .body catch error( {\\\"error\\\": \\\\$in })) catch {}\"`\n# echo \"Captured comments: $allComments\"\n\n# avoid posting dupe comment\nif [[ $allComments == *\"$commentMessage\"* ]]; then\n  echo \"Rebuild comment already posted\"\nelse\n  jsonData='{\"body\":\"The app has been scheduled for rebuild\"}'\n  curlAuth=\"Authorization: token $GITHUB_TOKEN\"\n  HTTP_RESPONSE=$(curl -X POST -H \"$curlAuth\" -H \"Content-Type: application/json\" --data \"$jsonData\" -L --silent --write-out \"HTTPSTATUS:%{http_code}\" $comments_url)\n  HTTP_BODY=$(echo $HTTP_RESPONSE | sed -e 's/HTTPSTATUS\\:.*//g')\n  HTTP_STATUS=$(echo $HTTP_RESPONSE | tr -d '\\n' | sed -e 's/.*HTTPSTATUS://')\n  if [[ \"$HTTP_STATUS\" == \"201\" ]]; then\n    echo \" ---> GitHub server HTTP response to POST Comment: $HTTP_STATUS\"\n  else\n    echo \" ---> Comment post failed, HTTP_RESPONSE : $HTTP_RESPONSE\"\n  fi\nfi\n"
  },
  {
    "path": ".github/tools/deploy.sh",
    "content": "#!/bin/bash\n\njson_escape () {\n    printf '%s' \"$1\" | python -c 'import json,sys; print(json.dumps(sys.stdin.read()))'\n  #printf '%s' \"$1\" | php -r 'echo json_encode(file_get_contents(\"php://stdin\"));'\n}\n\nset -e\n\n#Cmdline options\n# -t: tag (*_RC* determines prerelease version, can be overriden be -p)\n# -a: GitHub API access token\n# -s: GitHub repository slug (user/repo)\n# -p: prerelease true/false\n# -f: files to upload (ie assets. delim = ';', must come quoted)\n# -d: directory to upload (by adding dir contents to assets)\nwhile getopts \":t:,:a:,:s:,:p:,:f:,:d:,:b:\" opt; do\n  case $opt in\n  t)\n    varTagName=$OPTARG\n    echo \"TAG: $varTagName\" >&2\n    ;;\n  a)\n    varAccessToken=$OPTARG\n    echo \"ACCESS TOKEN: $varAccessToken\" >&2\n    ;;\n  s)\n    varRepoSlug=$OPTARG\n    echo \"REPO SLUG: $varRepoSlug\" >&2\n    ;;\n  p)\n    varPrerelease=$OPTARG\n    echo \"PRERELEASE: $varPrerelease\" >&2\n    ;;\n  f)\n    varAssets=$OPTARG\n    echo \"ASSETS: $varAssets\" >&2\n    ;;\n  d)\n    varAssetsDir=$OPTARG\n    echo \"ASSETS DIR: $varAssetsDir\" >&2\n    ;;\n  b)\n    varAssetsPlatform=$OPTARG\n    echo \"ASSETS PLATFORM: $varAssetsPlatform\" >&2\n    ;;\n  \\?)\n    echo \"Invalid option: -$OPTARG\" >&2\n    exit 1\n    ;;\n  :)\n    echo \"Option -$OPTARG requires an argument.\" >&2\n    exit 1\n    ;;\n  esac\ndone\n\nif [ -z $varAssetsPlatform ]; then\n  varAssetsPlatform=m5stack\nfi\n\n# use TravisCI env as default, if available\nif [ -z $varTagName ] && [ ! -z $TRAVIS_TAG ]; then\n  varTagName=$TRAVIS_TAG\nfi\n\nif [ -z $varTagName ]; then\n  # varTagName=`$WORK_SPACE/tools/git-describe.awk https://github.com/tobozo/M5Stack-App-Registry`\n  varTagName=`$WORK_SPACE/tools/git-describe.awk $WORK_SPACE`\n  if [ -z $varTagName ]; then\n    echo \"No tag name available => aborting\"\n    exit 1\n  fi\nfi\n\n#Check tag name for release/prerelease (prerelease tag contains '_RC' as for release-candidate. case-insensitive)\nshopt -s nocasematch\nif [ -z $varPrerelease ]; then\n  if [[ $varTagName == *-RC* ]]; then\n    varPrerelease=true\n  else\n    varPrerelease=false\n  fi\nfi\nshopt -u nocasematch\n\n#\n# Prepare Markdown release notes:\n#################################\n# - annotated tags only, lightweight tags just display message of referred commit\n# - tag's description conversion to relnotes:\n#   first 3 lines (tagname, commiter, blank): ignored\n#  4th line: relnotes heading\n#  remaining lines: each converted to bullet-list item\n#  empty lines ignored\n#  if '* ' found as a first char pair, it's converted to '- ' to keep bulleting unified\necho\necho Preparing release notes\necho -----------------------\necho \"Tag's message:\"\n\nrelNotesRaw=`git show -s --format=%b $varTagName`\nreadarray -t msgArray <<<\"$relNotesRaw\"\narrLen=${#msgArray[@]}\n\n#process annotated tags only\nif [ $arrLen > 3 ] && [ \"${msgArray[0]:0:3}\" == \"tag\" ]; then\n  ind=3\n  while [ $ind -lt $arrLen ]; do\n    if [ $ind -eq 3 ]; then\n      releaseNotes=\"#### ${msgArray[ind]}\"\n      releaseNotes+=$'\\r\\n'\n    else\n      oneLine=\"$(echo -e \"${msgArray[ind]}\" | sed -e 's/^[[:space:]]*//')\"\n\n      if [ ${#oneLine} -gt 0 ]; then\n        if [ \"${oneLine:0:2}\" == \"* \" ]; then oneLine=$(echo ${oneLine/\\*/-}); fi\n        if [ \"${oneLine:0:2}\" != \"- \" ]; then releaseNotes+=\"- \"; fi\n        releaseNotes+=\"$oneLine\"\n        releaseNotes+=$'\\r\\n'\n\n        #debug output\n        echo \"   ${oneLine}\"\n      fi\n    fi\n    let ind=$ind+1\n  done\nfi\n\necho \"$releaseNotes\"\n\n# - list of commits (commits.txt must exit in the output dir)\ncommitFile=$varAssetsDir/commits.txt\nif [ -s \"$commitFile\" ]; then\n\n  releaseNotes+=$'\\r\\n##### Commits\\r\\n'\n\n  echo\n  echo \"Commits:\"\n\n  IFS=$'\\n'\n  for next in `cat $commitFile`\n  do\n    IFS=' ' read -r commitId commitMsg <<< \"$next\"\n    commitLine=\"- [$commitId](https://github.com/$varRepoSlug/commit/$commitId) $commitMsg\"\n    echo \"   $commitLine\"\n\n    releaseNotes+=\"$commitLine\"\n    releaseNotes+=$'\\r\\n'\n  done\n  rm -f $commitFile\nfi\n\n# Check possibly existing release for current tag\necho\necho \"Processing GitHub release record for $varTagName:\"\necho \"-------------------------------------------------\"\n\necho \" - check $varTagName possible existence...\"\n\n# (eg build invoked by Create New Release GHUI button -> GH default release pack created immediately including default assests)\nHTTP_RESPONSE=$(curl -L --silent --write-out \"HTTPSTATUS:%{http_code}\" https://api.github.com/repos/$varRepoSlug/releases/tags/$varTagName?access_token=$varAccessToken)\nif [ $? -ne 0 ]; then echo \"FAILED: $? => aborting\"; exit 1; fi\n\nHTTP_BODY=$(echo $HTTP_RESPONSE | sed -e 's/HTTPSTATUS\\:.*//g')\nHTTP_STATUS=$(echo $HTTP_RESPONSE | tr -d '\\n' | sed -e 's/.*HTTPSTATUS://')\necho \" ---> GitHub server HTTP response: $HTTP_STATUS\"\n\n# if the release exists, append/update recent files to its assets vector\nif [ $HTTP_STATUS -eq 200 ]; then\n  releaseId=$(echo $HTTP_BODY | jq -r '.id')\n  echo \" - $varTagName release found (id $releaseId)\"\n\n  #Merge release notes and overwrite pre-release flag. all other attributes remain unchanged:\n\n  # 1. take existing notes from server (added by release creator)\n  releaseNotesGH=$(echo $HTTP_BODY | jq -r '.body')\n\n  # - strip possibly trailing CR\n  if [ \"${releaseNotesGH: -1}\" == $'\\r' ]; then\n    releaseNotesTemp=\"${releaseNotesGH:0:-1}\"\n  else\n    releaseNotesTemp=\"$releaseNotesGH\"\n  fi\n  # - add CRLF to make relnotes consistent for JSON encoding\n  releaseNotesTemp+=$'\\r\\n'\n\n  # 2. #append generated relnotes (usually commit oneliners)\n  releaseNotes=\"$releaseNotesTemp$releaseNotes\"\n\n  # 3. JSON-encode whole string for GH API transfer\n  releaseNotes=$(json_escape \"$releaseNotes\")\n\n  # 4. remove extra quotes returned by python (dummy but whatever)\n  releaseNotes=${releaseNotes:1:-1}\n\n  #Update current GH release record\n  echo \" - updating release notes and pre-release flag:\"\n\n  curlData=\"{\\\"body\\\": \\\"$releaseNotes\\\",\\\"prerelease\\\": $varPrerelease}\"\n  echo \"   <data.begin>$curlData<data.end>\"\n  echo\n  #echo \"DEBUG: curl --data \\\"$curlData\\\" https://api.github.com/repos/$varRepoSlug/releases/$releaseId?access_token=$varAccessToken\"\n\n  curl --data \"$curlData\" https://api.github.com/repos/$varRepoSlug/releases/$releaseId?access_token=$varAccessToken\n  if [ $? -ne 0 ]; then echo \"FAILED: $? => aborting\"; exit 1; fi\n\n  echo \" - $varTagName release record successfully updated\"\n\n#... or create a new release record\nelse\n  releaseNotes=$(json_escape \"$releaseNotes\")\n  releaseNotes=${releaseNotes:1:-1}\n\n  echo \" - release $varTagName not found, creating a new record:\"\n\n  curlData=\"{\\\"tag_name\\\": \\\"$varTagName\\\",\\\"target_commitish\\\": \\\"master\\\",\\\"name\\\": \\\"v$varTagName\\\",\\\"body\\\": \\\"$releaseNotes\\\",\\\"draft\\\": false,\\\"prerelease\\\": $varPrerelease}\"\n  echo \"   <data.begin>$curlData<data.end>\"\n\n  #echo \"DEBUG: curl --data \\\"${curlData}\\\" https://api.github.com/repos/${varRepoSlug}/releases?access_token=$varAccessToken | jq -r '.id'\"\n  releaseId=$(curl --data \"$curlData\" https://api.github.com/repos/$varRepoSlug/releases?access_token=$varAccessToken | jq -r '.id')\n  if [ $? -ne 0 ]; then echo \"FAILED: $? => aborting\"; exit 1; fi\n\n  echo \" - $varTagName release record successfully created (id $releaseId)\"\nfi\n\n# Assets defined by dir contents\nif [ ! -z $varAssetsDir ]; then\n  varAssetsTemp=$(ls -p $varAssetsDir | grep -v / | tr '\\n' ';')\n  for item in $(echo $varAssetsTemp | tr \";\" \"\\n\")\n  do\n    varAssets+=$varAssetsDir/$item;\n    varAssets+=';'\n  done\nfi\n\n\n\nfunction gen_json_hook {\n\n  if [[ \"$1\" == \"\" ]]; then\n    echo \"[ERROR] No json path provided\"\n    exit 1\n  fi\n\n  tee $1 << XXX\n{\n    \"action\":\"$json_action\",\n    \"app_slug\":\"$REMOTE_APP_SLUG\",\n    \"repo_slug\":\"$REMOTE_REPO_SLUG\",\n    \"base_name\":\"$filename\",\n    \"tag\":\"$varTagName\",\n    \"release_id\":\"$releaseId\",\n    \"remote_app_url\":\"$REMOTE_APP_URL\",\n    \"app_path\":\"$APP_PATH\",\n    \"platform\":\"$varAssetsPlatform\",\n    \"data\":\"\"\n}\nXXX\n\n}\n\n\n#Upload additional assets\nif [ ! -z $varAssets ]; then\n  echo\n  echo \"Uploading assets:\"\n  echo \"-----------------\"\n  echo \" Files to upload:\"\n  echo \"   $varAssets\"\n  echo\n\n  curlAuth=\"Authorization: token $varAccessToken\"\n  for filename in $(echo $varAssets | tr \";\" \"\\n\")\n  do\n    echo \" - ${filename}:\"\n    base_name=$(basename $filename)\n    json_action=\"binary-added\"\n\n    if [[ `echo $base_name | grep M5Burner` != '' ]]; then\n      json_action=\"firmware-added\"\n    fi\n    if [[ `echo $base_name | grep OdroidFW` != '' ]]; then\n      json_action=\"firmware-added\"\n    fi\n\n    gen_json_hook /tmp/payload.json\n    jsonCode=`cat /tmp/payload.json`\n    rm /tmp/payload.json\n\n    php $WORK_SPACE/tools/hooker.php \"${GITHUB_HOOK_URL}\" \"action-deploy\" \"$jsonCode\" \"${GITHUB_HOOK_SECRET}\"\n\n    if [ $? -eq 0 ]; then\n      echo \"yay\";\n      # TODO: confirm build success\n    else\n      echo \"nay\";\n    fi\n\n  done\nfi\n"
  },
  {
    "path": ".github/tools/git-describe.awk",
    "content": "#!/usr/bin/awk -f\nBEGIN {\n  if (ARGC != 2) {\n    print \"git-describe-remote.awk $GITHUB_REPOSITORY\"\n    exit\n  }\n  FS = \"[ /^]+\"\n  while (\"git ls-remote \" ARGV[1] \"| sort -Vk2\" | getline) {\n    if (!sha)\n      sha = substr($0, 1, 7)\n    tag = $3\n  }\n  while (\"curl -s \" ARGV[1] \"/releases/tag/\" tag | getline)\n    if ($3 ~ \"commits\")\n      com = $2\n  printf com ? \"%s-%s-g%s\\n\" : \"%s\\n\", tag, com, sha\n}\n"
  },
  {
    "path": ".github/tools/hooker.php",
    "content": "<?php\n\n/*\n\nBash usage example:\n\n  php hooker.php [Hook URL] [Event Name] [JSON Code] [Hook Secret]\n\n  php hooker.php \"${hookURL}\" \"${eventName}\" \"${jsonCode}\" \"${hookSecret}\";\n  if [ $? -eq 0 ]; then\n    echo \"yay\";\n  else\n    echo \"nay\";\n  fi\n\n*/\n\n\nfunction usage( $msg ) {\n  if( trim( $msg ) !='' ) {\n    echo \"\\n$msg\\n\";\n  }\n  echo \"\\n[Hooker Usage]\\n\";\n  //echo '    php hooker.php \"${hookURL}\" \"${eventName}\" \"${jsonCode}\" \"${hookSecret}\"'.\"\\n\";\n  echo \"    php hooker.php [Hook URL] [Event Name] [JSON Code] [Hook Secret]\\n\";\n  echo \"    php \".basename(__FILE__).' \"https://my.site/hook\" \"my-custom-event\" \\'{\"action\":\"custom\",\"data\":[\"blah\",\"bleb\",\"foo\",\"bar\"]}\\''.\" \\\"My Secret pass\\\"\\n\\n\";\n}\n\n\nif( basename( $argv[0] ) !== basename( __FILE__ ) ) {\n  usage( \"[ERROR] API Mode only !!\");\n  exit(1);\n}\n\nif( !isset($argv[1]) || !isset($argv[2]) || !isset($argv[3]) || !isset($argv[4]) ) {\n  usage( \"[ERROR] Missing arg\");\n  exit(1);\n}\n\n$hookURL    = trim( $argv[1] );\n$eventName  = trim( $argv[2] );\n$payload    = trim( $argv[3] );\n$hookSecret = trim( $argv[4] );\n\nif( empty( $payload ) || empty ( $eventName ) || empty( $hookSecret ) || empty( $hookURL ) ) {\n  usage( \"[ERROR] Empty arg\" );\n  exit(1);\n}\n\n$json = json_decode( $payload, true );\n\nif(! $json ) {\n  usage(\"\\n[ERROR] Payload has invalid json: $payload\\n\\n\");\n  exit(1);\n}\nif( !isset( $json['action'] ) ) {\n  usage(\"\\n[ERROR] Payload has no action: $payload\\n\\n\");\n  exit(1);\n}\nif( trim( $json['action'] ) == '' ) {\n  usage(\"\\n[ERROR] Payload has empty action: $payload\\n\\n\");\n  exit(1);\n}\n// turn full path + empty data into basename + b64 encoded data\nif( isset( $json['base_name'] ) && file_exists( $json['base_name'] ) ) {\n  $file_path = $json['base_name'];\n  $json['data'] = 'fakedata';\n  $json['base_name'] = basename( $json['base_name'] );\n  echo \"file $json[base_name] is reachable :-)\\n\";\n} else {\n  echo \"file $json[base_name] is not reachable :-(\\n\";\n}\n\n// re-encode\n$payload = json_encode( $json );\n$algo = 'sha256'; // go strong :-)\n\n$rawPost = 'payload='.$payload; // simulate rawPost for GH hook compliance\n$rawPostSize = strlen( $payload );\n$hashed = hash_hmac($algo, $rawPost, $hookSecret);\n\n$headers = sprintf( '-H \"Content-Type: multipart/form-data\" -H \"User-Agent: PHP-M5-Registry-UA\" -H \"X-Github-Event: %s\" -H \"X-Hub-Signature: %s=%s\"',\n  $eventName,\n  $algo,\n  $hashed\n);\n$cmd = \"curl -X POST $headers -F 'payload=$payload' -F binary=@\\\"$file_path\\\" $hookURL\";\n\necho \"[Hooker CMD]\\n\\n\\n\\n\\n\".$cmd.\"\\n\\n\\n\\n\\n\";\nexec( $cmd, $out );\necho \"[Hook RESPONSE]\\n\\n\\n\\n\\n\".implode(\"\\n\", $out).\"\\n\\n\\n\\n\\n\"; exit;\n\n$context = stream_context_create( $opts );\n$response = file_get_contents( $hookURL, false, $context );\n\nif( ! $response ) {\n  usage( \"[ERROR] Hooker cannot post comment (size: $rawPostSize): bad gh api response\\n\\n\" );\n  exit(1);\n}\n\nif( !preg_match(\"/200/\", $http_response_header[0]) ) {\n  usage( \"[ERROR] Hooker got bad response code from post (size: $rawPostSize): $response / \".print_r($http_response_header, 1) );\n  exit(1);\n} else {\n  echo json_encode([ 'headers' => $http_response_header, 'response_body' => $response ]);\n  exit(0);\n}\n"
  },
  {
    "path": ".github/tools/ino2fw.sh",
    "content": "#!/bin/bash\n\n# Credits: https://github.com/lstux/OdroidGO/\n\nVERBOSE=0\nARDUINO_BOARD=\"esp32:esp32:odroid_esp32\"\nSKETCHBOOK_PATH=\"\"\n\nusage() {\n  exec >&2\n  [ -n \"${1}\" ] && printf \"Error : ${1}\\n\"\n  printf \"Usage : $(basename \"${0}\") [options] {sketch_dir|sketch_file.ino|sketch_file.bin}\\n\"\n  printf \"  Create a .fw file for Odroid-Go from arduino sketch\\n\"\n  printf \"options:\\n\"\n  printf \"  -b build_path  : build in build_path [/tmp/ino2fw]\\n\"\n  printf \"  -t tile.png    : use specified image as tile (resized to 86x48px)\\n\"\n  printf \"  -l label       : set application label\\n\"\n  printf \"  -d description : set application description\\n\"\n  printf \"  -v             : increase verbosity level\\n\"\n  printf \"  -h             : display this help message\\n\"\n  exit 1\n}\n\nprn() { printf \"$(date '+%Y/%m/%d %H:%M:%S') [$1] \"; printf \"$2\\n\"; }\nmsg() { prn MSG \"$1\"; }\nlog() { [ ${VERBOSE} -ge 1 ] || return 0; prn LOG \"$1\" >&2; }\ndbg() { [ ${VERBOSE} -ge 2 ] || return 0; prn DBG \"$1\" >&2; }\nerr() { prn ERR \"$1\" >&2; [ -n \"${2}\" ] && exit ${2}; }\n\ncheckbin() {\n  local bin=\"${1}\" package=\"${2}\"\n  which \"${1}\" >/dev/null 2>&1 && return 0\n  err \"can't find '${1}' binary in PATH, make sure that '${2}' package is installed\" 2\n}\n\n\nFW_TILE=\"\"\nFW_LABEL=\"\"\nFW_DESC=\"\"\nBUILD_PATH=\"/tmp/ino2fw\"\nwhile getopts b:t:l:d:vh opt; do case \"${opt}\" in\n  b) BUILD_PATH=\"${OPTARG}\";;\n  t) [ -e \"${OPTARG}\" ] || usage \"[ERROR] FW_TILE unreachable: ${OPT_ARG}\"; FW_TILE=\"${OPTARG}\";;\n  l) FW_LABEL=\"${OPTARG}\";;\n  d) FW_DESC=\"${OPTARG}\";;\n  v) VERBOSE=$(expr ${VERBOSE} + 1);;\n  *) usage \"[WARNING] strange opt arg: ${OPT_ARG}\";;\nesac; done\nshift $(expr ${OPTIND} - 1)\n\nif [ -d \"${1}\" ]; then\n  SKETCH_DIR=\"$(readlink -m \"${1}\")\"\n  SKETCH_FILE=\"${SKETCH_DIR}/$(basename \"${SKETCH_DIR}\").ino\"\nelse\n  if [ -e \"${1}\" ]; then\n    SKETCH_FILE=\"$(readlink -m \"${1}\")\"\n    SKETCH_DIR=\"$(dirname \"${1}\")\"\n  else\n    usage \"[ERROR] arg 1 ${1} does not exist\"\n  fi\nfi\n[ -e \"${SKETCH_FILE}\" ] || usage \"[ERROR] No '$(basename \"${SKETCH_FILE}\")' file found in '${SKETCH_DIR}'\"\n\n# TODO : what if no .arduino15/preferences.txt is available?\nif ! [ -n \"${SKETCHBOOK_PATH}\" ]; then\n  if [ -e \"${HOME}/.arduino15/preferences.txt\" ]; then\n   SKETCHBOOK_PATH=\"$(sed -n 's/^sketchbook.path=//p' \"${HOME}/.arduino15/preferences.txt\")\"\n   log \"set sketchbook.path to '${SKETCHBOOK_PATH}'\"\n  fi\nfi\n\n# Create ${BUILD_PATH} directory (will be used as build.path for arduino)\nif ! [ -d \"${BUILD_PATH}\" ]; then\n  dbg \"creating '${BUILD_PATH}' directory\"\n  install -d \"${BUILD_PATH}\"\nfi\n\nif [[ $SKETCH_FILE == *\".bin\" ]]; then\n  cp ${SKETCH_FILE} ${BUILD_PATH}/\n  SKETCH_BIN=\"$(basename \"${SKETCH_FILE}\")\"\nfi\n\n# Try to find a tile if none was specified on cmdline\nif ! [ -e \"${FW_TILE}\" ]; then\n  FW_TILE=\"${SKETCH_DIR}/$(basename \"${SKETCH_DIR}\").png\"\n  if [ -e \"${FW_TILE}\" ]; then\n    log \"using '${FW_TILE}' as tile\"\n  else\n    FW_TILE=\"${SKETCH_DIR}/tile.png\"\n    if [ -e \"${FW_TILE}\" ]; then\n      log \"using '${FW_TILE}' as tile\"\n    else\n      log \"no tile specified, using default\"\n      checkbin base64 coreutils\n      FW_TILE=\"${BUILD_PATH}/tile.png\"\n      sed -e '0,/^### EMBEDDED_BASE64_START tile.png/d' -e '/^### EMBEDDED_BASE64_END tile.png/,$d' \"${0}\" | base64 --decode > \"${FW_TILE}\"\n    fi\n  fi\nfi\n\n# Set application label if none was specified on cmdline\nif ! [ -n \"${FW_LABEL}\" ]; then\n  FW_LABEL=\"$(sed -n 's/^[/\\* ]*Label *: *//p' \"${SKETCH_FILE}\")\"\n  if ! [ -n \"${FW_LABEL}\" ]; then\n    FW_LABEL=\"$(basename \"${SKETCH_DIR}\")\"\n    read -p \"Enter application label [${FW_LABEL}] : \" label\n    [ -n \"${label}\" ] && FW_LABEL=\"${label}\"\n  else\n    log \"using label found in .ino file : ${FW_LABEL}\"\n  fi\nfi\n\n# Set application description if none was specified on cmdline\nif ! [ -n \"${FW_DESC}\" ]; then\n  FW_DESC=\"$(sed -n 's/^[/\\* ]*Description *: *//p' \"${SKETCH_FILE}\")\"\n  if ! [ -n \"${FW_DESC}\" ]; then\n    FW_DESC=\"$(basename \"${SKETCH_DIR}\") for Odroid-GO\"\n    read -p \"Enter application description [${FW_DESC}] : \" desc\n    [ -n \"${desc}\" ] && FW_DESC=\"${desc}\"\n  else\n    log \"using description found in .ino file : ${FW_DESC}\"\n  fi\nfi\n\nif [[ \"$SKETCH_BIN\" == \"\" ]]; then\n\n  echo \"Compiling ino sketch\"\n  checkbin arduino arduino\n  log \"building sketch with arduino\"\n  dbg \"arduino --pref build.path=${BUILD_PATH} --pref sketchbook.path='${SKETCHBOOK_PATH}' --verify '${SKETCH_FILE}'\"\n  arduino --pref build.path=${BUILD_PATH} --pref sketchbook.path=\"${SKETCHBOOK_PATH}\" --verify \"${SKETCH_FILE}\" || exit 4\n\n  # Produce fw file using mkfw\n  SKETCH_BIN=\"$(basename \"${SKETCH_DIR}\").ino.bin\"\n\nelse\n\n  echo \"Skipping build as a bin name has been provided\";\n\nfi\n\n# Create \"raw\" tile using ffmpeg\ncheckbin ffmpeg ffmpeg\nlog \"creating tile.raw from ${FW_TILE}\"\ndbg \"ffmpeg -i ${FW_TILE} -vf scale=86:48 -f rawvideo -pix_fmt rgb565 '${BUILD_PATH}/tile.raw'\"\nffmpeg -i ${FW_TILE} -vf scale=86:48 -f rawvideo -pix_fmt rgb565 \"${BUILD_PATH}/tile.raw\" || exit 8\n\n\nBIN_SIZE=\"$(du -sb \"${BUILD_PATH}/${SKETCH_BIN}\" | awk '{print $1}')\"\nlog \"building fw file\"\ndbg \"mkfw '${FW_DESC}' tile.raw 0 16 ${BIN_SIZE} '${FW_LABEL}' '${SKETCH_BIN}'\"\ncd \"${BUILD_PATH}\" && mkfw \"${FW_DESC}\" tile.raw 0 16 ${BIN_SIZE} \"${FW_LABEL}\" \"${SKETCH_BIN}\" || exit 16\n\nlog \"copying fw file to '${SKETCH_DIR}'\"\ncp firmware.fw \"${SKETCH_DIR}/${FW_LABEL}.fw\" || exit 32\n\nls -la\n\nexit 0\n\n\n### EMBEDDED_BASE64_START tile.png\niVBORw0KGgoAAAANSUhEUgAAAFYAAAAwCAYAAACL+42wAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAwRSURBVHhe7Zt5dBVFFsa/J7IlBAIkIYEgBILIJiCCih4VBCQIKIoCAgLuC+466Og4o+PMiAujsiiMIqhRUA6LoGxRYBSiAoIjKLIFw5awKdkTIJnvVtc7vqW635L3jv/kd06fJNX9qm99feveW/U6riqCGiLOWfpnDRGmRtgoUSNslKgRNkrUCBslIlIVlJ85jWX79uDbI4ex++SvOF5eikp2G1+nHlo3bITuCUnon5qG1AZx+hN/DPklxVi5PwffHc1DTuFJ/FZeBpfLhab16iO9YWP0SkrB4NbpqFurlv5E+FRL2L9++yVe+X4jik8cBWrXof9zAshBYxXStRyVlcDpU0CduhjZsRuevOBinN80ybomyvxCAZ/btB6zt20BSkuAs88GRDix0dNOsVGOUxVo0DQRj3TthWd7XmadD4OwhB2dtRQfbM4G6talobUtQ4NBCXyaLl6Ghk0S8N5VgzE0rZ0+GVmy8w5i5OpPkHtoP1CvnmWnPPRgOHPGcoTycozucQne7zdEnwiekISdt+snjFqSaXmnHO4nHg5ifFkJGjVOwObh49G2Ubw+UT0KKyrQY8Ec7DqwD4iJpZjaO8NBpGF/OF2BD68bjZHpHfSJwAQt7KBPP8by7ZxODRpWT1BfROCiAozltHuXHlwdnmFo+vsXnwGxFFQ8NFKIRLQxo1N3fHbNjbrRmaCEbfP+m8hhwAeDfFQQExgeatWPQeHtD6N+GKKkvjsdB4/mA+wjog/eE86wtMQU7B1zt26wJ6Cw7T6Yhd3M9qjLOOWEdCPxs5IeWMVYKr26GNNqycGEEUx8Y3UBZu71t9yH3sktdKMzB4oK0fI/r1j3qB3EA5E4L/dxJyt5BmKnhAxJbIEeCh2gHauHnTffqRvMOAo7isF/3g+bLS+wQ6YybyYJLKNdR1yWkopWDBdn0cA8irSJnr5gz8+oYBmmHo4M3sl4MYfXzr1+LG5p31k3mtnABHXp3GlWeHJ6cOqhSzKinbFxuImxslezFDSPaYAzPJdbWICv8g5g+a4frfGInU4JmdXFqC498EH/obrBH1th1x7MRZ/33wDiGpmFkI9xasTGxeOjAddiUKu2+oQZEXnS12vx7qYNgQWWvgt+w5SMG/Bw15660ZtPf9mDwZxNzH7OD0oE5b2HdLkQUy7ti3S53gHpd8SqJSgu/I2hzyasiH0s49aMvQdXNj9HN3pjK6zrtefsSxTd8fMsQ57q0Vs3Bs+QzxZgmcwEJ0/T4k4bfBPu63yBbrRYkbsXGZkznUWVzxcXohsL/uxhY1FPpnkIPL95A/7CstLWsVTpeApVDz6jG7wxjuq5jeutDzqI+s34B8ISVVg6aDg23/ogi3F6kxwmZDAN4zFx2UdYkrNLN0KtmjIyA3iqitVFWHjDOGy5cULIogpPc2zfjL9fjVWN2RfRhvlEFh8mjB7r+vffrOJfgronWtTPR9+NvqmtdGP1OOe9Gdh/7Ih9xSH3pOfuu+9JJNWPRcwUeohTyVdRjtrsq/zOx3iJzTUh8Dnr4X4yO0yeK87H+1U9TL188HPJRXt36mxp8FZ2Mo71ZqREFXLH3osBUnjTw4zIYDioNJZ8SXNeZ/JpYC9qWSnaJSaj4q7HHUVdum83sijYcoaUvXxoTlyV2lqNWcbuh/LaSizKoWY++Kk3Y/t31qrKF/EcllFz+l6jGyLHyiEjMILJxVZcDqCK9y7iElOVRSYoas+WaQHLIGHo3OnoP382BtET39y+Vbfao8YszmYKCdRqhuxD+OAnbBZLI1XP+cKl3WMXXan/iDzzWFkM7djN2igxoWpNwywSWEZ1bdEK3w4fpxsCEMNsL8tdlpGxwdS+5NGLOXZZ3vpCrbL27NB//I6XpQXi7vJh0zQ6VYFHbEqfSLGE5VV3ep1sfgQN7WreOAFbb5qgG0KjQZDCqrHzXn6IVtRMaeeBV/Jae4i164dvWU/TE7lEgvRDf9UN/jSd/RqaMbk4UcZs3Z8xa+aVA3WLmfqzXkaZGCqrKSekmCdVE/+sftpxz7qVWLE/h0vls+lJLmw/xuW59M3pncCxJvM4U1mF/NJiHJ/AasUmPLtefVZtffo5HuvkNaNu96ppvTz2IJeHRm+lAa0Tmuk/zJzIP4SfjuU7HrLfsDtAshBKmdGV15pimhs5x7BRINcGYI9UFbz3T0fzfxdVYGg5RlG2HeE52ndClu42ogqtE5KsWOsLNVPaeeAlbLGsUkzCchCNA+0VRJg5sotkysRueG7GoBsQV8eQaKOE0sD0sKmZ0s4DL2HryZM0fhAoMsUXD5o0a44O9GqnI42lUDqL/mAYz2WlsTpxw3P3fvGp/sOZtryn3LtDYjN04k+1gBB0KOicxHO0r0lSCsdvnTJRJIsZk0dTM6WdB14xdhXj0NUfveMfY1WpUclY9pRuiC4pc6ch7yRDRqDEwoGmNIrHoXETdUNwuF7iOGSRwWri5T4ZeLRrL33GGdfUf6jw4VedMJysZPIcIIlX43VFl6aJ5hgiHZWUoFS2BaPMgKXzkXfiWGBRBV5zmNdevWy+bggd5YVBoMYupaCp5KNm54t2HnhdlRKjVzWmcMBYNn0bFw9R5I61y7H6522hbajz2lU7tqnPBg2dRLxMhCoOUtjp2zYrDfwQrahZsmjngd9eQc8Fc7Hp8H7/rza0J1dxzR4NHvhyNaZ+vdaaoibcZpqSq1BUgPsv6YPXL+unG+yRJa2UXqc4pvbxTdAmiLjvmv4v6xdfj2XSujClJTb6LE78/PqOjl3NO07SIWPS5C1f64bIMSZrKUVd5yyqPFg5TLNJ4GenZq9VfQViSOt09GM9nXFOm6BEnfwdxyyb5KYwQK2UZj74XXmnLCsla5oGwGn3xKpP1IsPkaI7k2Xm9xspjM3LHGIH69CtI2/HFh7yu724caqvCz6eoxuqj4z1idWfmMOT2EGtlGY+GB4BcOuFsptjs3yj8ckzX6p2IssrKYJrxgvYKmHHtwpxI4YXnsSsoaPQlcV5Nx4zh4603yMV2NcWriCl7+o6gIxRxqoeuikEUSOllQGjsG+zBMEpm5WPTAcG8Rgu79YfPqAbQ+PBr7KQIqWLTG1ZIpqQezNuPt1nkNdUE+94WuzjOVtxpU/2nTz1eTzEe4XDVxybjFElLFMIkHtTI6WVAduvZqQCmLhiofryzYh8rLAAfTp0weKB16OhnUAeSHx+gut2hWnN7Ub3PemKAXhBdpUMTGI8ffG/q4A4h01v6UeWxjw9+YqB+FP3i/QJe2Qz5brlC7Fmxw/OfRcXYjrHfa/P10ZubIUVOs17Cz/mH3L2KlXfFaMNk8H49p1xqce3tIeKi9SLch/v2YHs3Tus2lT6MnmAG+mTcfTFgcPweDdnIV7c8g0mrVykvsKxFUCQmSHLYyaa3ukdMLxte/UCXPNY/S0tvV88dO7PP2DvgV8A2UySrVO7PtlXR640t0vMt8FRWMH1xmTrm06nlyikC3mfQESWHSd3l2KY+r6ehyz5nAYvSB/01MUjbsO1Qb7TtThnJ4bNn215l90muBuxSxKz2OlZYZzltpM2yk8nO7UWVfdM0g1mAgoruKb90xq009q9OogJshfBAR287SF6kk34seEgp2Xq269a/YiNgR5guIiNtWqxlnfephSCElZIfOd1HPv1uFV2RNJw8ZyiQlzOMLLuupt1Y3hcvjgTX+7cbuUFp3ATKiJRWSkSGjfF0QkP6EZnghZWuIuJZ1b2GstwmTbVQQSVopse9vn1Y9C3RWS+oFTfqi7KtLxLtvmqK7CEDc6Iu7iqe/OKq3VjYEISVpB3pXovfA/781hqyZsiTkHeF7mVxGBJJJxSwWbqcFCJbd0K636SMOWVoVDsFEHLStCSy9XsYWPQIsS30UMW1s3/jh/Boxu+QJaUJRLwxXBlPD3EPQDpWl6QE++UAdKLzuMyUl6GGH1uJ+uaKJPJ0CBvtezI3WvFX7FRvNhkp9goB/NJv/O6YErvq6wdvzAIW1hPZPqtyM3x+h8E6TaeUzEtrhF6JCartfmwNufqT/wxyDsTq2mr+38QfmUokrIwgXmjrfwPQrMUZLRsE5H3JiIibA3+RDB11uBJjbBRokbYKFEjbFQA/g9OsDPAO7z7oAAAAABJRU5ErkJggg==\n### EMBEDDED_BASE64_END tile.png\n"
  },
  {
    "path": ".github/tools/mkfw-build.sh",
    "content": "#!/bin/sh\n\n# Credits: https://github.com/lstux/OdroidGO/\n\nBUILDDIR=\"/tmp/odroid-go-mkfw\"\n\n\nif ! [ -d \"${BUILDDIR}\" ]; then\n  install -d \"${BUILDDIR}\" || exit 1\nfi\n\ncd \"${BUILDDIR}/\" || exit 2\nif ! [ -d \"${BUILDDIR}/odroid-go-firmware\" ]; then\n cd \"${BUILDDIR}/\" && git clone https://github.com/othercrashoverride/odroid-go-firmware.git -b factory || exit 3\nfi\n\nmake -C \"${BUILDDIR}/odroid-go-firmware/tools/mkfw\" || exit 4\n\nif [ -n \"$(which sudo)\" ]; then\n  sudo install -v -oroot -groot -m755 \"${BUILDDIR}/odroid-go-firmware/tools/mkfw/mkfw\" \"/usr/bin/mkfw\" || exit 5\nelse\n  su -c \"install -v -oroot -groot -m755 '${BUILDDIR}/odroid-go-firmware/tools/mkfw/mkfw' '/usr/bin/mkfw'\" || exit 5\nfi\n\nrm -rf \"${BUILDDIR}\"\n"
  },
  {
    "path": ".github/workflows/ArduinoBuild.yml",
    "content": "name: ArduinoBuild\non:\n  push:\n    paths:\n    - '**.ino'\n    - '**.cpp'\n    - '**.hpp'\n    - '**.h'\n    - '**.c'\n    - '**ArduinoBuild.yml'\n  pull_request:\n  release:\n    types: [published]\n\njobs:\n\n  matrix_build:\n    name: ${{ matrix.matrix-context }}@${{ matrix.sdk-version }}\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n\n        sdk-version:\n          #- 1.0.6\n          #- 2.0.0\n          #- 2.0.1\n          # - 2.0.2 # has broken SD Support\n          #- 2.0.3\n          #- 2.0.4\n          #- 2.0.5\n          #- 2.0.6\n          # - 2.0.7\n          # - 2.0.8\n          # - 2.0.9\n          - 2.0.11\n          - 2.0.12\n          - 2.0.13\n\n        matrix-context:\n          - M5Core2-test\n          - M5Stack-test\n          - M5StickC-test\n          - LGFX-test\n          - M5Unified-test\n          - S3Box-Test\n          #- TTGO-LoRa32-V2-test\n          - M5Stack\n          - M5Core2\n          - M5Fire\n          - OdroidGo\n          - SdFat-test\n\n        #exclude:\n          #- sdk-version: 2.0.2 # has broken SD Support\n          # There's no esp32s3box support before 2.0.3\n          #- { matrix-context: S3Box-Test, sdk-version: 1.0.6 }\n          #- { matrix-context: S3Box-Test, sdk-version: 2.0.0 }\n          #- { matrix-context: S3Box-Test, sdk-version: 2.0.1 }\n          #- { matrix-context: S3Box-Test sdk-version: 2.0.2 }\n          #- { matrix-context: S3Box-Test, sdk-version: 2.0.4 } # will be fixed in 2.0.5 https://github.com/espressif/arduino-esp32/pull/6962/files\n          #- { matrix-context: M5Core2-test, sdk-version: 1.0.6 } # M5Core2.h suddenly ceased to support espressif package 1.0.6/2.0.0 (I2S and I2C broken)\n\n\n        include:\n\n          # buildable sdk versions\n          #- sdk-version: 1.0.6\n          #- sdk-version: 2.0.0\n          #- sdk-version: 2.0.1\n          #- sdk-version: 2.0.5\n          #- sdk-version: 2.0.6\n          # - sdk-version: 2.0.7\n          # - sdk-version: 2.0.8\n          # - sdk-version: 2.0.9\n          - sdk-version: 2.0.11\n          - sdk-version: 2.0.12\n          - sdk-version: 2.0.13\n\n          # library health test sketches\n          - { matrix-context: M5Core2-test,   arduino-board: m5stack-core2,      sketch-names: M5Core2-SDLoader-Snippet.ino,       required-libraries: \"M5Core2\", ... }\n          - { matrix-context: M5Stack-test,   arduino-board: m5stack-core-esp32, sketch-names: M5Stack-SDLoader-Snippet.ino,       required-libraries: \"ESP32-Chimera-Core,LovyanGFX\", ... }\n          - { matrix-context: S3Box-Test,     arduino-board: esp32s3box,         sketch-names: M5Stack-SDLoader-Snippet.ino,       required-libraries: \"ESP32-Chimera-Core,LovyanGFX\", ... }\n          - { matrix-context: M5Unified-test, arduino-board: m5stack-core2,      sketch-names: M5Unified.ino,                      required-libraries: \"M5GFX,M5Unified\", ... }\n          - { matrix-context: M5StickC-test,  arduino-board: m5stick-c,          sketch-names: M5StickC-SPIFFS-Loader-Snippet.ino, required-libraries: \"M5StickC\", ... }\n          - { matrix-context: LGFX-test,      arduino-board: m5stack-core-esp32, sketch-names: LGFX-SDLoader-Snippet.ino,          required-libraries: \"LovyanGFX\", ... }\n          - { matrix-context: SdFat-test,     arduino-board: m5stack-core2,      sketch-names: SdFatUpdater.ino,                   required-libraries: \"SdFat,M5GFX,M5Unified\", ... }\n\n          # Launcher and Appstore\n          - matrix-context: M5Stack\n            arduino-board: m5stack-core-esp32\n            sketch-names: M5Stack-SD-Menu.ino,AppStore.ino\n            launcher-name: M5stack-Launcher\n            appstore-name: M5stack-AppStore\n            required-libraries: \"ESP32-Chimera-Core,LovyanGFX,ArduinoJson\"\n          - matrix-context: M5Core2\n            arduino-board: m5stack-core2\n            sketch-names: M5Stack-SD-Menu.ino,AppStore.ino\n            launcher-name: M5Core2-Launcher\n            appstore-name: M5Core2-AppStore\n            required-libraries: \"ESP32-Chimera-Core,LovyanGFX,ArduinoJson\"\n          - matrix-context: M5Fire\n            arduino-board: m5stack-fire\n            sketch-names: M5Stack-SD-Menu.ino,AppStore.ino\n            launcher-name: M5Fire-Launcher\n            appstore-name: M5Fire-AppStore\n            required-libraries: \"ESP32-Chimera-Core,LovyanGFX,ArduinoJson\"\n          - matrix-context: OdroidGo\n            arduino-board: odroid_esp32\n            sketch-names: M5Stack-SD-Menu.ino,AppStore.ino\n            launcher-name: OdroidGo-Launcher\n            appstore-name: OdroidGo-AppStore\n            extra-fqbn: \":PartitionScheme=min_spiffs\"\n            required-libraries: \"ESP32-Chimera-Core,LovyanGFX,ArduinoJson,Button2\"\n          #- matrix-context: TTGO-LoRa32-V2-test\n            #arduino-board: ttgo-lora32-v2\n            #sketch-names: TTGO-test.ino\n            #required-libraries: \"ESP32-Chimera-Core,LovyanGFX,ArduinoJson\"\n          - matrix-context: M5Atom\n            sdk-version: 2.0.13\n            arduino-board: m5stack-atom\n            sketch-names: M5Stack-SD-Menu.ino\n            launcher-name: M5Atom-Launcher\n            required-libraries: \"ESP32-Chimera-Core,LovyanGFX,ArduinoJson,Button2\"\n\n          - matrix-context: M5CoreS3\n            sdk-version: 2.0.13\n            arduino-board: m5stack-cores3\n            sketch-names: M5Stack-SD-Menu.ino,M5Stack-FW-Menu.ino\n            launcher-name: M5CoreS3-Launcher\n            factory-name: M5CoreS3-FW-Launcher\n            required-libraries: \"ESP32-Chimera-Core,LovyanGFX,ArduinoJson,Button2\"\n\n\n      fail-fast: false\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n      - name: ${{ matrix.matrix-context }}\n        uses: ArminJo/arduino-test-compile@v3.2.0\n        with:\n          platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json\n          #platform-url: ${{ matrix.platform-url }}\n          arduino-board-fqbn: esp32:esp32:${{ matrix.arduino-board}}${{ matrix.extra-fqbn }}\n          arduino-platform: esp32:esp32@${{ matrix.sdk-version }}\n          required-libraries: ESP32-targz,${{ matrix.required-libraries }}\n          extra-arduino-lib-install-args: --no-deps\n          # extra-arduino-cli-args: ${{ matrix.extra-arduino-cli-args }}\n          extra-arduino-cli-args: \"--warnings default \" # see https://github.com/ArminJo/arduino-test-compile/issues/28\n          sketch-names: ${{ matrix.sketch-names }}\n          set-build-path: true\n          build-properties: ${{ toJson(matrix.build-properties) }}\n          #debug-install: true\n      - name: Copy compiled binaries\n        if: startsWith(matrix.sketch-names, 'M5Stack-SD-Menu')\n        run: |\n          cp examples/M5Stack-SD-Menu/build/M5Stack-SD-Menu.ino.bin examples/M5Stack-SD-Menu/build/${{ matrix.launcher-name }}-${{ matrix.sdk-version }}.bin\n          FILE=examples/AppStore/build/AppStore.ino.bin && [ -f \"$FILE\" ] && cp $FILE examples/M5Stack-SD-Menu/build/${{ matrix.appstore-name }}-${{ matrix.sdk-version }}.bin || true\n          FILE=examples/M5Stack-FW-Menu/build/M5Stack-FW-Menu.ino.bin && [ -f \"$FILE\" ] && cp $FILE examples/M5Stack-SD-Menu/build/${{ matrix.factory-name }}-${{ matrix.sdk-version }}.bin || true\n      - name: Upload artifact ${{ matrix.matrix-context }}\n        uses: actions/upload-artifact@v3\n        if: startsWith(matrix.sketch-names, 'M5Stack-SD-Menu')\n        with:\n          name: ${{ matrix.matrix-context }}\n          path: |\n            examples/M5Stack-SD-Menu/build/${{ matrix.launcher-name }}-${{ matrix.sdk-version }}.bin\n            examples/M5Stack-SD-Menu/build/${{ matrix.appstore-name }}-${{ matrix.sdk-version }}.bin\n            examples/M5Stack-SD-Menu/build/${{ matrix.factory-name  }}-${{ matrix.sdk-version }}.bin\n\n  post_build:\n    name: Gather Artefacts\n    runs-on: ubuntu-latest\n    # wait until matrix jobs are all finished\n    needs: matrix_build\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: Create artifacts dir\n        #if: startsWith(github.ref, 'refs/tags/')\n        run: mkdir -p /home/runner/builds\n      - name: Download artifacts\n        uses: actions/download-artifact@v4.1.7\n        #if: startsWith(github.ref, 'refs/tags/')\n        with:\n          path: builds\n      - name: Release check\n        uses: softprops/action-gh-release@v1\n        if: startsWith(github.ref, 'refs/tags/')\n        with:\n          files: |\n            /home/runner/work/M5Stack-SD-Updater/M5Stack-SD-Updater/builds/**\n"
  },
  {
    "path": ".github/workflows/PlatformioBuild.yml",
    "content": "name: PlatformIOBuild\n\nenv:\n  PROJECT_DIR: examples/Test/build_test\n  BRANCH_NAME: ${{ github.head_ref || github.ref_name }}\n\non:\n  push:\n    paths:\n    - '**.ino'\n    - '**.ini'\n    - '**.cpp'\n    - '**.hpp'\n    - '**.h'\n    - '**.c'\n    - '**PlatformioBuild.yml'\n  pull_request:\n  workflow_dispatch:\n\njobs:\n\n  set_matrix:\n    name: Version planner ⊹\n    runs-on: ubuntu-latest\n    env:\n      max-versions: 3 # maximum core versions to test, starting at latest\n    outputs:\n      matrix: ${{steps.set-matrix.outputs.matrix}}\n      project_dir: ${{steps.set-matrix.outputs.project_dir}}\n      branch_name: ${{steps.set-matrix.outputs.branch_name}}\n\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v3\n      with:\n        ref: ${{ github.event.pull_request.head.sha }}\n\n    - name: Setup matrix\n      id: set-matrix\n      run: |\n        json=`curl -s \"https://espressif.github.io/arduino-esp32/package_esp32_index.json\"` # get a list of arduino-esp32 packages\n        core_versions_arr=(`echo $json | jq -r .packages[0].platforms[].version | awk \"NR <= ${{ env.max-versions}}\"`) # extract platform versions\n        core_versions_json=`printf '%s\\n' \"${core_versions_arr[@]}\" | jq -R . | jq -s .` # convert to json\n        pio_envs_arr=(`cat ${{env.PROJECT_DIR}}/platformio.ini | awk 'match($0, /^\\[env:([a-zA-Z0-9\\-_])+/){print substr($0, RSTART+5, RLENGTH-5)}'`) # get pio [env:*] names\n        pio_envs_json=`printf '%s\\n' \"${pio_envs_arr[@]}\" | jq -R . | jq -s .` # convert to json array\n        matrix=`printf '{\"pio-env\":%s,\"platform-version\":%s}' \"$pio_envs_json\" \"$core_versions_json\"` # create the matrix array\n        matrix=\"${matrix//'%'/'%25'}\" # escape percent entities\n        matrix=\"${matrix//$'\\n'/''}\"  # remove lf\n        matrix=\"${matrix//$'\\r'/''}\"  # remove cr\n        echo \"matrix=${matrix}\" >> $GITHUB_OUTPUT\n        echo \"project_dir=${{env.PROJECT_DIR}}\" >> $GITHUB_OUTPUT\n        echo \"branch_name=${{env.BRANCH_NAME}}\" >> $GITHUB_OUTPUT\n\n  build:\n    name: ${{ matrix.pio-env }}@${{ matrix.platform-version }}\n    needs: set_matrix\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix: ${{fromJSON(needs.set_matrix.outputs.matrix)}}\n      fail-fast: false\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n\n      - name: Cache pip\n        uses: actions/cache@v3\n        with:\n          path: ~/.cache/pip\n          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}\n          restore-keys: |\n            ${{ runner.os }}-pip-\n      - name: Cache PlatformIO\n        uses: actions/cache@v3\n        with:\n          path: ~/.platformio\n          key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.9'\n\n      - name: Install PlatformIO\n        run: |\n          python -m pip install --upgrade pip\n          pip install --upgrade platformio\n          pio update\n          pio upgrade\n\n      - name: Run PlatformIO\n        run: |\n          cd ${{ needs.set_matrix.outputs.project_dir }}\n          export pio_ver=${{ matrix.platform-version }}\n          # append \"test\" profile to the current platformio.ini\n          echo \"[env:test]\">>platformio.ini\n          echo \"extends = env:${{ matrix.pio-env }}\">>platformio.ini\n          echo \"platform = https://github.com/tasmota/platform-espressif32\">>platformio.ini\n          echo \"platform_packages = framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/${pio_ver}/esp32-${pio_ver}.zip\">>platformio.ini\n          echo \"\">>platformio.ini\n          # keep cache and dev_lib_deps.ini unless running from the master branch\n          [[ \"${{ needs.set_matrix.outputs.branch_name }}\" == \"master\" ]] && rm dev_lib_deps.ini || echo \"Develop!\" && pio system prune -f\n          # install local version of the library\n          pio pkg install -e test --no-save --library file://$(realpath ../../../)\n          pio run -e test\n"
  },
  {
    "path": ".github/workflows/cron.yml",
    "content": "name: \"Cron ➕ workflows\"\non:\n  schedule:\n    - cron: '*/10 * * * *'\n  workflow_dispatch:\n    inputs:\n      dispatch-fqwn:\n        description: 'Dispatch workflow (fully qualified workflow name)'\n        required: false\n        default: ''\n\njobs:\n  check-lambda-app:\n    runs-on: ubuntu-latest\n    steps:\n\n      - name: \"Next app checker\"\n        if: github.event.inputs.dispatch-fqwn == ''\n        run: |\n          jsonW=`curl -L --silent https://phpsecu.re/api/registry/workflow`\n          nextAW=`echo $jsonW | grep '^{' | jq -r .need_workflow[0]`\n          if [[ \"$nextAW\" == \"\" || \"$nextAW\" == \"null\" ]]; then echo \"[INFO] No workflow in queue\"; exit 0; fi\n          fqwn=`echo \"$nextAW\" | jq  -r .fqwn`;\n          echo \"FQWN=$fqwn\" >> $GITHUB_ENV\n\n      - name: \"Dispatched setter (fqwn)\"\n        if: github.event.inputs.dispatch-fqwn != ''\n        run: |\n          echo \"FQWN=${{ github.event.inputs.dispatch-fqwn }}\" >> $GITHUB_ENV\n\n      - name: \"FQWN Parser\"\n        if: env.FQWN != ''\n        run: |\n          fqwn=${{ env.FQWN }}\n          board=\"$(echo $fqwn | cut -d':' -f1)\"\n          platform=\"$(echo $fqwn | cut -d':' -f2)\"\n          folder=\"$(echo $fqwn | cut -d':' -f3)\"\n          repo=\"$(echo $fqwn | cut -d':' -f4)\"\n          app_slug=\"${repo//\\//_}\"\n          app_path=\"$folder/$repo.json\"\n          jsonString=\"{\\\"board-name\\\":\\\"$board\\\", \\\"platform-name\\\":\\\"$platform\\\", \\\"remote-app-path\\\":\\\"$app_path\\\", \\\"remote-app-slug\\\":\\\"$app_slug\\\", \\\"remote-repo-slug\\\":\\\"$repo\\\"}\"\n          echo \"WORKFLOW_NAME=Dispatched Build\" >> $GITHUB_ENV\n          echo \"JSON_INPUTS=$jsonString\" >> $GITHUB_ENV\n\n      - name: \"Dispatch\"\n        if: env.WORKFLOW_NAME != ''\n        uses: benc-uk/workflow-dispatch@v1\n        with:\n          workflow: ${{ env.WORKFLOW_NAME }}\n          repo: ${{ secrets.REGISTRY_REPOSITORY }}\n          token: ${{ secrets.GHTOKEN }}\n          inputs: '${{ env.JSON_INPUTS }}'\n"
  },
  {
    "path": ".github/workflows/onrelease.yml",
    "content": "on:\n  release:\n    types:\n      - published\n  workflow_dispatch:\n    inputs:\n      versiontype:\n        description: 'Version upgrade type (p)atch/(m)inor/(M)ajor/(F)orced'\n        required: true\n        default: 'p'\n      versionforced:\n        description: 'Optional (F)orced version, leave blank for auto-raise'\n        required: false\n        default: ''\njobs:\n  on_release_semver_next:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          ref: master # this must match the main/master branch name !!\n      - name: Semver-Iterator\n        run: |\n          chmod +x $GITHUB_WORKSPACE/.github/scripts/semver.sh\n          echo \"Running $GITHUB_WORKSPACE/.github/scripts/semver.sh -${{ github.event.inputs.versiontype }} ${{ github.event.inputs.versionforced }} \"\n          $GITHUB_WORKSPACE/.github/scripts/semver.sh -${{ github.event.inputs.versiontype }} ${{ github.event.inputs.versionforced }}\n\n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v3\n        with:\n          commit-message: \"raising version\"\n          title: \"Semver-Iterator auto-raise\"\n          body: |\n            A version change occured\n\n"
  },
  {
    "path": ".gitignore",
    "content": "build\n.pio/\n.directory\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"examples/M5Stack-SD-Menu/SD-Apps/M5Stack_lifegame\"]\n\tpath = examples/M5Stack-SD-Menu/SD-Apps/M5Stack_lifegame\n\turl = https://gist.github.com/shioken/fa2c3f5923170d86dd358b313d6ab244\n[submodule \"examples/M5Stack-SD-Menu/SD-Apps/M5_reversi\"]\n\tpath = examples/M5Stack-SD-Menu/SD-Apps/M5_reversi\nurl = https://gist.github.com/shioken/e90b9fa3b43d8b067adde77a75768efd\n"
  },
  {
    "path": ".travis.yml",
    "content": "sudo: required\n\nlanguage: python\npython: 3.6\n\nenv:\n  global:\n    # The Arduino IDE will be installed at APPLICATION_FOLDER/arduino\n    - APPLICATION_FOLDER=\"${HOME}/arduino-ide\"\n    - SKETCHBOOK_FOLDER=\"${HOME}/arduino-sketchbook\"\n\nbefore_install:\n\n  # TODO: undo\n  # remove submodules, we don't want those to be actually tested for compliance\n  #- git submodule status | git rm --cached `cut -d ' ' -f 3`\n  - git submodule status | rm -Rf `cut -d ' ' -f 3`\n\n  # Formatting checks:\n  # Check for files starting with a blank line\n  #- find . -path './.git' -prune -or -type f -print0 | xargs -0 -L1 bash -c 'head -1 \"$0\" | grep --binary-files=without-match --regexp=\"^$\"; if [[ \"$?\" == \"0\" ]]; then echo \"Blank line found at start of $0.\"; false; fi'\n  # Check for tabs\n  #- find . -path './.git' -prune -or -type f \\( ! -iname \".gitmodules\" \\) -exec grep --with-filename --line-number --binary-files=without-match --regexp=$'\\t' '{}' \\; -exec echo 'Tab found.' \\; -exec false '{}' +\n  # Check for trailing whitespace\n  #- find . -path './.git' -prune -or -type f -exec grep --with-filename --line-number --binary-files=without-match --regexp='[[:blank:]]$' '{}' \\; -exec echo 'Trailing whitespace found.' \\; -exec false '{}' +\n  # Check for non-Unix line endings\n  #- find . -path './.git' -prune -or -type f -exec grep --files-with-matches --binary-files=without-match --regexp=$'\\r$' '{}' \\; -exec echo 'Non-Unix EOL detected.' \\; -exec false '{}' +\n  # Check for blank lines at end of files\n  #- find . -path './.git' -prune -or -type f -print0 | xargs -0 -L1 bash -c 'tail -1 \"$0\" | grep --binary-files=without-match --regexp=\"^$\"; if [[ \"$?\" == \"0\" ]]; then echo \"Blank line found at end of $0.\"; false; fi'\n  # Check for files that don't end in a newline (https://stackoverflow.com/a/25686825)\n  #- find . -path './.git' -prune -or -type f -print0 | xargs -0 -L1 bash -c 'if test \"$(grep --files-with-matches --binary-files=without-match --max-count=1 --regexp='.*' \"$0\")\" && test \"$(tail --bytes=1 \"$0\")\"; then echo \"No new line at end of $0.\"; false; fi'\n\n  # this repository is broken and won't work as a submodule\n  #- git clone https://github.com/matsumo/m5stickc_tiny_menu examples/m5stickc_tiny_menu\n  #- git submodule update --init examples/m5stickc_tiny_menu/\n  #- git rm --cached examples/m5stickc_tiny_menu\n  #- git submodule sync examples/m5stickc_tiny_menu\n  #- git submodule update examples/m5stickc_tiny_menu\n\n  - git clone https://github.com/per1234/arduino-ci-script.git \"${HOME}/scripts/arduino-ci-script\"\n  - cd \"${HOME}/scripts/arduino-ci-script\"\n  # Get new tags from the remote\n  - git fetch --tags\n  # Checkout the latest tag\n  - git checkout $(git describe --tags `git rev-list --tags --max-count=1`)\n  - source \"${HOME}/scripts/arduino-ci-script/arduino-ci-script.sh\"\n\n  #- set_script_verbosity 1\n  #- set_verbose_output_during_compilation \"true\"\n\n  # Check for library issues that don't affect compilation\n  - set_library_testing \"true\"\n\n  - set_application_folder \"$APPLICATION_FOLDER\"\n  - set_sketchbook_folder \"$SKETCHBOOK_FOLDER\"\n\n  - install_ide '(\"1.8.12\" \"1.8.13\" \"newest\")'\n\n  # Install the library from the repository\n  - install_library\n  - install_library \"M5Stack\"\n  - install_library \"M5StickC\"\n  - install_library \"ESP32-targz\"\n  - install_library \"ESP32-Chimera-Core\"\n  #- install_library \"https://github.com/tobozo/ESP32-Chimera-Core/archive/master.zip\"\n  - install_library \"LovyanGFX\"\n  # make sure chimera-core does not collide with m5core\n  #- install_library \"https://github.com/M5Stack/M5Core2.git\" # \"M5Core2\" << Official repo slacking on the PR's :-(\n  - install_library \"https://github.com/ropg/M5Core2/archive/master.zip\"\n  - install_library 'https://github.com/bblanchon/ArduinoJson.git' # \"ArduinoJSON\"\n\n  - ls ${TRAVIS_BUILD_DIR} -la\n  - ls ${TRAVIS_BUILD_DIR}/examples/M5Stack-SD-Menu/ -la\n  - ls $SKETCHBOOK_FOLDER -la\n  - ls $SKETCHBOOK_FOLDER/libraries -la\n  - pwd\n  #- install_package \"esp32:esp32\" \"https://dl.espressif.com/dl/package_esp32_index.json\" #  # esp32:esp32:m5stack-core-esp32\n  - install_package \"esp32:esp32\" \"https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json\"\n  #- install_package \"m5stack:esp32\" \"https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json\"\n\n  # load custom boards from chimera-core\n  # - cp -R $SKETCHBOOK_FOLDER/libraries/ESP32-Chimera-Core-master/boards/* ~/.arduino15/packages/esp32/hardware/esp32/1.0.4/\n  - pip install pyserial\n\nscript:\n  # Compile all example sketches included with the library\n  # build_sketch arguments: sketch name, fqbn, allow failure, IDE version/list/range\n  - cd ${TRAVIS_BUILD_DIR}\n  - rm -Rf \"${TRAVIS_BUILD_DIR}/examples/M5Stack-SD-Menu/SD-Apps\"\n  #- git submodule foreach git fetch && git submodule deinit -f . && git submodule update --init # reload the submodules since 'check_library_manager_compliance' complains about missing files :-(\n  - check_library_manager_compliance \"$TRAVIS_BUILD_DIR\" # why this fails on submodules for an existing '.exe' file is beyond me ...\n  - set_ide_preference \"compiler.warning_level=auto\"\n\n  - build_sketch \"${TRAVIS_BUILD_DIR}/examples/M5Stack-SDLoader-Snippet/M5Stack-SDLoader-Snippet.ino\" \"esp32:esp32:m5stack-core2\" \"false\" \"newest\"\n  - build_sketch \"${TRAVIS_BUILD_DIR}/examples/M5Stack-SDLoader-Snippet/M5Stack-SDLoader-Snippet.ino\" \"esp32:esp32:m5stack-core-esp32\" \"false\" \"newest\"\n  - build_sketch \"${TRAVIS_BUILD_DIR}/examples/M5StickC-SPIFFS-Loader-Snippet/M5StickC-SPIFFS-Loader-Snippet.ino\" \"esp32:esp32:m5stick-c\" \"false\" \"newest\"\n\n  - build_sketch \"${TRAVIS_BUILD_DIR}/examples/M5Stack-SD-Menu/M5Stack-SD-Menu.ino\" \"esp32:esp32:m5stack-core-esp32:FlashFreq=80,UploadSpeed=921600\" \"false\" \"newest\"\n  - build_sketch \"${TRAVIS_BUILD_DIR}/examples/LGFX-SDLoader-Snippet/LGFX-SDLoader-Snippet.ino\" \"esp32:esp32:m5stack-core-esp32:FlashFreq=80,UploadSpeed=921600\" \"false\" \"newest\"\n  - build_sketch \"${TRAVIS_BUILD_DIR}/examples/CopySketchToFS/CopySketchToFS.ino\" \"esp32:esp32:m5stack-core-esp32:FlashFreq=80,UploadSpeed=921600\" \"false\" \"newest\"\n\n  #- build_sketch \"${TRAVIS_BUILD_DIR}/examples/m5stickc_tiny_menu/m5stickc_tiny_menu.ino\" \"esp32:esp32:m5stick-c\" \"false\" \"newest\"\n\nafter_script:\n  # Commit a report of the job results to the CI-reports repository\n  - USER_NAME=\"$(echo \"$TRAVIS_REPO_SLUG\" | cut -d'/' -f 1)\"\n  - REPOSITORY_NAME=\"$(echo \"$TRAVIS_REPO_SLUG\" | cut -d'/' -f 2)\"\n  - publish_report_to_repository \"$REPORT_GITHUB_TOKEN\" \"https://github.com/${USER_NAME}/CI-reports.git\" \"$REPOSITORY_NAME\" \"build_$(printf \"%05d\\n\" \"${TRAVIS_BUILD_NUMBER}\")\" \"false\"\n  # Print a tab separated report of all sketch verification results to the log\n  - display_report\n\nnotifications:\n  email:\n    on_success: always\n    on_failure: always\n  webhooks:\n    urls:\n      - https://www.travisbuddy.com/\n    on_success: never\n    on_failure: always\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright 2018 tobozo http://github.com/tobozo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![License: MIT](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/tobozo/M5Stack-SD-Updater/blob/master/LICENSE)\n[![Gitter](https://badges.gitter.im/M5Stack-SD-Updater/community.svg)](https://gitter.im/M5Stack-SD-Updater/community)\n[![arduino-library-badge](https://www.ardu-badge.com/badge/M5Stack-SD-Updater.svg?)](https://www.ardu-badge.com/M5Stack-SD-Updater)\n[![PlatformIO Registry](https://badges.registry.platformio.org/packages/tobozo/library/M5Stack-SD-Updater.svg?)](https://registry.platformio.org/packages/libraries/tobozo/M5Stack-SD-Updater)\n\n![Arduino Build](https://github.com/tobozo/M5Stack-SD-Updater/actions/workflows/ArduinoBuild.yml/badge.svg?branch=master)\n![Platformio Build](https://github.com/tobozo/M5Stack-SD-Updater/actions/workflows/PlatformioBuild.yml/badge.svg?branch=master)\n\n![Library Downloads](https://img.shields.io/github/downloads/tobozo/M5Stack-SD-Updater/total)\n\n# M5Stack-SD-Updater\n\n<br />\n\n\n[![Click to enlarge](https://github.com/PartsandCircuits/M5Stack-SD-Updater/blob/master/SDUpdaterpic.png \"Click to enlarge\")](https://github.com/PartsandCircuits/M5Stack-SD-Updater/blob/master/SDUpdaterpic02.png)\n\n\n<br />\n\n## ABOUT\n\n- **M5Stack-SD-Updater is an [Platform.io](https://platformio.org/lib/show/2575/M5Stack-SD-Updater)/[Arduino library](https://www.arduinolibraries.info/libraries/m5-stack-sd-updater) for [M5Stack](http://m5stack.com/) or [Odroid-Go](https://forum.odroid.com/viewtopic.php?t=31705) to package your apps on a SD card and load them from a menu such as the [default SD Menu example](https://github.com/tobozo/M5Stack-SD-Updater/tree/master/examples/M5Stack-SD-Menu) of this project or [@lovyan03](https://github.com/lovyan03)'s [Treeview based SD Menu](https://github.com/lovyan03/M5Stack_LovyanLauncher). For a Micropython compatible version of this library, see the [M5Stack_MicroPython  custom_sdupdater](https://github.com/ciniml/M5Stack_MicroPython/tree/custom_sdupdater)**\n\n- It is inspired by gamebuino, however it does not use a modified bootloader.\n\n- [Video demonstration](https://www.youtube.com/watch?v=myQfeYxyc3o)\n\n- This project by [tobozo](https://github.com/tobozo) - Demo built on M5Stack-SAM by Tom Such. Further credits listed below.\n\n- Contributors welcome !\n\n\n<br />\n\n🏭 M5Stack-SD-Menu EXAMPLE SKETCH PREREQUISITES:\n------------------------------------------------\n<br />\n\n**Micro SD Card (TF Card)** - formatted using FAT32. Max size 32 Gb.\nSDCard is recommended but the SDUpdater supports other filesystems such as SdFat, SD_MMC and LittleFS (SPIFFS will soon be deprecated).\n\n<br />\n\n**Make sure you have the following libraries:** - they should be installed in: `~/Arduino/libraries`\n\n- [ESP32-Chimera-Core](https://github.com/tobozo/ESP32-Chimera-Core), [LovyanGFX](https://github.com/lovyan03/LovyanGFX), [M5GFX](https://github.com/m5stack/M5GFX), [M5Unified](https://github.com/m5stack/M5Unified) or [M5Stack Core](https://github.com/m5stack/M5Stack).\n\n- [M5Stack-SD-Updater](https://github.com/tobozo/M5Stack-SD-Updater) (this library + its examples).\n\n- [ArduinoJSON](https://github.com/bblanchon/ArduinoJson/) (Optional, used by SD-Menu).\n\n- [ESP32-targz](https://github.com/tobozo/ESP32-targz) (Optional if using gzipped firmwares)\n\nAll those are available in the [Arduino Library Manager](https://www.arduinolibraries.info/libraries/m5-stack-sd-updater) or by performing a [manual installation](https://www.arduino.cc/en/Guide/Libraries#toc5).\n\n\n<br />\n\n\n🍱 UNPACKING THE BINARIES\n-------------------------\n\n\n**obsolete**\n~~For your own lazyness, you can use @micutil's awesome [M5Burner](https://github.com/micutil/M5Burner_Mic) and skip the next steps.~~\n~~[![https://github.com/micutil/M5Burner_Mic/releases](https://raw.githubusercontent.com/micutil/M5Burner_Mic/master/images/m5burnermic128.png)](https://github.com/micutil/M5Burner_Mic/releases)~~\n~~... or customize your own menu and make the installation manually :~~\n\n\n**1) Open the `examples/M5Stack-SD-Update` sketch from the Arduino ID Examples menu.**\n\n<br />\n\n**outdated binaries**\n~~**2) Download the [SD-Content :floppy_disk:](https://github.com/tobozo/M5Stack-SD-Updater/releases/download/v0.4.1/SD-Apps-Folder.zip) folder from the release page and unzip it into the root of the SD Card.** Then put the SD Card into the M5Stack. This zip file comes preloaded with [precompiled apps](https://github.com/tobozo/M5Stack-SD-Updater/tree/master/examples/M5Stack-SD-Menu/SD-Apps) and the relative meta information for the menu.~~\n\n<br />\n\n**2) Compile and flash the `M5Stack-SD-Menu.ino` example.** <br />\nThis sketch is the **menu** app. It shoul reside in the root directory of a micro SD card for persistence and also executed once.\n\nOnce flashed it will **copy itself** on OTA2 partition and on the SDCard, then rolled back and executed from the OTA2 partition.\n\nThanks to @Lovyan03 this self-propagation logic is very convenient: by residing on OTA2 the `menu.bin` will always be available for fast re-loading.\n\n\n<br />\n\n**3) Make application sketches compatible with the SD-Updater Menu .** <br />\n\n\nThe snippet of code in the `M5Stack-SDLoader-Snippet.ino` sketch can be used as a model to make any ESP32 sketch compatible with the SD-Updater menu.\n\n\n In your sketch, find the line where the core library is included:\n\n ```C\n\n    // #include <M5Stack.h>\n    // #include <M5Core2.h>\n    // #include <LovyanGFX.h>\n    // #include <M5GFX.h>\n    // #include <ESP32-Chimera-Core.h>\n    // #include <M5StickC.h>\n    // #include <M5Unified.h>\n\n```\n\n And add this after the include:\n\n```C\n    // #define SDU_ENABLE_GZ // optional: support for gzipped firmwares\n    #include <M5StackUpdater.h>\n```\n\n In your `setup()` function, find the following statements:\n\n```C++\n    M5.begin();\n    // Serial.begin(115200);\n```\n\n And add this after serial is started:\n\n```C\n    checkSDUpdater( SD );\n```\n\n Then do whatever you need to do (button init, timer, network signal) in the setup and the loop. Your app will\n run normally except at boot (e.g. if the `Button A` is pressed), when it can load the `/menu.bin` binary from\n the filesystem, or go on with it application duties.\n\n ⚠️Touch UI has no buttons, this raises the problem of detecting a 'pushed' state when the touch is off.\n As a compensation, an UI lobby will be visible for 2 seconds after every `ESP.restart()`. The visibility\n of the lobby can be forced in the setup :\n\n```C\n    checkSDUpdater( SD, MENU_BIN, 2000 );\n```\n\n  Custom SD-Load scenarios can be achieved using non default values:\n\n```C\n\n    M5.begin();\n\n    checkSDUpdater(\n      SD,           // filesystem (SD, SD_MMC, SPIFFS, LittleFS, PSRamFS)\n      MENU_BIN,     // path to binary (default = /menu.bin, empty string = rollback only)\n      5000          // wait delay, (default=0, will be forced to 2000 upon ESP.restart() or with headless build )\n      TFCARD_CS_PIN // optional for SD use only, usually default=4 but your mileage may vary)\n    );\n\n```\n\n\n  Headless setup can bypass `onWaitForAction` lobby option with their own button/sensor/whatever detection routine.\n\n\n```C\n\n    Serial.begin( 115200 );\n\n    if(digitalRead(BUTTON_A_PIN) == 0) {\n      Serial.println(\"Will Load menu binary\");\n      updateFromFS(SD);\n      ESP.restart();\n    }\n\n```\n\n  Headless setup can also be customized in complex integrations:\n\n```C++\n\n    Serial.begin( 115200 );\n\n    SDUCfg.setCSPin( TFCARD_CS_PIN );\n    SDUCfg.setFS( &SD );\n\n    // set your own button response trigger\n\n    static int buttonState;\n\n    SDUCfg.setSDUBtnA( []() {\n      return buttonState==LOW ? true : false;\n    });\n\n    SDUCfg.setSDUBtnPoller( []() {\n      buttonState = digitalRead( 16 );\n    });\n\n    // Or set your own serial input trigger\n    // SDUCfg.setWaitForActionCb( mySerialActionTrigger );\n\n    SDUpdater sdUpdater( &SDUCfg );\n\n    sdUpdater.checkSDUpdaterHeadless( MENU_BIN, 30000 ); // wait 30 seconds for serial input\n\n```\n\n\n\n<br />\n\n\n\n  Use one of following methods to get the app on the filesystem:\n\n  - Have the app copy itself to filesystem using BtnC from the lobby or implement `saveSketchToFS( SD, \"/my_application.bin\" );` from an option inside your app.\n\n  - Manually copy it to the filesystem:\n    - In the Arduino IDE menu go to \"Sketch / Export Compiled Binary\".\n    - Rename the file to remove unnecessary additions to the name. The filename will be saved as \"filename.ino.esp32.bin\".\n      Edit the name so it reads \"filename.bin\". This is purely for display purposes. The file will work without this change.\n\n<br />\n\n\n\n⌾ SD-Updater customizations:\n----------------------------\n\n\n  These callback setters are populated by default but only fit the best scenario (M5Stack with display+buttons).\n\n  ⚠️ If no supported combination of display/buttons exists, it will fall back to headless behaviour and will only accept update/rollback signals from Serial.\n\n  As a result, any atypical setup (e.g. headless+LittleFS) should make use of those callback setters:\n\n```C++\n  SDUCfg.setCSPin       ( TFCARD_CS_PIN );      // const int\n  SDUCfg.setFS          ( &FS );                // fs::FS* (SD, SD_MMC, SPIFFS, LittleFS, PSRamFS)\n  SDUCfg.setProgressCb  ( myProgress );         // void (*myProgress)( int state, int size )\n  SDUCfg.setMessageCb   ( myDrawMsg );          // void (*myDrawMsg)( const String& label )\n  SDUCfg.setErrorCb     ( myErrorMsg );         // void (*myErrorMsg)( const String& message, unsigned long delay )\n  SDUCfg.setBeforeCb    ( myBeforeCb );         // void (*myBeforeCb)()\n  SDUCfg.setAfterCb     ( myAfterCb );          // void (*myAfterCb)()\n  SDUCfg.setSplashPageCb( myDrawSplashPage );   // void (*myDrawSplashPage)( const char* msg )\n  SDUCfg.setButtonDrawCb( myDrawPushButton );   // void (*myDrawPushButton)( const char* label, uint8_t position, uint16_t outlinecolor, uint16_t fillcolor, uint16_t textcolor )\n  SDUCfg.setWaitForActionCb( myActionTrigger ); // int  (*myActionTrigger)( char* labelLoad, char* labelSkip, unsigned long waitdelay )\n  SDUCfg.setSDUBtnPoller( myButtonPoller );     // void (*myButtonPoller)()\n  SDUCfg.setSDUBtnA( myBtnAPushedcb );          // bool (*myBtnAPushedcb )()\n  SDUCfg.setSDUBtnB( myBtnBPushedcb );          // bool (*myBtnBPushedcb )()\n  SDUCfg.setSDUBtnC( myBtnCPushedcb );          // bool (*myBtnCPushedcb )()\n\n```\n\n\n\nSet custom action trigger for `update`, `rollback`, `save` and `skip` lobby options:\n```C++\n  // int myActionTrigger( char* labelLoad,  char* labelSkip, unsigned long waitdelay )\n  // return values: 1=update, 0=rollback, -1=skip\n  SDUCfg.setWaitForActionCb( myActionTrigger );\n\n  // Or separately if a UI is available:\n\n  static int buttonAState;\n  static int buttonBState;\n  static int buttonCState;\n\n  SDUCfg.setSDUBtnPoller( []() {\n    buttonAState = digitalRead( 32 );\n    buttonBState = digitalRead( 33 );\n    buttonCState = digitalRead( 13 );\n    delay(50);\n  });\n\n  SDUCfg.setSDUBtnA( []() {\n    return buttonState==LOW ? true : false;\n  });\n\n  SDUCfg.setSDUBtnB( []() {\n    return buttonState==LOW ? true : false;\n  });\n\n  SDUCfg.setSDUBtnC( []() {\n    return buttonState==LOW ? true : false;\n  });\n\n\n```\n\n  Example:\n\n```C++\n\nstatic int myActionTrigger( char* labelLoad,  char* labelSkip, char* labelSave, unsigned long waitdelay )\n{\n  int64_t msec = millis();\n  do {\n    if( Serial.available() ) {\n      String out = Serial.readStringUntil('\\n');\n      if(      out == \"update\" )  return SDU_BTNA_MENU; // load \"/menu.bin\"\n      else if( out == \"rollback\") return SDU_BTNA_ROLLBACK; // rollback to other OTA partition\n      else if( out == \"save\")     return SDU_BTNC_SAVE; // save current sketch to SD card\n      else if( out == \"skip\" )    return SDU_BTNB_SKIP; // do nothing\n      else Serial.printf(\"Ignored command: %s\\n\", out.c_str() );\n    }\n  } while( msec > int64_t( millis() ) - int64_t( waitdelay ) );\n  return -1;\n}\n\nvoid setup()\n{\n  Serial.begin(115200);\n\n  SDUCfg.setAppName( \"My Application\" );         // lobby screen label: application name\n  SDUCfg.setAuthorName( \"by @myself\" );          // lobby screen label: application author\n  SDUCfg.setBinFileName( \"/MyApplication.bin\" ); // if file path to bin is set for this app, it will be checked at boot and created if not exist\n\n  SDUCfg.setWaitForActionCb( myActionTrigger );\n\n  checkSDUpdater( SD );\n\n}\n\n```\n\nSet custom progress (for filesystem operations):\n```C++\n  // void (*myProgress)( int state, int size )\n  SDUCfg.setProgressCb( myProgress );\n```\n\nSet custom notification/warning messages emitter:\n```C++\n  // void (*myDrawMsg)( const String& label )\n  SDUCfg.setMessageCb( myDrawMsg );\n```\n\nSet custom error messages emitter:\n```C++\n  // void (*myErrorMsg)( const String& message, unsigned long delay )\n  SDUCfg.setErrorCb( myErrorMsg );\n```\n\nSet pre-update actions (e.g. capture display styles):\n```C++\n  // void (*myBeforeCb)()\n  SDUCfg.setBeforeCb( myBeforeCb );\n```\n\nSet post-update actions (e.g. restore display styles):\n```C++\n  // void (*myAfterCb)()\n  SDUCfg.setAfterCb( myAfterCb );\n```\n\nSet lobby welcome message (e.g. draw UI welcome screen):\n```C++\n  // void (*myDrawSplashPage)( const char* msg )\n  SDUCfg.setSplashPageCb( myDrawSplashPage );\n```\n\nSet buttons drawing function (useful with Touch displays)\n```C++\n  // void (*myDrawPushButton)( const char* label, uint8_t buttonIndex, uint16_t outlinecolor, uint16_t fillcolor, uint16_t textcolor )\n  SDUCfg.setButtonDrawCb( myDrawPushButton );\n```\n\nSet buttons state polling function (typically M5.update()\n```C++\n  // void(*myButtonPollCb)();\n  SDUCfg.setSDUBtnPoller( myButtonPollCb );\n```\n\nSet each button state getter function, it must return true when the state is \"pushed\".\n```C++\n  SDUCfg.setSDUBtnA( myBtnAPushedcb ); // bool (*myBtnAPushedcb )()\n  SDUCfg.setSDUBtnB( myBtnBPushedcb ); // bool (*myBtnBPushedcb )()\n  SDUCfg.setSDUBtnC( myBtnCPushedcb ); // bool (*myBtnCPushedcb )()\n```\n\n\n\n<br />\n<br />\n\n📚 SD-Menu loading usage:\n-------------------------\n\n**Default behaviour:** when an app is loaded in memory, booting the M5Stack with the `Button A` pushed will load and run `menu.bin` from the filesystem (or from OTA2 if using persistence).\n\n**Custom behaviour:** the `Button A` push event car be replaced by any other means (Touch, Serial, Network, Sensor).\n\nIdeally that SD-Menu application should list all available apps on the sdcard and provide means to load them on demand.\n\nFor example in the SD-Menu application provided with the examples of this repository, booting the M5Stack with the `Button A` pushed will power it off.\n\nThe built-in Download utility of the SD-Menu is has been moved to the `AppStore.ino` example, as a result the menu.bin size is reduced and loads faster.\nThis is still being reworked though.\n\nAlong with the default SD-Menu example of this repository, some artwork/credits can be added for every uploaded binary.\nThe default SD-Menu application will scan for these file types:\n\n  - .bin compiled application binary\n\n  - .jpg image/icon (max 100x100)\n\n  - .json file with dimensions descriptions:\n\n  `{\"width\":120,\"height\":120,\"authorName\":\"tobozo\",\"projectURL\":\"http://short.url\",\"credits\":\"** http://very.very.long.url ~~\"}`\n\n\n<br />\n\n  ⚠️ The jpg/json file names must match the bin file name, case matters!\n  jpg/json meta files are optional but must both be set if provided.\n  The value for \"credits\" JSON property will be scrolled on the top of the screen while the value for *projectURL* JSON property\n  will be rendered as a QR Code in the info window. It is better provide a short URL for *projectURL* so the resulting QR Code\n  has more error correction.\n\n<br />\n<br />\n\n\n🚫 LIMITATIONS:\n---------------\n- SPIFFS/LittleFS libraries limit their file names (including path) to 32 chars but 16 is recommended as it is also printed in the lobby screen.\n- Long file names will eventually get covered by the jpg image, better stay under 16 chars (not including the extension).\n- Short file names may be treated as 8.3 (e.g 2048.bin becomes 2048.BIN).\n- FAT specifications prevent having more than 512 files on the SD Card, but this menu is limited to 256 Items anyway.\n\n\n<br />\n\n🔘 OPTIONAL:\n------------\n\n- The lobby screen at boot can be customized using `SDUCfg.setAppName`, `SDUCfg.setAuthorName` and `SDUCfg.setBinFileName`.\nWhen set, the app name and the binary path will be visible on the lobby screen, and an extra button `Button C` labelled `Save` is added to the UI.\nPushing this button while the M5Stack is booting will create or overwrite the sketch binary to the SD Card.\nThis can be triggered manually by using `saveSketchToFS(SD, fileName, TFCARD_CS_PIN)`.\n\n```C++\n  SDUCfg.setAppName( \"My Application\" ); // lobby screen label: application name\n  SDUCfg.setBinFileName( \"/MyApplication.bin\" ); // if file path to bin is set for this app, it will be checked at boot and created if not exist\n```\n\n- It can also be disabled (although this requires to use the early callback setters):\n\n```C++\n#define SDU_HEADLESS\n#include \"M5StackUpdater.h\"\n```\n\n\n- Although not extensively tested, the default binary name to be loaded (`/menu.bin`) can be changed at compilation time by defining the `MENU_BIN` constant:\n\n```C++\n#define MENU_BIN \"/my_custom_launcher.bin\"\n#include \"M5StackUpdater.h\"\n```\n\n- Gzipped firmwares are supported when `SDU_ENABLE_GZ` macro is defined or when [ESP32-targz.h](https://github.com/tobozo/ESP32-targz) was previously included.\n  The firmware must have the `.gz.` extension and be a valid gzip file to trigger the decompression.\n\n```C++\n#define SDU_ENABLE_GZ // enable support for gzipped firmwares\n#include \"M5StackUpdater.h\"\n\nvoid setup()\n{\n  checkSDUpdater( SD, \"/menu.gz\", 2000 );\n}\n```\n\n\n- The JoyPSP and [M5Stack-Faces](https://github.com/m5stack/faces) Controls for M5Stack SD Menu necessary code are now disabled in the menu example but the code stays here and can be used as a boilerplate for any other two-wires input device.\n\n\n<br />\n\n ⚠️ KNOWN ISSUES\n------------\n\n- `SD was not declared in this scope`: make sure your `#include <SD.h>` is made *before* including `<M5StackUpdater.h>`\n- Serial message `[ERROR] No filesystem selected` or `[ERROR] No valid filesystem selected`: try `SDUCfg.setFS( &SD )` prior to calling the SDUpdater.\n\n\n<br /><br />\n\n\n\n🏭 Factory Partition\n--------------------\n\nAbuse the OTA partition scheme and store up to 5 applications on the flash, plus the firmware loader.\n\n⚠️ This scenario uses a special [firmware loader](https://github.com/tobozo/M5Stack-SD-Updater/tree/1.2.8/examples/M5Stack-FW-Menu) `M5Stack-FW-Menu`, a custom partition scheme, and a different integration of M5Stack-SD-Updater in the loadable applications.\n\nAlthough it can work without the SD Card, `M5Stack-FW-Menu` can still act as a low profile replacement for the classic SD Card `/menu.bin`, and load binaries from the SD Card or other supported filesystems.\n\n\n#### Requirements:\n\n- Flash size must be 8MB or 16MB\n- custom partitions.csv must have more than 2 OTA partitions followed by one factory partition (see annotated example below)\n- loadable applications and firmware loader must share the same custom partitions scheme at compilation\n- `SDUCfg.rollBackToFactory = true;` must be set in all loadable applications (see `Detect factory support`)\n\n#### Custom partition scheme annotated example:\n\n```csv\n# 6 Apps + Factory\n# Name,   Type, SubType,    Offset,     Size\nnvs,      data, nvs,        0x9000,   0x5000\notadata,  data, ota,        0xe000,   0x2000\nota_0,    0,    ota_0,     0x10000, 0x200000  ,<< Default partition for flashing (UART, 2MB)\nota_1,    0,    ota_1,    0x210000, 0x200000  ,<< Default partition for flashing (OTA, 2MB)\nota_2,    0,    ota_2,    0x410000, 0x200000  ,<< Application (2MB)\nota_3,    0,    ota_3,    0x610000, 0x200000  ,<< Application (2MB)\nota_4,    0,    ota_4,    0x810000, 0x200000  ,<< Application (2MB)\nota_5,    0,    ota_5,    0xA10000, 0x200000  ,<< Application (2MB)\nfirmware, app,  factory,  0xC10000, 0x0F0000  ,<< Factory partition holding the firmware menu (960KB)\nspiffs,   data, spiffs,   0xD00000, 0x2F0000  ,<< SPIFFS (2MB)\ncoredump, data, coredump, 0xFF0000,  0x10000\n```\n\n#### Quick Start:\n\n- Set a custom partition scheme according to the requirements (see annotated example above)\n- Compile and flash the [M5Stack-FW-Menu]https://github.com/tobozo/M5Stack-SD-Updater/tree/master/examples/M5Stack-FW-Menu)\n- On first run the `M5Stack-FW-Menu` firmware loader will automatically populate the factory partition and restart from there\n\nThen for every other app you want to store on the flash:\n\n- Set the same custom partition scheme as `M5Stack-FW-Menu`\n- Add `SDUCfg.rollBackToFactory = true;` and second argument must be empty e.g. `checkSDUpdater( SD, \"\", 5000, TFCARD_CS_PIN )`\n- Compile your app\n- Copy the binary to the SD Card (e.g. copy the bin manually or use the `Save SD` option from the app's SD-Updater lobby)\n- Use `FW Menu` option from the app's SD-Updater lobby (note: **it should load the firmware loader, not the /menu.bin from the SD Card**)\n- Use the firmware loader menu `Manage Partitions/Add Firmware` to copy the recently added app to one of the available slots\n\nNote: the firmware loader can copy applications from any filesystem (SD, SD_MMC, SPIFFS, LittleFS, FFat).\n\n\n#### Detect factory support\n\n```cpp\n#if M5_SD_UPDATER_VERSION_INT >= VERSION_VAL(1, 2, 8)\n// New SD Updater support, requires version >=1.2.8 of https://github.com/tobozo/M5Stack-SD-Updater/\nif( Flash::hasFactoryApp() ) {\n  SDUCfg.rollBackToFactory = true;\n  SDUCfg.setLabelMenu(\"FW Menu\");\n  SDUCfg.setLabelRollback(\"Save FW\");\n  checkFWUpdater( 5000 );\n} else\n#endif\n{\n  checkSDUpdater( SD, MENU_BIN, 5000, TFCARD_CS_PIN );\n}\n```\n\n\n\n\n\n\n\n🛣 ROADMAP:\n----------\n- Completely detach the UI/Display/Touch/Buttons from the codebase\n- Support gzipped binaries\n~~- Migrate Downloader / WiFiManager to external apps~~\n- esp-idf support\n- Contributors welcome!\n\n<br />\n\n#️⃣  REFERENCES:\n--------------\n<br />\n\n|              |                          |                                              |\n| ------------ |:------------------------ | :------------------------------------------- |\n| :clapper:   | Video demonstration      | https://youtu.be/myQfeYxyc3o                 |\n| :clapper:   | [Video demo of Pacman + sound](https://youtu.be/36fgNCecoEg) | [Source](https://github.com/tobozo/M5Stack-Pacman-JoyPSP) |\n| :clapper:   | [Video demo of NyanCat](https://youtu.be/Zxh2mtWwfaE) |  [Source](https://github.com/tobozo/M5Stack-NyanCat)  |\n| 🎓        | [Macsbug's article on M5Stack SD-Updater](https://macsbug.wordpress.com/2018/03/12/m5stack-sd-updater/) | [🇯🇵](https://macsbug.wordpress.com/2018/03/12/m5stack-sd-updater/) [🇬🇧](https://translate.google.com/translate?hl=en&sl=ja&tl=en&u=https%3A%2F%2Fmacsbug.wordpress.com%2F2018%2F03%2F12%2Fm5stack-sd-updater%2F) (google translate)|\n\n<br />\n\n🙏 CREDITS:\n-----------\n\n<br />\n\n|        |                     |                  |                                              |\n| ------ |:------------------- | :--------------- | :------------------------------------------- |\n| 👍     | M5Stack             | M5Stack          | https://github.com/m5stack/M5Stack           |\n| 👍     | M5StackSam          | Tom Such         | https://github.com/tomsuch/M5StackSAM        |\n| 👍     | ArduinoJSON         | Benoît Blanchon  | https://github.com/bblanchon/ArduinoJson/    |\n| 👍     | QRCode              | Richard Moore    | https://github.com/ricmoo/qrcode             |\n| 👍     | @Reaper7            | Reaper7          | https://github.com/reaper7                   |\n| 👍     | @PartsandCircuits   | PartsandCircuits | https://github.com/PartsandCircuits          |\n| 👍     | @lovyan03           | らびやん           | https://github.com/lovyan03                 |\n| 👍     | @matsumo            | Matsumo          | https://github.com/matsumo                   |\n| 👍     | @riraosan           | Riraosan         | https://github.com/riraosan                  |\n| 👍     | @ockernuts          | ockernuts        | https://github.com/ockernuts                 |\n"
  },
  {
    "path": "component.mk",
    "content": "#\n# Main Makefile. This is basically the same as a component makefile.\n#\n# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)\n\nCOMPONENT_SRCDIRS := src\nCOMPONENT_ADD_INCLUDEDIRS := src\n\n"
  },
  {
    "path": "examples/AppStore/AppStore.ino",
    "content": "\n#include \"main/main.cpp\"\n"
  },
  {
    "path": "examples/AppStore/main/main.cpp",
    "content": "\n#include \"../modules/AppStoreMain/AppStoreMain.cpp\"\n\n\nvoid setup()\n{\n  AppStore::setup();\n}\n\n\nvoid loop()\n{\n  AppStore::loop();\n}\n\n#if !defined ARDUINO\nextern \"C\" {\n  void loopTask(void*)\n  {\n    setup();\n    for(;;) {\n      loop();\n    }\n  }\n  void app_main()\n  {\n    xTaskCreatePinnedToCore( loopTask, \"loopTask\", 8192, NULL, 1, NULL, 1 );\n  }\n}\n#endif\n"
  },
  {
    "path": "examples/AppStore/modules/AppStoreActions/AppStoreActions.cpp",
    "content": "#pragma once\n\n#include <ArduinoJson.h>\n#include \"AppStoreActions.hpp\"\n#include \"../AppStoreUI/AppStoreUI.hpp\"\n#include \"../Registry/Registry.hpp\"\n#include \"../FSUtils/FSUtils.hpp\"\n#include \"../Downloader/Downloader.hpp\"\n\nextern AppRegistry Registry;\nextern LogWindow *Console;\n\n\nnamespace UIDo\n{\n\n  using namespace FSUtils;\n  using namespace UILists;\n  using namespace UIUtils;\n  using namespace RegistryUtils;\n\n  void BtnA()\n  {\n    // Action button\n    UI->execBtnA();\n  }\n\n  void BtnB()\n  {\n    // Navigation button\n    //UI->pageDown();\n    UI->menuUp();\n  }\n\n  void BtnC()\n  {\n    // Navigation button\n    UI->menuDown();\n  }\n\n\n  void checkSleepTimer()\n  {\n    if( lastpush + MsBeforeSleep < millis() ) { // go to sleep if nothing happens for a while\n      if( brightness > 1 ) { // slowly dim the screen first\n        brightness--;\n        if( brightness %10 == 0 ) {\n          Serial.print(\"(\\\".¬.\\\") \");\n        }\n        if( brightness %30 == 0 ) {\n          Serial.print(\" Yawn... \");\n        }\n        if( brightness %7 == 0 ) {\n          Serial.println(\" .zzZzzz. \");\n        }\n        UI->getGfx()->setBrightness( brightness );\n        lastpush = millis() - (MsBeforeSleep - brightness*10); // exponential dimming effect\n        return;\n      }\n      gotoSleep();\n    }\n  }\n\n\n  void gotoSleep()\n  {\n    Serial.println( GOTOSLEEP_MESSAGE );\n    #ifdef ARDUINO_M5STACK_Core2\n      esp_sleep_enable_ext0_wakeup(GPIO_NUM_39, 0); // gpio39 == touch INT\n    #else\n      #if defined HAS_POWER || defined HAS_IP5306\n        // M5Fire / M5Stack / Odroid-Go\n        M5.setWakeupButton( BUTTON_B_PIN );\n        //M5.powerOFF();\n      #endif\n    #endif\n    delay(100);\n    M5.Lcd.fillScreen( UI->getTheme()->BgColor );\n    M5.Lcd.sleep();\n    M5.Lcd.waitDisplay();\n    esp_deep_sleep_start();\n  }\n\n\n  void clearTLS()\n  {\n    // cleanup /cert/ and /.registry/ folders\n    Console = new LogWindow();\n    cleanDir( SD_CERT_PATH );\n    cleanDir( appRegistryFolder.c_str() );\n    delete Console;\n    buildRootMenu();\n  }\n\n\n  void clearApps()\n  {\n    int resp = modalConfirm( MODAL_DELETEALL_TITLE, nullptr, MODAL_DELETEALL_MSG, MENUACTION_APPDELETE, MENU_BTN_CANCEL, MENU_BTN_NO );\n    if( resp == HID_BTN_A ) {\n      #if !defined HAS_RTC\n        setTimeFromLastFSAccess(); // set the time before cleaning up the folder\n      #endif\n      Console = new LogWindow();\n      cleanDir( CATALOG_DIR );\n      // cleanDir( ROOT_DIR ); // <<< TODO: filter from app list\n      // cleanDir( DIR_jpg ); // <<< TODO: filter from app list\n      // cleanDir( DIR_json ); // <<< TODO: filter from app list\n      cleanDir( \"/tmp/\" );\n      delete Console;\n    }\n    buildRootMenu();\n  }\n\n\n  void setNtpServer()\n  {\n    uint8_t menuId = UI->getListID();\n    if( menuId > 0 ) { // first element is \"back to root menu\"\n      NTP::setServer( menuId-1 );\n    }\n    buildRootMenu();\n  }\n\n\n  void installApp( const char* appName )\n  {\n    // TODO: confirmation dialog\n    if( !Downloader::downloadApp( String(appName) ) ) {\n      log_e(\"Download of %s app failed\");\n    }\n  }\n\n  void installApp()\n  {\n    MenuActionLabels* tempLabels = UI->getMenuActionLabels();\n    installApp( UI->getListItemTitle() );\n    UI->setMenuActionLabels( tempLabels );\n  }\n\n  void deleteApp( const char* appName )\n  {\n    drawInfoWindow( String( \"Deleting \" + String(appName) ).c_str() );\n    removeInstalledApp( String( appName ) );\n  }\n\n  void deleteApp()\n  {\n    deleteApp( UI->getListItemTitle() );\n  }\n\n  void removeHiddenApp()\n  {\n    // TODO: open appinfowindow + hide button\n    String appName = String( UI->getListItemTitle() );\n    drawInfoWindow( String( \"Unhiding \" + appName).c_str() );\n    toggleHiddenApp( UI->getListItemTitle(), false );\n  }\n\n  void addHiddenApp()\n  {\n    // TODO: open appinfowindow + hide button\n    String appName = String( UI->getListItemTitle() );\n    drawInfoWindow( String( \"Hiding \" + appName).c_str() );\n    toggleHiddenApp( UI->getListItemTitle(), true );\n  }\n\n\n  void downloadCatalog()\n  {\n    bool loop = true;\n    do {\n      loop = Downloader::downloadGzCatalog()\n            ? false\n            : modalConfirm( MODAL_DOWNLOADFAIL_TITLE, nullptr, MODAL_DOWNLOADFAIL_MSG, MENU_BTN_RETRY, MENU_BTN_CANCEL, MENU_BTN_NO ) == HID_BTN_A\n      ;\n    } while( loop == true );\n    buildRootMenu();\n  }\n\n\n  void idle()\n  {\n\n  }\n\n  void modal()\n  {\n    cycleAppAssets();\n  }\n\n\n  #if !defined FS_CAN_CREATE_PATH\n    void doFSChecks()\n    {\n      #if !defined HAS_RTC\n        setTimeFromLastFSAccess();\n      #endif\n      scanDataFolder(); // do SD health checks, create folders\n    }\n  #endif\n\n};\n\n\nnamespace UIDraw\n{\n\n  using namespace MenuItems;\n  using namespace UIUtils;\n  using namespace Downloader;\n  using namespace RegistryUtils;\n\n  void drawDownloaderMenu( const char* title, const char* body )\n  {\n    UI->windowClr( UI->getTheme()->MenuColor );\n    UI->setMenuActionLabels( &RefreshAppsActionButtons );\n    UI->drawMenu( false );\n    drawStatusBar();\n    if( title != nullptr ) {\n      drawInfoWindow( title, body );\n    }\n  }\n\n\n  void drawList( bool renderButtons )\n  {\n    if( renderButtons ) {\n      UI->drawMenu( false );\n      drawStatusBar();\n    }\n    UI->showList();\n  }\n\n\n  void drawStatusBar()\n  {\n    UITheme* theme = UI->getTheme();\n    LGFX* gfx = UI->getGfx();\n    ForkIcon.draw( gfx, 4, (TITLEBAR_HEIGHT-2)/2-ForkIcon.height/2 );\n    drawTextShadow(gfx, UI->channel_name, ForkIcon.width+8, (TITLEBAR_HEIGHT-2)/2, theme->TextColor, theme->TextShadowColor, &TomThumb, ML_DATUM );\n\n    if( wifisetup ) {\n      int8_t rssiminmaxed = min( (int8_t)-70, max( (int8_t)-50, (int8_t)WiFi.RSSI() ) );\n      int16_t rssimapped = map( rssiminmaxed, -50, -70, 1, 5 );\n      drawRSSIBar( 290, 4, rssimapped, theme->MenuColor, 2.0 );\n    } else {\n      SDUpdaterIcon.draw( gfx, 298, 6 );\n    }\n    if( ntpsetup ) {\n      // TODO: draw some ntp icon\n    }\n  }\n\n\n  void drawRegistryMenu()\n  {\n\n    String _mc = \"\"; // macro separator\n    String modalMessage = _mc + CHANNEL_TOOL_TEXT + \"\\n\"\n                        + \"\\nCurrent: \" + Registry.defaultChannel.name\n                        + \"\\nAlternate: \" + (Registry.defaultChannel.name == REGISTRY_MASTER ? Registry.unstableChannel.name : Registry.masterChannel.name)\n    ;\n\n    int resp = modalConfirm( CHANNEL_TOOL, ROOTACTION_SWITCH, modalMessage.c_str(), DOWNLOADER_MODAL_CHANGE, MENU_BTN_UPDATE, MENU_BTN_CANCEL );\n    // choose between updating the JSON or changing the default channel\n    switch( resp ) {\n      case HID_BTN_A: // pick a channel\n        resp = modalConfirm( CHANNEL_CHOOSER, CHANNEL_CHOOSER_PROMPT, CHANNEL_CHOOSER_TEXT, DOWNLOADER_MODAL_CHANGE, MENU_BTN_CANCEL, MENU_BTN_BACK );\n        if( resp == HID_BTN_A ) {\n          if( Registry.pref_default_channel == REGISTRY_MASTER ) {\n            Registry.pref_default_channel = REGISTRY_UNSTABLE;\n          } else {\n            Registry.pref_default_channel = REGISTRY_MASTER;\n          }\n          registrySave( Registry, appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName );\n          // TODO: download registry JSON\n          //Serial.println(\"Will reload in 5 sec\");\n          delay(5000);\n          ESP.restart();\n        }\n      break;\n      case HID_BTN_B: // update channel\n        resp = modalConfirm( CHANNEL_DOWNLOADER, CHANNEL_DOWNLOADER_PROMPT, CHANNEL_DOWNLOADER_TEXT, MENU_BTN_UPDATE, MENU_BTN_CANCEL, MENU_BTN_BACK );\n        if( resp == HID_BTN_A ) {\n          registryFetch( Registry, appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName );\n        }\n      break;\n      default: // run wifi manager ?\n\n      break;\n    }\n    drawList( true );\n  }\n\n};\n\n\n\n\nnamespace AppRenderer\n{\n  using namespace UILists;\n  using namespace FSUtils;\n  //LGFX* gfx;\n\n  void AppInfo::clear()\n  {\n    appNameStr = \"\";\n    authorNameStr = NOT_IN_REGISTRY;\n    projectURLStr = \"\";\n    descriptionStr = \"\";\n    creditsStr = \"\";\n    type = REG_UNKNOWN;\n    assets_folder = nullptr;\n    assets.clear();\n    binSize = packageSize = assetsCount = rawtime = 0;\n    log_v(\"Cleared\");\n  }\n\n  void AppInfo::parseAssets( JsonObject root )\n  {\n    if( assetsCount > 0 ) {\n      String appImageShaSum = \"\";\n      String authorImageShaSum = \"\";\n      assets.clear();\n      for (JsonVariant value : root[\"json_meta\"][\"assets\"].as<JsonArray>() ) {\n        String assetName      = value[\"name\"].as<String>();//: \"9axis_data_publisher.jpg\",\n        String assetPath      = value[\"path\"].as<String>();//: \"/jpg/\",\n        String assetSha256Sum = value[\"sha256_sum\"].as<String>();\n        size_t assetSize      = value[\"size\"].as<size_t>();//: 7215,\n        rawtime               = value[\"created_at\"].as<time_t>();\n        log_d(\"Collected rawtime: %d for file %s%s\", rawtime, assetPath.c_str(), assetName.c_str() );\n        String assetFullPath  = assetPath+assetName;\n        packageSize += assetSize;\n        if( isBinFile( assetFullPath.c_str() ) ) {\n          if( M5_FS.exists( assetFullPath ) ) {\n            type = REG_LOCAL;\n          }\n          binSize = assetSize;\n          rawtime = rawtime;//: 1573146468,\n        } else {\n          if( assetName == appNameStr + EXT_jpg ) {\n            // is app image\n            appImageShaSum = assetSha256Sum;\n          } else if ( assetName == appNameStr + \"_gh\" + EXT_jpg ) {\n            // is author image\n            authorImageShaSum = assetSha256Sum;\n          }\n        }\n        assets.push_back({ assetFullPath, assetName, assetPath,  assetSha256Sum, assetSize, rawtime });\n      }\n      has_app_image = false;\n      if( appImageShaSum != \"\" && authorImageShaSum != \"\" ) {\n        if( appImageShaSum != authorImageShaSum ) {\n          log_w(\"Author image and app image differ!\");\n          has_app_image = true;\n        } else {\n          log_w(\"Author image and app image are similar!\");\n        }\n      } else {\n        log_w(\"Author image and app image shasums are missing!\");\n      }\n\n    }\n  }\n\n  void AppInfo::draw()\n  {\n    log_v(\"rendering\");\n    UI->windowClr();\n    UITheme* theme = UI->getTheme();\n    AppInfoPosY = 64;\n\n    if( appInfo.appNameStr ) {\n      drawTextShadow( _gfx, appInfo.appNameStr.c_str(), _gfx->width()/2, AppInfoPosY-20, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, MC_DATUM );\n    }\n\n    if( authorNameStr ) {\n      String authorString = authorNameStr;\n      if( strcmp( NOT_IN_REGISTRY, authorString.c_str() ) !=0 ) {\n        showAppImage( assets_folder, \"_gh\");\n        authorString = \"By: \"+authorString;\n      } else {\n        UnknownAppIcon.draw( _gfx, theme->assetPosX, theme->assetPosY );\n      }\n      drawTextShadow( _gfx, authorString.c_str(), AppInfoPosX, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM );\n      AppInfoPosY += 21;\n    } else { log_v(\"NO AUTHOR\"); }\n    if( assetsCount > 0 ) {\n      String assetsString = \"Assets: \" + String( assetsCount );\n      drawTextShadow( _gfx, assetsString.c_str(), AppInfoPosX, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM );\n      AppInfoPosY += 21;\n    } else { log_v(\"NO ASSETS\"); }\n    if( binSize > 0 ) {\n      String sizeString   = \"Bin.: \" + String(formatBytes( binSize, formatBuffer ));\n      drawTextShadow( _gfx, sizeString.c_str(), AppInfoPosX, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM );\n      AppInfoPosY += 21;\n    } else { log_v(\"NO BIN SIZE\"); }\n    if( packageSize > 0 ) {\n      String totalSize    = \"Tot.: \" + String(formatBytes( packageSize, formatBuffer ));\n      drawTextShadow( _gfx, totalSize.c_str(), AppInfoPosX, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM );\n      AppInfoPosY += 21;\n    } else { log_v(\"NO PACK SIZE\"); }\n    if( rawtime > 0 ) {\n      struct tm * timeinfo;\n      //time (&rawtime);\n      timeinfo = localtime (&rawtime);\n      drawTextShadow( _gfx, \"Creation date:\", AppInfoPosX, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM );\n      AppInfoPosY += 21;\n\n      strftime( formatBuffer, 64, \"%F\", timeinfo ); // to ISO date format\n      String binDateStr = String( formatBuffer );\n      drawTextShadow( _gfx, binDateStr.c_str(), AppInfoPosX+12, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM );\n      AppInfoPosY += 21;\n\n      strftime( formatBuffer, 64, \"%T\", timeinfo ); // to ISO time format\n      String binTimeStr = String( formatBuffer );\n      drawTextShadow( _gfx, binTimeStr.c_str(), AppInfoPosX+12, AppInfoPosY, theme->TextColor, theme->TextShadowColor, &FreeMono9pt7b, ML_DATUM );\n    } else { log_v(\"NO DATETIME\"); }\n    lastrender = millis();\n  }\n\n};\n\n\n\nnamespace UIShow\n{\n  using namespace FSUtils;\n  using namespace UILists;\n  using namespace UIDo;\n  using namespace MenuItems;\n  using namespace AppRenderer;\n\n  std::vector<onrender_cb_t> callbacks;\n\n\n  void showAppQrCode()\n  {\n    UITheme* th=UI->getTheme();\n    qrRender(UI->getGfx(), appInfo.projectURLStr, th->assetPosX, th->assetPosY, th->assetWidth, th->assetHeight );\n  }\n\n\n  void showAppAssetImage()\n  {\n    showAppImage( appInfo.assets_folder, \"\" );\n  }\n\n\n  void showAppAssetAuthor()\n  {\n    showAppImage( appInfo.assets_folder, \"_gh\" );\n  }\n\n\n  void showNTPImage()\n  {\n    const char* serverNameCstr = UI->getListItemTitle();\n    String serverNameStr = String( serverNameCstr );\n    String _ms = \"\"; // macro separator\n    String iconPath = CATALOG_DIR + _ms + DIR_png + \"NTP-\" + serverNameStr + EXT_png;\n    UITheme* theme = UI->getTheme();\n    RemoteAsset appIcon = { iconPath.c_str(), theme->assetWidth, theme->assetHeight, serverNameCstr };\n    drawAssetReveal( &appIcon, UI->getGfx(), theme->assetPosX, theme->assetPosY );\n  }\n\n\n\n  void updateCheckShowAppImage()\n  {\n    AppInfo tmpInfo;\n    getAppInfo( &tmpInfo, JSON_LOCAL );\n    getAppInfo( &appInfo, JSON_REMOTE );\n    // \"IN REGISTRY\" => compare binary size with meta, propose sha256_sum check or update + delete\n    showAppImage( \"\", \"\");\n    UITheme* theme = UI->getTheme();\n    LGFX* gfx = UI->getGfx();\n    // add icon status as an overlay to the image\n    if( appInfo.binSize != tmpInfo.binSize ) {\n      UpdateIcon.draw( gfx, theme->assetPosX, theme->assetPosY );\n      log_v(\"Bin sizes differ (local=%d, remote=%d)\", tmpInfo.binSize, appInfo.binSize);\n    } else {\n      log_v(\"Bin sizes match (%d)\", appInfo.binSize);\n      CheckIcon.draw( gfx, theme->assetPosX, theme->assetPosY );\n    }\n  }\n\n  void updateMetaShowAppImage()\n  {\n    // \"BINARY INSTALLED\" && \"IN REGISTRY\" => missing local meta ? => suggest copy meta + delete\n    log_d(\"TODO: check if missing local meta\");\n    getAppInfo( &appInfo, JSON_REMOTE );\n    showAppImage( CATALOG_DIR, \"\");\n    UITheme* theme = UI->getTheme();\n    UpdateIcon.draw( UI->getGfx(), theme->assetPosX, theme->assetPosY );\n  }\n\n  void showDeleteAppImage()\n  {\n    String _ms = \"\"; // for macro separation\n    String imageLocal  =                     DIR_jpg + String(UI->getListItemTitle()) + EXT_jpg;\n    String imageRemote = CATALOG_DIR + _ms + DIR_jpg + String(UI->getListItemTitle()) + EXT_jpg;\n    getAppInfo( &appInfo, JSON_LOCAL );\n    UITheme* theme = UI->getTheme();\n    RemoteAsset appIcon = { nullptr, theme->assetWidth, theme->assetHeight, UI->getListItemTitle() };\n    String imageRender = \"\";\n    if( M5_FS.exists( imageLocal ) ) {\n      appIcon.path = imageLocal.c_str();\n      log_d(\"local image\");\n    } else if( M5_FS.exists( imageRemote ) ) {\n      appIcon.path = imageRemote.c_str();\n      log_d(\"remote image\");\n    } else {\n      appIcon = UnknownAppIcon;\n      log_d(\"default image\");\n    }\n    drawAssetReveal( &appIcon, UI->getGfx(), theme->assetPosX, theme->assetPosY );\n  }\n\n\n  void showAppImage()\n  {\n    showAppImage( UI->getList()->assets_folder, \"\");\n    if( strcmp( UI->getListItemTitle(), appInfo.appNameStr.c_str() ) != 0 ) {\n      log_d(\"AppInfo expired (expecting:%s, got:%s)\", UI->getListItemTitle(), appInfo.appNameStr.c_str() );\n      getAppInfo( &appInfo, UI->getList()->assets_folder[0] == '0' ? JSON_LOCAL : JSON_REMOTE );\n    } else {\n      log_d(\"re-using existing appinfo\");\n    }\n    appInfo.lastrender = millis();\n  }\n\n\n  void showAppImage( const char* prefix, const char* suffix )\n  {\n    const char* appNameStr      = UI->getListItemTitle();\n    const char* menuNameStr     = UI->getListTitle();\n    char appImageFile[255] = {0};\n    memset( appImageFile, 0, 255 );\n    snprintf( appImageFile, 255, \"%s\" DIR_jpg \"%s%s\" EXT_jpg, prefix, appNameStr, suffix );\n    log_v(\"Extrapolated Icon Path: '%s' (%d bytes) for '%s' menu item\", appImageFile, strlen(appImageFile), menuNameStr );\n    UITheme* theme = UI->getTheme();\n    RemoteAsset appIcon = { appImageFile, theme->assetWidth, theme->assetHeight, appNameStr };\n    drawAssetReveal( &appIcon, UI->getGfx(), theme->assetPosX, theme->assetPosY );\n    appInfo.lastrender = millis();\n  }\n\n\n  void getAppInfo( AppInfo *appInfo, AppJSONType jsonType )\n  {\n    appInfo->clear();\n    appInfo->appNameStr    = String( UI->getListItemTitle() );\n    appInfo->type          = isHiddenApp( appInfo->appNameStr ) ? REG_HIDDEN : M5_FS.exists( ROOT_DIR+appInfo->appNameStr+EXT_bin ) ? REG_LOCAL : REG_REMOTE;\n    appInfo->assets_folder = UI->getList()->assets_folder;\n    String assetsFolderStr = jsonType == JSON_LOCAL ? \"\" : CATALOG_DIR;\n    String jsonFile        = assetsFolderStr + DIR_json + appInfo->appNameStr + EXT_json;\n    String appImageFile    = assetsFolderStr + DIR_jpg + appInfo->appNameStr + EXT_jpg;\n    String binFile         = ROOT_DIR + appInfo->appNameStr + EXT_bin;\n    if( M5_FS.exists( binFile ) ) getFileAttrs( binFile.c_str(), &appInfo->binSize, &appInfo->rawtime );\n    JsonObject root;\n    DynamicJsonDocument jsonBuffer( 8192 );\n    if( !getJson( jsonFile.c_str(), root, jsonBuffer ) ) {\n      appInfo->type = NOREG;\n      log_d(\"NOREG: '%s' has no JSON\", jsonFile.c_str() );\n      return;\n    }\n    if ( root.isNull() ) {\n      appInfo->type = NOREG;\n      log_e(\"No parsable JSON in %s file\", jsonFile.c_str() );\n      return;\n    }\n\n    size_t jsonMetaPropsCount;\n    fs::File file;\n    switch( jsonType ) {\n      case JSON_LOCAL:\n        if( M5_FS.exists( appImageFile ) ) appInfo->assetsCount++;\n        if( M5_FS.exists( jsonFile ) ) appInfo->assetsCount++;\n        appInfo->descriptionStr = root[\"description\"].isNull() ? \"\" : root[\"description\"].as<String>();\n        appInfo->authorNameStr  = root[\"authorName\"].isNull()  ? \"\" : root[\"authorName\"].as<String>();\n        appInfo->projectURLStr  = root[\"projectURL\"].isNull()  ? \"\" : root[\"projectURL\"].as<String>();\n        appInfo->creditsStr     = root[\"credits\"].isNull()     ? \"\" : root[\"credits\"].as<String>();\n        log_d(\"JSON_LOCAL: %s (%d bytes)\", appInfo->appNameStr.c_str(), appInfo->binSize );\n      break;\n      case JSON_REMOTE:\n        if( !root[\"name\"].as<String>().equals( appInfo->appNameStr ) ) {\n          log_e(\"AppName mismatch (expecting:'%s', got:'%s'\", appInfo->appNameStr.c_str(), root[\"name\"].as<String>().c_str() );\n          return;\n        }\n        jsonMetaPropsCount      = root[\"json_meta\"].size(); // only present on remote appinfo\n        appInfo->binSize        = root[\"size\"].as<size_t>();\n        appInfo->descriptionStr = jsonMetaPropsCount > 0 ? root[\"json_meta\"][\"description\"].isNull() ? \"\" : root[\"json_meta\"][\"description\"].as<String>() : \"\";\n        appInfo->assetsCount    = jsonMetaPropsCount > 0 ? root[\"json_meta\"][\"assets\"].size() : 0;\n        appInfo->authorNameStr  = jsonMetaPropsCount > 0 ? root[\"json_meta\"][\"authorName\"].as<String>() : \"\";\n        appInfo->projectURLStr  = jsonMetaPropsCount > 0 ? root[\"json_meta\"][\"projectURL\"].as<String>() : \"\";\n        appInfo->creditsStr     = jsonMetaPropsCount > 0 ? root[\"json_meta\"][\"credits\"].as<String>() : \"\";\n        if( appInfo->appNameStr == \"\" || appInfo->binSize <= 0 || jsonMetaPropsCount == 0 ) {\n          log_e(\"Invalid AppInfo in JSON (path: %s)\", jsonFile.c_str() );\n          return;\n        }\n        appInfo->parseAssets( root );\n        log_d(\"JSON_REMOTE: %s (%d bytes)\", appInfo->appNameStr.c_str(), appInfo->binSize );\n      break;\n      default:\n       log_e(\"INVALID TYPE\");\n      break;\n    }\n\n    cycleid = 0;\n    cycleanimation = false;\n    callbacks.clear();\n\n    if( appInfo->projectURLStr !=\"\" ) {\n      callbacks.push_back( &showAppQrCode );\n      cycleanimation = true;\n    }\n\n    callbacks.push_back( &showAppAssetAuthor );\n\n    if( appInfo->has_app_image ) {\n      callbacks.push_back( &showAppAssetImage );\n      cycleanimation = true;\n    }\n\n  }\n\n\n  void handleModalAction( AppInfo * appInfo )\n  {\n    onselect_cb_t aftermodal = [](){};\n    MenuActionLabels *confirmLabels = nullptr;\n    int hidState = HID_BTN_C;\n\n    switch( appInfo->type ) {\n      case REG_HIDDEN:\n        log_v(\"REG_HIDDEN\");\n        confirmLabels = &UnhideAppsActionButtons;\n        aftermodal = &buildHiddenAppList;\n      break;\n      case NOREG:\n        log_v(\"NOREG\");\n        confirmLabels = &DeleteAppsActionButtons;\n        aftermodal = &buildMyAppsMenu;\n      break;\n      case REG_LOCAL:\n        log_v(\"REG_LOCAL\");\n        confirmLabels = &DeleteAppsActionButtons;\n        aftermodal = &buildMyAppsMenu;\n      break;\n      case REG_REMOTE:\n        log_v(\"REG_REMOTE\");\n        confirmLabels = &InstallHideAppsActionButtons;\n        aftermodal = &buildStoreMenu;\n      break;\n      case REG_UNKNOWN:\n      default:\n        log_e(\"REG_UNKNOWN, No valid AppInfo->type (#%d) provided for menu element '%s'\", appInfo->type, UI->getListTitle() );\n      break;\n    }\n\n    if( confirmLabels ) {\n      confirmLabels->title = appInfo->appNameStr.c_str();\n      hidState = modalConfirm( confirmLabels, true );\n    }\n\n    switch( hidState ) {\n      case HID_BTN_A: confirmLabels->Buttons[0]->onClick(); aftermodal(); break;\n      case HID_BTN_B: confirmLabels->Buttons[1]->onClick(); aftermodal(); break;\n      case HID_BTN_C: UI->drawMenu( true ); UI->showList(); break;\n    }\n  }\n\n\n  void showAppInfo()\n  {\n    appInfo.draw();\n    handleModalAction( &appInfo );\n  }\n\n\n  void scrollAppInfo()\n  {\n    LGFX* gfx = UI->getGfx();\n    String scrollStr = appInfo.creditsStr;\n    if( appInfo.projectURLStr != \"\" ) scrollStr = scrollStr+ SCROLL_SEPARATOR + appInfo.projectURLStr;\n    if( appInfo.descriptionStr != \"\" ) scrollStr = scrollStr+ SCROLL_SEPARATOR + appInfo.descriptionStr;\n    scrollStr = scrollStr+SCROLL_SEPARATOR;\n    HeaderScroll.render( gfx, scrollStr, 10, 2, nullptr, 0, 0, gfx->width(), TITLEBAR_HEIGHT );\n  }\n\n\n  void cycleAppAssets()\n  {\n    if( !cycleanimation ) return;\n    uint32_t elapsed = millis()-appInfo.lastrender;\n    if( elapsed > cbdelay ) {\n      uint32_t cbid = cycleid%callbacks.size();\n      callbacks[cbid]();\n      appInfo.lastrender = millis();\n      cycleid++;\n    } else {\n      if( elapsed > 0 ) {\n        UITheme* th=UI->getTheme();\n        float progress = float(float(elapsed)/float(cbdelay))*th->assetWidth;\n        UI->getGfx()->fillRect( th->assetPosX, th->assetPosY+th->assetHeight-2, progress, 2, TFT_RED );\n      }\n      delay(10); // kill that buzz sound coming out of the speaker :D\n    }\n  }\n\n\n};\n\n\n\n\n\n\n\n\nnamespace UILists\n{\n  using namespace MenuItems;\n  using namespace FSUtils;\n  using namespace UIDo;\n  using namespace UIShow;\n  using namespace Downloader;\n\n  void buildNtpMenu()\n  {\n    size_t before = ESP.getFreeHeap();\n    NtpMenuGroup.clear();\n    NtpMenuGroup.push( &BackToRootMenu );\n    size_t servers_count = sizeof( NTP::Servers ) / sizeof( NTP::Server );\n\n    for( int i=0; i<servers_count; i++ ) {\n      log_v(\"Adding action menu %d : %s\", i, NTP::Servers[i].name );\n      NtpMenuGroup.push( NTP::Servers[i].name, &NtpItemCallbacks );\n    }\n    NtpMenuGroup.selectedindex = NTP::currentServer+1;\n    UI->setList( &NtpMenuGroup );\n    UIDraw::drawList( true ); // render the menu\n    log_v(\"Servers count: %d (bytes free: before=%d, after=%d)\", UI->getListSize(), before, ESP.getFreeHeap() );\n  }\n\n\n  void buildRootMenu()\n  {\n    size_t before = ESP.getFreeHeap();\n    countApps();\n    String jsonFile = CATALOG_DIR + Registry.defaultChannel.catalog_endpoint /*\"/catalog.json\"*/;\n\n    bool has_catalog = M5_FS.exists( jsonFile );\n    bool has_certs   = M5_FS.exists( SD_CERT_PATH );\n\n    log_v(\"Root menu: has_catalog:%s, has_certs:%s\", has_catalog?\"true\":\"false\", has_certs?\"true\":\"false\" );\n\n    RootMenuGroup.clear();\n\n    if( has_catalog ) {\n      RootActionRefresh.setTitle( ROOTACTION_REFRESH );\n    } else {\n      RootActionRefresh.setTitle( ROOTACTION_DOWNLOAD );\n    }\n\n    RootActionBrowse.setTitle( MENUTITLE_MANAGEAPPS );\n\n    if( has_catalog || appsCount > 0 ) {\n      RootMenuGroup.push( &RootActionBrowse );\n    }\n    RootMenuGroup.push( &RootActionRefresh );\n    RootMenuGroup.push( &RootActionSwitch );\n    RootMenuGroup.push( &RootActionNtp );\n    if( has_certs ) {\n      RootMenuGroup.push( &RootActionClearTls );\n    }\n    if( has_catalog ) {\n      RootMenuGroup.push( &RootActionClearAll );\n    }\n    RootMenuGroup.push( &RootActionSleep );\n\n    UI->setList( &RootMenuGroup );\n    UI->setMenuActionLabels( &RootListActionButtons );\n    UIDraw::drawList( true ); // render the menu\n    log_v(\"Rootmenu items count: %d (bytes free: before=%d, after=%d)\", UI->getListSize(), before, ESP.getFreeHeap() );\n  }\n\n\n  void buildBrowseAppsMenu()\n  {\n    countApps();\n    ManageAppsGroup.clear();\n    ManageAppsGroup.push( &BackToRootMenu );\n    String jsonFile = CATALOG_DIR + Registry.defaultChannel.catalog_endpoint /*\"/catalog.json\"*/;\n    if( M5_FS.exists( jsonFile ) ) {\n      ManageAppsGroup.push( &BrowseAppStore ); // [Browse/Install/Hide] Available Apps\n    } else { // no catalog available\n      ManageAppsGroup.push( &RootActionRefresh ); // Download Catalog\n    }\n    if( appsCount > 0 ) {\n      ManageAppsGroup.push( &ManageMyApps );   // [Browse/Delete] Installed Apps\n    }\n    if( M5_FS.exists( HIDDEN_APPS_FILE ) ) {\n      ManageAppsGroup.push( &ManageAppStore ); // [Unhide] Available Apps\n    }\n    UI->setList( &ManageAppsGroup );\n    UI->setMenuActionLabels( &MyAppsActionButtons );\n    UIDraw::drawList( true ); // render the menu\n  }\n\n\n\n  void buildHiddenAppList()\n  {\n    getHiddenApps();\n    if( HiddenFiles.size() == 0 ) {\n      return;\n    }\n    HiddenAppsMenuGroup.clear();\n    HiddenAppsMenuGroup.push( &BackToManageApps );\n\n    for( int i=0; i<HiddenFiles.size(); i++ ) {\n      HiddenAppsMenuGroup.push( HiddenFiles[i].c_str(), &HiddenAppsCallbacks, nullptr, isLauncher(HiddenFiles[i].c_str())?LAUNCHER_COLOR:TEXT_COLOR );\n    }\n    UI->setList( &HiddenAppsMenuGroup );\n    UIDraw::drawList( true ); // render the menu\n  }\n\n\n\n  void buildMyAppsMenu()\n  {\n    countApps();\n    if( appsCount == 0 ) {\n      log_v(\"No apps on SD Card, falling back to root menu\");\n      buildRootMenu();\n    }\n\n    std::vector<String> files;\n    getInstalledApps( files );\n\n    if( files.size() <= 0 ) {\n      log_v(\"No apps found, falling back to root menu\");\n      buildRootMenu();\n      return;\n    }\n\n    std::sort( files.begin(), files.end() );\n\n    MyAppsMenuGroup.clear();\n\n    for( int i=0; i<files.size(); i++ ) {\n      if( i%7==0 ) MyAppsMenuGroup.push( &BackToManageApps );\n      String _ms = \"\"; // for macro separation\n      String jsonLocalFile  =                     DIR_json + String( files[i].c_str() ) +  EXT_json;\n      String jsonRemoteFile = CATALOG_DIR + _ms + DIR_json + String( files[i].c_str() ) +  EXT_json;\n      if( M5_FS.exists( jsonLocalFile ) ) { // \"IN REGISTRY\" => compare size with meta, propose sha256_sum check or update + delete\n        log_v(\"[#%d] mb=updateCheckShowAppImage(%s)\", i, files[i].c_str() );\n        MyAppsMenuGroup.push( files[i].c_str(), &UpdateCheckCallbacks, nullptr, isLauncher(files[i].c_str())?LAUNCHER_COLOR:TEXT_COLOR );\n      } else if( M5_FS.exists( jsonRemoteFile ) ) { // \"BINARY INSTALLED\" && \"IN REGISTRY\" => missing local meta ? => suggest copy meta + delete\n        log_v(\"[#%d] mb=updateMetaShowAppImage(%s)\", i, files[i].c_str() );\n        MyAppsMenuGroup.push( files[i].c_str(), &UpdateMetaCallbacks, nullptr, isLauncher(files[i].c_str())?LAUNCHER_COLOR:TEXT_COLOR );\n      } else { // \"BINARY INSTALLED\" && \"NOT IN REGISTRY\" => only suggest delete\n        log_v(\"[#%d] mb=showDeleteAppImage(%s)\", i, files[i].c_str() );\n        MyAppsMenuGroup.push( files[i].c_str(), &DeleteAppCallbacks, nullptr, isLauncher(files[i].c_str())?LAUNCHER_COLOR:TEXT_COLOR );\n      }\n    }\n\n    UI->setList( &MyAppsMenuGroup );\n    // TODO: focus selected item\n    UIDraw::drawList( true ); // render the menu\n    log_v(\"Added %d items to menu\", MyAppsMenuGroup.actions_count );\n  }\n\n\n\n  void buildStoreMenu()\n  {\n    size_t before = ESP.getFreeHeap();\n\n    getHiddenApps(); // Fill 'HiddenFiles' vector\n\n    String jsonFile = CATALOG_DIR + Registry.defaultChannel.catalog_endpoint; // \"/catalog.json\"\n\n    JsonObject root;\n    DynamicJsonDocument jsonBuffer( 8192 );\n    if( !getJson( jsonFile.c_str(), root, jsonBuffer ) ) {\n      log_e(\"Failed to get json from %s\", jsonFile.c_str() );\n      return;\n    }\n    if ( root.isNull() ) {\n      log_e(\"No parsable JSON in %s file\", jsonFile.c_str() );\n      return;\n    }\n    if( root[\"apps\"].size() <= 0 ) {\n      log_e(\"No apps in catalog\");\n      return;\n    }\n    if( root[\"apps_count\"].as<size_t>() == 0 ) { // TODO: root[\"generated_at\"]\n      log_e(\"Empty catalog\");\n      return;\n    }\n    if( root[\"base_url\"].as<String>() == \"\" ) {\n      log_e(\"No base url\");\n      return;\n    }\n    if( root[\"gz_url\"].as<String>() == \"\" ) {\n      log_e(\"No gz url\");\n      return;\n    }\n\n    baseCatalogURL = root[\"base_url\"].as<String>();\n    gzCatalogURL   = root[\"gz_url\"].as<String>();\n\n    AppStoreMenuGroup.clear();\n    std::vector<String> files;\n\n    for( JsonVariant appEntry : root[\"apps\"].as<JsonArray>() ) {\n      String fName = appEntry[\"name\"].as<String>();\n      fName.trim();\n      if( fName != \"\" && !isHiddenApp(fName) && !M5_FS.exists( ROOT_DIR + fName + EXT_bin ) )\n        files.push_back( fName ); // don't list installed or hidden apps\n    }\n\n    std::sort( files.begin(), files.end() );\n\n    for( int i=0; i<files.size(); i++ ) {\n      if( i%7==0 ) AppStoreMenuGroup.push( &BackToManageApps );\n      log_v(\"Adding action menu %d : %s\", i, files[i].c_str() );\n      AppStoreMenuGroup.push( files[i].c_str(), &AppStoreCallbacks, nullptr, isLauncher(files[i].c_str())?LAUNCHER_COLOR:TEXT_COLOR );\n    }\n    files.clear();\n\n    UI->setList( &AppStoreMenuGroup );\n    UIDraw::drawList( true ); // render the menu\n    log_v(\"Added %d apps (bytes free: before=%d, after=%d)\", AppStoreMenuGroup.actions_count-1, before, ESP.getFreeHeap() );\n  }\n\n};\n"
  },
  {
    "path": "examples/AppStore/modules/AppStoreActions/AppStoreActions.hpp",
    "content": "#pragma once\n\n#include \"../misc/core.h\"\n#include <ArduinoJson.h> // https://github.com/bblanchon/ArduinoJson/\n\nnamespace UIDo\n{\n  unsigned long MsBeforeSleep  = MS_BEFORE_SLEEP;\n  unsigned long lastcheck      = millis(); // timer check\n  unsigned long lastpush       = millis(); // keypad/keyboard activity\n  uint8_t brightness = MAX_BRIGHTNESS; // used for fadeout before sleep\n\n  void gotoSleep();\n  void clearTLS();\n  void clearApps();\n  void setRootMenu();\n  void setMyAppsMenu();\n  void setStoreMenu();\n  void setNtpServer();\n  void setTimezone( float tz );\n  void setDst( bool set );\n  void downloadCatalog();\n  #if !defined FS_CAN_CREATE_PATH\n    void doFSChecks();\n  #endif\n  void checkSleepTimer();\n  void deleteApp();\n  void deleteApp( const char* appName );\n  void removeHiddenApp();\n  void addHiddenApp();\n  void installApp();\n  void installApp( const char* appName );\n  void downloadCatalog();\n  void BtnA();\n  void BtnB();\n  void BtnC();\n  void idle();\n  void modal();\n};\n\nnamespace UIDraw\n{\n  void drawBrowseAppsMenu();\n  void drawStatusBar();\n  void drawDownloaderMenu( const char* title = nullptr, const char* body = nullptr );\n  void drawList( bool renderButtons = false );\n  void drawRegistryMenu();\n};\n\nnamespace UILists\n{\n  void buildBrowseAppsMenu();\n  void buildMyAppsMenu();\n  void buildStoreMenu();\n  void buildRootMenu();\n  void buildNtpMenu();\n  void buildHiddenAppList();\n};\n\nenum AppJSONType\n{\n  JSON_LOCAL, // local json type, short version with authorName/width/height/projectURL/credits\n  JSON_REMOTE // remote json type, long version with meta assets\n};\n\nenum AppType\n{\n  REG_UNKNOWN, // default when unscanned\n  REG_LOCAL,   // in registry and installed\n  REG_REMOTE,  // in registry but not installed\n  REG_HIDDEN,  // in registry and hidden\n  NOREG        // not in resistry but present\n};\n\nstruct AppAsset\n{\n  String assetFullPath; //: \"/jpg/9axis_data_publisher.jpg\"\n  String name;          //: \"9axis_data_publisher.jpg\"\n  String path;          //: \"/jpg/\"\n  String assetSha256Sum;\n  size_t size;\n  time_t created_at;\n};\n\nnamespace AppRenderer\n{\n  uint16_t AppInfoPosY = 46;\n  uint16_t AppInfoPosX = LISTITEM_OFFSETX;\n  uint32_t cycleid = 0;\n  uint32_t cbdelay = 5000;  // msec cycle per callback\n  bool cycleanimation = false;\n\n  struct AppInfo\n  {\n    LGFX* _gfx;\n    String appNameStr;\n    String authorNameStr = NOT_IN_REGISTRY;\n    String projectURLStr;\n    String creditsStr;\n    String descriptionStr;\n    AppType type;\n    std::vector<AppAsset> assets;\n    bool has_app_image;\n    const char* assets_folder;\n    size_t binSize;\n    size_t packageSize;\n    size_t assetsCount;\n    time_t rawtime;\n    void clear();\n    void parseAssets( JsonObject root );\n    void draw();\n    uint32_t lastrender;\n  };\n  AppInfo appInfo;\n};\n\n\nnamespace UIShow\n{\n  using namespace AppRenderer;\n\n  void updateCheckShowAppImage();\n  void updateMetaShowAppImage();\n  void showDeleteAppImage();\n  void handleModalAction( AppInfo * appInfo );\n\n  void showAppInfo();\n  void scrollAppInfo();\n  void getAppInfo( AppInfo *appInfo, AppJSONType jsonType );\n  void showNTPImage();\n  void showAppImage();\n  void showAppImage( const char* prefix, const char* suffix );\n  void cycleAppAssets();\n};\n"
  },
  {
    "path": "examples/AppStore/modules/AppStoreMain/AppStoreMain.cpp",
    "content": "/*\n *\n * M5Stack Application Store\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2021 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n */\n\n#pragma once\n\n#include \"AppStoreMain.hpp\"\n#include \"../Console/Console.cpp\"\n#include \"../Assets/Assets.cpp\"\n#include \"../MenuItems/MenuItems.cpp\"\n#include \"../MenuUtils/MenuUtils.cpp\"\n#include \"../AppStoreUI/AppStoreUI.cpp\"\n#include \"../AppStoreActions/AppStoreActions.cpp\"\n#include \"../FSUtils/FSUtils.cpp\"\n#include \"../Downloader/Downloader.cpp\"\n#include \"../Registry/Registry.cpp\"\n#include \"../CertsManager/CertsManager.cpp\"\n\nAppStoreUI *UI = nullptr;\nLogWindow *Console = nullptr;\nAppRegistry Registry;\n\nnamespace AppStore\n{\n\n  void setup()\n  {\n    M5.begin( true, true, true, false, ScreenShotEnable ); // bool LCDEnable, bool SDEnable, bool SerialEnable, bool I2CEnable, bool ScreenShotEnable\n\n    #if !defined FS_CAN_CREATE_PATH\n      UIDo::doFSChecks();\n    #endif\n\n    UI = new AppStoreUI( &tft );\n\n    #if !defined HAS_RTC\n      FSUtils::setTimeFromLastFSAccess();\n    #endif\n\n    checkSDUpdater( M5_FS, MENU_BIN, 5000, TFCARD_CS_PIN );\n\n    Registry = RegistryUtils::init(); // load registry profile\n    UI->channel_name = Registry.defaultChannel.name.c_str();\n\n    DummyAsset::setup( &UIUtils::drawCaption, &MenuItems::BrokenImage, UI->getTheme()->TextColor, UI->getTheme()->BgColor );\n\n    TLS::wget = &Downloader::wget; // attach downloader to TLS\n    TLS::modalConfirm = &UIUtils::modalConfirm;\n    TLS::certProvider = Registry.defaultChannel.api_cert_provider_url_https; // set TLS cert provider\n    NTP::loadPrefServer();\n\n    BackToRootMenu.textcolor = DIMMED_COLOR;\n    BackToManageApps.textcolor = DIMMED_COLOR;\n\n    Serial.println( WELCOME_MESSAGE );\n    Serial.println( INIT_MESSAGE );\n    Serial.printf( MENU_SETTINGS, LINES_PER_PAGE, LIST_MAX_COUNT);\n    Serial.printf(\"Has PSRam: %s\\n\", psramInit() ? \"true\" : \"false\");\n    Serial.println(\"Build DateTime: \"+ Downloader::ISODateTime );\n\n    log_i(\"\\nRAM SIZE:\\t%s\\nFREE RAM:\\t%s\\nMAX ALLOC:\\t%s\",\n      String( formatBytes(ESP.getHeapSize(), formatBuffer) ).c_str(),\n      String( formatBytes(ESP.getFreeHeap(), formatBuffer) ).c_str(),\n      String( formatBytes(ESP.getMaxAllocHeap(), formatBuffer) ).c_str()\n    );\n\n    tft.setBrightness(100);\n    lastcheck = millis();\n\n    FSUtils::countApps();\n    UILists::buildRootMenu();\n\n  }\n\n\n  void loop()\n  {\n    HIDSignal hidState = getControls();\n\n    if( hidState!=HID_INERT && UIDo::brightness != MAX_BRIGHTNESS ) {\n      // some activity occured, restore brightness\n      Serial.println(\".. !!! Waking up !!\");\n      UIDo::brightness = MAX_BRIGHTNESS;\n      tft.setBrightness( UIDo::brightness );\n    }\n\n    UIDo::lastcheck = millis();\n\n    switch( hidState ) {\n      case HID_BTN_C:\n        UIDo::BtnC();\n      break;\n      case HID_BTN_A:\n        UI->getList()->selectedindex = UI->getListID();\n        UIDo::BtnA();\n      break;\n      case HID_BTN_B:\n        UI->getList()->selectedindex = UI->getListID();\n        UIDo::BtnB();\n      break;\n      default:\n      case HID_INERT:\n        UIDo::lastcheck = UIDo::lastpush;\n      break;\n      case HID_SCREENSHOT:\n        #if defined USE_SCREENSHOT\n          M5.ScreenShot->snap( \"screenshot\" );\n        #endif\n      break;\n    }\n    UIDo::lastpush = UIDo::lastcheck;\n    UI->idle();\n  }\n\n};\n"
  },
  {
    "path": "examples/AppStore/modules/AppStoreMain/AppStoreMain.hpp",
    "content": "/*\n *\n * M5Stack Application Store\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2021 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n */\n\n#pragma once\n\n#include \"../misc/core.h\"       // base software stack (ESP32-Chimera-Core + SD, etc )\n#include \"../misc/config.h\"     // config settings\n#include \"../misc/controls.h\"   // keypad / joypad / keyboard controls\n\nnamespace AppStore\n{\n  void setup();\n  void loop();\n};\n"
  },
  {
    "path": "examples/AppStore/modules/AppStoreUI/AppStoreUI.cpp",
    "content": "#pragma once\n\n#include \"AppStoreUI.hpp\"\n#include \"lgfx/utility/lgfx_qrcode.h\"\n\nusing namespace MenuItems;\nusing namespace UIDraw;\nusing namespace UIDo;\nusing namespace UIUtils;\n\n// destructor\nAppStoreUI::~AppStoreUI()\n{\n  clearList();\n}\n\n// constructor\nAppStoreUI::AppStoreUI( LGFX* _gfx, UITheme * theme )\n{\n  gfx   = _gfx;\n  Theme = theme;\n  Theme->setupCanvas( gfx );\n  initSprites( gfx );\n}\n\n// some public getters\nsize_t            AppStoreUI::getListPages() {        return ListTotalPages; }\nsize_t            AppStoreUI::getListSize() {         return ListCount; }\nuint16_t          AppStoreUI::getListID() {           return MenuID; }\nuint16_t          AppStoreUI::getPageID() {           return PageID; }\nconst char*       AppStoreUI::getListTitle() {        return Menu->ActionLabels->title; }\nconst char*       AppStoreUI::getListItemTitle() {    return Menu->Actions[MenuID]->title; }\nMenuActionLabels* AppStoreUI::getMenuActionLabels() { return Menu->ActionLabels; }\nUITheme*          AppStoreUI::getTheme() {            return Theme; }\nLGFX*             AppStoreUI::getGfx() {              return gfx; }\n\n\nbool AppStoreUI::empty( const char* str )\n{\n  return str ? (str[0] == '\\0') : true;\n}\n\n\nvoid AppStoreUI::clearList()\n{\n  PageID              = 0;\n  MenuID              = 0;\n  ListCount           = 0;\n  ListTotalPages      = 0;\n  ListLinesInLastPage = 0;\n}\n\n\nvoid AppStoreUI::setPageID( uint16_t idx )\n{\n  if( idx < ListCount/LinesPerPage ) {\n    PageID = idx;\n  }\n}\n\n\nvoid AppStoreUI::setListID( uint16_t idx )\n{\n  uint16_t was = PageID;\n  if( idx < ListCount ) {\n    MenuID = idx;\n  } else {\n    MenuID = ListCount -1;\n  }\n  PageID = MenuID / LinesPerPage;\n  log_v(\"Setting list id as %d (was %d)\", idx, was );\n}\n\n\nvoid AppStoreUI::nextList( bool renderAfter )\n{\n  if( MenuID < ( PageID * LinesPerPage + LinesInCurrentPage - 1 ) ) {\n    MenuID++;\n  } else {\n    if( PageID<ListTotalPages - 1 ) {\n      PageID++;\n    } else {\n      PageID = 0;\n    }\n    MenuID = PageID * LinesPerPage;\n  }\n  log_v(\"Next to %d/%d\", PageID, MenuID);\n  if( renderAfter ) showList();\n}\n\n\nvoid AppStoreUI::pageDown( bool renderAfter )\n{\n  if( PageID < ListTotalPages -1 ) {\n    PageID++;\n    MenuID = (PageID * LinesPerPage) -1;\n    nextList( false );\n  } else {\n    PageID = 0;\n    MenuID = 0;\n  }\n  log_v(\"Paging down to %d/%d\", PageID, MenuID);\n  if( renderAfter ) showList();\n}\n\n\nvoid AppStoreUI::pageUp( bool renderAfter )\n{\n  if( PageID > 0 ) {\n    PageID--;\n    MenuID -= LinesPerPage;\n    log_v(\"Paging up to %d/%d\", PageID, MenuID);\n    if( renderAfter ) showList();\n  }\n}\n\n\nvoid AppStoreUI::menuDown( bool renderAfter )\n{\n  int16_t lastId = MenuID;\n  int16_t oldPageID = PageID;\n\n  if( MenuID == ListCount-1 ) {\n    setListID( 0 );\n  } else {\n    setListID( MenuID+1 );\n  }\n\n  if( PageID != oldPageID ) {\n    if( renderAfter ) showList();\n  } else {\n    if( renderAfter ) updateList( lastId );\n  }\n}\n\n\nvoid AppStoreUI::menuUp( bool renderAfter )\n{\n  int16_t oldPageID = PageID;\n  int16_t lastId = MenuID;\n\n  if( MenuID == 0 ) {\n    // jump to end\n    setListID( ListCount-1 );\n  } else {\n    setListID( MenuID-1 );\n  }\n\n  if( PageID != oldPageID ) {\n    if( renderAfter ) showList();\n  } else {\n    if( renderAfter ) updateList( lastId );\n  }\n  log_v(\"Menu up to %d/%d\", PageID, MenuID);\n\n}\n\n\nvoid AppStoreUI::setList( MenuGroup* menu )\n{\n  if( menu->actions_count == 0 ) {\n    log_e(\"Cowardly refusing to insert an empty menu list for collection %s, aborting\", menu->Title );\n    return;\n  }\n  clearList();\n\n  ListCount = menu->actions_count;\n\n  if( ListCount>0 ) {\n    if( ListCount > LinesPerPage ) {\n      ListLinesInLastPage = ListCount % LinesPerPage;\n      if( ListLinesInLastPage>0 ) {\n        ListTotalPages = ( ListCount - ListLinesInLastPage ) / LinesPerPage;\n        ListTotalPages++;\n      } else {\n        ListTotalPages = ListCount / LinesPerPage;\n      }\n    } else {\n      ListTotalPages = 1;\n    }\n  }\n  // if( Menu && menu->Parent != nullptr ) menu->Parent = Menu;\n  Menu = menu;\n  if( Menu->selectedindex > 0 ) {\n    setListID( Menu->selectedindex );\n  }\n  log_v(\"Added list '%s' with %d elements\", menu->Title, menu->actions_count );\n}\n\n\nvoid AppStoreUI::execBtn( uint8_t bnum )\n{\n  if( Menu->ActionLabels && Menu->ActionLabels->Buttons && Menu->ActionLabels->Buttons[bnum] && Menu->ActionLabels->Buttons[bnum]->onClick ) {\n    log_v(\"Button #%d Inherited Callback for Item '%s' in menu '%s' (#%d / %d)\", bnum, getListItemTitle(), getListTitle(), MenuID, Menu->actions_count );\n    Menu->ActionLabels->Buttons[bnum]->onClick();\n  } else {\n    log_e(\"Button #%d MISSED Callback for Item #%d / %d\", bnum, MenuID, Menu->actions_count );\n  }\n}\n\n\nvoid AppStoreUI::execBtnA()\n{\n  if( Menu && MenuID < Menu->actions_count ) {\n    if( Menu->Actions[MenuID]->callbacks && Menu->Actions[MenuID]->callbacks->onSelect ) {\n      log_v(\"ButtonA Callback for Item '%s' (#%d / %d)\", getListItemTitle(), MenuID, Menu->actions_count );\n      Menu->Actions[MenuID]->callbacks->onSelect();\n    } else {\n      execBtn( 0 );\n    }\n  } else {\n    log_e(\"No menu or bad menu range( %d ) to exec from\", MenuID );\n  }\n}\n\n\nvoid AppStoreUI::execBtnB()\n{\n  execBtn( 1 );\n}\n\n\nvoid AppStoreUI::execBtnC()\n{\n  execBtn( 2 );\n}\n\n\nvoid AppStoreUI::idle()\n{\n  checkSleepTimer();\n  if( Menu->Actions[MenuID]->callbacks && Menu->Actions[MenuID]->callbacks->onIdle ) {\n    Menu->Actions[MenuID]->callbacks->onIdle();\n  }\n}\n\nvoid AppStoreUI::modal()\n{\n  checkSleepTimer();\n  if( Menu->Actions[MenuID]->callbacks && Menu->Actions[MenuID]->callbacks->onModal ) {\n    Menu->Actions[MenuID]->callbacks->onModal();\n  }\n}\n\n\nvoid AppStoreUI::buttonsClr()\n{\n  gfx->fillRect( Theme->WinPosX, Theme->ButtonsPosY, Theme->WinWidth, TITLEBAR_HEIGHT, Theme->BgColor );\n}\n\n\nvoid AppStoreUI::windowClr( uint32_t fillcolor )\n{\n  gfx->fillRect( Theme->WinPosX, Theme->WinPosY, Theme->WinWidth, Theme->WinHeight, fillcolor );\n}\n\n\nvoid AppStoreUI::windowClr()\n{\n  uint32_t begin = millis();\n\n  if( !getGradientLine() ) {\n    log_d(\"Failed to create sprite (%d x %d ), will fill with single color\", Theme->WinWidth, 1 );\n    gfx->fillScreen( Theme->MenuColor );\n    return;\n  }\n  gfx->startWrite();\n  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() );\n  GradienSprite->deleteSprite();\n  cropRoundRect( gfx, Theme->WinPosX, Theme->WinPosY, Theme->WinWidth, Theme->WinHeight, 5, Theme->BgColor );\n  gfx->endWrite();\n\n  log_v(\"clear took %d ms\", millis()-begin ); // avg 179ms\n}\n\n\nvoid AppStoreUI::drawListItem( uint16_t inIDX, uint16_t posY )\n{\n  if( inIDX==MenuID ) {\n    drawTextShadow( gfx, Menu->Actions[inIDX]->title, Theme->LIOffsetX, Theme->LIOffsetY+(posY*Theme->LIHeight), Menu->Actions[inIDX]->textcolor, Theme->TextShadowColor, LIFont, TL_DATUM );\n    drawCheckMark( gfx, 4, Theme->LIOffsetY+(posY*Theme->LIHeight)+3, Theme->arrowWidth, LIFontHeight/2, Menu->Actions[inIDX]->textcolor, Theme->TextShadowColor );\n  } else {\n    drawTextShadow( gfx, Menu->Actions[inIDX]->title, Theme->LIOffsetX, Theme->LIOffsetY+(posY*Theme->LIHeight), Menu->Actions[inIDX]->textcolor, Theme->TextShadowColor, LIFont, TL_DATUM );\n  }\n}\n\n\nvoid AppStoreUI::updateList( uint16_t clearid )\n{\n  if( !getGradientLine() ) { // can't blit\n    showList();\n    return;\n  }\n  uint16_t prevPosY   = Theme->LIOffsetY+((clearid%LinesPerPage)*Theme->LIHeight)+3; // last arrow position\n  uint16_t currPosY   = Theme->LIOffsetY+((MenuID%LinesPerPage)*Theme->LIHeight)+3;  // current arrow position\n  uint16_t clipPosX   = 4;\n  uint16_t clipWidth  = 1+Theme->arrowWidth;\n  uint16_t clipHeight = 1+LIFontHeight/2;\n  uint16_t *sprPtr    = (uint16_t*)GradienSprite->getBuffer();\n  uint16_t *clipPtr   = &sprPtr[clipPosX];\n  bool clear_asset    = false;\n  // delete old checkmark\n  gfx->pushImageRotateZoom( clipPosX+clipWidth/2, prevPosY+clipHeight/2, clipWidth/2, 0, 0.0, 1, clipHeight+1, clipWidth+1, GradienSprite->height()+1, clipPtr );\n  // draw new checkmark\n  drawCheckMark( gfx, 4, currPosY, Theme->arrowWidth, LIFontHeight/2, Menu->Actions[MenuID]->textcolor, Theme->TextShadowColor );\n\n  GradienSprite->deleteSprite();\n\n  callShowListHooks();\n}\n\n\nvoid AppStoreUI::showList()\n{\n  windowClr();\n  uint16_t i, items = 0;\n  gfx->startWrite();\n  if( ListTotalPages > 1 ) {\n    snprintf(paginationStr, 16, paginationTpl, PageID+1, ListTotalPages );\n    drawTextShadow( gfx, paginationStr, Theme->LICaptionPosX, Theme->LICaptionPosY, Theme->TextColor, Theme->TextShadowColor, &Font2, TR_DATUM );\n    snprintf(paginationStr, 16, totalCountTpl, ListCount );\n    drawTextShadow( gfx, paginationStr, Theme->LICaptionPosX, gfx->height()-(Theme->LICaptionPosY-3), Theme->TextColor, Theme->TextShadowColor, &Font2, BR_DATUM );\n  }\n  if( (PageID + 1) == ListTotalPages ) { // in last page\n    if( ListLinesInLastPage == 0 and ListCount >= LinesPerPage ) {\n      LinesInCurrentPage = LinesPerPage;\n      items = LinesPerPage;\n    } else {\n      if( ListTotalPages>1 ) {\n        LinesInCurrentPage = ListLinesInLastPage;\n        items = ListLinesInLastPage;\n      } else {\n        LinesInCurrentPage = ListCount;\n        items = ListCount;\n      }\n    }\n  } else { // in first page or paginaged\n    LinesInCurrentPage = LinesPerPage;\n    items = LinesPerPage;\n  }\n  for( i = 0; i<items; i++ ) {\n    drawListItem( i+(PageID*LinesPerPage), i );\n  }\n  gfx->endWrite();\n  callShowListHooks();\n}\n\n\nvoid AppStoreUI::callShowListHooks()\n{\n  if( Menu->Actions[MenuID]->callbacks && Menu->Actions[MenuID]->callbacks->onRender ) {\n    log_v(\"Side Callback for MenuItem '%s' (item #%d)\", Menu->Actions[MenuID]->title, MenuID );\n    Menu->Actions[MenuID]->callbacks->onRender();\n  }\n  if( Menu->Actions[MenuID]->icon ) {\n    log_v(\"Icon attached: %s\", Menu->Actions[MenuID]->icon->path );\n    RemoteAsset* asset = (RemoteAsset*)Menu->Actions[MenuID]->icon;\n    drawAssetReveal( asset, gfx, Theme->assetPosX, Theme->assetPosY );\n  }\n}\n\n\nvoid AppStoreUI::setMenuActionLabels( MenuActionLabels* _ActionLabels )\n{\n  log_v(\"Overwriting Menu ActionLabels '%s' with '%s'\", Menu->ActionLabels->title, _ActionLabels->title );\n  Menu->ActionLabels = _ActionLabels;\n}\n\n\nvoid AppStoreUI::drawButton( uint8_t bnum )\n{\n  if( Menu && Menu->ActionLabels && Menu->ActionLabels->Buttons && Menu->ActionLabels->Buttons[bnum] ) {\n    ButtonAction* btn = Menu->ActionLabels->Buttons[bnum];\n    ButtonSprite->pushSprite( buttonsXOffset[bnum], Theme->ButtonsPosY );\n    if( btn->asset && M5_FS.exists( btn->asset->path ) ) {\n      RemoteAsset *btnIcon = btn->asset;\n      btnIcon->draw( gfx, buttonsXOffset[bnum]+BUTTON_HWIDTH - btnIcon->width/2, Theme->ButtonsPosY+BUTTON_HEIGHT/2 - btnIcon->height/2 );\n    } else if( btn->title && !empty( btn->title ) ) {\n      drawTextShadow( gfx, btn->title, buttonsXOffset[bnum]+BUTTON_HWIDTH, Theme->ButtonsPosY+BUTTON_HEIGHT/2, Theme->TextColor, Theme->TextShadowColor, ButtonFont, MC_DATUM );\n    } else {\n      // empty button wut ?\n    }\n  } else {\n    gfx->fillRect( buttonsXOffset[bnum], Theme->ButtonsPosY, BUTTON_WIDTH, BUTTON_HEIGHT, Theme->BgColor );\n  }\n}\n\n\nvoid AppStoreUI::drawButtons()\n{\n  createButtonMask();\n  for( int i=0;i<BUTTONS_COUNT;i++ ) {\n    drawButton(i);\n  }\n  clearButtonMask();\n}\n\n\nvoid AppStoreUI::drawMenu( bool clearWindow )\n{\n  // header background fill\n  gfx->startWrite();\n  if( !getGradientLine( true ) ) {\n    gfx->fillRect( 0, 0, Theme->WinWidth, TITLEBAR_HEIGHT-2, Theme->MenuColor );\n  } else {\n    for( int i=0;i<TITLEBAR_HEIGHT-2; i++ ) {\n      GradienSprite->pushSprite( 0, i );\n    }\n    GradienSprite->deleteSprite();\n  }\n  // header text\n  drawTextShadow( gfx, Menu->Title, Theme->WinWidth/2, (TITLEBAR_HEIGHT-2)/2, Theme->TextColor, Theme->TextShadowColor, HeaderFont, MC_DATUM );\n  cropRoundRect( gfx, 0, 0, Theme->WinWidth, TITLEBAR_HEIGHT-2, 3, Theme->BgColor );\n  gfx->endWrite();\n  // window\n  if( clearWindow ) windowClr();\n  // buttons\n  drawButtons();\n}\n\n\n\n\n\nvoid UITheme::setupCanvas( LGFX* gfx )\n{\n  fontHeightFM9p7 = gfx->fontHeight(InfoWindowFont);\n  LIFontHeight    = gfx->fontHeight(LIFont);\n  fontHeight0     = gfx->fontHeight(&Font0);\n  arrowWidth      = gfx->textWidth(\">\");\n  ButtonsPosY     = gfx->height()-BUTTON_HEIGHT;\n  WinHeight       = gfx->height()-TITLEBAR_HEIGHT*2;\n  WinWidth        = gfx->width();\n  WinPosY         = TITLEBAR_HEIGHT+1;\n  WinPosX         = 0;\n  WinMargin       = WINDOW_MARGINX;\n  LIHeight        = LISTITEM_HEIGHT;\n  LIOffsetY       = LISTITEM_OFFSETY; // pixels offset from top for list items\n  LIOffsetX       = LISTITEM_OFFSETX; // pixels offset from left for list items\n  LICaptionPosX   = WinWidth-WinMargin; // text cursor position-x for list caption\n  LICaptionPosY   = LISTCAPTION_POSY; // text cursor position-Y for list caption\n  assetPosY       = ASSET_POSY; // assets position\n  assetPosX       = WinWidth-(assetWidth+WinMargin); // assets positionx\n  pgW             = gfx->width()-LISTITEM_OFFSETX*2; // progress bar width\n  pgX             = gfx->width()/2 - pgW/2; // pogress bar posx, based on width\n  BgColor         = BG_COLOR;\n  MenuColor       = MENU_COLOR;\n  TextColor       = TEXT_COLOR;\n  TextShadowColor = SHADOW_COLOR;\n  gzProgressBar.setup(  gfx, pgX,   106, pgW,   10, GZ_PROGRESS_COLOR,  MenuColor, true ); // gzip\n  tarProgressBar.setup( gfx, pgX,   152, pgW,   5,  TAR_PROGRESS_COLOR, MenuColor, true ); // tar\n  dlProgressBar.setup(  gfx, pgX+1, 188, pgW-2, 4,  DL_PROGRESS_COLOR,  MenuColor, false ); // download/sha sum\n  createGradients();\n}\n\n\nvoid UITheme::createGradients()\n{\n  uint8_t r = (MenuColor >> 16) & 0xff; // red\n  uint8_t g = (MenuColor >> 8) & 0xff;  // green\n  uint8_t b = MenuColor & 0xff;         // blue\n  float sf = 0.2; // shade factor\n  float tf = 0.2; // tint factor\n  TintedMenuColor = ( uint8_t(r + (255 - r) * tf) << 16 ) + ( uint8_t(g + (255 - g) * tf) << 8 ) + ( uint8_t(b + (255 - b) * tf) ) ;\n  ShadedMenuColor = ( uint8_t(r * (1 - sf)) << 16 ) + ( uint8_t(g * (1 - sf)) << 8 ) + ( uint8_t(b * (1 - sf)) ) ;\n}\n\n\nvoid ProgressBarTheme::clear()\n{\n  gfx->fillRect( x, y, w, caption?h+fontHeight0+2:h, bg );\n}\n\n\nvoid ProgressBarTheme::progress( uint8_t progress )\n{\n  uint16_t th = fontHeight0+2;\n  uint16_t ty = y+h+2;\n  drawProgressBar( gfx, x, y, w, h, progress, fg, bg );\n  if( caption ) {\n    String progressStr = \"   \" + String(progress)+\"%   \";\n    drawCaption( gfx, progressStr.c_str(), x, ty, w, th, &Font0, TC_DATUM, fg, bg );\n  }\n}\n\n\nvoid ProgressBarTheme::setup(LGFX* _gfx, uint16_t _x, uint16_t _y, uint16_t _w, uint16_t _h, uint32_t _fg, uint32_t _bg, bool _cap )\n{\n  gfx  = _gfx;\n  x = _x;\n  y = _y;\n  w = _w;\n  h = _h;\n  fg = _fg;\n  bg = _bg;\n  caption = _cap;\n}\n\n\n\n\nnamespace UIUtils\n{\n\n  using namespace MenuItems;\n  using namespace AppRenderer;\n\n  void initSprites( LGFX* gfx )\n  {\n    appInfo._gfx = gfx;\n\n    GradienSprite = new LGFX_Sprite( gfx );\n    GradienSprite->setColorDepth(16);\n\n    ButtonSprite = new LGFX_Sprite( gfx );\n    ButtonSprite->setColorDepth(16);\n\n    MaskSprite = new LGFX_Sprite( gfx );\n    MaskSprite->setColorDepth(16);\n\n    AssetSprite = new LGFX_Sprite( gfx );\n    AssetSprite->setColorDepth(16);\n\n    ScrollSprite = new LGFX_Sprite( gfx );\n    ScrollSprite->setColorDepth(1);\n\n  }\n\n  void drawProgressBar( LGFX* gfx, int x, int y, int w, int h, uint8_t val, uint32_t color, uint32_t bgcolor )\n  {\n    gfx->drawRect(x, y, w, h, color);\n    if( val>100) val = 100;\n    if( val==0 ) {\n      gfx->fillRect(x + 1,         y + 1, w-2,       h - 2, bgcolor);\n    } else {\n      int fillw = (w * (((float)val) / 100.0)) -2;\n      gfx->fillRect(x + 1,         y + 1, fillw-2,   h - 2, color);\n      gfx->fillRect(x + fillw + 1, y + 1, w-fillw-2, h - 2, bgcolor);\n    }\n  }\n\n\n  void gzProgressCallback( uint8_t progress )\n  {\n    static int8_t gzLibLastProgress = -1;\n    if( gzLibLastProgress != progress ) {\n      gzLibLastProgress = progress;\n      UI->getTheme()->gzProgressBar.progress( progress );\n    }\n  }\n\n\n  void tarProgressCallback( uint8_t progress )\n  {\n    static int8_t tarLibLastProgress = -1;\n    if( tarLibLastProgress != progress ) {\n      tarLibLastProgress = progress;\n      UI->getTheme()->tarProgressBar.progress( progress );\n    }\n  }\n\n\n  void tarStatusCallback( const char* name, size_t size, size_t total_unpacked )\n  {\n    log_d(\"[TAR] %-64s %8d bytes - %8d Total bytes\", name, size, total_unpacked );\n    size_t th_small = fontHeight0*1.3;\n    size_t th_big   = fontHeightFM9p7*1.3;\n    uint16_t posy = 162; // bottom text position (file size, overall size)\n    static bool clean;\n    UITheme* theme = UI->getTheme();\n    LGFX* gfx = UI->getGfx();\n    if( clean != true ) {\n      clean = true;\n      UI->windowClr( theme->MenuColor );\n      drawInfoWindow( TAR_PROGRESS_TITLE );\n    }\n    // TODO: don't redraw OVERALL_PROGRESS_TITLE on every callback\n    drawCaption( gfx, OVERALL_PROGRESS_TITLE, theme->gzProgressBar.x, theme->gzProgressBar.y - (th_big+2), theme->gzProgressBar.w, th_big, InfoWindowFont, MC_DATUM );\n\n    drawCaption( gfx, name, WINDOW_MARGINX, theme->tarProgressBar.y - (th_small+2), gfx->width()-WINDOW_MARGINX*2, th_small, &Font0, MC_DATUM );\n    drawCaption( gfx, String( formatBytes( size, formatBuffer ) ).c_str(), WINDOW_MARGINX, posy+th_big, gfx->width()/3-WINDOW_MARGINX, th_big, InfoWindowFont, TL_DATUM );\n    const char* caption =  String( \"Total: \" + String( formatBytes( total_unpacked, formatBuffer ) ) ).c_str();\n    drawCaption( gfx, caption, gfx->width()/3, posy+th_big, gfx->width()*2/3-WINDOW_MARGINX, th_big, InfoWindowFont, TR_DATUM );\n  }\n\n\n  bool getGradientLine( bool invert )\n  {\n    uint32_t width = UI->getGfx()->width();\n    if( !GradienSprite->createSprite( width, 1 ) ) {\n      log_d(\"Failed to create sprite (%d x %d ), will fill with single color\", width, 1 );\n      return false;\n    }\n    UITheme* theme = UI->getTheme();\n    GradienSprite->drawGradientHLine( 0, 0, width, invert?theme->TintedMenuColor:theme->ShadedMenuColor, invert?theme->ShadedMenuColor:theme->TintedMenuColor );\n    return true;\n  }\n\n\n  void createButtonMask()\n  {\n    if( !ButtonSprite->createSprite( BUTTON_WIDTH, BUTTON_HEIGHT ) ) {\n      ButtonSprite->setColorDepth( ButtonSprite->getColorDepth()/2 || 1 );\n      log_d(\"Failed to create Button Sprite (%d x %d )\", BUTTON_WIDTH, 1 );\n      return;\n    }\n    UITheme* theme = UI->getTheme();\n    // fill with gradient\n    for( int i=0; i<BUTTON_HEIGHT; i++ ) {\n      ButtonSprite->drawGradientHLine( 0, i, BUTTON_WIDTH, theme->TintedMenuColor, theme->ShadedMenuColor );\n    }\n    // make a button skin\n    MaskSprite->createSprite( BUTTON_WIDTH, BUTTON_HEIGHT );\n    // colors will be cut diagonally, prepare coord for triangles\n    uint16_t x1 = 0              , y1 = 0;\n    uint16_t x2 = 0              , y2 = BUTTON_HEIGHT-1;\n    uint16_t x3 =  BUTTON_WIDTH-1, y3 = 0;\n    uint16_t x4 =  BUTTON_WIDTH-1, y4 = BUTTON_HEIGHT-1;\n    // apply both colors\n    for( int i=-1; i<1; i++ ) {\n      drawButtonMask( (LGFX*)MaskSprite, 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, 3, 3, theme->BgColor, i?theme->ShadedMenuColor:theme->TintedMenuColor );\n      MaskSprite->fillTriangle( x2, y2, i?x4:x1, i?y4:y1, x3, y3, theme->BgColor );\n      MaskSprite->pushSprite( ButtonSprite, 0, 0, theme->BgColor );\n    }\n    MaskSprite->deleteSprite();\n    // apply rounded borders\n    cropRoundRect( (LGFX*)ButtonSprite, 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, 5, theme->BgColor );\n  }\n\n\n  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 )\n  {\n    gfx->fillRect( x, y, w, h, fillcolor );\n    for( int i=0; i<thickness;i++ ) {\n      gfx->drawRoundRect( x+i, y+i, w-i*2, h-i*2, radius+i, bgcolor );\n    }\n  }\n\n\n  void clearButtonMask()\n  {\n    ButtonSprite->deleteSprite();\n  }\n\n\n  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 )\n  {\n    gfx->fillTriangle( dirx+gx, diry+gy, dirx+gx, diry+gy+gh, dirx+gx+gw, diry+gy+gh/2, shadowcolor );\n    gfx->fillTriangle( gx,      gy,      gx,      gy+gh,      gx+gw,      gy+gh/2,      textcolor );\n  }\n\n\n  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 )\n  {\n    gfx->setTextDatum( datum );\n    gfx->setFont( font );\n    gfx->setTextColor( shadowcolor );\n    gfx->drawString( str, dirx+x, diry+y );\n    gfx->setTextColor( textcolor );\n    gfx->drawString( str, x, y );\n  }\n\n\n  template<typename T> void cropRoundRect( T* gfx, int32_t x, int32_t y, uint32_t w, uint32_t h, uint8_t radius, uint32_t bgcolor )\n  {\n    if( cropSprite1bit == nullptr ) {\n      cropSprite1bit = new LGFX_Sprite( gfx );\n      cropSprite1bit->setColorDepth(1);\n      cropSprite1bit->createSprite( 1, 1 );\n    }\n    if( w != cropSprite1bit->width() || h != cropSprite1bit->width() ) {\n      cropSprite1bit->deleteSprite();\n      if( ! cropSprite1bit->createSprite( w, h ) ) {\n        log_e(\"Could not create sprite (%d x %x)\", w, h);\n        return;\n      }\n      log_v(\"Using 1BitMask@[%d:%d]->[%dx%d] radius(%d), color(0x%06x)\", x, y, w, h, radius, bgcolor );\n      cropSprite1bit->setPaletteColor( 0, 0x123456U );\n      cropSprite1bit->setPaletteColor( 1, bgcolor );\n      cropSprite1bit->fillSprite( 1 );\n      cropSprite1bit->fillRoundRect( 0, 0, w, h, radius, 0 );\n    }\n    cropSprite1bit->pushSprite( gfx, x, y, 0 );\n  }\n\n\n  void drawDownloadIcon( uint32_t color, int16_t x, int16_t y, float size )\n  {\n    float halfsize = size/2;\n    LGFX* gfx = UI->getGfx();\n    gfx->fillTriangle( x,      y+2*size, x+4*size, y+2*size, x+2*size, y+5*size, color );\n    gfx->fillTriangle( x+size, y,        x+3*size, y,        x+2*size, y+5*size, color );\n    gfx->fillRect( x, -halfsize+y+6*size, 1+4*size, size, color );\n  }\n\n  void drawRSSIBar( int16_t x, int16_t y, int16_t rssi, uint32_t bgcolor, float size )\n  {\n    RGBColor heatMapColors[4] = { {0xff, 0, 0}, {0xff, 0xa5, 0x00}, {0xff, 0xff, 0}, {0, 0xff, 0} }; // # [RED, ORANGE, YELLOW, GREEN ]\n    uint32_t heatColor32 = getHeatMapColor( rssi%6, 0, 5, heatMapColors, sizeof(heatMapColors)/sizeof(RGBColor) );\n    uint8_t bars = 0;\n    uint32_t barColors[4] = { bgcolor, bgcolor, bgcolor, bgcolor };\n    LGFX* gfx = UI->getGfx();\n    switch(rssi%6) {\n    case 5: bars = 4; break;\n      case 4: bars = 3; break;\n      case 3: bars = 3; break;\n      case 2: bars = 2; break;\n      case 1: bars = 1; break;\n      default:\n      case 0: bars = 1; break;\n    }\n    for( int i=0; i<bars; i++ ) {\n      barColors[i] = heatColor32;\n    }\n    for( int i=0; i<4; i++ ) {\n      gfx->fillRect(x + (i*3*size),  y + (4-i)*size, 2*size, (4+i)*size, barColors[i]);\n    }\n  }\n\n\n  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 )\n  {\n    if( captionSprite == nullptr ) {\n      captionSprite = new LGFX_Sprite( gfx );\n      captionSprite->setColorDepth(1);\n    }\n    captionSprite->createSprite( w, h );\n    captionSprite->setPaletteColor(0, bgcolor );\n    captionSprite->setPaletteColor(1, fgcolor );\n    captionSprite->setFont((lgfx::v1::IFont*)font );\n    captionSprite->setTextDatum( TL_DATUM ); // we manage our own datum here\n    captionSprite->setTextWrap(false);\n    int32_t tx, ty;\n    uint16_t tw = captionSprite->textWidth( caption );\n    uint16_t th = captionSprite->fontHeight();\n    bool wrap = true;\n    switch( datum ) {\n      case TL_DATUM: tx=0;        ty=0;        wrap = true;  break;\n      case TC_DATUM: tx=w/2-tw/2; ty=0;        wrap = false; break;\n      case TR_DATUM: tx=w-tw-1;   ty=0;        wrap = true;  break;\n      case ML_DATUM: tx=0;        ty=h/2-th/2; wrap = true;  break;\n      case MC_DATUM: tx=w/2-tw/2; ty=h/2-th/2; wrap = false; break;\n      case MR_DATUM: tx=w-tw-1;   ty=h/2-th/2; wrap = true;  break;\n      case BL_DATUM: tx=0;        ty=h-1;      wrap = true;  break;\n      case BC_DATUM: tx=w/2-tw/2; ty=h-1;      wrap = false; break;\n      case BR_DATUM: tx=w-tw-1;   ty=h-1;      wrap = true;  break;\n    }\n    captionSprite->setTextWrap(wrap);\n    captionSprite->setCursor( tx, ty );\n    captionSprite->println( caption );\n    captionSprite->pushSprite( gfx, x, y );\n    captionSprite->deleteSprite();\n  }\n\n\n  void drawInfoWindow( const char* title, const char* body, unsigned long waitdelay )\n  {\n    UITheme* theme = UI->getTheme();\n    LGFX* gfx = UI->getGfx();\n    UI->windowClr( theme->MenuColor );\n    if( title ) {\n      drawCaption( gfx, title, LISTITEM_OFFSETX, LISTITEM_OFFSETY, gfx->width()-LISTITEM_OFFSETX*2, TITLEBAR_HEIGHT, &FreeMonoBold12pt7b, MC_DATUM, theme->TextColor, theme->MenuColor );\n    }\n    if( body ) {\n      drawCaption( gfx, body, LISTITEM_OFFSETX, 80, gfx->width()-LISTITEM_OFFSETX*2, TITLEBAR_HEIGHT*3, InfoWindowFont, TL_DATUM );\n    }\n    if( waitdelay > 0 ) {\n      delay( waitdelay );\n    }\n  }\n\n\n  int modalConfirm( MenuActionLabels* labels, bool clearAfter )\n  {\n    // backup UI labels\n    MenuActionLabels* tempLabels = UI->getMenuActionLabels();\n    const char* tempTitle = UI->getList()->Title;\n    // set temporary labels and redraw\n    UI->setMenuActionLabels( labels );\n    UI->getList()->Title = labels->title;\n    UI->drawMenu( false );\n    // wait for button input\n    int hidState = HID_INERT;\n    while( hidState == HID_INERT ) {\n      hidState = getControls();\n      UI->modal();\n    }\n    // deinit scroll if necessary\n    if( HeaderScroll.scrollInited ) HeaderScroll.scrollEnd();\n    // restore labels to lask known values and redraw UI\n    UI->setMenuActionLabels( tempLabels );\n    UI->getList()->Title = tempTitle;\n    if( clearAfter ) {\n      UI->drawMenu( true );\n      drawStatusBar();\n    }\n    return hidState;\n  }\n\n  int modalConfirm( const char* question, const char* title, const char* body, const char* labelA, const char* labelB, const char* labelC )\n  {\n    ButtonAction btnA = { labelA, nullptr };\n    ButtonAction btnB = { labelB, nullptr };\n    ButtonAction btnC = { labelC, nullptr };\n    ButtonAction *buttons[BUTTONS_COUNT] = { &btnA, &btnB, &btnC };\n    MenuActionLabels ActionLabels = { question, buttons };\n    LGFX* gfx = UI->getGfx();\n    drawInfoWindow( title?title:UI->getListItemTitle(), body );\n    CautionModalIcon.draw( gfx, gfx->width()-CautionModalIcon.width-LISTITEM_OFFSETX, gfx->height()-CautionModalIcon.height-LISTITEM_OFFSETY );\n    HIDSignal hidState = HID_INERT;\n    return modalConfirm( &ActionLabels );\n  }\n\n  // like map( value, min, max, COLOR_MIN, COLOR_MAX ) but with RGBColor Palette\n  uint32_t getHeatMapColor( int value, int minimum, int maximum, RGBColor *p, size_t psize  )\n  {\n    // They float, they all float ​🤡\n    float f = float(value-minimum) / float(maximum-minimum) * float(psize-1); // convert to palette index\n    int i = int(f/1); // closest color index in palette\n    float dst = f - float(i); // distance to next color in palette\n    if( dst < std::numeric_limits<float>::epsilon() ) { // exact color match\n      return ( p[i].r << 16 ) + ( p[i].g << 8 ) + p[i].b;\n    } else { // apply distance to matched color\n      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 ) ) ) ;\n    }\n  }\n\n\n  /* give up on redundancy and ECC to produce less and bigger squares */\n  uint8_t getLowestQRVersionFromString( String text, uint8_t ecc )\n  {\n    #define QR_MAX_VERSION 9\n    if(ecc>QR_MAX_VERSION) return QR_MAX_VERSION; // fail fast\n    uint16_t len = text.length();\n    uint8_t QRMaxLenByECCLevel[4][QR_MAX_VERSION] = {\n      // https://www.qrcode.com/en/about/version.html\n      // Handling version 1-9 only since there's no point with M5Stack's 320x240 display (next version is at 271)\n      { 17, 32, 53, 78, 106, 134, 154, 192, 230 }, // L\n      { 14, 26, 45, 62, 84,  106, 122, 152, 180 }, // M\n      { 11, 20, 32, 46, 60,  74,  86,  108, 130 }, // Q\n      { 7,  14, 24, 34, 44,  58,  64,  84,  98  }  // H\n    };\n    for( uint8_t i=0; i<QR_MAX_VERSION; i++ ) {\n      if( len <= QRMaxLenByECCLevel[ecc][i] ) {\n        log_d(\"string len=%d bytes, fits in version %d / ecc %d (max=%d)\", len, i+1, ecc, QRMaxLenByECCLevel[ecc][i] );\n        return i+1;\n      }\n    }\n    log_e(\"String length exceeds output parameters\");\n    return QR_MAX_VERSION;\n  }\n\n\n  void qrRender( LGFX* gfx, String text, int posX, int posY, uint32_t width, uint32_t height )\n  {\n    if( text.length()==0 ) {\n      log_d(\"Cowardly refusing to qRender an empty string\");\n      return; // empty string for some reason\n    }\n\n    // see https://github.com/Kongduino/M5_QR_Code/blob/master/M5_QRCode_Test.ino\n    // Create the QR code\n    QRCode qrcode;\n\n    uint8_t ecc = 0; // QR on TFT can do with minimal ECC\n    uint8_t version = getLowestQRVersionFromString( text, ecc );\n\n    uint8_t qrcodeData[lgfx_qrcode_getBufferSize( version )];\n    lgfx_qrcode_initText( &qrcode, qrcodeData, version, ecc, text.c_str() );\n\n    uint32_t gridSize  = (qrcode.size + 4);   // margin: 2 dots\n    uint32_t dotWidth  = width / gridSize;    // rounded\n    uint32_t dotHeight = height / gridSize;   // rounded\n    uint32_t realWidth  = dotWidth*gridSize;  // recalculated\n    uint32_t realHeight = dotHeight*gridSize; // recalculated\n    if( realWidth > width || realHeight > height ) {\n      log_e(\"Can't fit QR with gridsize(%d),dotSize(%d) =>[%dx%d] => [%dx%d]\", gridSize, dotWidth, realWidth, realHeight, width, height );\n      return;\n    } else {\n      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 );\n    }\n\n    uint8_t marginX = (width  - qrcode.size*dotWidth)/2;\n    uint8_t marginY = (height - qrcode.size*dotHeight)/2;\n\n    gfx->fillRect( posX, posY, width, height, TFT_WHITE );\n\n    for ( uint8_t y = 0; y < qrcode.size; y++ ) {\n      // Each horizontal module\n      for ( uint8_t x = 0; x < qrcode.size; x++ ) {\n        bool q = lgfx_qrcode_getModule( &qrcode, x, y );\n        if (q) {\n          gfx->fillRect( x*dotWidth +posX+marginX, y*dotHeight +posY+marginY, dotWidth, dotHeight, TFT_BLACK );\n        }\n      }\n    }\n  }\n\n\n  void fillAssetSprite( LGFX_Sprite* dst, int32_t offsetX, int32_t offsetY=0 )\n  {\n    if( !getGradientLine() ) { // can't blit\n      uint16_t transcolor = 0x1234;\n      dst->fillSprite( UI->getTheme()->MenuColor );\n      return;\n    }\n    uint16_t *sprPtr = (uint16_t*)GradienSprite->getBuffer();\n    uint16_t *clipPtr = &sprPtr[offsetX];\n    int32_t width = dst->width(), height = dst->height();\n    dst->pushImageRotateZoom( width/2, height/2, width/2, 0, 0.0, 1, height, width, 1, clipPtr );\n    GradienSprite->deleteSprite();\n  }\n\n\n  template<typename T> void drawAssetReveal( T *asset, LGFX*_gfx, int32_t posX, int32_t posY )\n  {\n    if( ! AssetSprite->createSprite( UI->getTheme()->assetWidth, UI->getTheme()->assetHeight ) ) {\n      log_w(\"Can't create %dx%d sprite, drawing raw\", UI->getTheme()->assetWidth, UI->getTheme()->assetHeight );\n      asset->draw( _gfx, posX, posY );\n      return;\n    }\n    fillAssetSprite( AssetSprite, posX );\n    asset->draw( (LGFX*)AssetSprite, 0, 0 );\n    uint16_t *sprPtr = (uint16_t*)AssetSprite->getBuffer();\n    for( int y=0; y<asset->height*2; y++ ) {\n      int scanY = y%2==0? y/4 : (-1+asset->height-y/4);\n      _gfx->pushImageRotateZoom( posX+asset->width/2, posY+scanY, asset->width/2, 0, 0.0, 1, 1, asset->width, 1, &sprPtr[scanY*asset->width] );\n      delayMicroseconds(300);\n    }\n    AssetSprite->deleteSprite();\n  }\n\n};\n\n\n\n\ntemplate<typename T> bool UIHScroll::init( T* gfx, String _scrollText, uint32_t width, uint32_t height, size_t textSize, const void*font )\n{\n  if( scrollInited ) return true;\n  if( _scrollText==\"\" ) {\n    log_e(\"No text to scroll\");\n    return false;\n  }\n  scrollText = _scrollText;\n  ScrollSprite->setColorDepth( 16 );\n  if( width==0 || height==0 || !ScrollSprite->createSprite( width, height ) ) {\n    log_e(\"Can't create scrollsprite\");\n    return false;\n  }\n  if( !getGradientLine() ) {\n    log_d(\"Failed to create gradient sprite (%d x %d )\", width, 1 );\n    return false;\n  }\n\n  ScrollSprite->setTextWrap( false ); // lazy way to solve a wrap bug\n  ScrollSprite->setFont((lgfx::v1::IFont*)font );\n  ScrollSprite->setTextSize( textSize ); // setup text size before it's measured\n  ScrollSprite->setTextDatum( ML_DATUM );\n\n  scrollTextColor1 = TFT_WHITE;\n  scrollTextColor2 = ScrollSprite->color565(0x40,0xaa,0x40);\n\n  if( !scrollText.endsWith( \" \" )) {\n    scrollText += SCROLL_SEPARATOR; // append a space since scrolling text *will* repeat\n  }\n  while( ScrollSprite->textWidth( scrollText ) < width ) {\n    scrollText += scrollText; // grow text to desired width\n  }\n\n  scrollWidth1 = ScrollSprite->textWidth( scrollText );\n  ScrollSprite->setTextSize( textSize+1 ); // setup text size before it's measured\n  scrollWidth2 = ScrollSprite->textWidth( scrollText );\n\n  log_w(\"Inited scroll to [%dx%d] virtual [%d / %d]\", width, height, scrollWidth1, scrollWidth2 );\n\n  scrollInited = true;\n  return true;\n}\n\n\ntemplate<typename T> 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 )\n{\n  if( millis() - lastScrollRender < delay ) return; // debouncing\n  if( !init( gfx, _scrollText, width, height, textSize, font ) ) return; // first call\n  if( scrollText==\"\" ) {\n    log_e(\"No text to scroll\");\n    return;\n  }\n  // push background\n  ScrollSprite->pushImageRotateZoom( ScrollSprite->width()/2, ScrollSprite->height()/2, width/2, 0, 0.0, 1, height, GradienSprite->width(), 1, (uint16_t*)GradienSprite->getBuffer() );\n  // draw text to scroll\n  printText( ScrollSprite, scrollText, 0.75, scrollWidth2, scrollTextColor2, delay, textSize+1, width, height );\n  printText( ScrollSprite, scrollText, 1.5,   scrollWidth1, scrollTextColor1, delay, textSize, width, height );\n  cropRoundRect( ScrollSprite, 0, 0, width, height, 5, 0x000000U );\n  ScrollSprite->pushSprite( gfx, x, y );\n  lastScrollRender = millis();\n}\n\ntemplate<typename T> 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 )\n{\n  gfx->setTextSize( textSize ); // setup text size before it's measured\n  int32_t tick = int(millis()/(delay*speed)) % textWidth*2;\n  gfx->setTextColor( textColor );\n  if( -tick > -textWidth ) {\n    gfx->drawString( scrollText, -tick, height/2 );\n  }\n  gfx->drawString( scrollText, textWidth - tick, height/2 );\n  if( textWidth*2 - tick < width ) {\n    gfx->drawString( scrollText, textWidth*2 - tick, height/2 );\n  }\n}\n\n\nvoid UIHScroll::scrollEnd()\n{\n  GradienSprite->deleteSprite();\n  ScrollSprite->deleteSprite();\n  scrollInited = false;\n  log_e(\"Deinited scroll\");\n}\n"
  },
  {
    "path": "examples/AppStore/modules/AppStoreUI/AppStoreUI.hpp",
    "content": "#pragma once\n\n#include \"../MenuItems/MenuItems.hpp\"\n#include \"../MenuUtils/MenuUtils.hpp\"\n#include \"../AppStoreActions/AppStoreActions.hpp\"\n\nstatic size_t fontHeight0;\nstatic size_t LIFontHeight;\nstatic size_t fontHeightFM9p7;\n\n// Progressbar theme\nstruct ProgressBarTheme\n{\n  LGFX* gfx;\n  uint16_t x;\n  uint16_t y;\n  uint16_t w;\n  uint16_t h;\n  uint32_t fg;\n  uint32_t bg;\n  bool caption;\n  void progress( uint8_t progress );\n  void setup(LGFX* _gfx, uint16_t _x, uint16_t _y, uint16_t _w, uint16_t _h, uint32_t _fg, uint32_t _bg, bool _cap );\n  void clear();\n};\n\n\n// UI Colors\nstruct UITheme\n{\n  uint32_t BgColor; // dark\n  uint32_t MenuColor;       // light\n  ProgressBarTheme gzProgressBar;\n  ProgressBarTheme tarProgressBar;\n  ProgressBarTheme dlProgressBar;\n  uint32_t TextColor;       // light\n  uint32_t TextShadowColor; // dark\n  uint32_t TintedMenuColor; // generated\n  uint32_t ShadedMenuColor; // generated\n\n  uint16_t pgW; // Window width minus the margins ( gfx->width()-LISTITEM_OFFSETX*2)\n  uint16_t pgX; // pogress bar posx, based on width\n\n  uint16_t WinHeight;\n  uint16_t WinWidth;\n  uint16_t WinPosY;\n  uint16_t WinPosX;\n  uint16_t WinMargin;\n\n  uint16_t ButtonsPosY;\n  uint16_t arrowWidth;\n\n  uint16_t LIOffsetY;\n  uint16_t LIOffsetX;\n  uint16_t LIHeight;\n  uint16_t LICaptionPosX;\n  uint16_t LICaptionPosY;\n\n  // base dimensions for assets placeholder\n  uint16_t assetPosX;\n  uint16_t assetPosY;\n  uint16_t assetWidth  = 120;\n  uint16_t assetHeight = 120;\n\n  // const lgfx::v1::IFont* ButtonFont     = _ButtonFont;\n  // const lgfx::v1::IFont* HeaderFont     = _HeaderFont;\n  // const lgfx::v1::IFont* LIFont         = _LIFont;\n  // const lgfx::v1::IFont* InfoWindowFont = _InfoWindowFont;\n\n  void setupCanvas( LGFX* gfx );\n  void createGradients();\n\n};\n\n\nstruct UIHScroll\n{\n  int16_t scrollPointer = 0; // pointer to the scrollText position\n  unsigned long lastScrollRender = millis(); // timer for scrolling\n  String lastScrollMessage; // last scrolling string state\n  int16_t lastScrollOffset; // last scrolling string position\n  bool scrollInited = false;\n  uint16_t scrollTextColor1;// TFT_WHITE\n  uint16_t scrollTextColor2;// = gfx->color565(0x40,0xaa,0x40);\n  size_t scrollWidth1;\n  size_t scrollWidth2;\n  String scrollText;\n\n  void scrollEnd();\n  template<typename T> bool init( T* gfx, String _scrollText, uint32_t width, uint32_t height, size_t textSize, const void*font );\n  template<typename T> 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 );\n  template<typename T> 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 );\n};\n\n\n\nnamespace UIUtils\n{\n  // buttons, masks, gradients, effects, etc\n  LGFX_Sprite* captionSprite = nullptr;\n  LGFX_Sprite* cropSprite1bit = nullptr;\n  LGFX_Sprite* GradienSprite = nullptr;\n  LGFX_Sprite* ButtonSprite = nullptr;\n  LGFX_Sprite* MaskSprite = nullptr;\n  LGFX_Sprite* AssetSprite = nullptr;\n  LGFX_Sprite* ScrollSprite = nullptr;\n  UITheme Theme;\n\n  // all draw primitives\n  void initSprites( LGFX*gfx );\n  int modalConfirm( MenuActionLabels* labels, bool clearMenu = false );\n  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);\n  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 );\n  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 );\n  void drawInfoWindow( const char* title = nullptr, const char* body = nullptr, unsigned long waitdelay = 0 );\n  void drawTextShadow(LGFX*gfx, const char*str, int32_t x, int32_t y, uint32_t txtcol=TEXT_COLOR, uint32_t shcol=SHADOW_COLOR,\n                      const lgfx::v1::IFont* font=&Font2, textdatum_t dt=MC_DATUM, int8_t dirx=1, int8_t diry=1 );\n  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 );\n  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\n  void drawDownloadIcon( uint32_t color=0x00ff00U, int16_t x=272, int16_t y=7, float size=2.0 );\n  void drawRSSIBar( int16_t x, int16_t y, int16_t rssi, uint32_t bgcolor, float size=1.0 );\n  void gzProgressCallback( uint8_t progress );\n  void tarProgressCallback( uint8_t progress );\n  void tarStatusCallback( const char* name, size_t size, size_t total_unpacked );\n  bool getGradientLine( bool invert = false );\n  void createButtonMask();\n  void clearButtonMask();\n  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 );\n\n  UIHScroll HeaderScroll;\n\n  template<typename T> void cropRoundRect( T *gfx, int32_t x, int32_t y, uint32_t w, uint32_t h, uint8_t radius, uint32_t bgcolor );\n  template<typename T> void drawAssetReveal( T *asset, LGFX*_gfx, int32_t posX, int32_t posY );\n\n};\n\n\nclass AppStoreUI\n{\n  public:\n    AppStoreUI( LGFX*_gfx, UITheme * theme = &UIUtils::Theme );\n    ~AppStoreUI();\n    void windowClr();\n    void windowClr( uint32_t fillcolor );\n    void buttonsClr();\n    void drawMenu( bool clearWindow = true );\n    void drawButtons();\n    void drawButton( uint8_t bnum );\n    void showList();\n    void clearList();\n    uint16_t getListID();\n    uint16_t getPageID();\n    size_t getListSize();\n    size_t getListPages();\n    void setListID( uint16_t idx );\n    void setPageID( uint16_t idx );\n    void nextList( bool renderAfter = true );\n    void pageUp( bool renderAfter = true );\n    void pageDown( bool renderAfter = true );\n    void menuUp( bool renderAfter = true );\n    void menuDown( bool renderAfter = true );\n    void execBtn( uint8_t bnum );\n    void execBtnA();\n    void execBtnB();\n    void execBtnC();\n    void idle();\n    void modal();\n\n    void setList( MenuGroup *actions );\n    MenuGroup* getList() { return Menu; }\n\n    void setMenuActionLabels( MenuActionLabels *labels );\n    MenuActionLabels* getMenuActionLabels();\n\n    void setTheme( UITheme* theme );\n    UITheme *getTheme();\n\n    LGFX* getGfx();\n\n    const char* getListItemTitle();\n    const char* getListTitle();\n    const char* channel_name;\n\n  private:\n\n    bool empty( const char* str );\n    void drawListItem( uint16_t inIDX, uint16_t postIDX );\n    void updateList( uint16_t clearid );\n    void callShowListHooks();\n\n    LGFX*      gfx = nullptr; // tft instance\n    MenuGroup* Menu = nullptr;\n    UITheme*   Theme = nullptr;\n\n    const char* emptyString = \"\";\n    char paginationStr[16];\n    const char* paginationTpl = \"%d/%d\";\n    const char* totalCountTpl = \"Total: %d\";\n\n    uint16_t LinesPerPage = LINES_PER_PAGE;\n    uint16_t ListLinesInLastPage;\n    uint16_t ListCount;\n    uint16_t ListTotalPages;\n    int16_t PageID;\n    int16_t MenuID;\n    uint16_t LinesInCurrentPage;\n\n    bool CyclePageList = false; // affects menuDown()/menuUp() behaviour when hitting end/bottom of page\n};\n\n\nextern AppStoreUI* UI;\n"
  },
  {
    "path": "examples/AppStore/modules/Assets/Assets.cpp",
    "content": "#pragma once\n\n#include \"Assets.hpp\"\n\nvoid LocalAsset::draw( LGFX* gfx, int32_t x, int32_t y, int32_t w, int32_t h  )\n{\n  log_v(\"Image '%s'\", alt_text);\n  switch( type ) {\n    case IMG_JPG: gfx->drawJpg( data, data_len, x, y, w>0?w:width, h>0?h:height ); break;\n    case IMG_PNG: gfx->drawPng( data, data_len, x, y, w>0?w:width, h>0?h:height ); break;\n    case IMG_RAW: gfx->pushImage( x, y, width, height, data ); break;\n  }\n}\n\n\nvoid RemoteAsset::draw( LGFX* gfx, int32_t x, int32_t y, int32_t w, int32_t h )\n{\n  log_v(\"Image File: '%s'\", path );\n  fs::File iconFile = M5_FS.open( path  );\n  if( !iconFile ) {\n    log_v(\"File not found: %s, will draw caption\", path );\n    DummyAsset::drawAltText( gfx, alt_text, x, y, w>0?w:width, h>0?h:height );\n    return;\n  }\n\n  if( String(path).endsWith(EXT_png) ) {\n    gfx->drawPng( &iconFile, x, y, w>0?w:width, h>0?h:height );\n  } else if( String(path).endsWith(EXT_jpg) ) {\n    gfx->drawJpg( &iconFile, x, y, w>0?w:width, h>0?h:height );\n  } else {\n    log_w(\"Unsupported image format: %s, will draw caption\", path );\n    DummyAsset::drawAltText( gfx, alt_text, x, y, w>0?w:width, h>0?h:height );\n  }\n  iconFile.close();\n}\n\n\nnamespace DummyAsset\n{\n  void drawAltText( LGFX* gfx, const char* caption, int32_t x, int32_t y, uint32_t w, uint32_t h )\n  {\n    if( drawCaption ) {\n      drawCaption( gfx, caption, x, y, w, h, &Font0, MC_DATUM, fgcolor, bgcolor );\n    }\n    BrokenImage->draw( gfx, x, y );\n  }\n\n  void setup( captionDrawer_t drawCb, LocalAsset * brokenImage, uint32_t _fgcolor, uint32_t _bgcolor )\n  {\n    BrokenImage = brokenImage;\n    drawCaption = drawCb;\n    bgcolor = _bgcolor;\n    fgcolor = _fgcolor;\n  }\n\n};\n"
  },
  {
    "path": "examples/AppStore/modules/Assets/Assets.h",
    "content": "/*\n * Image Source: https://i.imgur.com/llemDHF.gif\n *\n * - ImageMagick:\n *    #> convert -strip disk.gif disk%02d.jpg\n *\n * - Crop visual to 30x30 in an image editor\n *\n * - Export to C uchar format:\n *    #> xxd -i disk00.jpg >> assets.h\n *    #> xxd -i disk01.jpg >> assets.h\n *\n *\n */\n\n#pragma once\n\n\nstatic const unsigned char disk01_jpg[1486] = {\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x1e, 0x00, 0x1e, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n  0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,\n  0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n  0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,\n  0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n  0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25,\n  0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n  0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56,\n  0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86,\n  0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n  0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n  0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n  0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n  0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00,\n  0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n  0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,\n  0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n  0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08,\n  0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n  0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18,\n  0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n  0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55,\n  0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n  0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84,\n  0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n  0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa,\n  0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n  0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n  0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00,\n  0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfe,\n  0xc7, 0x7f, 0x6f, 0xc0, 0x7f, 0xe1, 0x45, 0x78, 0x04, 0x16, 0x04, 0xaf,\n  0xed, 0xaf, 0xff, 0x00, 0x04, 0xd4, 0x0d, 0xb7, 0x20, 0x93, 0xff, 0x00,\n  0x0f, 0x16, 0xfd, 0x95, 0xf2, 0x06, 0x64, 0x24, 0x65, 0x8a, 0xb9, 0x0e,\n  0x64, 0x25, 0x41, 0x03, 0x2c, 0x55, 0xc7, 0xa9, 0xfe, 0xd7, 0x7a, 0xff,\n  0x00, 0xc5, 0x4f, 0x09, 0xfe, 0xc9, 0xff, 0x00, 0xb4, 0xff, 0x00, 0x8a,\n  0xbe, 0x05, 0x43, 0xad, 0xdc, 0xfc, 0x6f, 0xf0, 0xcf, 0xec, 0xf1, 0xf1,\n  0xab, 0xc4, 0x1f, 0x07, 0x2d, 0xfc, 0x37, 0xe1, 0xa8, 0xfc, 0x6d, 0xe2,\n  0x29, 0xfe, 0x2a, 0xe8, 0xdf, 0x0d, 0xbc, 0x4b, 0xa8, 0xfc, 0x3d, 0x87,\n  0x40, 0xf0, 0x64, 0xfa, 0x66, 0xb7, 0x07, 0x8b, 0xb5, 0xa9, 0x7c, 0x5d,\n  0x6d, 0xa4, 0x26, 0x97, 0xe1, 0x89, 0xb4, 0x5d, 0x5e, 0x2d, 0x7e, 0xf9,\n  0xa0, 0xd2, 0xa4, 0xd2, 0xef, 0xd2, 0xec, 0xda, 0x4b, 0xe4, 0xff, 0x00,\n  0xb4, 0xaf, 0xc5, 0x3f, 0x0c, 0xeb, 0xba, 0xb6, 0xbb, 0xfb, 0x33, 0xc7,\n  0xfb, 0x34, 0xfc, 0x6c, 0xfd, 0xaa, 0x35, 0xcf, 0xf8, 0x42, 0xbc, 0x0f,\n  0xf1, 0x2f, 0xe2, 0x27, 0x86, 0x3e, 0x0c, 0x6b, 0xff, 0x00, 0x07, 0xbc,\n  0x0d, 0xaa, 0x7c, 0x2a, 0xd0, 0xbc, 0x4b, 0xe3, 0x2f, 0x14, 0xc5, 0xf0,\n  0x4b, 0xc7, 0xb0, 0xf8, 0xf3, 0xe3, 0x37, 0xc7, 0xdf, 0xd9, 0xda, 0xeb,\n  0x43, 0xf1, 0x9d, 0xd7, 0xc4, 0x0f, 0x83, 0x1e, 0x39, 0xf1, 0x17, 0xc2,\n  0xef, 0x15, 0xfc, 0x12, 0xf1, 0x4e, 0xa7, 0xf1, 0x1b, 0xe1, 0x6f, 0x8d,\n  0x7e, 0x13, 0x58, 0xf8, 0xde, 0xe2, 0xf3, 0xe1, 0xb6, 0xb3, 0x27, 0xc3,\n  0x6d, 0x6f, 0x5d, 0xf8, 0xc0, 0xfc, 0x2b, 0x38, 0x38, 0xfd, 0x84, 0x3f,\n  0xe0, 0xb5, 0xd9, 0xc1, 0xc6, 0x7f, 0xe0, 0xac, 0x8a, 0xc3, 0x3d, 0xbe,\n  0x57, 0xff, 0x00, 0x82, 0xd2, 0x3a, 0x37, 0xd1, 0xd1, 0x94, 0xf4, 0x65,\n  0x61, 0x90, 0x40, 0x3c, 0xa3, 0xc4, 0x9e, 0x2e, 0xf0, 0x06, 0xb7, 0x69,\n  0xf1, 0x2b, 0xc3, 0xff, 0x00, 0xb3, 0x97, 0xc6, 0xbd, 0x6f, 0xe3, 0xef,\n  0xec, 0xa3, 0xa0, 0xfe, 0xd0, 0xff, 0x00, 0xf0, 0x44, 0xad, 0x67, 0xc2,\n  0x9e, 0x39, 0x9b, 0xf6, 0x89, 0xf1, 0xb7, 0xed, 0x67, 0xe1, 0xcb, 0x2f,\n  0xda, 0x17, 0xc4, 0xbf, 0xf0, 0x54, 0x2d, 0x2a, 0xcb, 0xe3, 0x8f, 0x83,\n  0xb4, 0xaf, 0x8f, 0xde, 0x3d, 0xf1, 0xef, 0xc5, 0x8f, 0x12, 0x1d, 0x63,\n  0x47, 0xf8, 0x6d, 0xe1, 0x7f, 0xd9, 0x9b, 0x5a, 0xd7, 0xbe, 0x10, 0x43,\n  0xf1, 0x02, 0x5d, 0x23, 0xc0, 0x36, 0x3e, 0x22, 0xf0, 0xcf, 0x8c, 0x34,\n  0xff, 0x00, 0x07, 0xf8, 0x7a, 0xe3, 0xe2, 0xd6, 0xab, 0xad, 0x78, 0xd7,\n  0xeb, 0xcf, 0xf8, 0x28, 0xa7, 0xfc, 0x12, 0x1f, 0xf6, 0x42, 0xff, 0x00,\n  0x82, 0x9b, 0xb7, 0x81, 0x35, 0x5f, 0x8f, 0x5a, 0x6f, 0x8c, 0xfc, 0x27,\n  0xf1, 0x07, 0xe1, 0xd8, 0x97, 0x4e, 0xd0, 0xbe, 0x2c, 0xfc, 0x21, 0xd4,\n  0x7c, 0x2b, 0xe1, 0xbf, 0x88, 0x77, 0x9e, 0x0d, 0x95, 0xb5, 0x3b, 0x89,\n  0xbe, 0x1c, 0xf8, 0x8f, 0x50, 0xf1, 0x67, 0x82, 0xfc, 0x79, 0xa1, 0xf8,\n  0x8b, 0xc1, 0xb6, 0xfa, 0xe6, 0xa5, 0x27, 0x89, 0x34, 0x8b, 0x4d, 0x5b,\n  0xc3, 0xf7, 0x3a, 0xaf, 0x85, 0xf5, 0xc9, 0x35, 0x5b, 0x9f, 0x05, 0xeb,\n  0x5e, 0x1e, 0xb2, 0xf1, 0x8f, 0x8f, 0xec, 0x3c, 0x55, 0xe6, 0x3e, 0x1c,\n  0xf0, 0x1c, 0x5e, 0x18, 0xf1, 0x26, 0x81, 0xe2, 0x67, 0xff, 0x00, 0x82,\n  0x70, 0xff, 0x00, 0xc1, 0x57, 0x3c, 0x75, 0x71, 0xe1, 0xad, 0x67, 0x4b,\n  0xf1, 0x26, 0x8f, 0xa0, 0xfc, 0x62, 0xff, 0x00, 0x82, 0x82, 0x7c, 0x2b,\n  0xf8, 0xeb, 0xf0, 0xfe, 0x3f, 0x11, 0xe8, 0x17, 0xf1, 0x6b, 0x7e, 0x18,\n  0xd7, 0xf5, 0x0f, 0x86, 0x1f, 0x1a, 0xff, 0x00, 0xe0, 0xae, 0x7e, 0x3d,\n  0xf8, 0x6f, 0xab, 0x6b, 0xbe, 0x10, 0xf1, 0x1d, 0x8e, 0x97, 0xe2, 0xdf,\n  0x05, 0xeb, 0x7a, 0xbf, 0x85, 0x35, 0x1d, 0x4f, 0xc1, 0xbe, 0x34, 0xd1,\n  0xbc, 0x3d, 0xe3, 0x4f, 0x0c, 0x5c, 0x69, 0x3e, 0x2d, 0xf0, 0xee, 0x85,\n  0xad, 0x69, 0xff, 0x00, 0xa4, 0x9f, 0x03, 0x7e, 0x36, 0xe9, 0x9f, 0x1a,\n  0xf4, 0xcf, 0x1a, 0xff, 0x00, 0xc5, 0x19, 0xe3, 0x8f, 0x86, 0x9e, 0x33,\n  0xf8, 0x63, 0xe3, 0x86, 0xf8, 0x75, 0xf1, 0x47, 0xe1, 0x77, 0xc4, 0x41,\n  0xe0, 0xf9, 0xbc, 0x5f, 0xf0, 0xf7, 0xc6, 0x73, 0x78, 0x3f, 0xc1, 0xff,\n  0x00, 0x12, 0x34, 0x8d, 0x1f, 0x54, 0xd5, 0xbe, 0x1b, 0x78, 0xd3, 0xe2,\n  0x37, 0xc3, 0x7d, 0x69, 0x35, 0xef, 0x86, 0x3f, 0x11, 0x3e, 0x1e, 0x7c,\n  0x40, 0xd3, 0xaf, 0x3c, 0x0d, 0xf1, 0x03, 0xc5, 0xfa, 0x55, 0x9e, 0x93,\n  0xe3, 0x4d, 0x3f, 0x43, 0xd6, 0x75, 0x0d, 0x23, 0xc6, 0xda, 0x3f, 0x8b,\n  0x3c, 0x25, 0xe1, 0xb0, 0x0f, 0x1b, 0xf0, 0x18, 0x3f, 0xf0, 0xf1, 0xaf,\n  0xda, 0xa8, 0xe3, 0x8f, 0xf8, 0x62, 0x8f, 0xd8, 0x09, 0x73, 0xdb, 0x70,\n  0xf8, 0xe9, 0xff, 0x00, 0x05, 0x2a, 0x24, 0x67, 0xd4, 0x06, 0x52, 0x47,\n  0x50, 0x19, 0x4f, 0x42, 0x2b, 0xed, 0x8a, 0xf8, 0x8f, 0xc0, 0x60, 0xff,\n  0x00, 0xc3, 0xc6, 0x3f, 0x6a, 0x77, 0xda, 0xa4, 0xb7, 0xec, 0x55, 0xfb,\n  0x02, 0x23, 0x64, 0xf3, 0x94, 0xf8, 0xe7, 0xff, 0x00, 0x05, 0x27, 0x3b,\n  0x4b, 0x6c, 0x25, 0xd1, 0x09, 0x92, 0x48, 0xd8, 0xed, 0x7d, 0xee, 0xc1,\n  0xbe, 0x56, 0x1e, 0x5f, 0xdb, 0x7c, 0xfa, 0x0f, 0xcc, 0xff, 0x00, 0x85,\n  0x00, 0x2d, 0x7c, 0x4f, 0xfb, 0x2b, 0x02, 0x3e, 0x3a, 0xff, 0x00, 0xc1,\n  0x4a, 0xf2, 0x31, 0x9f, 0xdb, 0x5f, 0xc0, 0x6c, 0x33, 0xdd, 0x7f, 0xe1,\n  0xdc, 0xdf, 0xb0, 0x12, 0xe4, 0x7a, 0x8d, 0xca, 0xc3, 0x3d, 0x32, 0xa4,\n  0x75, 0x06, 0xbe, 0xd6, 0x3b, 0xb0, 0x70, 0xaa, 0x4e, 0x0f, 0x05, 0x88,\n  0x07, 0xd8, 0x9d, 0xa7, 0x00, 0xf7, 0xe0, 0xfd, 0x0d, 0x7c, 0x49, 0xfb,\n  0x2b, 0xab, 0x7f, 0xc2, 0xf3, 0xff, 0x00, 0x82, 0x94, 0x15, 0x0b, 0xba,\n  0x4f, 0xdb, 0x57, 0xc0, 0x92, 0x7d, 0xf2, 0x99, 0x03, 0xfe, 0x09, 0xd3,\n  0xfb, 0x02, 0xa8, 0x77, 0x75, 0x8c, 0x93, 0x21, 0x50, 0x8a, 0x54, 0x2e,\n  0xc0, 0x88, 0x80, 0x12, 0x54, 0xb3, 0x80, 0x7f, 0xff, 0xd9\n};\nstatic const unsigned int disk01_jpg_len = 1486;\n\n\n// 16 x 18\nstatic const unsigned char broken_png[] = {\n  0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,\n  0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x12,\n  0x08, 0x06, 0x00, 0x00, 0x00, 0x52, 0x3b, 0x5e, 0x6a, 0x00, 0x00, 0x00,\n  0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0e, 0xc4, 0x00, 0x00, 0x0e,\n  0xc4, 0x01, 0x95, 0x2b, 0x0e, 0x1b, 0x00, 0x00, 0x02, 0x9f, 0x49, 0x44,\n  0x41, 0x54, 0x38, 0x8d, 0x85, 0x91, 0x5f, 0x48, 0x53, 0x61, 0x1c, 0x86,\n  0x9f, 0x63, 0xe7, 0xb0, 0x69, 0x63, 0xb8, 0x94, 0x0d, 0x74, 0x61, 0x24,\n  0xa9, 0x24, 0x76, 0x91, 0x19, 0x06, 0x21, 0x08, 0x11, 0x9c, 0x42, 0x21,\n  0x42, 0x22, 0x08, 0xba, 0x49, 0x54, 0x90, 0xae, 0x22, 0x2c, 0xa2, 0xae,\n  0xba, 0xe8, 0x0f, 0x24, 0x08, 0x82, 0x90, 0x09, 0x26, 0x06, 0xa6, 0x84,\n  0x41, 0x64, 0x9a, 0x90, 0x56, 0x0a, 0xa9, 0x9b, 0x0c, 0x5d, 0x2c, 0x19,\n  0x1e, 0x54, 0x2a, 0x25, 0xb5, 0xb6, 0xf4, 0x6c, 0x3b, 0xf3, 0x9c, 0x2e,\n  0x64, 0xb1, 0x75, 0x24, 0xbf, 0xcb, 0xf7, 0x7b, 0x7f, 0xcf, 0xef, 0xcf,\n  0x2b, 0x60, 0xc5, 0x10, 0xad, 0x22, 0x02, 0x02, 0xbb, 0xbd, 0xc6, 0x2b,\n  0x8d, 0x14, 0x1d, 0x2a, 0xaa, 0xad, 0xab, 0xab, 0x7b, 0x9c, 0xd0, 0x04,\n  0x51, 0x14, 0x8d, 0xd1, 0xd1, 0x51, 0x5c, 0x2e, 0xd7, 0xae, 0x00, 0x8f,\n  0xc7, 0xc3, 0xda, 0xda, 0xda, 0x9a, 0xae, 0xeb, 0xf7, 0x1a, 0x1a, 0x1a,\n  0xee, 0x03, 0x08, 0x92, 0x24, 0x19, 0x8a, 0xa2, 0x10, 0x8f, 0xc7, 0xd9,\n  0xdc, 0xdc, 0xfc, 0x2f, 0xc0, 0xe7, 0xf3, 0x61, 0xb7, 0xdb, 0x71, 0x38,\n  0x1c, 0x4b, 0xd3, 0xd3, 0xd3, 0x9f, 0xea, 0xeb, 0xeb, 0xcf, 0x8b, 0x89,\n  0x4f, 0x55, 0x55, 0xd9, 0xd8, 0xd8, 0x00, 0x60, 0x36, 0x3c, 0x4b, 0xe7,\n  0x4a, 0x27, 0x36, 0x6c, 0xdc, 0xc8, 0xbb, 0x81, 0x28, 0x6e, 0xdb, 0xb6,\n  0xb6, 0xb6, 0xb0, 0x5a, 0xad, 0x14, 0x14, 0x14, 0xb8, 0x6d, 0x76, 0x9b,\n  0xa3, 0xad, 0xad, 0xad, 0x47, 0xfc, 0xb7, 0xcb, 0x54, 0x68, 0x8a, 0x5e,\n  0xbd, 0x97, 0xe1, 0xdc, 0x61, 0xac, 0x5e, 0x2b, 0xd9, 0xc3, 0xd9, 0x54,\n  0x9f, 0xad, 0x26, 0x37, 0x37, 0x17, 0xbf, 0xdf, 0xcf, 0xe0, 0xe0, 0x20,\n  0x6e, 0xb7, 0x9b, 0x48, 0x24, 0xb2, 0x77, 0x6e, 0x7e, 0xae, 0x26, 0xcd,\n  0xb4, 0xe7, 0x6f, 0x0f, 0x43, 0xbf, 0x86, 0x00, 0x88, 0xc4, 0x22, 0xb4,\n  0x77, 0xb4, 0xb3, 0xb2, 0xb2, 0x02, 0x80, 0xc5, 0x62, 0x61, 0x7d, 0x7d,\n  0x9d, 0x40, 0x20, 0xc0, 0xfc, 0xfc, 0x3c, 0xfd, 0x03, 0xfd, 0x98, 0x26,\n  0x70, 0xa7, 0xbb, 0x29, 0x5c, 0x2a, 0x24, 0xf0, 0x21, 0x00, 0x2a, 0x64,\n  0xef, 0xcb, 0x46, 0x92, 0x24, 0x00, 0x64, 0x59, 0x46, 0x96, 0x65, 0x6c,\n  0x36, 0x1b, 0x9a, 0xa6, 0xd1, 0x37, 0xd0, 0x67, 0x06, 0xc8, 0x0e, 0x19,\n  0xcb, 0x6f, 0x0b, 0xd7, 0x1f, 0x5d, 0x47, 0x92, 0x24, 0xba, 0x5f, 0x75,\n  0x13, 0x0e, 0xef, 0x27, 0x18, 0x4c, 0x38, 0x96, 0xc9, 0xc9, 0x51, 0x97,\n  0xb2, 0x32, 0xb3, 0x16, 0xd9, 0x4a, 0x4a, 0x21, 0x1c, 0x0e, 0xa3, 0x2e,\n  0x2f, 0x63, 0xa4, 0xa5, 0xa1, 0xa7, 0xa7, 0xa7, 0x40, 0xab, 0xaa, 0x8e,\n  0xf2, 0x4d, 0x15, 0x20, 0x0a, 0xa8, 0x2d, 0xc0, 0xd5, 0xbb, 0xc0, 0x2d,\n  0x80, 0x94, 0x1b, 0xb8, 0x9b, 0x9b, 0x71, 0x75, 0x75, 0xed, 0x18, 0x21,\n  0x2f, 0x80, 0x4b, 0x66, 0x39, 0xf5, 0x88, 0xf1, 0x16, 0x72, 0x9e, 0xe4,\n  0x70, 0xe0, 0xe6, 0x4d, 0xb3, 0xf1, 0x1c, 0xf0, 0xf4, 0x3d, 0xe0, 0xa8,\n  0x4d, 0x74, 0x37, 0x03, 0xb0, 0x80, 0x56, 0x47, 0xe6, 0xc7, 0x0b, 0x1c,\n  0xbc, 0x76, 0x2d, 0x49, 0x3f, 0x8d, 0xfe, 0xb3, 0x14, 0x22, 0x0f, 0x2f,\n  0xc2, 0xf7, 0x67, 0x86, 0x61, 0x14, 0xfb, 0x7c, 0xbe, 0x1e, 0x32, 0xe8,\n  0x31, 0xc5, 0x08, 0x2e, 0xf6, 0xa8, 0x67, 0xb0, 0x4f, 0x5c, 0x26, 0xbf,\n  0xa9, 0x09, 0x34, 0x0d, 0x49, 0xfa, 0x72, 0x12, 0x3c, 0x27, 0xe0, 0xe5,\n  0x6b, 0xaf, 0xf7, 0xd4, 0x61, 0x45, 0x51, 0xee, 0x08, 0x82, 0x50, 0x83,\n  0x40, 0x4d, 0x4a, 0x0a, 0x1d, 0x42, 0x07, 0x7e, 0xfc, 0xa0, 0x43, 0xe6,\n  0x46, 0xda, 0xe7, 0x87, 0x13, 0x13, 0xb7, 0x0d, 0x41, 0x60, 0x61, 0x61,\n  0x61, 0x0c, 0x30, 0x00, 0x74, 0x5d, 0x2f, 0x8e, 0x46, 0xa3, 0x35, 0x89,\n  0x9a, 0x14, 0x40, 0xec, 0x78, 0x8c, 0x99, 0xaf, 0x33, 0x23, 0x93, 0x93,\n  0x93, 0x53, 0xc0, 0x5c, 0xf3, 0x2f, 0x7a, 0x29, 0x2f, 0x07, 0xc0, 0xeb,\n  0xf5, 0x96, 0xc5, 0xe3, 0xf1, 0x3c, 0xc3, 0x30, 0xca, 0x92, 0x6b, 0xfe,\n  0xc6, 0x18, 0x8b, 0xc5, 0x9e, 0xaf, 0xae, 0xae, 0x2e, 0x0a, 0x82, 0x30,\n  0x58, 0x5a, 0x5a, 0xfa, 0x26, 0xd9, 0x64, 0x18, 0x46, 0x71, 0x30, 0x18,\n  0x7c, 0xa0, 0x69, 0x9a, 0x0c, 0x90, 0x91, 0x91, 0x41, 0x28, 0x14, 0xa2,\n  0xa4, 0xbc, 0x04, 0x51, 0xd7, 0x75, 0xc6, 0xc7, 0xc7, 0xc9, 0xca, 0xcf,\n  0x6a, 0xad, 0x3c, 0x56, 0xf9, 0xce, 0x7c, 0x13, 0x18, 0x1b, 0x1d, 0x6b,\n  0xd5, 0x0c, 0xad, 0x22, 0x59, 0x53, 0x14, 0x65, 0x7b, 0x02, 0xa7, 0xd3,\n  0xb9, 0xbd, 0xdb, 0x11, 0xbd, 0xf2, 0xc7, 0xdb, 0x1f, 0x3b, 0x02, 0x9c,\n  0x4e, 0xe7, 0x08, 0x50, 0x61, 0xfa, 0xb0, 0xc1, 0x1f, 0xc0, 0xbf, 0x14,\n  0x66, 0xb5, 0xad, 0x80, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,\n  0x44, 0xae, 0x42, 0x60, 0x82\n};\nstatic const unsigned int broken_png_len = 749;\n"
  },
  {
    "path": "examples/AppStore/modules/Assets/Assets.hpp",
    "content": "#pragma once\n\n#include \"../misc/core.h\"\n#include \"Assets.h\"\n\n\nclass RemoteAsset\n{\npublic:\n  RemoteAsset( const char* p, uint32_t w, uint32_t h, const char* a ) : path(p), width(w), height(h), alt_text(a) { }\n  const char* path; // jpg or png file\n  uint32_t width;\n  uint32_t height;\n  const char* alt_text;\n  void draw( LGFX* gfx, int32_t x=0, int32_t y=0, int32_t w=0, int32_t h=0 );\n};\n\n\nenum imageType_t\n{\n  IMG_JPG,\n  IMG_PNG,\n  IMG_RAW\n};\n\nclass LocalAsset\n{\npublic:\n  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) { }\n  const unsigned char* data; // MUST BE bytes array of png, jpg, or raw colors\n  size_t data_len;\n  imageType_t type;\n  uint32_t width;\n  uint32_t height;\n  const char* alt_text;\n  void draw( LGFX* gfx, int32_t x=0, int32_t y=0, int32_t w=0, int32_t h=0 );\n};\n\n\nnamespace DummyAsset\n{\n  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 );\n  uint32_t fgcolor;\n  uint32_t bgcolor;\n  captionDrawer_t drawCaption = nullptr;\n  LocalAsset *BrokenImage = nullptr;\n  void setup( captionDrawer_t drawCb, LocalAsset *brokenImage, uint32_t _fgcolor, uint32_t _bgcolor );\n  void drawAltText( LGFX* gfx, const char* caption, int32_t x, int32_t y, uint32_t w, uint32_t h );\n};\n"
  },
  {
    "path": "examples/AppStore/modules/CertsManager/CertsManager.cpp",
    "content": "#pragma once\n\n#include \"CertsManager.hpp\"\n\nnamespace TLS\n{\n\n  const char* updateWallet( String host, const char* ca)\n  {\n    int8_t idx = -1;\n    uint8_t sizeOfWallet = sizeof( TLSWallet ) / sizeof( TLSWallet[0] );\n    for(uint8_t i=0; i<sizeOfWallet; i++) {\n      if( TLSWallet[i].host==NULL ) {\n        if( idx == -1 ) {\n          idx = i;\n        }\n        continue;\n      }\n      if( String( TLSWallet[i].host ) == host ) {\n        log_v(\"[WALLET SKIP UPDATE] Wallet #%d exists ( %s )\\n\", i, TLSWallet[i].host );\n        return TLSWallet[i].ca;\n      }\n    }\n    if( idx > -1 ) {\n      int hostlen = host.length() + 1;\n      int certlen = String(ca).length() + 1;\n      char *newhost = (char*)malloc( hostlen );\n      char *newcert = (char*)malloc( certlen );\n      memcpy( newhost, host.c_str(), hostlen );\n      memcpy( newcert, ca, certlen);\n      TLSWallet[idx] = { (const char*)newhost , (const char*)newcert };\n      log_v(\"[WALLET UPDATE] Wallet #%d loaded ( %s )\\n\", idx, TLSWallet[idx].host );\n      return TLSWallet[idx].ca;\n    }\n    return ca;\n  }\n\n\n  const char* fetchLocalCert( String host )\n  {\n    String certPath = SD_CERT_PATH PATH_SEPARATOR + host;\n    File certFile = M5_FS.open( certPath );\n    if(! certFile ) { // failed to open the cert file\n      log_w(\"[WARNING] Failed to open the cert file %s, TLS cert checking therefore disabled\", certPath.c_str() );\n      return NULL;\n    }\n    String certStr = \"\";\n    while( certFile.available() ) {\n      certStr += certFile.readStringUntil('\\n') + \"\\n\";\n    }\n    certFile.close();\n    log_v(\"\\n%s\\n\", certStr.c_str() );\n    const char* certChar = updateWallet( host, certStr.c_str() );\n    return certChar;\n  }\n\n\n  const char* getWalletCert( String host )\n  {\n    uint8_t sizeOfWallet = sizeof( TLSWallet ) / sizeof( TLSWallet[0] );\n    log_v(\"\\nChecking wallet (%d items)\",  sizeOfWallet );\n    for(uint8_t i=0; i<sizeOfWallet; i++) {\n      if( TLSWallet[i].host==NULL ) continue;\n      if( String( TLSWallet[i].host ) == host ) {\n        log_v(\"Wallet #%d ( %s ) : [OK]\", i, TLSWallet[i].host );\n        //log_d(\" [OK]\");\n        return TLSWallet[i].ca;\n      } else {\n        log_v(\"Wallet #%d ( %s ) : [KO]\", i, TLSWallet[i].host );\n        //log_d(\" [KO]\");\n      }\n    }\n    const char* nullcert = NULL;\n    return nullcert;\n  }\n\n\n  const char* fetchCert( String host, bool checkWallet, bool checkFS )\n  {\n    //const char* nullcert = NULL;\n    if( checkWallet ) {\n      const char* walletCert = getWalletCert( host );\n      if( walletCert != NULL ) {\n        log_v(\"[FETCHED WALLET CERT] -> %s\", host.c_str() );\n        return walletCert;\n      } else {\n        //\n      }\n    }\n    String certPath = SD_CERT_PATH PATH_SEPARATOR + host;\n    String certURL = certProvider + host;\n    if( !checkFS || !M5_FS.exists( certPath ) ) {\n      //log_d(\"[FETCHING REMOTE CERT] -> \");\n      //wget(certURL , certPath );\n      return fetchLocalCert( host );\n    } else {\n      log_v(\"[FETCHING LOCAL (SD) CERT] -> %s\", certPath.c_str() );\n    }\n    return fetchLocalCert( host );\n  }\n\n\n  bool isInWallet( String host )\n  {\n    uint8_t sizeOfWallet = sizeof( TLSWallet ) / sizeof( TLSWallet[0] );\n    for(uint8_t i=0; i<sizeOfWallet; i++) {\n      if( TLSWallet[i].host==NULL ) continue;\n      if( String( TLSWallet[i].host ) == host ) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n\n  bool init( String host )\n  {\n    if( fetchLocalCert( host ) != NULL ) {\n      return true;\n    }\n    bool ret = false;\n    int hidState = HID_INERT;\n    String certPath = SD_CERT_PATH PATH_SEPARATOR + host;\n    String certURL = certProvider + host;\n    if( wget == nullptr ) {\n      log_e(\"ERROR: wget has not been inited\");\n      return false;\n    }\n\n    if( !wget( certURL , certPath ) ) {\n      log_e( MODAL_TLSCERT_FETCHINGFAILED_MSG );\n      hidState = modalConfirm ? modalConfirm( MODAL_CANCELED_TITLE, MODAL_TLSCERT_FETCHINGFAILED_MSG, WGET_MSG_FAIL, MENU_BTN_RESTART, MENU_BTN_CONTINUE, MENU_BTN_CANCEL ) : HID_BTN_A;\n      goto finally;\n    }\n    if( fetchLocalCert( host ) == NULL ) {\n      log_e( MODAL_TLSCERT_INSTALLFAILED_MSG );\n      hidState = modalConfirm ? modalConfirm( MODAL_CANCELED_TITLE, MODAL_TLSCERT_INSTALLFAILED_MSG, FS_MSG_FAIL, MENU_BTN_RESTART, MENU_BTN_CONTINUE, MENU_BTN_CANCEL ) : HID_BTN_A;\n      goto finally;\n    }\n    log_w( NEW_TLS_CERTIFICATE_TITLE );\n    ret = true;\n\n    finally:\n\n    if( hidState == HID_BTN_C ) { // BTN_CANCEL, explicitely return false\n      ret = false;\n    } else if( hidState == HID_BTN_A ) { // BTN_RESTART\n      ESP.restart();\n    }\n    return ret;\n  }\n\n};\n\n"
  },
  {
    "path": "examples/AppStore/modules/CertsManager/CertsManager.hpp",
    "content": "#pragma once\n\n#include \"../misc/config.h\"\n\nnamespace TLS\n{\n  struct TLSCert\n  {\n    const char* host;\n    const char* ca;\n  };\n\n  String certProvider; // URL where the certificate chains are fetched\n  const char* nullHost = nullptr; // dummy host\n  const char* nullCa   = nullptr; // dummy CA\n  TLSCert NULLCert = { nullHost, nullCa }; // dummy cert\n  TLSCert TLSWallet[8] = {NULLCert, NULLCert, NULLCert, NULLCert, NULLCert, NULLCert, NULLCert, NULLCert }; // dummy wallet\n\n  const char* fetchCert( String host, bool checkWallet = true , bool checkFS = true );\n  const char* fetchLocalCert( String host );\n  const char* getWalletCert( String host );\n  const char* updateWallet( String host, const char* ca);\n\n  bool isInWallet( String host );\n  bool init( String host ); // cert provider url, host\n\n  int (*modalConfirm)( const char* question, const char* title, const char* body, const char* labelA, const char* labelB, const char* labelC) = nullptr;\n  bool (*wget)(String url, String path) = nullptr;\n\n\n};\n"
  },
  {
    "path": "examples/AppStore/modules/Console/Console.cpp",
    "content": "#pragma once\n\n#include \"Console.hpp\"\n#include \"../misc/config.h\"\n#include \"../misc/i18n.h\"\n\n// Scrollable text window\nLogWindow::LogWindow( LGFX* gfx, int32_t _x, int32_t _y, uint32_t _w, uint32_t _h,  uint32_t _fgcolor, uint32_t _bgcolor )\n{\n  x = _x;\n  y = _y;\n  w = _w;\n  h = _h;\n  fgcolor = _fgcolor;\n  bgcolor = _bgcolor;\n\n  if( gfx==nullptr ) gfx = &tft;\n  if( w == 0 )       w = gfx->width()-LISTITEM_OFFSETX*2;\n  if( h == 0 )       h = TITLEBAR_HEIGHT*3.5;\n  if( x == -1 )      x = gfx->width()/2 - w/2;\n  if( y == -1 )      y = 75;\n\n  sprite = new LGFX_Sprite( gfx );\n  sprite->setColorDepth(1);\n  sprite->createSprite( w, h );\n  sprite->setPaletteColor(0, bgcolor );\n  sprite->setPaletteColor(1, fgcolor );\n  sprite->setTextDatum( TL_DATUM );\n  sprite->setTextWrap( false, true );\n  sprite->setFont( &Font0 );\n  th = sprite->fontHeight();\n  tw = sprite->textWidth(\"@\");\n  sprite->setCursor( tw, th );\n}\n\nLogWindow::~LogWindow()\n{\n  sprite->deleteSprite();\n}\n\nvoid LogWindow::log( const char* str )\n{\n  if( str ) sprite->println( str );\n  else sprite->println();\n  render();\n}\n\nvoid LogWindow::clear()\n{\n  sprite->fillSprite( bgcolor );\n  sprite->setCursor( tw, th );\n  render();\n}\n\nvoid LogWindow::render()\n{\n  checkScroll();\n  sprite->fillRect( 0, 0, sprite->width()-(tw+1), th, bgcolor );\n  sprite->fillRect( sprite->width()-(tw+1), 0, tw, sprite->height(), bgcolor );\n  sprite->pushSprite( x, y );\n}\n\nvoid LogWindow::checkScroll()\n{\n  int32_t posy = sprite->getCursorY();\n  if( posy + th > sprite->height() ) {\n    sprite->scroll( 0, -th );\n    posy -= th;\n  }\n  sprite->setCursor( tw, posy );\n}\n"
  },
  {
    "path": "examples/AppStore/modules/Console/Console.hpp",
    "content": "#pragma once\n\n#include \"Console.hpp\"\n\n// Scrollable text window\nclass LogWindow\n{\n  public:\n    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 );\n    ~LogWindow();\n    void log( const char* str = nullptr );\n    void clear();\n    void render();\n  private:\n    int32_t x;\n    int32_t y;\n    uint32_t w;\n    uint32_t h;\n    uint32_t fgcolor;\n    uint32_t bgcolor;\n    LGFX_Sprite* sprite;\n    uint16_t th; // text height\n    uint16_t tw; // text width for \"@\" sign\n    void checkScroll();\n};\n"
  },
  {
    "path": "examples/AppStore/modules/Downloader/Downloader.cpp",
    "content": "#pragma once\n\n#include \"Downloader.hpp\"\n#include \"../AppStoreUI/AppStoreUI.hpp\"\n#include \"../FSUtils/FSUtils.hpp\"   // filesystem bin formats, functions, helpers\n#include \"../Registry/Registry.hpp\" // registry this launcher is tied to\n#include \"../CertsManager/CertsManager.hpp\"\n\n//#define USE_SODIUM // as of 2.0.1-rc1 this produces a bigger binary (+4Kb) + occasional crashes\n#define USE_MBEDTLS // old mbedtls still more stable and produces a smaller binary\n\n#ifdef USE_SODIUM\n  #include \"sodium/crypto_hash_sha256.h\"\n  crypto_hash_sha256_state ctx;\n  #define SHA_START() [](){}\n  #define SHA_INIT crypto_hash_sha256_init\n  #define SHA_UPDATE crypto_hash_sha256_update\n  #define SHA_FINAL crypto_hash_sha256_final\n#elif defined USE_MBEDTLS\n  #define MBEDTLS_SHA256_ALT\n  #define MBEDTLS_ERROR_C\n  #include \"mbedtls/sha256.h\"\n  mbedtls_sha256_context ctx;\n  #define SHA_START() mbedtls_sha256_starts(&ctx,0)\n  #define SHA_INIT mbedtls_sha256_init\n  #define SHA_UPDATE mbedtls_sha256_update\n  #define SHA_FINAL mbedtls_sha256_finish\n#endif\n\n// inherit progress bar from SD-Updater library\n//#define M5SDMenuProgress SDUCfg.onProgress\n\nnamespace NTP\n{\n\n  const char* NVS_NAMESPACE = \"NTP\";\n  const char* NVS_KEY       = \"Server\";\n  uint32_t nvs_handle = 0;\n\n  void setTimezone( float tz )\n  {\n    timezone = tz;\n  }\n\n  void setDst( bool set )\n  {\n    daysavetime = set ? 1 : 0;\n  }\n\n  void setServer( uint8_t id )\n  {\n    size_t servers_count = sizeof( Servers ) / sizeof( Server );\n\n    if( id < servers_count ) {\n      if( id != currentServer ) {\n        currentServer = id;\n        log_v(\"Setting NTP server to #%d ( %s / %s )\", currentServer, Servers[currentServer].name, Servers[currentServer].addr );\n        if (ESP_OK == nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle)) {\n          if( ESP_OK == nvs_set_u8(nvs_handle, NVS_KEY, currentServer) ) {\n            log_i(\"[NTP] saved to nvs::%s.%s=%d\", NVS_NAMESPACE, NVS_KEY, currentServer);\n          } else {\n            log_e(\"[NTP] saving failed for nvs::%s.%s=%d\", NVS_NAMESPACE, NVS_KEY, currentServer);\n          }\n          nvs_close(nvs_handle);\n        } else {\n          log_e(\"Can't open nvs::%s for writing\", NVS_NAMESPACE );\n        }\n      }\n      return;\n    }\n    log_e(\"Invalid NTP requested: #%d\", id );\n  }\n\n\n  void loadPrefServer()\n  {\n    if (ESP_OK == nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle)) {\n      uint8_t nvs_ntpserver = 0;\n      if(ESP_OK == nvs_get_u8(nvs_handle, NVS_KEY, static_cast<uint8_t*>(&nvs_ntpserver)) ) {\n        currentServer = nvs_ntpserver;\n        log_i(\"[NTP] load from NVS: server=%d\", nvs_ntpserver);\n      } else {\n        log_w(\"Can't access nvs::%s.%s\", NVS_NAMESPACE, NVS_KEY );\n      }\n      nvs_close(nvs_handle);\n    } else {\n      log_e(\"Can't open nvs::%s for reading\", NVS_NAMESPACE );\n    }\n  }\n\n\n}\n\n\nnamespace Downloader\n{\n\n  using namespace MenuItems;\n  using namespace UIDraw;\n  using namespace UIUtils;\n  using namespace RegistryUtils;\n  using namespace FSUtils;\n\n  void httpSetup()\n  {\n    http.setUserAgent( UserAgent );\n    http.setConnectTimeout( 10000 ); // 10s timeout = 10000\n    http.setReuse(true); // handle 301 redirects gracefully\n  }\n\n\n  URLParts parseURL( String url ) // logic stolen from HTTPClient::beginInternal()\n  {\n    URLParts urlParts;\n    int index = url.indexOf(':');\n    if(index < 0) {\n      log_e(\"failed to parse protocol\");\n      return urlParts;\n    }\n    urlParts.url = \"\" + url;\n    urlParts.protocol = url.substring(0, index);\n    url.remove(0, (index + 3)); // remove http:// or https://\n    index = url.indexOf('/');\n    String host = url.substring(0, index);\n    url.remove(0, index); // remove host part\n    index = host.indexOf('@'); // get Authorization\n    if(index >= 0) { // auth info\n      urlParts.auth = host.substring(0, index);\n      host.remove(0, index + 1); // remove auth part including @\n    }\n    index = host.indexOf(':'); // get port\n    if(index >= 0) {\n      urlParts.host = host.substring(0, index); // hostname\n      host.remove(0, (index + 1)); // remove hostname + :\n      urlParts.port = host.toInt(); // get port\n    } else {\n      urlParts.host = host;\n    }\n    urlParts.uri = url;\n    return urlParts;\n  }\n\n\n  URLParts parseURL( const char* url )\n  {\n    return parseURL( String( url ) );\n  }\n\n\n  bool tinyBuffInit()\n  {\n    if( tinyBuff == nullptr ) {\n      tinyBuff = (uint8_t *)heap_caps_malloc(sizeOfTinyBuff, MALLOC_CAP_8BIT);\n      if( tinyBuff == NULL ) {\n        return false;\n      } else {\n        log_v(\"Allocated %d bytes for wget buffer\", sizeOfTinyBuff );\n      }\n    } else {\n      log_v(\"Reusing wget buffer\");\n    }\n    return true;\n  }\n\n\n  void sha_sum_to_str()\n  {\n    shaResultStr = \"\";\n    char str[3];\n    for(int i= 0; i< sizeof(shaResult); i++) {\n      sprintf(str, \"%02x\", (int)shaResult[i]);\n      shaResultStr += String( str );\n    }\n  }\n\n\n  void sha256_sum(const char* fileName)\n  {\n    log_d(\"SHA256: checking file %s\\n\", fileName);\n    File checkFile = M5_FS.open( fileName );\n    size_t fileSize = checkFile.size();\n    size_t len = fileSize;\n    if( !checkFile || fileSize==0 ) {\n      downloadererrors++;\n      log_e(\"  [ERROR] Can't open %s file for reading, aborting\\n\", fileName);\n      return;\n    }\n    //CheckSumIcon.draw( &tft, 288, 125 );\n\n    tinyBuffInit();\n    *shaResult = {0};\n\n    SHA_INIT(&ctx);\n    SHA_START();\n\n    size_t n;\n    while ((n = checkFile.read(tinyBuff, sizeOfTinyBuff)) > 0) {\n      SHA_UPDATE(&ctx, (const unsigned char *) tinyBuff, n);\n      if( fileSize/10 > sizeOfTinyBuff && fileSize != len ) {\n        //shaProgressBar.draw( 100*len / fileSize );\n      }\n      len -= n;\n      delay(1);\n    }\n    //shaProgressBar.clear();\n    checkFile.close();\n\n    SHA_FINAL(&ctx, shaResult);\n    sha_sum_to_str();\n  }\n\n\n  void sha256_sum( String fileName )\n  {\n    return sha256_sum( fileName.c_str() );\n  }\n\n\n\n  bool wget( const char* url, const char* path )\n  {\n    WiFiClientSecure *client = new WiFiClientSecure;\n    UITheme* theme = UI->getTheme();\n\n    if( !tinyBuffInit() ) {\n      log_e(\"Failed to allocate memory for download buffer, aborting\");\n      delete client;\n      return false;\n    }\n\n    URLParts urlParts = parseURL( url );\n\n    const char* cert = TLS::fetchCert( urlParts.host );\n    if( cert == NULL ) client->setInsecure();\n    else client->setCACert( cert );\n\n    httpSetup();\n\n    if( ! http.begin(*client, url ) ) {\n      log_e(\"Can't open url %s\", url );\n      delete client;\n      return false;\n    }\n\n    http.collectHeaders(headerKeys, numberOfHeaders);\n\n    log_v(\"URL = %s\", url);\n\n    int httpCode = http.GET();\n\n    // file found at server\n    if (httpCode == HTTP_CODE_FOUND || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {\n      String newlocation = \"\";\n      for(int i = 0; i< http.headers(); i++) {\n        String headerContent = http.header(i);\n        if( headerContent !=\"\" ) {\n          newlocation = headerContent;\n          Serial.printf(\"%s: %s\\n\", headerKeys[i], headerContent.c_str());\n        }\n      }\n\n      http.end();\n      if( newlocation != \"\" ) {\n        log_i(\"Found 302/301 location header: %s\", newlocation.c_str() );\n        delete client;\n        return wget( newlocation.c_str(), path );\n      } else {\n        log_e(\"Empty redirect !!\");\n        delete client;\n        return false;\n      }\n    }\n\n    WiFiClient *stream = http.getStreamPtr();\n\n    if( stream == nullptr ) {\n      http.end();\n      log_e(\"Connection failed!\");\n      delete client;\n      return false;\n    }\n\n    #if defined FS_CAN_CREATE_PATH\n      File outFile = M5_FS.open( path, FILE_WRITE, true );\n    #else\n      File outFile = M5_FS.open( path, FILE_WRITE );\n    #endif\n\n    if( ! outFile ) {\n      log_e(\"Can't open %s file to save url %s\", path, url );\n      delete client;\n      return false;\n    }\n\n    int len = http.getSize();\n    int bytesLeftToDownload = len;\n    int bytesDownloaded = 0;\n\n    *tinyBuff = {0};\n    *shaResult = {0};\n\n    SHA_INIT( &ctx );\n    SHA_START();\n\n    //CheckSumIcon.draw( &tft, 288, 125 );\n    //DownloadIcon.draw( &tft, 252, 125 );\n\n    while(http.connected() && (len > 0 || len == -1)) {\n      size_t size = stream->available();\n      if(size) {\n        // read up to 512 byte\n        int c = stream->readBytes(tinyBuff, sizeOfTinyBuff);\n        if( c > 0 ) {\n          SHA_UPDATE(&ctx, (const unsigned char *)tinyBuff, c);\n          outFile.write( tinyBuff, c );\n          bytesLeftToDownload -= c;\n          bytesDownloaded += c;\n          //Serial.printf(\"%d bytes left\\n\", bytesLeftToDownload );\n          float progress = (((float)bytesDownloaded / (float)len) * 100.00);\n          theme->dlProgressBar.progress( progress );\n        }\n      }\n      if( bytesLeftToDownload == 0 ) break;\n    }\n\n    SHA_FINAL(&ctx, shaResult);\n    outFile.close();\n    sha_sum_to_str();\n\n    // clear progress bar\n    theme->dlProgressBar.clear();\n    delete client;\n    return true;\n  }\n\n\n  // aliases\n  bool wget( String bin_url, String outputFile )\n  {\n    return wget( bin_url.c_str(), outputFile.c_str() );\n  }\n  bool wget( String bin_url, const char* outputFile )\n  {\n    return wget( bin_url.c_str(), outputFile );\n  }\n  bool wget( const char* bin_url, String outputFile )\n  {\n    return wget( bin_url, outputFile.c_str() );\n  }\n\n\n  WiFiClient *wgetptr( WiFiClientSecure *client, const char* url, const char *cert )\n  {\n    if( cert == NULL ) client->setInsecure();\n    else client->setCACert( cert );\n\n    httpSetup();\n\n    if( ! http.begin(*client, url ) ) {\n      log_e(\"Can't open url %s\", url );\n      return nullptr;\n    }\n\n    http.collectHeaders(headerKeys, numberOfHeaders);\n    int httpCode = http.GET();\n    // file found at server\n    if (httpCode == HTTP_CODE_FOUND || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {\n      String newlocation = \"\";\n      String headerLocation = http.header(\"location\");\n      String headerRedirect = http.header(\"redirect\");\n      if( headerLocation !=\"\" ) {\n        newlocation = headerLocation;\n        log_i(\"302 (location): %s => %s\", url, headerLocation.c_str());\n      } else if ( headerRedirect != \"\" ) {\n        log_i(\"301 (redirect): %s => %s\", url, headerLocation.c_str());\n        newlocation = headerRedirect;\n      }\n      http.end();\n      if( newlocation != \"\" ) {\n        log_i(\"Found 302/301 location header: %s\", newlocation.c_str() );\n        // delete client;\n        return wgetptr( client, newlocation.c_str(), cert );\n      } else {\n        log_e(\"Empty redirect !!\");\n        return nullptr;\n      }\n    }\n    if( httpCode != 200 ) return nullptr;\n\n    log_v(\"Got response with Content:Type: %s / Content-Length: %s\", http.header(\"Content-Type\").c_str(), http.header(\"Content-Length\").c_str() );\n\n    return http.getStreamPtr();\n  }\n\n  #if ARDUHAL_LOG_LEVEL < ARDUHAL_LOG_LEVEL_DEBUG\n    void WiFiEvent(WiFiEvent_t event)\n    {\n      const char * arduino_event_names[] = {\n          \"WIFI_READY\",\n          \"SCAN_DONE\",\n          \"STA_START\", \"STA_STOP\", \"STA_CONNECTED\", \"STA_DISCONNECTED\", \"STA_AUTHMODE_CHANGE\", \"STA_GOT_IP\", \"STA_GOT_IP6\", \"STA_LOST_IP\",\n          \"AP_START\", \"AP_STOP\", \"AP_STACONNECTED\", \"AP_STADISCONNECTED\", \"AP_STAIPASSIGNED\", \"AP_PROBEREQRECVED\", \"AP_GOT_IP6\",\n          \"FTM_REPORT\",\n          \"ETH_START\", \"ETH_STOP\", \"ETH_CONNECTED\", \"ETH_DISCONNECTED\", \"ETH_GOT_IP\", \"ETH_GOT_IP6\",\n          \"WPS_ER_SUCCESS\", \"WPS_ER_FAILED\", \"WPS_ER_TIMEOUT\", \"WPS_ER_PIN\", \"WPS_ER_PBC_OVERLAP\",\n          \"SC_SCAN_DONE\", \"SC_FOUND_CHANNEL\", \"SC_GOT_SSID_PSWD\", \"SC_SEND_ACK_DONE\",\n          \"PROV_INIT\", \"PROV_DEINIT\", \"PROV_START\", \"PROV_END\", \"PROV_CRED_RECV\", \"PROV_CRED_FAIL\", \"PROV_CRED_SUCCESS\"\n      };\n      log_i(\"[WiFi-event]: #%d (%s)\", event, arduino_event_names[event]);\n    }\n  #endif\n\n  void disableWiFi()\n  {\n    WiFi.mode(WIFI_OFF);\n    delay(500);\n    wifisetup = false;\n  }\n\n\n  void enableWiFi()\n  {\n    //WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector\n    WiFi.mode(WIFI_OFF);\n    delay(500);\n    WiFi.mode(WIFI_STA);\n    String mac = WiFi.macAddress();\n    Serial.println( mac ); // print mac address to serial so it can eventually be copy/pasted\n    #if ARDUHAL_LOG_LEVEL < ARDUHAL_LOG_LEVEL_DEBUG\n      // raise DEBUG messages to INFO level\n      WiFi.onEvent(WiFiEvent);\n    #endif\n    WiFi.begin(); // set SSID/PASS from another app (i.e. WiFi Manager) and reload this app\n    unsigned long startup = millis();\n\n    const String nnnn = \"\\n\\n\\n\\n\";\n    drawDownloaderMenu( \"WiFi Setup\", String( WIFI_MSG_WAITING+nnnn+mac ).c_str());\n    size_t rssi = 0;\n\n    while ( WiFi.status() != WL_CONNECTED ) {\n      drawRSSIBar( 122, 100, rssi++, UI->getTheme()->MenuColor, 4.0 );\n      delay(500);\n      if(startup + 30000 < millis()) {\n        drawInfoWindow( WIFI_TITLE_TIMEOUT, WIFI_MSG_TIMEOUT, 1000 );\n        return;\n      }\n    }\n    drawInfoWindow( WIFI_TITLE_CONNECTED, WIFI_MSG_CONNECTED );\n    wifisetup = true;\n  }\n\n\n  void enableNTP()\n  {\n    drawDownloaderMenu(NTP_TITLE_SETUP, NTP_MSG_SETUP );\n    LGFX* gfx = UI->getGfx();\n    NtpIcon.draw( gfx, gfx->width()/2-NtpIcon.width/2, gfx->height()/2-NtpIcon.height/2 );\n    configTime(NTP::timezone*3600, NTP::daysavetime*3600, NTP::Servers[NTP::currentServer].addr, NTP::Servers[3].addr, NTP::Servers[4].addr );\n    struct tm tmstruct ;\n    tmstruct.tm_year = 0;\n\n    int max_attempts = 5;\n    while( !ntpsetup && max_attempts-->0 ) {\n      if( getLocalTime(&tmstruct, 5000) ) {\n        // TODO: draw ntp-success icon\n        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);\n        Serial.println(\"\");\n        ntpsetup = true;\n      }\n      delay(500);\n    }\n\n    if( !ntpsetup ) { // all attempts consumed\n      // TODO: modal-confirm retry/restart-wifi/restart-esp\n      drawInfoWindow( NTP_TITLE_FAIL, NTP_MSG_FAIL, 1500 );\n    } else {\n      drawInfoWindow( NTP_TITLE_SETUP, NTP_MSG_SETUP, 500 );\n    }\n  }\n\n\n  bool wifiSetupWorked()\n  {\n    int16_t maxAttempts = 5;\n\n    while( !wifisetup ) {\n      enableWiFi();\n      maxAttempts--;\n      if( maxAttempts < 0 ) {\n        WiFi.mode(WIFI_OFF);\n        break;\n      }\n    }\n    if( wifisetup ) {\n      if( !ntpsetup ) {\n        enableNTP();\n      }\n      drawDownloaderMenu();\n    }\n    return wifisetup;\n  }\n\n\n\n  void registryFetch( AppRegistry registry, String appRegistryLocalFile )\n  {\n    if( !wifiSetupWorked() ) {\n      modalConfirm( MODAL_CANCELED_TITLE, MODAL_WIFI_NOCONN_MSG, MODAL_SAME_PLAYER_SHOOT_AGAIN, MENU_BTN_REBOOT, MENU_BTN_RESTART, MENU_BTN_CANCEL );\n      ESP.restart();\n    }\n    URLParts urlParts = parseURL( registry.url );\n\n    if( ! TLS::init( urlParts.host ) ) {\n      log_e(\"Unable to init tls, aborting\");\n      return;\n    }\n\n    if( appRegistryLocalFile == \"\" ) {\n      appRegistryLocalFile = appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName;\n    } else {\n      appRegistryLocalFile = appRegistryFolder + PATH_SEPARATOR + urlParts.host + \".json\";\n    }\n    if( !wget( registry.url , appRegistryLocalFile ) ) {\n      modalConfirm( MODAL_CANCELED_TITLE, MODAL_REGISTRY_DAMAGED, MODAL_SAME_PLAYER_SHOOT_AGAIN, MENU_BTN_REBOOT, MENU_BTN_RESTART, MENU_BTN_CANCEL );\n    } else {\n      String appRegistryDefaultFile = appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName;\n      File sourceFile = M5_FS.open( appRegistryLocalFile );\n      if( M5_FS.exists( appRegistryDefaultFile ) ) {\n        M5_FS.remove( appRegistryDefaultFile );\n      }\n      #if defined FS_CAN_CREATE_PATH\n        File destFile   = M5_FS.open( appRegistryDefaultFile, FILE_WRITE, true );\n      #else\n        File destFile   = M5_FS.open( appRegistryDefaultFile, FILE_WRITE );\n      #endif\n      static uint8_t buf[512];\n      size_t packets = 0;\n      while( (packets = sourceFile.read( buf, sizeof(buf))) > 0 ) {\n        destFile.write( buf, packets );\n      }\n      destFile.close();\n      sourceFile.close();\n      modalConfirm( UPDATE_SUCCESS, MODAL_REGISTRY_UPDATED, MODAL_REBOOT_REGISTRY_UPDATED, MENU_BTN_REBOOT, MENU_BTN_RESTART, MENU_BTN_CANCEL );\n    }\n    ESP.restart();\n  }\n\n\n\n  bool downloadGzCatalog()\n  {\n    if( !wifiSetupWorked() ) {\n      modalConfirm( MODAL_CANCELED_TITLE, MODAL_WIFI_NOCONN_MSG, MODAL_SAME_PLAYER_SHOOT_AGAIN, MENU_BTN_REBOOT, MENU_BTN_RESTART, MENU_BTN_CANCEL );\n      ESP.restart();\n    }\n\n    Console = new LogWindow();\n\n    bool has_backup = false;\n    if( M5_FS.exists( CATALOG_DIR ) ) {\n      if( M5_FS.exists( CATALOG_DIR_BKP ) ) {\n        drawInfoWindow( DL_FSCLEANUP_TITLE, DL_FSCLEANUP_MSG, 500 );\n        log_d(\"Removing previous backup\");\n        cleanDir( CATALOG_DIR_BKP );\n      }\n      log_i(\"Backing up registry\");\n      drawInfoWindow( DL_FSBACKUP_TITLE, DL_FSBACKUP_MSG, 500 );\n      M5_FS.rename( CATALOG_DIR, CATALOG_DIR_BKP );\n      has_backup = true;\n    }\n\n    TarGzUnpacker *TARGZUnpacker = new TarGzUnpacker();\n    //TARGZUnpacker->haltOnError( true ); // stop on fail (manual restart/reset required)\n    //TARGZUnpacker->setTarVerify( true ); // true = enables health checks but slows down the overall process\n    TARGZUnpacker->setupFSCallbacks( targzTotalBytesFn, targzFreeBytesFn ); // prevent the partition from exploding, recommended\n    TARGZUnpacker->setGzProgressCallback( gzProgressCallback /*BaseUnpacker::defaultProgressCallback*/ ); // targzNullProgressCallback or defaultProgressCallback\n    TARGZUnpacker->setTarProgressCallback( tarProgressCallback /*BaseUnpacker::defaultProgressCallback*/ ); // prints the untarring progress for each individual file\n    TARGZUnpacker->setTarStatusProgressCallback( tarStatusCallback /*BaseUnpacker::defaultTarStatusProgressCallback*/ ); // print the filenames as they're expanded\n    //TARGZUnpacker->setPsram( true );\n\n    gzCatalogURL = String( Registry.defaultChannel.api_url_https ) + \"/catalog.tar.gz\";\n\n    URLParts urlParts = parseURL( gzCatalogURL );\n\n    drawInfoWindow( DL_TLSFETCH_TITLE, DL_TLSFETCH_MSG, 500 );\n\n    const char* cert = TLS::fetchCert( urlParts.host );\n\n    if( cert == nullptr ) {\n      if( ! TLS::init( urlParts.host ) ) {\n        log_e(\"Unable to init TLS, aborting\");\n        drawInfoWindow( DL_TLSFAIL_TITLE, DL_TLSFAIL_MSG, 1000 );\n        delete TARGZUnpacker;\n        disableWiFi();\n        return false;\n      }\n      cert = TLS::fetchCert( urlParts.host );\n    }\n\n    drawInfoWindow( DL_HTTPINIT_TITLE, DL_HTTPINIT_MSG );\n\n    WiFiClientSecure *client = new WiFiClientSecure;\n    Stream* streamptr = wgetptr( client, gzCatalogURL.c_str(), cert );\n    bool ret = false;\n\n    if( streamptr != nullptr ) {\n      log_i(\"Fetching %s\", gzCatalogURL.c_str() );\n      drawInfoWindow( DL_AWAITING_TITLE, nullptr );\n      // see if content-length was provided, and enable download progress\n      String contentLengthStr = http.header(\"Content-Length\");\n      contentLengthStr.trim();\n      int64_t streamSize = -1;\n      if( contentLengthStr != \"\" ) {\n        streamSize = atoi( contentLengthStr.c_str() );\n      }\n      // wait for data to happen, for some reason this is necessary\n      unsigned long start = millis();\n      unsigned long timeout = 10000; // wait 10 secs max\n      float blah = -PI;\n\n      while( !streamptr->available() && millis()-start < timeout ) {\n        blah += .1;\n        uint8_t r = abs(sin(blah))*128 + 128;\n        uint8_t g = abs(cos(blah))*128 + 128;\n        uint8_t b = abs(cos(blah))*256 - 128;\n        //uint16_t color = tft.color565( r, g, b );\n        uint32_t color = (r << 16) + (g << 8) + b;\n        drawDownloadIcon( color );\n        vTaskDelay(1);\n      }\n\n      drawDownloadIcon();\n\n      if( !TARGZUnpacker->tarGzStreamExpander( streamptr, SD, CATALOG_DIR, streamSize ) ) {\n        drawDownloadIcon( 0x800000U );\n        log_e(\"tarGzStreamExpander failed with return code #%d\", TARGZUnpacker->tarGzGetError() );\n        drawInfoWindow( \"Unpacking failed\", \"GZ archive could\\nnot be unpacked.\", 1000 );\n        if( has_backup ) { // restore backup\n          log_d(\"Restoring backup\");\n          drawInfoWindow( DL_FSBACKUP_TITLE, DL_FSRESTORE_MSG, 500 );\n          M5_FS.rename( CATALOG_DIR_BKP, CATALOG_DIR );\n        }\n      } else {\n        drawInfoWindow( DL_SUCCESS_TITLE, DL_SUCCESS_MSG, 500 );\n        drawDownloadIcon( UI->getTheme()->MenuColor );\n        ret = true; // success !\n        if( has_backup ) {\n          log_d(\"Removing old backup\");\n          drawInfoWindow( DL_FSCLEANUP_TITLE, DL_FSCLEANUP_MSG, 500 );\n          cleanDir( CATALOG_DIR_BKP ); // no need to keep the backup\n        }\n      }\n    } else {\n      drawInfoWindow( DL_HTTPFAIL_TITLE, DL_FAIL_MSG, 1000 );\n    }\n    delete client;\n    delete TARGZUnpacker;\n    delete Console;\n    disableWiFi();\n    log_v(\" Leaving registry fetcher with %d bytes free\", ESP.getFreeHeap() );\n    return ret;\n  }\n\n\n  bool downloadApp( String appName )\n  {\n\n    if( !wifisetup ) {\n      if( !wifiSetupWorked() ) {\n        modalConfirm( MODAL_CANCELED_TITLE, MODAL_WIFI_NOCONN_MSG, MODAL_SAME_PLAYER_SHOOT_AGAIN, MENU_BTN_REBOOT, MENU_BTN_RESTART, MENU_BTN_CANCEL );\n        ESP.restart();\n      }\n    }\n\n    drawInfoWindow( \"Downloading\", \"...\" );\n\n    if( baseCatalogURL == \"\" ) {\n      log_e(\"No base catalog url set, download catalog first \");\n      return false;\n    }\n\n    String macroseparator = \"\";\n    String jsonFile = CATALOG_DIR + macroseparator + DIR_json + appName + EXT_json;\n\n    JsonObject root;\n    DynamicJsonDocument jsonBuffer( 8192 );\n    if( !getJson( jsonFile.c_str(), root, jsonBuffer ) ) {\n      log_e(\"Failed to get json from %s\", jsonFile.c_str() );\n      return false;\n    }\n\n    progress_modulo = 100; // progress_modulo = 100/appsCount;\n    size_t assets_count = root[\"json_meta\"][\"assets\"].size();\n\n    if( assets_count == 0 ) return false;\n    int i=0;\n\n    Console = new LogWindow();\n\n    for (JsonVariant value : root[\"json_meta\"][\"assets\"].as<JsonArray>() ) {\n      String finalName = value[\"path\"].as<String>() + value[\"name\"].as<String>();\n      String tempFileName = finalName + String(\".tmp\");\n      Console->log( value[\"name\"].as<const char*>() );\n      if(M5_FS.exists( finalName.c_str() )) {\n        // local file already exists, calculate sha shum and compare to registry\n        sha256_sum( finalName );\n        if( value[\"sha256_sum\"].as<String>().equals(shaResultStr) ) {\n          // doesn't need to be updated\n          Console->log( WGET_SKIPPING );\n          i++;\n          continue; // no need to download\n        }\n        Console->log( WGET_UPDATING );\n      } else {\n        // file does not exist locally, see if it is present in the catalog folder\n        if(M5_FS.exists( CATALOG_DIR+finalName )) {\n          // file exists in the catalog folder as it should, no need to download => copy it locally\n          if( copyFile( String( CATALOG_DIR+finalName ).c_str(), finalName.c_str() ) ) {\n            Console->log( WGET_SKIPPING );\n            i++;\n            continue;\n          }\n        }\n        Console->log( WGET_CREATING );\n      }\n      String appURL = baseCatalogURL + finalName;\n      drawDownloadIcon();\n      if( !wget( appURL, tempFileName ) ) { // uh-oh\n        log_e(\"\\n%s\\n\", \"[ERROR] could not download %s to %s\", appURL.c_str(), tempFileName.c_str() );\n        drawDownloadIcon( 0x800000U );\n        drawInfoWindow( DL_HTTPFAIL_TITLE, tempFileName.c_str(), 500 );\n        i++;\n        continue;\n      }\n      drawDownloadIcon( UI->getTheme()->MenuColor );\n      if( value[\"sha256_sum\"].as<String>().equals(shaResultStr) ) {\n        if( M5_FS.exists( tempFileName ) ) {\n          if( M5_FS.exists( finalName ) ) M5_FS.remove( finalName ); // remove existing as it'll be replaced\n          M5_FS.rename( tempFileName, finalName );\n        } else { // download failed, error was previously disclosed\n          drawInfoWindow( DL_FSFAIL_TITLE, DOWNLOAD_FAIL, 500 );\n        }\n      } else {\n        log_e(\"  [SHA256 SUM ERROR] Remote hash: %s, Local hash: %s ### keeping local file and removing temp file ###\", value[\"sha256_sum\"].as<const char*>(), shaResultStr.c_str() );\n        drawInfoWindow( DL_SHAFAIL_TITLE, SHASHUM_FAIL, 500 );\n        M5_FS.remove( tempFileName );\n      }\n      uint16_t myprogress = progress + (i* float(progress_modulo/assets_count));\n      i++;\n    }\n    return true;\n  }\n\n};\n"
  },
  {
    "path": "examples/AppStore/modules/Downloader/Downloader.hpp",
    "content": "/*\n *\n * M5Stack SD Menu\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2019 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n */\n\n//\n\n#pragma once\n\n#include <HTTPClient.h>\n#include <WiFiClientSecure.h>\n#include \"../misc/compile_time.h\" // for app watermarking & user-agent customization\n#include \"../misc/config.h\"\n\nnamespace NTP\n{\n  struct Server\n  {\n    const char* name;\n    const char* addr;\n  };\n\n  // Timezone is using a float because Newfoundland, India, Iran, Afghanistan, Myanmar, Sri Lanka, the Marquesas,\n  // as well as parts of Australia use half-hour deviations from standard time, also some nations such as Nepal\n  // and some provinces such as the Chatham Islands of New Zealand, use quarter-hour deviations.\n  float timezone = 0; // UTC\n  uint8_t daysavetime = 1; // UTC + 1\n  const char* defaultServer = \"pool.ntp.org\";\n  uint8_t currentServer = 0;\n\n  void setTimezone( float tz );\n  void setDst( bool set );\n  void setServer( uint8_t id );\n  void loadPrefServer();\n\n  const Server Servers[] =\n  {\n    { \"Global\",        \"pool.ntp.org\" },\n    { \"Africa\",        \"africa.pool.ntp.org\" },\n    { \"Asia\",          \"asia.pool.ntp.org\" },\n    { \"Europe\",        \"europe.pool.ntp.org\" },\n    { \"North America\", \"north-america.pool.ntp.org\" },\n    { \"Oceania\",       \"oceania.pool.ntp.org\" },\n    { \"South America\", \"south-america.pool.ntp.org\" },\n  };\n\n};\n\n\nnamespace Downloader\n{\n\n  HTTPClient http;\n  // interesting http headers to watch for this module\n  const char * headerKeys[] = {\"location\", \"redirect\", \"Content-Type\", \"Content-Length\", \"Content-Disposition\" };\n  const size_t numberOfHeaders = 5;\n\n\n  struct URLParts\n  {\n    String url;\n    String protocol;\n    String host;\n    String port;\n    String auth;\n    String uri;\n  };\n\n\n  URLParts parseURL( String url );\n  URLParts parseURL( const char* url );\n\n  const String _ds = \"-\", _is = \" \", _ts = \":\"; // for iso datetime\n  const String _sdp = \"HTTPClient (SDU-\", _sdc = \"+Chimera-Core-\", _sds = \", \", _sde = \")\"; // for user agent\n\n  // This sketch build date/time in iso format\n  const String ISODateTime = __TIME_YEARS__+_ds+__TIME_MONTH__+_ds+__TIME_DAYS__+_is+__TIME_HOURS__+_ts+__TIME_MINUTES__+_ts+__TIME_SECONDS__;\n  // A comprehensive user agent to provide some hardware/software identity to the remote registry\n  const String UserAgent   = PLATFORM_NAME+_sdp+M5_SD_UPDATER_VERSION+_sdc+CHIMERA_CORE_VERSION+_sds+ISODateTime+_sde;\n\n  String gzCatalogURL = \"\";\n  String baseCatalogURL = \"\";\n\n  // Tiny buffer shared by HTTP and sha256 sum\n  size_t sizeOfTinyBuff = 512; // smaller is better because sha256 hashing happens between reads\n  uint8_t *tinyBuff = nullptr;\n\n  bool wifisetup = false;\n  bool ntpsetup  = false;\n  bool done      = false;\n\n  uint8_t progress = 0;\n  float progress_modulo = 0;\n\n  uint8_t shaResult[32];\n  static String shaResultStr = \"****************************************************************\"; // cheap malloc: any string is good as long as it's 64 chars\n\n  int tlserrors = 0;\n  int jsonerrors = 0;\n  int downloadererrors = 0;\n  int updatedfiles = 0;\n  int newfiles = 0;\n  int checkedfiles = 0;\n\n  void httpSetup();\n  bool tinyBuffInit();\n\n  void sha_sum_to_str();\n  void sha256_sum(const char* fileName);\n  void sha256_sum( String fileName );\n\n  bool wget( const char* url, const char* path );\n  bool wget( String bin_url, String outputFile );\n  bool wget( String bin_url, const char* outputFile );\n  bool wget( const char* bin_url, String outputFile );\n\n  WiFiClient *wgetptr( WiFiClientSecure *client, const char* url, const char *cert = nullptr );\n  #if ARDUHAL_LOG_LEVEL < ARDUHAL_LOG_LEVEL_DEBUG\n    void WiFiEvent(WiFiEvent_t event);\n  #endif\n  void disableWiFi();\n  void enableWiFi();\n  void enableNTP();\n  bool wifiSetupWorked();\n\n  void registryFetch( AppRegistry registry, String appRegistryLocalFile = \"\" );\n\n  bool downloadGzCatalog();\n\n  bool downloadApp( String appName );\n\n};\n"
  },
  {
    "path": "examples/AppStore/modules/FSUtils/FSUtils.cpp",
    "content": "#pragma once\n\n#include \"FSUtils.hpp\"\n\nextern LogWindow *Console;\n\nnamespace FSUtils\n{\n\n  using namespace RegistryUtils;\n\n  void setTimeFromLastFSAccess()\n  {\n    // Try to get a timestamp from filesystem in order to set system time\n    // to a value closer to \"now\" than the defaults\n    File root;\n    if( M5_FS.exists( SDU_APP_PATH ) ) { // try self\n      root = M5_FS.open( SDU_APP_PATH );\n    } else if ( M5_FS.exists( MENU_BIN ) ) { // try /menu.bin\n      root = M5_FS.open( MENU_BIN );\n    } else { // try rootdir\n      root = M5_FS.open( ROOT_DIR );\n    }\n    time_t lastWrite;\n    String lastWriteSource;\n    if( root ) {\n      lastWrite = root.getLastWrite();\n      lastWriteSource = root.name();\n      root.close();\n    } else {\n      lastWrite = __TIME_UNIX__;\n      lastWriteSource = \"__TIME_UNIX__\";\n    }\n    // RTC-less devices:\n    // Set a pseudo realistic internal clock time when no NTP sync occured,\n    // and before writing to the SD Card. Timestamps are still inacurate but\n    // better than ESP32's [1980-01-01 00:00:00] default boot datetime.\n    int epoch_time = __TIME_UNIX__; // this macro is populated at compilation time\n    struct tm * tmstruct = localtime(&lastWrite);\n    String timeSource;\n    String timeStatus;\n\n    // TODO: manually change this limit every century\n    if( (tmstruct->tm_year)+1900 < 2000 || (tmstruct->tm_year)+1900 > 2100 ) {\n      timeStatus = \"an unreliable\";\n      timeSource = \"sketch build time\";\n    } else {\n      int tmptime = mktime(tmstruct); // epoch time ( seconds since 1st jan 1969 )\n      if( tmptime > epoch_time ) {\n        timeStatus = \"a realistic\";\n        timeSource = lastWriteSource+\" last write\";\n        epoch_time = tmptime;\n      } else {\n        timeStatus = \"an obsolete\";\n        timeSource = \"sketch build time\";\n      }\n    }\n\n    log_w(  DEBUG_TIMESTAMP_GUESS,\n      lastWriteSource.c_str(),\n      timeStatus.c_str(),\n      (tmstruct->tm_year)+1900,\n      ( tmstruct->tm_mon)+1,\n      tmstruct->tm_mday,\n      tmstruct->tm_hour,\n      tmstruct->tm_min,\n      tmstruct->tm_sec,\n      timeSource.c_str()\n    );\n\n    timeval epoch = {epoch_time, 0};\n    const timeval *tv = &epoch;\n    settimeofday(tv, NULL);\n\n    struct tm now;\n    getLocalTime(&now,0);\n\n    Serial.printf(\"[Hobo style] Clock set to %s source (%s): \", timeStatus.c_str(), timeSource.c_str());\n    Serial.println(&now,\"%B %d %Y %H:%M:%S (%A)\");\n  }\n\n\n  bool getFileAttrs( const char* name, size_t *_size, time_t *_time )\n  {\n    fs::File file = M5_FS.open( name );\n    if( !file ) {\n      log_v(\"Can't reach file: %s\", name );\n      return false;\n    }\n    *_size = file.size();\n    *_time = file.getLastWrite();\n    log_v(\"Extracted size/time (%d/%d) for file %s\", *_size, *_time, name );\n    file.close();\n    return true;\n  }\n\n\n\n  void countApps()\n  {\n    std::vector<String> files;\n    getInstalledApps( files );\n    appsCount = files.size();\n  }\n\n\n  bool getInstalledApps( std::vector<String> &files ) {\n    files.clear();\n    File root = M5_FS.open( ROOT_DIR );\n    if( !root ) {\n      log_e( \"%s\", DEBUG_DIROPEN_FAILED );\n      return false;\n    }\n    if( !root.isDirectory() ) {\n      log_e( \"%s\", DEBUG_NOTADIR );\n      return false;\n    }\n    File file = root.openNextFile();\n    if( !file ) {\n      // empty root\n      root.close();\n      log_w( DEBUG_EMPTY_FS );\n      //buildRootMenu();\n      return false;\n    }\n\n    size_t files_count = 0;\n\n    while( file ) {\n      files_count++;\n      String fPath = String( fs_file_path(&file ) );\n      if( fPath == MENU_BIN || fPath == SDU_APP_PATH ) {\n        file = root.openNextFile();\n        continue; // ignore self and menu.bin\n      }\n\n      if( isValidAppName( fPath.c_str() ) ) {\n\n        fPath = gnu_basename( fPath );\n        fPath = fPath.substring( 0, fPath.length()-4 ); // remove extension\n\n        files.push_back( fPath );\n        log_v(\"Found app %s\", fPath.c_str() );\n      } else {\n        log_v(\"Ignoring '%s' (not a valid appname)\", fPath.c_str() );\n      }\n      file = root.openNextFile();\n    }\n    root.close();\n    return true;\n  }\n\n\n  void removeInstalledApp( String appName )\n  {\n    log_d(\"Deleting App '%s' + its assets\", appName.c_str() );\n    // TODO: confirmation dialog\n    trashFile( ROOT_DIR + appName + EXT_bin );\n    trashFile( DIR_jpg + appName + EXT_jpg );\n    trashFile( DIR_jpg + appName + \"_gh\" + EXT_jpg );\n    // TODO: read json first and delete assets\n    trashFile( DIR_json + appName + EXT_json );\n  }\n\n\n  bool isBinFile( const char* fileName )\n  {\n    String fName = String( fileName );\n    fName.toLowerCase();\n    return fName.endsWith( EXT_bin );\n  }\n\n\n  bool isLauncher( const char* binFileName )\n  {\n    String fName = String( binFileName );\n    fName.toLowerCase();\n    return fName.indexOf( \"launcher\" )>=0 || fName.indexOf( \"menu\" )>=0;\n  }\n\n\n  bool isJsonFile( const char* fileName )\n  {\n    String fName = String( fileName );\n    fName.toLowerCase();\n    return fName.endsWith( EXT_json );\n  }\n\n\n  bool isValidAppName( const char* fileName )\n  {\n    if( ( isBinFile( fileName ) ) // ignore files not ending in \".bin\"\n      && !String( fileName ).startsWith( \"/.\" ) ) { // ignore dotfiles (thanks to https://twitter.com/micutil)\n      return true;\n    }\n    return false;\n  }\n\n\n  bool iFile_exists( fs::FS *fs, String &fname )\n  {\n    if( fs->exists( fname.c_str() ) ) {\n      return true;\n    }\n    String locasename = fname;\n    String hicasename = fname;\n    locasename.toLowerCase();\n    hicasename.toUpperCase();\n    if( fs->exists( locasename.c_str() ) ) {\n      fname = locasename;\n      return true;\n    }\n    if( fs->exists( hicasename.c_str() ) ) {\n      fname = hicasename;\n      return true;\n    }\n    return false;\n  }\n\n\n  void cleanDir( const char* dir )\n  {\n    String dirToOpen = String( dir );\n    bool selfdeletable = false;\n\n    if( Console ) Console->clear();\n\n    // trim last slash if any, except for rootdir\n    if( dirToOpen != ROOT_DIR ) {\n      selfdeletable = true;\n      if( dirToOpen.endsWith( PATH_SEPARATOR ) ) {\n        dirToOpen = dirToOpen.substring(0, dirToOpen.length()-1);\n      }\n    }\n\n    File root = M5_FS.open( dirToOpen );\n    if(!root){\n      log_e(\"%s\",  DEBUG_DIROPEN_FAILED );\n      return;\n    }\n    if(!root.isDirectory()){\n      log_e(\"%s\",  DEBUG_NOTADIR );\n      return;\n    }\n\n    File file = root.openNextFile();\n    while( file ) {\n      const char* path = fs_file_path(&file);\n      if(file.isDirectory()){\n        // go recursive\n        cleanDir( path );\n        M5_FS.rmdir( path );\n        Serial.printf( CLEANDIR_REMOVED, path );\n        file = root.openNextFile();\n        continue;\n      }\n      Serial.printf( CLEANDIR_REMOVED, path );\n      if( Console ) Console->log( path );\n      M5_FS.remove( path );\n      file = root.openNextFile();\n    }\n    root.close();\n    if( selfdeletable ) {\n      if( M5_FS.exists( dirToOpen ) ) {\n        M5_FS.rmdir( dirToOpen );\n        Serial.printf( CLEANDIR_REMOVED, dir );\n      }\n    }\n    if( Console ) Console->clear();\n  }\n\n\n\n  bool copyFile( const char* src, const char* dst )\n  {\n    fs::File sourceFile = M5_FS.open(src);\n    if( !sourceFile ) {\n      log_e(\"Can't open source file %s, aborting\", src);\n      return false;\n    }\n    #if !defined FS_CAN_CREATE_PATH\n      // WARNING: not creating traversing folders unless sdk version >= 2.0.0\n      // TODO: mkdir -p dirname( dst )\n      fs::File destFile = M5_FS.open(dst, FILE_WRITE);\n    #else\n      fs::File destFile = M5_FS.open(dst, FILE_WRITE, true);\n    #endif\n    if( !destFile ) {\n      log_e(\"Can't open dest file %s, aborting\", dst);\n      sourceFile.close();\n      return false;\n    }\n    while( sourceFile.available() ) destFile.write( sourceFile.read() );\n    destFile.close();\n    return true;\n  }\n\n\n  bool trashFile( String path )\n  {\n    if( !M5_FS.exists( path ) ) {\n      log_e(\"Can't trash unexistent file %s\", path.c_str() );\n      return false;\n    }\n\n    String newPath = trashFolderPathStr + PATH_SEPARATOR + gnu_basename( path );\n\n    if( M5_FS.exists( newPath ) ) M5_FS.remove( newPath );\n    else if( !M5_FS.exists( trashFolderPathStr ) ) M5_FS.mkdir( trashFolderPathStr );\n\n    if( ! M5_FS.rename( path.c_str(), newPath.c_str() ) ) {\n      log_e(\"Can't trash '%s', renaming to '%s' failed :(\", path.c_str(), newPath.c_str() );\n      return false;\n    }\n    return true;\n  }\n\n\n\n\n  bool getJson( const char* path, JsonObject &root, DynamicJsonDocument &jsonBuffer )\n  {\n    if( jsonBuffer.capacity() == 0 ) {\n      log_e(\"JSON Buffer allocation failed\" );\n      return false;\n    }\n    if( ! M5_FS.exists( path ) ) {\n      log_v(\"JSON File %s does not exist\", path );\n      return false;\n    }\n    fs::File file = M5_FS.open( path );\n    if( !file ) {\n      log_e(\"JSON File %s can't be opened\");\n      return false;\n    }\n    log_v(\"Opened JSON File %s for reading (%d bytes)\", path, file.size() );\n    DeserializationError error = deserializeJson( jsonBuffer, file );\n    if (error) {\n      log_e(\"JSON deserialization error #%d in %s\", error, path );\n      file.close();\n      return false;\n    }\n    root = jsonBuffer.as<JsonObject>();\n    file.close();\n    return true;\n  }\n\n\n  void getHiddenApps()\n  {\n    //String jsonFile = \"/.hidden-apps.json\";\n    if( !M5_FS.exists( HIDDEN_APPS_FILE ) ) return;\n    HiddenFiles.clear();\n    JsonObject root;\n    DynamicJsonDocument jsonBuffer( 8192 );\n    if( !getJson( HIDDEN_APPS_FILE , root, jsonBuffer ) ) {\n      log_v(\"No hidden apps (file %s not created)\", HIDDEN_APPS_FILE  );\n      return;\n    }\n    if ( root.isNull() ) {\n      log_e(\"No parsable JSON in %s file\", HIDDEN_APPS_FILE  );\n      return;\n    }\n    if( root[\"apps\"].size() <= 0 ) {\n      log_e(\"No apps in catalog\");\n      return;\n    }\n    for( int i=0; i<root[\"apps\"].size(); i++ ) {\n      HiddenFiles.push_back( root[\"apps\"][i].as<String>() );\n    }\n    std::sort( HiddenFiles.begin(), HiddenFiles.end() );\n  }\n\n\n  void toggleHiddenApp( String appName, bool add )\n  {\n    getHiddenApps();\n\n    if( add ) {\n      if ( std::find(HiddenFiles.begin(), HiddenFiles.end(), appName) != HiddenFiles.end() ) {\n        log_w(\"App %s is already hidden!\", appName.c_str() );\n        return;\n      }\n      HiddenFiles.push_back( appName );\n    }\n\n    std::sort( HiddenFiles.begin(), HiddenFiles.end() );\n\n    DynamicJsonDocument doc(8192);\n    if( doc.capacity() == 0 ) {\n      log_e(\"ArduinoJSON failed to allocate 2kb\");\n      return;\n    }\n\n    // Open file for writing\n    #if defined FS_CAN_CREATE_PATH\n      File file = M5_FS.open( HIDDEN_APPS_FILE , FILE_WRITE, true );\n    #else\n      File file = M5_FS.open( HIDDEN_APPS_FILE , FILE_WRITE );\n    #endif\n    if (!file) {\n      log_e(\"Failed to open file %s\", HIDDEN_APPS_FILE );\n      return;\n    }\n\n    JsonObject root = doc.to<JsonObject>();\n    JsonArray array = root.createNestedArray(\"apps\");\n\n    for( int i=0; i<HiddenFiles.size(); i++ ) {\n      if( HiddenFiles[i] != appName ) {\n        array.add( HiddenFiles[i] );\n      }\n    }\n    if( add ) {\n      array.add( appName );\n    }\n\n    if( array.size() > 0 ) {\n      log_i(\"Created json:\");\n      serializeJsonPretty(doc, Serial);\n      if (serializeJson(doc, file) == 0) {\n        log_e( \"Failed to write to file %s\", HIDDEN_APPS_FILE  );\n      } else {\n        log_i (\"Successfully created %s\", HIDDEN_APPS_FILE  );\n      }\n      file.close();\n    } else {\n      HiddenFiles.clear();\n      file.close();\n      M5_FS.remove( HIDDEN_APPS_FILE );\n    }\n\n  }\n\n\n  bool isHiddenApp( String appName )\n  {\n    if( HiddenFiles.size() == 0 ) getHiddenApps();\n    if( HiddenFiles.size() == 0 ) return false;\n    return std::find( HiddenFiles.begin(), HiddenFiles.end(), appName) != HiddenFiles.end();\n  }\n\n\n  #if !defined FS_CAN_CREATE_PATH\n    void scanDataFolder()\n    {\n      // check if mandatory folders exists and create if necessary\n      if( !M5_FS.exists( appRegistryFolder ) ) {\n        M5_FS.mkdir( appRegistryFolder );\n      }\n      for( uint8_t i=0; i<extensionsCount; i++ ) {\n        String dir = ROOT_DIR + allowedExtensions[i];\n        if( !M5_FS.exists( dir ) ) {\n          M5_FS.mkdir( dir );\n        }\n      }\n    }\n  #endif\n\n};\n\n\n\n"
  },
  {
    "path": "examples/AppStore/modules/FSUtils/FSUtils.hpp",
    "content": "/*\n *\n * M5Stack SD Menu\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2019 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n */\n\n\n#pragma once\n#include <FS.h>\n#include <ArduinoJson.h> // https://github.com/bblanchon/ArduinoJson/\n#include \"../misc/compile_time.h\" // for app watermarking & user-agent customization\n#include \"../misc/core.h\"\n#include \"../misc/config.h\"\n\n\n\nnamespace FSUtils\n\n{\n\n  uint16_t appsCount = 0;\n\n  const uint8_t extensionsCount = 7; // change this if you add / remove an extension\n  const String trashFolderPathStr = \"/.trash\";\n\n  String allowedExtensions[extensionsCount] =\n  {\n    // do NOT remove jpg and json or the menu will crash !!!\n    \"jpg\", \"png\", \"bmp\", \"json\", \"mod\", \"mp3\", \"cert\"\n  };\n\n  static std::vector<String> HiddenFiles;\n\n  bool getInstalledApps( std::vector<String> &files );\n  void removeInstalledApp( String appName );\n\n  void setTimeFromLastFSAccess();\n  void countApps();\n  bool getFileAttrs( const char* name, size_t *_size, time_t *_time );\n  bool isBinFile( const char* fileName );\n  bool isLauncher( const char* binFileName );\n  bool isJsonFile( const char* fileName );\n  bool isValidAppName( const char* fileName );\n  bool iFile_exists( fs::FS *fs, String &fname );\n  bool isHiddenApp( String appName );\n  void cleanDir( const char* dir );\n  bool getJson( const char* path, JsonObject &root, DynamicJsonDocument &jsonBuffer );\n  void getHiddenApps();\n  void toggleHiddenApp( String appName, bool add = true );\n\n  #if !defined FS_CAN_CREATE_PATH\n    void scanDataFolder();\n  #endif\n\n  bool copyFile( const char* src, const char* dst );\n  bool trashFile( String path );\n\n  // inherit espressif32-2.x.x file->name() to file->path() migration handler from M5StackUpdater\n  const char* (*fs_file_path)( fs::File *file ) = SDUpdater::fs_file_path;\n\n  static String gnu_basename( String path )\n  {\n    char *base = strrchr(path.c_str(), '/');\n    return base ? String( base+1) : path;\n  }\n\n};\n\n"
  },
  {
    "path": "examples/AppStore/modules/MenuItems/MenuItems.cpp",
    "content": "#pragma once\n\n#include \"MenuItems.hpp\"\n\nnamespace MenuItems\n{\n\n\n};\n"
  },
  {
    "path": "examples/AppStore/modules/MenuItems/MenuItems.hpp",
    "content": "#pragma once\n\n#include \"../misc/i18n.h\"\n#include \"../Assets/Assets.hpp\"\n#include \"../AppStoreActions/AppStoreActions.hpp\"\n#include \"../MenuUtils/MenuUtils.hpp\"\n\n\n\nnamespace MenuItems\n{\n\n  using namespace UIDo;\n  using namespace UIDraw;\n  using namespace UILists;\n  using namespace UIShow;\n\n  // JPG assets (bytes array)\n  LocalAsset DiskIcon              = { disk01_jpg, disk01_jpg_len, IMG_JPG,   0,   0, \"Insert SD\" };\n  LocalAsset BrokenImage           = { broken_png, broken_png_len, IMG_PNG,  16,  18, \"Broken asset\" };\n\n  // JPG assets (filesystem)\n  RemoteAsset SDUpdaterIcon        = { \"/catalog/jpg/sd-updater15x16.jpg\" ,  15,  16, \"SDUpdater\" };\n  RemoteAsset CautionModalIcon     = { \"/catalog/jpg/caution.jpg\"         ,  64,  46, \"Warning\" };\n\n  // PNG assets (filesystem)\n  RemoteAsset UnknownAppIcon       = { \"/catalog/png/unknown-app.png\"     , 120, 120, \"Unknown App\" };\n  RemoteAsset CheckIcon            = { \"/catalog/png/missing-meta.png\"    ,  32,  32, \"Meta\" }; // used to overlay app icon\n  RemoteAsset UpdateIcon           = { \"/catalog/png/update-icon.png\"     ,  32,  32, \"Update\" }; // used to overlay app icon\n  RemoteAsset ForkIcon             = { \"/catalog/png/fork.png\"            ,  12,  16, \"Channel\" };\n  RemoteAsset NtpIcon              = { \"/catalog/png/ntp-connect.png\"     ,  32,  32, \"Syncing to NTP\" };\n\n  RemoteAsset SelectBtnIcon        = { \"/catalog/png/select.png\"          ,  16,  16, \"Select\" };\n  RemoteAsset ArrowDownBtnIcon     = { \"/catalog/png/arrowdown.png\"       ,  16,  16, \"Arrow down\" };\n  RemoteAsset ArrowUpBtnIcon       = { \"/catalog/png/arrowup.png\"         ,  16,  16, \"Arrow up\" };\n  //RemoteAsset *defaultBtnIcons[3]  = { &SelectBtnIcon, &ArrowUpBtnIcon, &ArrowDownBtnIcon };\n\n  RemoteAsset ManageAppsImage      = { \"/catalog/png/manage-apps.png\"     , 120, 120, \"App Manager\" };\n  RemoteAsset RefreshCatalogImage  = { \"/catalog/png/refresh-catalog.png\" , 120, 120, \"Catalog Updater\" };\n  RemoteAsset SwitchChannelImage   = { \"/catalog/png/switch-channel.png\"  , 120, 120, \"Channel Switcher\" };\n  RemoteAsset ChangeNTPServerImage = { \"/catalog/png/ntp.png\"             , 120, 120, \"Region Selector\" };\n  RemoteAsset ClearTLSImage        = { \"/catalog/png/clear-TLS.png\"       , 120, 120, \"SD Cleaner\" };\n  RemoteAsset ClearAllImage        = { \"/catalog/png/clear-ALL.png\"       , 120, 120, \"Certs Cleaner\" };\n  RemoteAsset SleepImage           = { \"/catalog/png/sleep.png\"           , 120, 120, \"Turn Off\" };\n  RemoteAsset EditDeleteImage      = { \"/catalog/png/edit-delete.png\"     , 120, 120, \"Edit/Delete\" };\n  RemoteAsset InstallAppsImage     = { \"/catalog/png/add-apps.png\"        , 120, 120, \"Install/Hide\" };\n  RemoteAsset HideAppsImage        = { \"/catalog/png/toggle.png\"          , 120, 120, \"Unhide\" };\n  RemoteAsset Cpanel1Image         = { \"/catalog/png/cpanel2.png\"         , 120, 120, \"Settings\" };\n  RemoteAsset Cpanel2Image         = { \"/catalog/png/cpanel.png\"          , 120, 120, \"Back to Settings\" };\n\n\n  // buttons\n  ButtonAction defaultListSelect      = { MENU_BTN_INFO,         &BtnA,                &SelectBtnIcon };   // \"SELECT\" (exec selected item cb)\n  ButtonAction defaultListPrevItem    = { MENU_BTN_PREV,         &BtnB,                &ArrowUpBtnIcon }; // \">>\" (prev item)\n  ButtonAction defaultListNextItem    = { MENU_BTN_NEXT,         &BtnC,                &ArrowDownBtnIcon }; // \">\"  (next item)\n  ButtonAction BackToBrowseAppsButton = { MENUACTION_BACKTOAPPS, &buildBrowseAppsMenu, nullptr };\n  ButtonAction BackButton             = { MENU_BTN_BACK,         &BtnC,                nullptr };\n  ButtonAction CancelButton           = { MENU_BTN_CANCEL,       &BtnA,                nullptr };\n  ButtonAction GoButton               = { MENU_BTN_GO,           &BtnA,                nullptr };\n  ButtonAction InstallButton          = { MENUACTION_APPINSTALL, &installApp,          nullptr };\n  ButtonAction DeleteButton           = { MENUACTION_APPDELETE,  &deleteApp,           nullptr };\n  ButtonAction HideButton             = { MENUACTION_APPHIDE,    &addHiddenApp,        nullptr };\n  ButtonAction UnhideButton           = { MENUACTION_UNHIDE,     &removeHiddenApp,     nullptr };\n  ButtonAction RefreshButton          = { MENUACTION_REFRESH,    &downloadCatalog,     nullptr };\n  ButtonAction EmptyButton            = { \"\",                    [](){},               nullptr};\n\n  // buttons-sets\n  ButtonAction *defaultListButtons[3]       = { &defaultListSelect, &defaultListPrevItem, &defaultListNextItem }; // \"select\", \"next page\", \"next item\"\n  ButtonAction *emptyButtons[3]             = { &EmptyButton,       &EmptyButton,         &EmptyButton };\n  ButtonAction *manageAppsButtons[3]        = { &GoButton,          &defaultListPrevItem, &defaultListNextItem }; // \"go\", \"next page\", \"next item\"\n  ButtonAction *myAppsButtons[3]            = { &DeleteButton,      &EmptyButton,         &BackButton }; // \"delete\", [empty], \"back\"\n  ButtonAction *InstallHideActionButtons[3] = { &InstallButton,     &HideButton,          &BackButton };\n  ButtonAction *unhideAppsButtons[3]        = { &UnhideButton,      &EmptyButton,         &BackButton };\n\n  // buttons-sets/title for static lists\n  MenuActionLabels RootListActionButtons    = { MENUTITLE_DEFAULT, defaultListButtons };\n  MenuActionLabels ManageAppsActionButtons  = { MENUTITLE_MANAGEAPPS, manageAppsButtons };\n  MenuActionLabels RefreshAppsActionButtons = { MENUTITLE_DOWNLOADER, emptyButtons };\n\n  // buttons-sets/title for dynamic lists\n  MenuActionLabels MyAppsActionButtons      = { MENUTITLE_MYAPPS, defaultListButtons };\n  MenuActionLabels AppStoreActionButtons    = { MENUTITLE_APPSTORE, defaultListButtons };\n  MenuActionLabels HiddenAppsActionButtons  = { MENUTITLE_TOGGLEAPPS, defaultListButtons };\n  MenuActionLabels NTPServersActionButtons  = { MENUACTION_NTP, defaultListButtons };\n\n  // buttons-sets/title for dynamic list items\n  MenuActionLabels InstallHideAppsActionButtons = { MENUTITLE_DOWNLOADER, InstallHideActionButtons };\n  MenuActionLabels UnhideAppsActionButtons      = { MENUTITLE_TOGGLEAPPS, unhideAppsButtons };\n  MenuActionLabels DeleteAppsActionButtons      = { MENUTITLE_MYAPPS,     myAppsButtons };\n\n  // callbacks for menu events:                     select         /      render      /             idle     /       modal\n  MenuItemCallBacks BackToRootMenuCallbacks     = { &buildRootMenu,       nullptr,                  nullptr,         nullptr };\n  MenuItemCallBacks BackToManageAppsCallbacks   = { &buildBrowseAppsMenu, nullptr,                  nullptr,         nullptr };\n  MenuItemCallBacks RootActionRefreshCallbacks  = { &downloadCatalog,     nullptr,                  nullptr,         nullptr };\n  MenuItemCallBacks RootActionBrowseCallbacks   = { &buildBrowseAppsMenu, nullptr,                  nullptr,         nullptr };\n  MenuItemCallBacks ManageMyAppsCallbacks       = { &buildMyAppsMenu,     nullptr,                  nullptr,         nullptr };\n  MenuItemCallBacks BrowseAppStoreCallbacks     = { &buildStoreMenu,      nullptr,                  nullptr,         nullptr };\n  MenuItemCallBacks ManageAppStoreCallbacks     = { &buildHiddenAppList,  nullptr,                  nullptr,         nullptr };\n  MenuItemCallBacks RootActionSwitchCallbacks   = { &drawRegistryMenu,    nullptr,                  nullptr,         nullptr };\n  MenuItemCallBacks RootActionNtpCallbacks      = { &buildNtpMenu,        nullptr,                  nullptr,         nullptr };\n  MenuItemCallBacks RootActionClearAllCallbacks = { &clearApps,           nullptr,                  nullptr,         nullptr };\n  MenuItemCallBacks RootActionClearTlsCallbacks = { &clearTLS,            nullptr,                  nullptr,         nullptr };\n  MenuItemCallBacks RootActionSleepCallbacks    = { &gotoSleep,           nullptr,                  nullptr,         nullptr };\n  // callbacks for menuitem events:                 select         /      render      /             idle     /       modal\n  MenuItemCallBacks UpdateCheckCallbacks        = { &showAppInfo,         &updateCheckShowAppImage, &cycleAppAssets, &scrollAppInfo };\n  MenuItemCallBacks UpdateMetaCallbacks         = { &showAppInfo,         &updateMetaShowAppImage,  &cycleAppAssets, &scrollAppInfo };\n  MenuItemCallBacks DeleteAppCallbacks          = { &showAppInfo,         &showDeleteAppImage,      &cycleAppAssets, &scrollAppInfo };\n  MenuItemCallBacks AppStoreCallbacks           = { &showAppInfo,         &showAppImage,            &cycleAppAssets, &scrollAppInfo };\n  MenuItemCallBacks HiddenAppsCallbacks         = { &showAppInfo,         &showAppImage,            &cycleAppAssets, &scrollAppInfo };\n  MenuItemCallBacks NtpItemCallbacks            = { &setNtpServer,        &showNTPImage,            nullptr,         nullptr };\n\n\n\n  MenuAction BackToRootMenu   = MenuAction( MENUACTION_BACK \" to \" MENUTITLE_MAINMENU,   &BackToRootMenuCallbacks, &ManageAppsImage );\n  MenuAction BackToManageApps = MenuAction( MENUACTION_BACK \" to \" MENUTITLE_MANAGEAPPS, &BackToManageAppsCallbacks, &ManageAppsImage );\n\n  /*    Main Menu level 0           */\n  /*  */MenuGroup RootMenuGroup = MenuGroup( &RootListActionButtons );\n  /*  |    MenuItem 1 level 0       */  // Apps Management action: Refresh Catalog\n  /*  +--*/MenuAction RootActionRefresh = MenuAction( ROOTACTION_REFRESH, &RootActionRefreshCallbacks, &RefreshCatalogImage );\n  /*  |    MenuItem 2 level 0       */  // General Action: Open Apps Management Menu\n  /*  +--*/MenuAction RootActionBrowse = MenuAction( MENUTITLE_MANAGEAPPS, &RootActionBrowseCallbacks, &ManageAppsImage );\n  /*  |    MenuItem 3 level 0       */  // Apps Management Menu\n  /*  +--*/MenuGroup ManageAppsGroup = MenuGroup( &ManageAppsActionButtons );\n  /*  |  |    MenuItem 9 level 1    */  // Apps Management: SDCard applications list\n  /*  |  +--*/MenuGroup MyAppsMenuGroup = MenuGroup( &MyAppsActionButtons );\n  /*  |  |  |    MenuItem 13 level 2 */ // SDCard applications list actions: delete/update\n  /*  |  |  +--*/MenuAction ManageMyApps = MenuAction( MENUACTION_DEL_APPS, &ManageMyAppsCallbacks, &EditDeleteImage );\n  /*  |  |    MenuItem 10 level 1    */ // Apps Management: Installable applications list\n  /*  |  +--*/MenuGroup AppStoreMenuGroup = MenuGroup( &AppStoreActionButtons, CATALOG_DIR );\n  /*  |  |  |    MenuItem 14 level 2 */ // Installable applications list actions: install/hide\n  /*  |  |  +--*/MenuAction BrowseAppStore = MenuAction( MENUACTION_BROWSEAPP, &BrowseAppStoreCallbacks, &InstallAppsImage );\n  /*  |  |    MenuItem 11 level 1   */  // Apps Management: Hidden applications list\n  /*  |  +--*/MenuGroup HiddenAppsMenuGroup = MenuGroup( &HiddenAppsActionButtons, CATALOG_DIR );\n  /*  |     |    MenuItem 15 level 2 */ // Hidden applications list actions: unhide\n  /*  |     +--*/MenuAction ManageAppStore = MenuAction( MENUTITLE_TOGGLEAPPS, &ManageAppStoreCallbacks, &HideAppsImage );\n  /*  |    MenuItem 4 level 0       */  // Registry switcher\n  /*  +--*/MenuAction RootActionSwitch = MenuAction( ROOTACTION_SWITCH, &RootActionSwitchCallbacks, &SwitchChannelImage );\n  /*  |    MenuItem 5 level 0       */  // NTP Management: NTP Servers list\n  /*  +--*/MenuAction RootActionNtp = MenuAction( ROOTACTION_NTP_PICK, &RootActionNtpCallbacks, &ChangeNTPServerImage );\n  /*  |  |    MenuItem 12 level 1   */  // NTP Servers list actions: Pick server\n  /*  |  +--*/MenuGroup NtpMenuGroup = MenuGroup( &NTPServersActionButtons );\n  /*  |    MenuItem 6 level 0       */  // Full filesystem wipe\n  /*  +--*/MenuAction RootActionClearAll = MenuAction( ROOTACTION_CLEAR_ALL, &RootActionClearAllCallbacks, &ClearAllImage );\n  /*  |    MenuItem 7 level 0       */  // TLS wipe\n  /*  +--*/MenuAction RootActionClearTls = MenuAction( ROOTACTION_CLEAR_TLS, &RootActionClearTlsCallbacks, &ClearTLSImage );\n  /*  |    MenuItem 8 level 0       */  // Sleep mode\n  /*  +--*/MenuAction RootActionSleep = MenuAction( ROOTACTION_SLEEP, &RootActionSleepCallbacks, &SleepImage );\n\n\n};\n"
  },
  {
    "path": "examples/AppStore/modules/MenuUtils/MenuUtils.cpp",
    "content": "#pragma once\n\n#include \"MenuUtils.hpp\"\n\n\nMenuAction::MenuAction( const char* _title, MenuItemCallBacks* _callbacks, const RemoteAsset* _icon )\n{\n  //setTitle( _title );\n  title     = (char*)_title;\n  icon      = _icon;\n  callbacks = _callbacks;\n  needs_free = false;\n}\n\n\nMenuAction::~MenuAction()\n{\n  clear();\n}\n\n\nvoid MenuAction::setTitle( const char* _title )\n{\n  if( !_title || _title[0]=='\\0' ) return;\n  size_t title_len = strlen( _title );\n  title = (char*)calloc( title_len+1, sizeof(char));\n  if( title == NULL ) {\n    log_e(\"Failed to alloc %d bytes for string %s\", title_len+1, _title );\n    return;\n  }\n  memcpy( title, _title, title_len );\n  log_v(\"Allocated %d bytes for title %s\", title_len+1, _title );\n  needs_free = true;\n}\n\nvoid MenuAction::clear()\n{\n  if( needs_free ) {\n    log_v(\"Clearing %s\", title );\n    free( title );\n    title = nullptr;\n  }\n  needs_free = false;\n}\n\n\n\n\nMenuGroup::MenuGroup( MenuActionLabels* actionLabels, const char* _assets_folder )\n{\n  ActionLabels = actionLabels;\n  Title = ActionLabels->title;\n  assets_folder = _assets_folder;\n}\n\n\nMenuGroup::~MenuGroup()\n{\n  clear();\n}\n\nvoid MenuGroup::clear()\n{\n  if( actions_count == 0 || !Actions || needs_free == false ) {\n    log_v(\"Actions for menu %s do not need clearing\", Title );\n    return;\n  }\n  size_t before_free = ESP.getFreeHeap();\n  log_v(\"Clearing actions for menu %s\", Title );\n  for( int i=0; i<actions_count; i++ ) {\n    log_v(\"Clearing action #%d (%s)\", i, Actions[i]->title );\n    Actions[i]->clear();\n    free( Actions[i] );\n  }\n  if( Actions ) {\n    free( Actions );\n  }\n  log_v(\"%d bytes freed\", ESP.getFreeHeap() - before_free );\n  actions_count = 0;\n  Actions = nullptr;\n  needs_free = false;\n}\n\n\nvoid MenuGroup::push( MenuAction *action )\n{\n  push( action->title, action->callbacks, action->icon, action->textcolor );\n}\n\n\nvoid MenuGroup::push( const char* _title, MenuItemCallBacks* _callbacks, const RemoteAsset* _icon, uint32_t _textcolor )\n{\n  if( actions_count == 0 || Actions == nullptr ) {\n    Actions = (MenuAction**)calloc( 1, sizeof( MenuAction* ) );\n  } else {\n    Actions = (MenuAction**)realloc( Actions, (actions_count+1) * (sizeof( MenuAction* )) );\n  }\n  log_v(\"Pushing action #%d %s\", actions_count, _title?_title:\"\" );\n  if( Actions == NULL ) {\n    log_e(\"Failed to allocate %d bytes for menu Actions\", (actions_count+1) * sizeof( MenuAction* ) );\n    clear();\n    return;\n  }\n  needs_free = true;\n  Actions[actions_count] = (MenuAction*)calloc( 1, sizeof( MenuAction ) );\n  Actions[actions_count]->setTitle( _title );\n  Actions[actions_count]->icon      = _icon;\n  Actions[actions_count]->callbacks = _callbacks;\n  Actions[actions_count]->textcolor = _textcolor;\n  actions_count++;\n}\n\n\n\n\n"
  },
  {
    "path": "examples/AppStore/modules/MenuUtils/MenuUtils.hpp",
    "content": "#pragma once\n\n#include \"../misc/core.h\"\n#include \"../misc/config.h\"\n#include \"../Assets/Assets.hpp\"\n\ntypedef void(*onselect_cb_t)(void);\ntypedef void(*onrender_cb_t)(void);\ntypedef void(*onidle_cb_t)(void);\ntypedef void(*onmodal_cb_t)(void);\n\nstruct MenuItemCallBacks\n{\n  onselect_cb_t onSelect; // on BtnA Click\n  onrender_cb_t onRender; // on menuitem render\n  onrender_cb_t onIdle;   // on idle (animations)\n  onmodal_cb_t  onModal;  // on modal window (animations)\n};\n\n\nstruct ButtonAction\n{\n  const char* title;\n  onselect_cb_t onClick;\n  RemoteAsset *asset;\n};\n\n\nstruct MenuActionLabels\n{\n  const char *title;\n  ButtonAction **Buttons;\n};\n\n\nclass MenuGroup;\n\n// UI Menu Item\nclass MenuAction\n{\n  public:\n    MenuAction( const char* _title, MenuItemCallBacks* _callbacks, const RemoteAsset* _icon );\n    ~MenuAction();\n    void setTitle( const char* _title );\n    void clear();\n    char* title;\n    MenuItemCallBacks* callbacks;\n    const RemoteAsset* icon = nullptr;\n    uint32_t textcolor = TEXT_COLOR;\n  private:\n    bool needs_free = false;\n};\n\n// UI Menu ItemCollection\nclass MenuGroup\n{\n  public:\n    MenuGroup( MenuActionLabels* actionLabels, const char* _assets_folder = \"\" ); // empty menu group to be filled with push()\n    ~MenuGroup();\n    void push( const char* _title, MenuItemCallBacks* _callbacks, const RemoteAsset* _icon = nullptr, uint32_t _textcolor = TEXT_COLOR );\n    void push( MenuAction *action );\n    void setTitle( const char* title ) { Title = title; }\n    void clear();\n    size_t actions_count = 0;\n    const char* Title = nullptr;\n    MenuAction** Actions = nullptr;\n    MenuActionLabels* ActionLabels = nullptr;\n    const char* assets_folder = \"\";\n    uint16_t selectedindex;\n  private:\n    bool needs_free = false;\n};\n\n"
  },
  {
    "path": "examples/AppStore/modules/Registry/Registry.cpp",
    "content": "#pragma once\n\n#include \"Registry.hpp\"\n#include \"../Downloader/Downloader.hpp\"\n\n\nvoid AppRegistry::init()\n{\n  masterChannel.init();\n  unstableChannel.init();\n  if( pref_default_channel == REGISTRY_MASTER ) {\n    defaultChannel = masterChannel;\n  } else {\n    defaultChannel = unstableChannel;\n  }\n  print();\n}\n\n\nvoid AppRegistry::print()\n{\n  log_i(\"\\nRegistry infos:\\n\\tname: %s\\n\\tdescription: %s\\n\\turl: %s\\n\\tpref_default_channel: %s\",\n    name.c_str(),\n    description.c_str(),\n    url.c_str(),\n    pref_default_channel.c_str()\n  );\n  masterChannel.print();\n  unstableChannel.print();\n  defaultChannel.print();\n}\n\n\n\nvoid AppRegistryItem::init()\n{\n  api_cert_provider_url_http  = \"http://\" + api_host + api_path + api_cert_path;\n  api_cert_provider_url_https = \"https://\" + api_host + api_path + api_cert_path;\n  api_url_https               = \"https://\" + api_host + api_path + updater_path;\n  api_url_http                = \"http://\" + api_host + api_path + updater_path;\n}\n\nvoid AppRegistryItem::print()\n{\n  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\",\n    name.c_str(),\n    description.c_str(),\n    url.c_str(),\n    api_host.c_str(),\n    api_path.c_str(),\n    api_cert_path.c_str(),\n    updater_path.c_str(),\n    catalog_endpoint.c_str(),\n    api_cert_provider_url_https.c_str(),\n    api_url_https.c_str(),\n    api_url_http.c_str()\n  );\n}\n\n\n\nnamespace RegistryUtils\n{\n  using namespace Downloader;\n\n\n  void setJsonChannelItem( JsonObject json, AppRegistryItem reg )\n  {\n    json[\"name\"]         = reg.name;\n    json[\"description\"]  = reg.description;\n    json[\"url\"]          = reg.url;\n    json[\"api_host\"]     = reg.api_host;\n    json[\"api_path\"]     = reg.api_path;\n    json[\"cert_path\"]    = reg.api_cert_path;\n    json[\"updater_path\"] = reg.updater_path;\n    json[\"endpoint\"]     = reg.catalog_endpoint;\n  }\n\n\n  void registrySave( AppRegistry registry, String appRegistryLocalFile )\n  {\n    URLParts urlParts = parseURL( registry.url );\n    if( appRegistryLocalFile == \"\" ) {\n      log_d(\"Will attempt to create/save %s\", appRegistryLocalFile.c_str() );\n      appRegistryLocalFile = appRegistryFolder + PATH_SEPARATOR + urlParts.host + EXT_json;\n    }\n\n    DynamicJsonDocument jsonRegistryBuffer(2048);\n    if( jsonRegistryBuffer.capacity() == 0 ) {\n      log_e(\"ArduinoJSON failed to allocate 2kb\");\n      return;\n    }\n\n    if( M5_FS.exists( appRegistryLocalFile ) ) {\n      log_d(\"Removing %s before writing\", appRegistryLocalFile.c_str());\n      M5_FS.remove( appRegistryLocalFile );\n    }\n    // Open file for writing\n    #if defined FS_CAN_CREATE_PATH\n      File file = M5_FS.open( appRegistryLocalFile, FILE_WRITE, true );\n    #else\n      File file = M5_FS.open( appRegistryLocalFile, FILE_WRITE );\n    #endif\n    if (!file) {\n      log_e(\"Failed to create file %s\", appRegistryLocalFile.c_str());\n      return;\n    }\n\n    JsonObject channels            = jsonRegistryBuffer.createNestedObject(\"channels\");\n    JsonObject masterChannelJson   = channels.createNestedObject(REGISTRY_MASTER);\n    JsonObject unstableChannelJson = channels.createNestedObject(REGISTRY_UNSTABLE);\n\n    setJsonChannelItem( masterChannelJson,   registry.masterChannel );\n    setJsonChannelItem( unstableChannelJson, registry.unstableChannel );\n\n    jsonRegistryBuffer[\"name\"]                 = registry.name;\n    jsonRegistryBuffer[\"description\"]          = registry.description;\n    jsonRegistryBuffer[\"url\"]                  = registry.url;\n    jsonRegistryBuffer[\"pref_default_channel\"] = registry.pref_default_channel;\n\n    log_i(\"Created json:\");\n    // serializeJsonPretty(jsonRegistryBuffer, Serial);\n\n    if (serializeJson(jsonRegistryBuffer, file) == 0) {\n      log_e( \"Failed to write to file %s\", appRegistryLocalFile.c_str() );\n    } else {\n      log_i (\"Successfully created %s\", appRegistryLocalFile.c_str() );\n    }\n    file.close();\n  }\n\n\n  bool isValidRegistryChannel( JsonVariant json )\n  {\n    return( json[\"name\"].as<String>() !=\"\"\n        && json[\"description\"].as<String>() !=\"\"\n        && json[\"url\"].as<String>().startsWith(\"http\")\n        && json[\"api_host\"].as<String>() !=\"\"\n        && json[\"api_path\"].as<String>() !=\"\"\n        && json[\"cert_path\"].as<String>() !=\"\"\n        && json[\"updater_path\"].as<String>() !=\"\"\n        && json[\"endpoint\"].as<String>() !=\"\"\n    );\n  }\n\n\n  AppRegistryItem getJsonChannel( JsonVariant jsonChannels, const char* channel )\n  {\n    return {\n      String(channel),\n      jsonChannels[channel][\"description\"].as<String>(),\n      jsonChannels[channel][\"url\"].as<String>(),\n      jsonChannels[channel][\"api_host\"].as<String>(),\n      jsonChannels[channel][\"api_path\"].as<String>(),\n      jsonChannels[channel][\"cert_path\"].as<String>(),\n      jsonChannels[channel][\"updater_path\"].as<String>(),\n      jsonChannels[channel][\"endpoint\"].as<String>()\n    };\n  }\n\n\n  AppRegistry init( String appRegistryLocalFile )\n  {\n    if( appRegistryLocalFile == \"\" ) {\n      appRegistryLocalFile = appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName;\n    }\n    log_i(\"Opening channel file: %s\", appRegistryLocalFile.c_str());\n\n    if( !M5_FS.exists( appRegistryLocalFile ) ) {\n      // create file from registry default template, return template\n      log_i(\"Registry file %s does not exist, creating from firmware defaults\", appRegistryLocalFile.c_str() );\n      registrySave( defaultAppRegistry, appRegistryFolder + PATH_SEPARATOR + appRegistryDefaultName );\n      defaultAppRegistry.init();\n      return defaultAppRegistry;\n    }\n\n    // load registry profiles from file\n    File file = M5_FS.open( appRegistryLocalFile );\n\n    DynamicJsonDocument jsonRegistryBuffer(2048);\n    DeserializationError error = deserializeJson( jsonRegistryBuffer, file );\n    file.close();\n    if (error) {\n      log_e(\"JSON Error while reading registry file %s\", appRegistryLocalFile.c_str() );\n      defaultAppRegistry.init();\n      return defaultAppRegistry;\n    }\n\n    JsonObject root = jsonRegistryBuffer.as<JsonObject>();\n    if ( root.isNull() ) {\n      log_w(\"Registry file %s has empty JSON\", appRegistryLocalFile.c_str() );\n      defaultAppRegistry.init();\n      return defaultAppRegistry;\n    }\n\n    if( !isValidRegistryChannel( root[\"channels\"][REGISTRY_MASTER] ) ) {\n      // bad master item\n      log_w(\"%s\", \"Bad master channel in JSON file\");\n      defaultAppRegistry.init();\n      return defaultAppRegistry;\n    }\n\n    if( !isValidRegistryChannel( root[\"channels\"][REGISTRY_UNSTABLE] ) ) {\n      // bad master item\n      log_w(\"%s\", \"Bad unstable channel in JSON file\");\n      defaultAppRegistry.init();\n      return defaultAppRegistry;\n    }\n\n    if( root[\"name\"].as<String>() == \"\"\n      || root[\"description\"].as<String>() == \"\"\n      || root[\"url\"].as<String>() == \"\" ) {\n      log_w(\"%s\", \"Bad channel meta in JSON file\");\n      defaultAppRegistry.init();\n      return defaultAppRegistry;\n    }\n\n    String SDUpdaterChannelNameStr    = \"\";\n    if( !root[\"pref_default_channel\"].isNull() && root[\"pref_default_channel\"].as<String>() != \"\" ) {\n      // inherit from json\n      SDUpdaterChannelNameStr = root[\"pref_default_channel\"].as<String>();\n    } else {\n      // assign default\n      SDUpdaterChannelNameStr = REGISTRY_MASTER;\n    }\n\n    AppRegistry appRegistry = {\n      root[\"name\"].as<String>(),\n      root[\"description\"].as<String>(),\n      root[\"url\"].as<String>(),\n      SDUpdaterChannelNameStr, // default channel\n      getJsonChannel( root[\"channels\"], REGISTRY_MASTER ),\n      getJsonChannel( root[\"channels\"], REGISTRY_UNSTABLE )\n    };\n    appRegistry.init();\n    return appRegistry;\n  }\n\n};\n"
  },
  {
    "path": "examples/AppStore/modules/Registry/Registry.hpp",
    "content": "/*\n *\n * M5Stack SD Menu\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2019 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n */\n\n// load the registry information this launcher is attached to\n#pragma once\n\n#include <ArduinoJson.h>\n#include \"../misc/config.h\"\n\n\n// registry\nclass AppRegistryItem\n{\n  public:\n    String name;\n    String description;\n    String url;\n    String api_host;\n    String api_path;\n    String api_cert_path;\n    String updater_path;\n    String catalog_endpoint;\n    String api_cert_provider_url_http;\n    String api_cert_provider_url_https;\n    String api_url_https;\n    String api_url_http;\n    void init() ;\n    void print();\n};\n\nclass AppRegistry\n{\n  public:\n    String name;\n    String description;\n    String url;\n    String pref_default_channel; // local option for SDUpdater use only\n    AppRegistryItem masterChannel;\n    AppRegistryItem unstableChannel;\n    AppRegistryItem defaultChannel;\n    void init();\n    void print();\n};\n\n\nnamespace RegistryUtils\n{\n  const String appRegistryFolder = \"/.registry\";\n  const String appRegistryDefaultName = \"default.json\";\n  void setJsonChannelItem( JsonObject json, AppRegistryItem reg );\n  void registrySave( AppRegistry registry, String appRegistryLocalFile = \"\" );\n  bool isValidRegistryChannel( JsonVariant json );\n  AppRegistryItem getJsonChannel( JsonVariant jsonChannels, const char* channel );\n  AppRegistry init( String appRegistryLocalFile = \"\" );\n\n  AppRegistryItem defaultMasterChannel = {\n    REGISTRY_MASTER, // \"master\"\n    DEFAULT_MASTER_DESC,\n    DEFAULT_MASTER_URL,\n    DEFAULT_MASTER_API_HOST,\n    DEFAULT_MASTER_API_PATH,\n    DEFAULT_MASTER_API_CERT_PATH,\n    DEFAULT_MASTER_UPDATER_PATH,\n    DEFAULT_MASTER_CATALOG_ENDPOINT\n  };\n\n  AppRegistryItem defaultUnstableChannel = {\n    REGISTRY_UNSTABLE, // \"unstable\"\n    DEFAULT_UNSTABLE_DESC,\n    DEFAULT_UNSTABLE_URL,\n    DEFAULT_UNSTABLE_API_HOST,\n    DEFAULT_UNSTABLE_API_PATH,\n    DEFAULT_UNSTABLE_API_CERT_PATH,\n    DEFAULT_UNSTABLE_UPDATER_PATH,\n    DEFAULT_UNSTABLE_CATALOG_ENDPOINT\n  };\n\n  AppRegistry defaultAppRegistry = {\n    DEFAULT_REGISTRY_NAME,\n    DEFAULT_REGISTRY_DESC,\n    DEFAULT_REGISTRY_URL,\n    DEFAULT_REGISTRY_CHANNEL,\n    defaultMasterChannel,\n    defaultUnstableChannel\n  };\n\n};\n\n"
  },
  {
    "path": "examples/AppStore/modules/misc/compile_time.h",
    "content": "/*\n * compile_time.h\n *\n * Created: 30.05.2017 20:57:58\n *  Author: Dennis (instructable.com/member/nqtronix)\n *\n * This code provides the macro __TIME_UNIX__ which returns the current time in UNIX format. It can\n * be used to identify a version of code on an embedded device, to initialize its RTC and much more.\n * Along that several more constants for seconds, minutes, etc. are provided\n *\n * The macro is based on __TIME__ and __DATE__, which are assumed to be formatted \"HH:MM:SS\" and\n * \"MMM DD YYYY\", respectively. The actual value can be calculated by the C compiler at compile time\n * as all inputs are literals. MAKE SURE TO ENABLE OPTIMISATION!\n */\n\n#pragma once\n\n#include <sys/time.h>\n\n\n// extracts 1..4 characters from a string and interprets it as a decimal value\n#define CONV_STR2DEC_1(str, i)  (str[i]>'0'?str[i]-'0':0)\n#define CONV_STR2DEC_2(str, i)  (CONV_STR2DEC_1(str, i)*10 + str[i+1]-'0')\n#define CONV_STR2DEC_3(str, i)  (CONV_STR2DEC_2(str, i)*10 + str[i+2]-'0')\n#define CONV_STR2DEC_4(str, i)  (CONV_STR2DEC_3(str, i)*10 + str[i+3]-'0')\n\n// Some definitions for calculation\n#define SEC_PER_MIN             60UL\n#define SEC_PER_HOUR            3600UL\n#define SEC_PER_DAY             86400UL\n#define SEC_PER_YEAR            (SEC_PER_DAY*365)\n#define UNIX_START_YEAR         1970UL\n\n// Custom \"glue logic\" to convert the month name to a usable number\n#define GET_MONTH(str, i)      (str[i]=='J' && str[i+1]=='a' && str[i+2]=='n' ? 1 :     \\\n                                str[i]=='F' && str[i+1]=='e' && str[i+2]=='b' ? 2 :     \\\n                                str[i]=='M' && str[i+1]=='a' && str[i+2]=='r' ? 3 :     \\\n                                str[i]=='A' && str[i+1]=='p' && str[i+2]=='r' ? 4 :     \\\n                                str[i]=='M' && str[i+1]=='a' && str[i+2]=='y' ? 5 :     \\\n                                str[i]=='J' && str[i+1]=='u' && str[i+2]=='n' ? 6 :     \\\n                                str[i]=='J' && str[i+1]=='u' && str[i+2]=='l' ? 7 :     \\\n                                str[i]=='A' && str[i+1]=='u' && str[i+2]=='g' ? 8 :     \\\n                                str[i]=='S' && str[i+1]=='e' && str[i+2]=='p' ? 9 :     \\\n                                str[i]=='O' && str[i+1]=='c' && str[i+2]=='t' ? 10 :    \\\n                                str[i]=='N' && str[i+1]=='o' && str[i+2]=='v' ? 11 :    \\\n                                str[i]=='D' && str[i+1]=='e' && str[i+2]=='c' ? 12 : 0)\n\n#define GET_MONTH2DAYS(month)  ((month == 1 ? 0 : 31 +                      \\\n                                (month == 2 ? 0 : 28 +                      \\\n                                (month == 3 ? 0 : 31 +                      \\\n                                (month == 4 ? 0 : 30 +                      \\\n                                (month == 5 ? 0 : 31 +                      \\\n                                (month == 6 ? 0 : 30 +                      \\\n                                (month == 7 ? 0 : 31 +                      \\\n                                (month == 8 ? 0 : 31 +                      \\\n                                (month == 9 ? 0 : 30 +                      \\\n                                (month == 10 ? 0 : 31 +                     \\\n                                (month == 11 ? 0 : 30))))))))))))           \\\n\n\n#define GET_LEAP_DAYS           ((__TIME_YEARS__-1968)/4 - (__TIME_MONTH__ <=2 ? 1 : 0))\n\n\n\n#define __TIME_SECONDS__        CONV_STR2DEC_2(__TIME__, 6)\n#define __TIME_MINUTES__        CONV_STR2DEC_2(__TIME__, 3)\n#define __TIME_HOURS__          CONV_STR2DEC_2(__TIME__, 0)\n#define __TIME_DAYS__           CONV_STR2DEC_2(__DATE__, 4)\n#define __TIME_MONTH__          GET_MONTH(__DATE__, 0)\n#define __TIME_YEARS__          CONV_STR2DEC_4(__DATE__, 7)\n\n#define __TIME_UNIX__         ((__TIME_YEARS__-UNIX_START_YEAR)*SEC_PER_YEAR+       \\\n                                GET_LEAP_DAYS*SEC_PER_DAY+                          \\\n                                GET_MONTH2DAYS(__TIME_MONTH__)*SEC_PER_DAY+         \\\n                                __TIME_DAYS__*SEC_PER_DAY-SEC_PER_DAY+              \\\n                                __TIME_HOURS__*SEC_PER_HOUR+                        \\\n                                __TIME_MINUTES__*SEC_PER_MIN+                       \\\n                                __TIME_SECONDS__)\n\n"
  },
  {
    "path": "examples/AppStore/modules/misc/config.h",
    "content": "#pragma once\n\n#include \"core.h\"\n#include <vector>\n#include <M5StackUpdater.h>\n\n#define SD_CERT_PATH \"/cert\" // Filesystem (SD) temporary path where certificates are stored. without trailing slash\n#define ROOT_DIR    \"/\"\n#define CATALOG_DIR \"/catalog\"\n#define CATALOG_DIR_BKP \"/catalog.old\"\n#define HIDDEN_APPS_FILE \"/.hidden-apps.json\"\n\n#define REGISTRY_MASTER \"master\"\n#define REGISTRY_UNSTABLE \"unstable\"\n\n#define PATH_SEPARATOR \"/\"\n#define EXT_bin        \".bin\"\n#define EXT_json       \".json\"\n#define EXT_jpg        \".jpg\"\n#define EXT_png        \".png\"\n\n#define DIR_jpg  \"/jpg/\"\n#define DIR_png  \"/png/\"\n#define DIR_json \"/json/\"\n\n#define DEFAULT_REGISTRY_NAME \"SDUpdater\"\n#define DEFAULT_REGISTRY_DESC \"Tobozo's \" PLATFORM_NAME \" Application registry @ phpsecu.re\"\n#define DEFAULT_REGISTRY_URL \"https://phpsecu.re/\" DEFAULT_REGISTRY_BOARD \"/registry/phpsecu.re.json\" // should exist as \"/.registry/default.json\" on SD Card\n#define DEFAULT_REGISTRY_CHANNEL \"unstable\" // \"master\" or \"unstable\"\n\n#define DEFAULT_MASTER_DESC \"Master channel at phpsecu.re/\" DEFAULT_REGISTRY_BOARD \" registry\"\n#define DEFAULT_MASTER_URL \"https://phpsecu.re/\" DEFAULT_REGISTRY_BOARD \"/sd-updater/\"\n#define DEFAULT_MASTER_API_HOST \"phpsecu.re\"\n#define DEFAULT_MASTER_API_PATH PATH_SEPARATOR DEFAULT_REGISTRY_BOARD\n#define DEFAULT_MASTER_API_CERT_PATH \"/cert/\"\n#define DEFAULT_MASTER_UPDATER_PATH \"/sd-updater\"\n#define DEFAULT_MASTER_CATALOG_ENDPOINT \"/catalog.json\"\n\n#define DEFAULT_UNSTABLE_DESC \"Unstable channel at phpsecu.re/\" DEFAULT_REGISTRY_BOARD \" registry\"\n#define DEFAULT_UNSTABLE_URL \"https://phpsecu.re/\" DEFAULT_REGISTRY_BOARD \"/sd-updater/unstable/\"\n#define DEFAULT_UNSTABLE_API_HOST \"phpsecu.re\"\n#define DEFAULT_UNSTABLE_API_PATH PATH_SEPARATOR DEFAULT_REGISTRY_BOARD\n#define DEFAULT_UNSTABLE_API_CERT_PATH \"/cert/\"\n#define DEFAULT_UNSTABLE_UPDATER_PATH \"/sd-updater/unstable\"\n#define DEFAULT_UNSTABLE_CATALOG_ENDPOINT \"/catalog.json\"\n\n#define LIST_MAX_COUNT      96\n\n#define MENU_TITLE_MAX_SIZE 24\n#define BTN_TITLE_MAX_SIZE  6\n\n#define LIST_MAX_LABEL_SIZE 36 // list labels will be trimmed\n#define LINES_PER_PAGE      8\n\n#if !defined BUTTON_HEIGHT\n  #define BUTTON_HEIGHT   28\n#endif\n\n#if !defined BUTTON_WIDTH\n  #define BUTTON_WIDTH  60\n#endif\n\n#if !defined BUTTON_HWIDTH\n  #define BUTTON_HWIDTH BUTTON_WIDTH/2\n#endif\n\n#define TITLEBAR_HEIGHT  32\n#define WINDOW_MARGINX   10\n#define LISTITEM_OFFSETX 15\n#define LISTITEM_OFFSETY 42\n#define LISTITEM_HEIGHT  20\n#define LISTCAPTION_POSY 38\n#define ASSET_POSY       62\n\n#define BUTTONS_COUNT 3\n\n\n#define MAX_BRIGHTNESS 100\n\n#define MS_BEFORE_SLEEP 600000 // 600000 = 10mn\n\nconst uint32_t MENU_COLOR     = 0x008000U; // must be dark (0x00) on two colors\nconst uint32_t BG_COLOR       = 0x000000U; // default bgcolor when nothing is drawn (e.g. behind the buttons)\nconst uint32_t TEXT_COLOR     = 0xffffffU; // text color, must have high contrast compared to MENU_COLOR\nconst uint32_t DIMMED_COLOR   = 0xaaaaaaU; // text color, must have high contrast compared to MENU_COLOR\nconst uint32_t LAUNCHER_COLOR = 0xcccc00U; // text color, must have high contrast compared to MENU_COLOR\nconst uint32_t SHADOW_COLOR   = 0x202020U; // shadow color, must be a shaded version of TEXT_COLOR\n\nconst uint32_t GZ_PROGRESS_COLOR   = 0x40bb40U; // uint32_t ProgressBarColor1 (gzip)\nconst uint32_t TAR_PROGRESS_COLOR  = 0xbbbb40U; // uint32_t ProgressBarColor2 (tar)\nconst uint32_t DL_PROGRESS_COLOR   = 0xff0000U; // uint32_t ProgressBarColor3 (download/sha_sum)\n\nuint16_t buttonsXOffset[BUTTONS_COUNT] =\n{\n  31, 126, 221\n};\n\n#if defined USE_SCREENSHOT\n  static bool ScreenShotEnable = true;\n#else\n  static bool ScreenShotEnable = false;\n#endif\n\n\n#undef FS_CAN_CREATE_PATH\n\n#include \"esp32-hal-log.h\"\n\n#if defined ESP_ARDUINO_VERSION_VAL\n  #if __has_include(\"core_version.h\") // for platformio\n    #include \"core_version.h\"\n  #endif\n\n  #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2,0,1) || ARDUINO_ESP32_GIT_VER == 0x15bbd0a1 || ARDUINO_ESP32_GIT_VER == 0xd218e58f || ARDUINO_ESP32_GIT_VER == 0xcaef4006\n    // #pragma message \"Filesystem can create subfolders on file creation\"\n    #define FS_CAN_CREATE_PATH\n  #endif\n\n  #if ARDUINO_ESP32_GIT_VER == 0x15bbd0a1\n    //#pragma message \"ESP32 Arduino 2.0.1 RC1 (0x15bbd0a1) is only partially supported\"\n\n  #elif ARDUINO_ESP32_GIT_VER == 0xd218e58f\n    //#pragma message \"ESP32 Arduino 2.0.1 (0xd218e58f) has OTA support broken!!\"\n\n  #elif ARDUINO_ESP32_GIT_VER == 0xcaef4006\n    // readRAW() / writeRAW() / numSectors() / sectorSize() support\n    //#pragma message \"ESP32 Arduino 2.0.2 (0xcaef4006) has SD support broken!!\"\n\n  #else\n    // unknown but probably 2.0.0\n    //#pragma message \"ESP32 Arduino 2.x.x (unknown)\"\n    #undef FS_CAN_CREATE_PATH\n  #endif\n#endif\n\n"
  },
  {
    "path": "examples/AppStore/modules/misc/controls.h",
    "content": "#pragma once\n\n/*\n * Mandatory and optional controls for the menu to be usable\n */\nenum HIDSignal\n{\n  HID_INERT      = 0, // when nothing happens\n  HID_UP         = 1, // optional\n  HID_DOWN       = 2,\n  HID_SELECT     = 3,\n  HID_PAGE_DOWN  = 4,\n  HID_PAGE_UP    = 5,\n  HID_SCREENSHOT = 6\n};\n\n#define HID_BTN_A HID_SELECT\n#define HID_BTN_B HID_PAGE_DOWN\n#define HID_BTN_C HID_DOWN\n\n#define FAST_REPEAT_DELAY 50 // ms, push delay\n#define SLOW_REPEAT_DELAY 500 // ms, must be higher than FAST_REPEAT_DELAY and smaller than LONG_DELAY_BEFORE_REPEAT\n#define LONG_DELAY_BEFORE_REPEAT 1000 // ms, delay before slow repeat enables\nunsigned long fastRepeatDelay = FAST_REPEAT_DELAY;\nunsigned long beforeRepeatDelay = LONG_DELAY_BEFORE_REPEAT;\n\n\n#if defined ARDUINO_M5STACK_Core2\n  // enable M5Core2's haptic feedback !\n  static bool isVibrating = false;\n\n  static void vibrateTask( void * param )\n  {\n    if( !isVibrating ) {\n      isVibrating = true;\n      int ms = *((int*)param); // dafuq\n      M5.Axp.SetLDOEnable( 3,1 );\n      delay( ms );\n      M5.Axp.SetLDOEnable( 3,0 );\n      isVibrating = false;\n    }\n    vTaskDelete( NULL );\n  }\n\n  static void HIDFeedback( int ms )\n  {\n    // xTaskCreatePinnedToCore( vibrateTask, \"vibrateTask\", 2048, (void*)&ms, 1, NULL , 1 );\n  }\n\n#else\n\n  static void HIDFeedback( int ms ) { ;  }\n\n#endif\n\n\n\nHIDSignal HIDFeedback( HIDSignal signal, int ms = 50 )\n{\n  if( signal != HID_INERT ) {\n    HIDFeedback( ms );\n  }\n  return signal;\n}\n\n\nHIDSignal getControls()\n{\n  // no buttons? no problemo! (c) Arnold S.\n  if( Serial.available() ) {\n    char command = Serial.read(); // read one char\n    Serial.flush();\n    switch(command) {\n      case 'a':Serial.println(\"Sending HID_DOWN Signal\");      return HIDFeedback( HID_DOWN );\n      case 'b':Serial.println(\"Sending HID_UP Signal\");        return HIDFeedback( HID_UP );\n      case 'c':Serial.println(\"Sending HID_PAGE_DOWN Signal\"); return HIDFeedback( HID_PAGE_DOWN );\n      case 'd':Serial.println(\"Sending HID_PAGE_UP Signal\");   return HIDFeedback( HID_PAGE_UP );\n      case 'e':Serial.println(\"Sending HID_SCREENSHOT Signal\");return HIDFeedback( HID_SCREENSHOT );\n      case 'f':Serial.println(\"Sending HID_SELECT Signal\");    return HIDFeedback( HID_SELECT );\n      default: if( command != '\\n' ) { Serial.print(\"Ignoring serial input: \");Serial.println( String(command) ); }\n    }\n  }\n  M5.update();\n\n  // legacy buttons support\n  bool a = M5.BtnA.wasPressed() || M5.BtnA.pressedFor( 500 );\n  bool b = M5.BtnB.wasPressed() || M5.BtnB.pressedFor( 500 );\n  bool c = M5.BtnC.wasPressed() || M5.BtnC.pressedFor( 500 );\n  //bool d = ( M5.BtnB.wasPressed() && M5.BtnC.isPressed() );\n  //bool e = ( M5.BtnB.isPressed() && M5.BtnC.wasPressed() );\n\n  //if( d || e ) return HIDFeedback( HID_PAGE_UP ); // multiple push, suggested by https://github.com/mongonta0716\n  if( b ) return HIDFeedback( HID_PAGE_DOWN );\n  if( c ) return HIDFeedback( HID_DOWN );\n  if( a ) return HIDFeedback( HID_SELECT );\n  //HIDSignal padValue = HID_INERT;\n  return HID_INERT;\n}\n"
  },
  {
    "path": "examples/AppStore/modules/misc/core.h",
    "content": "#pragma once\n\n#include <ESP32-Chimera-Core.h> // https://github.com/tobozo/ESP32-Chimera-Core\n\n#define SDU_APP_NAME   \"M5Stack App Store\" // app title for the sd-updater lobby screen\n#define SDU_APP_PATH   \"/AppStore.bin\"     // app binary file name on the SD Card (also displayed on the sd-updater lobby screen)\n#define SDU_APP_AUTHOR \"@tobozo\"           // app binary author name for the sd-updater lobby screen\n#include <M5StackUpdater.h>  // https://github.com/tobozo/M5Stack-SD-Updater\n\n#define DEST_FS_USES_SD      // -> This instance will be ecompressing to the SDCard\n#include <ESP32-targz.h>     // https://github.com/tobozo/ESP32-targz\n\n#include <nvs.h> // use to store some prefs\n\n//#define USE_SCREENSHOT // keep this commented out unless you really need to take UI screenshots\n\n// auto-select board\n#if defined( ARDUINO_M5STACK_Core2 )\n  #pragma message \"M5Stack Core2 detected\"\n  #define PLATFORM_NAME \"M5Core2\"\n  #define DEFAULT_REGISTRY_BOARD \"m5core2\"\n#elif defined( ARDUINO_M5Stack_Core_ESP32 )\n  #pragma message \"M5Stack Classic detected\"\n  #define PLATFORM_NAME \"M5Stack\"\n  #define DEFAULT_REGISTRY_BOARD \"m5stack\"\n#elif defined( ARDUINO_M5STACK_FIRE )\n  #pragma message \"M5Stack Fire detected\"\n  #define PLATFORM_NAME \"M5Fire\"\n  #define DEFAULT_REGISTRY_BOARD \"m5fire\"\n#elif defined( ARDUINO_ODROID_ESP32 )\n  #pragma message \"Odroid Go detected\"\n  #define PLATFORM_NAME \"Odroid-GO\"\n  #define DEFAULT_REGISTRY_BOARD \"odroid\"\n#else\n  #pragma message \"Generic ESP32 detected\"\n  #define DEFAULT_REGISTRY_BOARD \"esp32\"\n  #define PLATFORM_NAME \"ESP32\"\n#endif\n\nstatic M5Display &tft( M5.Lcd );\nstatic fs::SDFS &M5_FS(SD);\n\nstatic char formatBuffer[64];\n\nstatic const char *formatBytes(long long bytes, char *str)\n{\n  const char *sizes[5] = { \"B\", \"KB\", \"MB\", \"GB\", \"TB\" };\n  int i;\n  double dblByte = bytes;\n  for (i = 0; i < 5 && bytes >= 1024; i++, bytes /= 1024)\n    dblByte = bytes / 1024.0;\n  sprintf(str, \"%.2f\", dblByte);\n  return strcat(strcat(str, \" \"), sizes[i]);\n}\n\n"
  },
  {
    "path": "examples/AppStore/modules/misc/i18n.h",
    "content": "#pragma once\n\n#define LANG_EN 0xff\n#define LANG_JP 0x12\n#define LANG_CN 0x07\n#define LANG_KR 0x14\n\n#define I18N_LANG LANG_EN\n\n#if I18N_LANG == LANG_CN\n\n  #if !defined ARDUINO_M5STACK_Core2 && !defined ARDUINO_M5STACK_FIRE\n    #error \"Chinese font require M5Core2 or M5Fire\"\n  #endif\n\n  #include \"lang/i18n.cn.h\"\n  #define ButtonFont     &efontCN_10\n  #define HeaderFont     &efontCN_10\n  #define LIFont         &efontCN_10\n  #define InfoWindowFont &efontCN_10\n\n#elif I18N_LANG == LANG_EN\n\n  #include \"lang/i18n.en.h\"\n  #define ButtonFont     &Font2\n  #define HeaderFont     &FreeMono9pt7b\n  #define LIFont         &Font2\n  #define InfoWindowFont &FreeMono9pt7b\n\n#elif I18N_LANG == LANG_JP\n\n  #include \"lang/i18n.jp.h\"\n  #define ButtonFont     &lgfxJapanGothic_12\n  #define HeaderFont     &lgfxJapanGothic_12\n  #define LIFont         &lgfxJapanGothic_12\n  #define InfoWindowFont &lgfxJapanGothic_12\n\n#elif I18N_LANG == LANG_KR\n\n  #include \"lang/i18n.kr.h\"\n  #define ButtonFont     &efontKR_10\n  #define HeaderFont     &efontKR_12_b\n  #define LIFont         &efontKR_10_b\n  #define InfoWindowFont &efontKR_12\n\n#else\n\n  #error \"Unsupported language\"\n\n#endif\n\n\n#define SCROLL_SEPARATOR \"   ***   \"\n"
  },
  {
    "path": "examples/AppStore/modules/misc/lang/i18n.cn.h",
    "content": "#define WELCOME_MESSAGE \"Welcome to the \" PLATFORM_NAME \" App Store!\"\n#define INIT_MESSAGE  PLATFORM_NAME \" App Store initializing...\"\n#define MENU_SETTINGS \"AppStoreUI loaded with %d labels per page, max %d items\\n\"\n#define GOTOSLEEP_MESSAGE \"Will go to sleep\"\n\n#define MENU_BTN_INFO     \"SELECT\"\n#define MENU_BTN_UPDATE   \"UPDATE\"\n#define MENU_BTN_BACK     \"Back\"\n#define MENU_BTN_PREV     \"<\"\n#define MENU_BTN_NEXT     \">\"\n#define MENU_BTN_YES      \"YES\"\n#define MENU_BTN_NO       \"NO\"\n#define MENU_BTN_GO       \"GO\"\n#define MENU_BTN_CANCEL   \"Cancel\"\n#define MENU_BTN_REBOOT   \"Reboot\"\n#define MENU_BTN_RESTART  \"Restart\"\n#define MENU_BTN_CONTINUE \"Continue\"\n#define MENU_BTN_RETRY    \"RETRY\"\n\n\n#define MENUTITLE_DEFAULT PLATFORM_NAME \" App Store\"\n#define MENUTITLE_MYAPPS \"My Applications\"\n#define MENUTITLE_APPSTORE \"Applications Store\"\n#define MENUTITLE_TOGGLEAPPS \"Unhide Apps\"\n#define MENUTITLE_DOWNLOADER \"Apps Downloader\"\n#define MENUTITLE_MANAGEAPPS \"Manage Applications\"\n#define MENUTITLE_MAINMENU   \"Main Menu\"\n\n#define MENUACTION_BACKTOAPPS \"Back to Browse Apps\"\n#define MENUACTION_APPINSTALL \"Install\"\n#define MENUACTION_APPUPDATE  \"Update\"\n#define MENUACTION_APPVERIFY  \"Verify\"\n#define MENUACTION_VIEW       \"View\"\n#define MENUACTION_APPHIDE    \"Hide\"\n#define MENUACTION_APPDELETE  \"Delete\"\n#define MENUACTION_BACK       \"Back\"\n#define MENUACTION_UNHIDE     \"Unhide\"\n#define MENUACTION_REFRESH    \"Refresh\"\n\n#define MENUACTION_NTP        \"NTP Server\"\n#define MENUACTION_MYAPPS     \"*My Applications\"\n#define MENUACTION_APPTOGGLE  \"Manage Hidden Apps\"\n#define MENUACTION_DOWNLOADER \"*Apps Downloader\"\n#define MENUACTION_BROWSEAPP  \"Browse App Store\"\n#define MENUACTION_DEL_APPS   \"Manage My Apps\"\n\n#define ROOTACTION_DOWNLOAD  \"Download Catalog\"\n#define ROOTACTION_REFRESH   \"Refresh Catalog\"\n#define ROOTACTION_GET       \"Install Applications\"\n\n#define ROOTACTION_SWITCH    \"Switch Channel\"\n#define ROOTACTION_NTP_PICK  \"Change NTP Server\"\n#define ROOTACTION_CLEAR_ALL \"Clear ALL\"\n#define ROOTACTION_CLEAR_TLS \"Clear TLS cache\"\n#define ROOTACTION_SLEEP     \"Sleep\"\n\n#define MODAL_DELETEALL_TITLE \"DELETE EVERYTHING\"\n#define MODAL_DELETEALL_MSG \"CAUTION! This will remove apps, assets, registries\\nand databases, even those\\noutside the scope of this\\nApplication Manager!\"\n\n#define MODAL_DOWNLOADFAIL_TITLE \"HTTP FAILED\"\n#define MODAL_DOWNLOADFAIL_MSG \"An HTTP error occured\\nwhile downloading\\nthe registry\\narchive.\\nTry again?\"\n\n#define MODAL_WIFI_NOCONN_MSG \"No connection\"\n\n#define DOWNLOADER_MODAL_CHANGE \"CHANGE\"\n\n#define MODAL_CANCELED_TITLE \"OPERATION CANCELED\"\n#define MODAL_SUCCESS_TITLE \"OPERATION SUCCESSFUL\"\n\n\n#define CHANNEL_TOOL \"CHANNEL TOOL\"\n#define CHANNEL_TOOL_TEXT \"Do you want to change or\\nupdate your SD Card\\nchannel?\"\n\n#define CHANNEL_CHOOSER \"CHANNEL CHOOSER\"\n#define CHANNEL_CHOOSER_PROMPT \"Change channel ?\"\n#define CHANNEL_CHOOSER_TEXT \"You are about to change\\nyour SD Card channel.\\n\\n    Are you sure ?\"\n\n#define CHANNEL_DOWNLOADER \"CHANNEL DOWNLOADER\"\n#define CHANNEL_DOWNLOADER_PROMPT \"Download channel ?\"\n#define CHANNEL_DOWNLOADER_TEXT \"You are about to overwrite\\nyour SD Card channel.\\r\\n    Are you sure ?\"\n\n#define DOWNLOADER_MODAL_NAME \"Update binaries ?\"\n#define DOWNLOADER_MODAL_TITLE \"This action will:\"\n#define DOWNLOADER_MODAL_ENDED \"Synchronization complete\"\n#define DOWNLOADER_MODAL_TITLE_ERRORS_OCCURED \"Some errors occured. \"\n\n#define OVERALL_PROGRESS_TITLE \"Overall progress\"\n#define TAR_PROGRESS_TITLE  \"Downloading Registry\"\n#define NOT_IN_REGISTRY \"NOT IN REGISTRY\"\n\n#define WGET_SKIPPING \" [Checksum OK]\"\n#define WGET_UPDATING \" [Outdated]\"\n#define WGET_CREATING \" [New file]\"\n#define SYNC_FINISHED \"Synch finished\"\n#define CLEANDIR_REMOVED \"Removed %s\\n\"\n#define DOWNLOAD_FAIL \" [DOWNLOAD FAIL]\"\n#define SHASHUM_FAIL \" [SHASUM FAIL]\"\n#define UPDATE_SUCCESS \"UPDATE SUCCESS\"\n\n#define WIFI_MSG_WAITING \"Enabling WiFi...\"\n#define WIFI_MSG_CONNECTING \"Connecting WiFi..\"\n#define WIFI_TITLE_TIMEOUT \"WiFi Timeout\"\n#define WIFI_MSG_TIMEOUT \"Timed out, will try again\"\n#define WIFI_TITLE_CONNECTED \"WiFi OK\"\n#define WIFI_MSG_CONNECTED \"Connected to wifi :-)\"\n\n#define NTP_TITLE_SETUP \"NTP Setup\"\n#define NTP_MSG_SETUP \"Contacting NTP Server\"\n#define NTP_TITLE_FAIL \"NTP Down\"\n#define NTP_MSG_FAIL \"Can't enable NTP\"\n\n\n#define DL_FSCLEANUP_TITLE \"Cleanup\"\n#define DL_FSCLEANUP_MSG \"Removing previous\\nbackup\"\n#define DL_FSBACKUP_TITLE \"Backup\"\n#define DL_FSBACKUP_MSG \"Backing up registry\"\n#define DL_FSRESTORE_MSG \"Restoring backup\"\n\n#define DL_TLSFETCH_TITLE \"TLS\"\n#define DL_TLSFETCH_MSG \"Fetching TLS Cert\"\n\n#define DL_TLSFAIL_TITLE \"TLS Error\"\n#define DL_TLSFAIL_MSG \"Could not init TLS\"\n\n#define DL_HTTPINIT_TITLE \"HTTP Init\"\n#define DL_HTTPINIT_MSG \"Contacting catalog\\nendpoint\"\n\n#define DL_HTTPFAIL_TITLE \"HTTP Error\"\n#define DL_FSFAIL_TITLE \"Filesystem Error\"\n#define DL_SHAFAIL_TITLE \"SHA256 Error\"\n\n#define DL_AWAITING_TITLE \"Awaiting response\"\n\n#define DL_SUCCESS_TITLE \"Success\"\n#define DL_SUCCESS_MSG \"Registry fetched!\"\n#define DL_FAIL_MSG \"Registry could not\\nbe reached.\"\n\n\n#define WGET_MSG_FAIL \"Please check the remote server\"\n#define FS_MSG_FAIL \"Please check the filesystem\"\n\n#define MODAL_TLSCERT_INSTALLFAILED_MSG \"Certificate fetching\\nOK but TLS Install\\nfailed\"\n#define MODAL_TLSCERT_FETCHINGFAILED_MSG \"Unable to wget()\\ncertificate\"\n#define NEW_TLS_CERTIFICATE_TITLE \"New TLS certificate installed\"\n#define NEW_TLS_CERTIFICATE_TEXT \"A new certificate\\nwas fetched and\\ninstalled successfully.\"\n#define MODAL_RESTART_REQUIRED \"Restarting is\\noptional.\\n\\nReboot anyway?\"\n#define MODAL_SAME_PLAYER_SHOOT_AGAIN \"Please try again.\\n\\nReboot now?\"\n#define MODAL_REGISTRY_UPDATED \"New Registry file\\nhas been updated\"\n#define MODAL_REGISTRY_DAMAGED \"New Registry file\\nmay be damaged\"\n#define MODAL_REBOOT_REGISTRY_UPDATED \"Please reboot and\\nchoose a channel.\\n\\nReboot now?\"\n\n\n#define DEBUG_DIROPEN_FAILED \"Failed to open directory\"\n#define DEBUG_EMPTY_FS \"Empty SD Card, falling back to root menu\"\n#define DEBUG_NOTADIR \"Not a directory\"\n#define DEBUG_DIRLABEL \"  DIR : \"\n#define DEBUG_IGNORED \"  IGNORED FILE: \"\n#define DEBUG_CLEANED \"  CLEANED FILE: \"\n#define DEBUG_ABORTLISTING \"  ***Max files reached for Menu, please adjust LIST_MAX_COUNT for more (maximum is 255, sorry :-)\"\n#define DEBUG_FILELABEL \"  FILE: \"\n\n#define DEBUG_FILECOPY \"Starting File Copy for \"\n#define DEBUG_FILECOPY_DONE \"Transfer finished\"\n#define DEBUG_WILL_RESTART \"Binary removed from SPIFFS, will now restart\"\n#define DEBUG_NOTHING_TODO \"No binary to transfer\"\n#define DEBUG_KEYPAD_NOTFOUND \"Keypad not installed\"\n#define DEBUG_KEYPAD_FOUND \"Keypad detected!\"\n#define DEBUG_JOYPAD_NOTFOUND \"No Joypad detected, disabling\"\n#define DEBUG_JOYPAD_FOUND \"Joypad detected!\"\n\n#define DEBUG_TIMESTAMP_GUESS \"%s has %s time set (%04d-%02d-%02d %02d:%02d:%02d), will use %s date to set the clock\"\n\n"
  },
  {
    "path": "examples/AppStore/modules/misc/lang/i18n.en.h",
    "content": "#define WELCOME_MESSAGE \"Welcome to the \" PLATFORM_NAME \" App Store!\"\n#define INIT_MESSAGE  PLATFORM_NAME \" App Store initializing...\"\n#define MENU_SETTINGS \"AppStoreUI loaded with %d labels per page, max %d items\\n\"\n#define GOTOSLEEP_MESSAGE \"Will go to sleep\"\n\n#define MENU_BTN_INFO     \"SELECT\"\n#define MENU_BTN_UPDATE   \"UPDATE\"\n#define MENU_BTN_BACK     \"Back\"\n#define MENU_BTN_PREV     \"<\"\n#define MENU_BTN_NEXT     \">\"\n#define MENU_BTN_YES      \"YES\"\n#define MENU_BTN_NO       \"NO\"\n#define MENU_BTN_GO       \"GO\"\n#define MENU_BTN_CANCEL   \"Cancel\"\n#define MENU_BTN_REBOOT   \"Reboot\"\n#define MENU_BTN_RESTART  \"Restart\"\n#define MENU_BTN_CONTINUE \"Continue\"\n#define MENU_BTN_RETRY    \"RETRY\"\n\n\n#define MENUTITLE_DEFAULT PLATFORM_NAME \" App Store\"\n#define MENUTITLE_MYAPPS \"My Applications\"\n#define MENUTITLE_APPSTORE \"Applications Store\"\n#define MENUTITLE_TOGGLEAPPS \"Unhide Apps\"\n#define MENUTITLE_DOWNLOADER \"Apps Downloader\"\n#define MENUTITLE_MANAGEAPPS \"Manage Applications\"\n#define MENUTITLE_MAINMENU   \"Main Menu\"\n\n#define MENUACTION_BACKTOAPPS \"Back to Browse Apps\"\n#define MENUACTION_APPINSTALL \"Install\"\n#define MENUACTION_APPUPDATE  \"Update\"\n#define MENUACTION_APPVERIFY  \"Verify\"\n#define MENUACTION_VIEW       \"View\"\n#define MENUACTION_APPHIDE    \"Hide\"\n#define MENUACTION_APPDELETE  \"Delete\"\n#define MENUACTION_BACK       \"Back\"\n#define MENUACTION_UNHIDE     \"Unhide\"\n#define MENUACTION_REFRESH    \"Refresh\"\n\n#define MENUACTION_NTP        \"NTP Server\"\n#define MENUACTION_MYAPPS     \"*My Applications\"\n#define MENUACTION_APPTOGGLE  \"Manage Hidden Apps\"\n#define MENUACTION_DOWNLOADER \"*Apps Downloader\"\n#define MENUACTION_BROWSEAPP  \"Browse App Store\"\n#define MENUACTION_DEL_APPS   \"Manage My Apps\"\n\n#define ROOTACTION_DOWNLOAD  \"Download Catalog\"\n#define ROOTACTION_REFRESH   \"Refresh Catalog\"\n#define ROOTACTION_GET       \"Install Applications\"\n\n#define ROOTACTION_SWITCH    \"Switch Channel\"\n#define ROOTACTION_NTP_PICK  \"Change NTP Server\"\n#define ROOTACTION_CLEAR_ALL \"Clear ALL\"\n#define ROOTACTION_CLEAR_TLS \"Clear TLS cache\"\n#define ROOTACTION_SLEEP     \"Sleep\"\n\n#define MODAL_DELETEALL_TITLE \"DELETE EVERYTHING\"\n#define MODAL_DELETEALL_MSG \"CAUTION! This will remove apps, assets, registries\\nand databases, even those\\noutside the scope of this\\nApplication Manager!\"\n\n#define MODAL_DOWNLOADFAIL_TITLE \"HTTP FAILED\"\n#define MODAL_DOWNLOADFAIL_MSG \"An HTTP error occured\\nwhile downloading\\nthe registry\\narchive.\\nTry again?\"\n\n#define MODAL_WIFI_NOCONN_MSG \"No connection\"\n\n#define DOWNLOADER_MODAL_CHANGE \"CHANGE\"\n\n#define MODAL_CANCELED_TITLE \"OPERATION CANCELED\"\n#define MODAL_SUCCESS_TITLE \"OPERATION SUCCESSFUL\"\n\n\n#define CHANNEL_TOOL \"CHANNEL TOOL\"\n#define CHANNEL_TOOL_TEXT \"Do you want to change or\\nupdate your SD Card channel?\"\n\n#define CHANNEL_CHOOSER \"CHANNEL CHOOSER\"\n#define CHANNEL_CHOOSER_PROMPT \"Change channel ?\"\n#define CHANNEL_CHOOSER_TEXT \"You are about to change\\nyour SD Card channel.\\n\\n    Are you sure ?\"\n\n#define CHANNEL_DOWNLOADER \"CHANNEL DOWNLOADER\"\n#define CHANNEL_DOWNLOADER_PROMPT \"Download channel ?\"\n#define CHANNEL_DOWNLOADER_TEXT \"You are about to overwrite\\nyour SD Card channel.\\r\\n    Are you sure ?\"\n\n#define DOWNLOADER_MODAL_NAME \"Update binaries ?\"\n#define DOWNLOADER_MODAL_TITLE \"This action will:\"\n#define DOWNLOADER_MODAL_ENDED \"Synchronization complete\"\n#define DOWNLOADER_MODAL_TITLE_ERRORS_OCCURED \"Some errors occured. \"\n\n#define OVERALL_PROGRESS_TITLE \"Overall progress\"\n#define TAR_PROGRESS_TITLE  \"Downloading Registry\"\n#define NOT_IN_REGISTRY \"NOT IN REGISTRY\"\n\n#define WGET_SKIPPING \" [Checksum OK]\"\n#define WGET_UPDATING \" [Outdated]\"\n#define WGET_CREATING \" [New file]\"\n#define SYNC_FINISHED \"Synch finished\"\n#define CLEANDIR_REMOVED \"Removed %s\\n\"\n#define DOWNLOAD_FAIL \" [DOWNLOAD FAIL]\"\n#define SHASHUM_FAIL \" [SHASUM FAIL]\"\n#define UPDATE_SUCCESS \"UPDATE SUCCESS\"\n\n#define WIFI_MSG_WAITING \"Enabling WiFi...\"\n#define WIFI_MSG_CONNECTING \"Connecting WiFi..\"\n#define WIFI_TITLE_TIMEOUT \"WiFi Timeout\"\n#define WIFI_MSG_TIMEOUT \"Timed out, will try again\"\n#define WIFI_TITLE_CONNECTED \"WiFi OK\"\n#define WIFI_MSG_CONNECTED \"Connected to wifi :-)\"\n\n#define NTP_TITLE_SETUP \"NTP Setup\"\n#define NTP_MSG_SETUP \"Contacting NTP Server\"\n#define NTP_TITLE_FAIL \"NTP Down\"\n#define NTP_MSG_FAIL \"Can't enable NTP\"\n\n\n#define DL_FSCLEANUP_TITLE \"Cleanup\"\n#define DL_FSCLEANUP_MSG \"Removing previous\\nbackup\"\n#define DL_FSBACKUP_TITLE \"Backup\"\n#define DL_FSBACKUP_MSG \"Backing up registry\"\n#define DL_FSRESTORE_MSG \"Restoring backup\"\n\n#define DL_TLSFETCH_TITLE \"TLS\"\n#define DL_TLSFETCH_MSG \"Fetching TLS Cert\"\n\n#define DL_TLSFAIL_TITLE \"TLS Error\"\n#define DL_TLSFAIL_MSG \"Could not init TLS\"\n\n#define DL_HTTPINIT_TITLE \"HTTP Init\"\n#define DL_HTTPINIT_MSG \"Contacting catalog\\nendpoint\"\n\n#define DL_HTTPFAIL_TITLE \"HTTP Error\"\n#define DL_FSFAIL_TITLE \"Filesystem Error\"\n#define DL_SHAFAIL_TITLE \"SHA256 Error\"\n\n#define DL_AWAITING_TITLE \"Awaiting response\"\n\n#define DL_SUCCESS_TITLE \"Success\"\n#define DL_SUCCESS_MSG \"Registry fetched!\"\n#define DL_FAIL_MSG \"Registry could not\\nbe reached.\"\n\n\n#define WGET_MSG_FAIL \"Please check the remote server\"\n#define FS_MSG_FAIL \"Please check the filesystem\"\n\n#define MODAL_TLSCERT_INSTALLFAILED_MSG \"Certificate fetching\\nOK but TLS Install\\nfailed\"\n#define MODAL_TLSCERT_FETCHINGFAILED_MSG \"Unable to wget()\\ncertificate\"\n#define NEW_TLS_CERTIFICATE_TITLE \"New TLS certificate installed\"\n#define NEW_TLS_CERTIFICATE_TEXT \"A new certificate\\nwas fetched and\\ninstalled successfully.\"\n#define MODAL_RESTART_REQUIRED \"Restarting is\\noptional.\\n\\nReboot anyway?\"\n#define MODAL_SAME_PLAYER_SHOOT_AGAIN \"Please try again.\\n\\nReboot now?\"\n#define MODAL_REGISTRY_UPDATED \"New Registry file\\nhas been updated\"\n#define MODAL_REGISTRY_DAMAGED \"New Registry file\\nmay be damaged\"\n#define MODAL_REBOOT_REGISTRY_UPDATED \"Please reboot and\\nchoose a channel.\\n\\nReboot now?\"\n\n\n#define DEBUG_DIROPEN_FAILED \"Failed to open directory\"\n#define DEBUG_EMPTY_FS \"Empty SD Card, falling back to root menu\"\n#define DEBUG_NOTADIR \"Not a directory\"\n#define DEBUG_DIRLABEL \"  DIR : \"\n#define DEBUG_IGNORED \"  IGNORED FILE: \"\n#define DEBUG_CLEANED \"  CLEANED FILE: \"\n#define DEBUG_ABORTLISTING \"  ***Max files reached for Menu, please adjust LIST_MAX_COUNT for more (maximum is 255, sorry :-)\"\n#define DEBUG_FILELABEL \"  FILE: \"\n\n#define DEBUG_FILECOPY \"Starting File Copy for \"\n#define DEBUG_FILECOPY_DONE \"Transfer finished\"\n#define DEBUG_WILL_RESTART \"Binary removed from SPIFFS, will now restart\"\n#define DEBUG_NOTHING_TODO \"No binary to transfer\"\n#define DEBUG_KEYPAD_NOTFOUND \"Keypad not installed\"\n#define DEBUG_KEYPAD_FOUND \"Keypad detected!\"\n#define DEBUG_JOYPAD_NOTFOUND \"No Joypad detected, disabling\"\n#define DEBUG_JOYPAD_FOUND \"Joypad detected!\"\n\n#define DEBUG_TIMESTAMP_GUESS \"%s has %s time set (%04d-%02d-%02d %02d:%02d:%02d), will use %s date to set the clock\"\n\n"
  },
  {
    "path": "examples/AppStore/modules/misc/lang/i18n.jp.h",
    "content": "#define WELCOME_MESSAGE \"Welcome to the \" PLATFORM_NAME \" App Store!\"\n#define INIT_MESSAGE  PLATFORM_NAME \" App Store initializing...\"\n#define MENU_SETTINGS \"AppStoreUI loaded with %d labels per page, max %d items\\n\"\n#define GOTOSLEEP_MESSAGE \"Will go to sleep\"\n\n#define MENU_BTN_INFO     \"撰ぶ\" // \"SELECT\"\n#define MENU_BTN_UPDATE   \"UPDATE\"\n#define MENU_BTN_BACK     \"Back\"\n#define MENU_BTN_PREV     \"<\"\n#define MENU_BTN_NEXT     \">\"\n#define MENU_BTN_YES      \"YES\"\n#define MENU_BTN_NO       \"NO\"\n#define MENU_BTN_GO       \"GO\"\n#define MENU_BTN_CANCEL   \"Cancel\"\n#define MENU_BTN_REBOOT   \"Reboot\"\n#define MENU_BTN_RESTART  \"Restart\"\n#define MENU_BTN_CONTINUE \"Continue\"\n#define MENU_BTN_RETRY    \"RETRY\"\n\n\n#define MENUTITLE_DEFAULT PLATFORM_NAME \" アプリストア\" // \" App Store\"\n#define MENUTITLE_MYAPPS \"My Applications\"\n#define MENUTITLE_APPSTORE \"Applications Store\"\n#define MENUTITLE_TOGGLEAPPS \"Unhide Apps\"\n#define MENUTITLE_DOWNLOADER \"Apps Downloader\"\n#define MENUTITLE_MANAGEAPPS \"アプリケーションの管理\" // \"Manage Applications\"\n#define MENUTITLE_MAINMENU   \"Main Menu\"\n\n#define MENUACTION_BACKTOAPPS \"Back to Browse Apps\"\n#define MENUACTION_APPINSTALL \"Install\"\n#define MENUACTION_APPUPDATE  \"Update\"\n#define MENUACTION_APPVERIFY  \"Verify\"\n#define MENUACTION_VIEW       \"View\"\n#define MENUACTION_APPHIDE    \"Hide\"\n#define MENUACTION_APPDELETE  \"Delete\"\n#define MENUACTION_BACK       \"Back\"\n#define MENUACTION_UNHIDE     \"Unhide\"\n#define MENUACTION_REFRESH    \"Refresh\"\n\n#define MENUACTION_NTP        \"NTP Server\"\n#define MENUACTION_MYAPPS     \"*My Applications\"\n#define MENUACTION_APPTOGGLE  \"Manage Hidden Apps\"\n#define MENUACTION_DOWNLOADER \"*Apps Downloader\"\n#define MENUACTION_BROWSEAPP  \"Browse App Store\"\n#define MENUACTION_DEL_APPS   \"Manage My Apps\"\n\n#define ROOTACTION_DOWNLOAD  \"Download Catalog\"\n#define ROOTACTION_REFRESH   \"カタログを更新\" // \"Refresh Catalog\"\n#define ROOTACTION_GET       \"Install Applications\"\n\n#define ROOTACTION_SWITCH    \"チャンネルの切り替え\" // \"Switch Channel\"\n#define ROOTACTION_NTP_PICK  \"NTPサーバーを変更する\" // \"Change NTP Server\"\n#define ROOTACTION_CLEAR_ALL \"すべてクリア\" // \"Clear ALL\"\n#define ROOTACTION_CLEAR_TLS \"TLSキャッシュをクリアする\" // \"Clear TLS cache\"\n#define ROOTACTION_SLEEP     \"寝る\" // \"Sleep\"\n\n#define MODAL_DELETEALL_TITLE \"DELETE EVERYTHING\"\n#define MODAL_DELETEALL_MSG \"CAUTION! This will remove apps, assets, registries\\nand databases, even those\\noutside the scope of this\\nApplication Manager!\"\n\n#define MODAL_DOWNLOADFAIL_TITLE \"HTTP FAILED\"\n#define MODAL_DOWNLOADFAIL_MSG \"An HTTP error occured\\nwhile downloading\\nthe registry\\narchive.\\nTry again?\"\n\n#define MODAL_WIFI_NOCONN_MSG \"No connection\"\n\n#define DOWNLOADER_MODAL_CHANGE \"CHANGE\"\n\n#define MODAL_CANCELED_TITLE \"OPERATION CANCELED\"\n#define MODAL_SUCCESS_TITLE \"OPERATION SUCCESSFUL\"\n\n\n#define CHANNEL_TOOL \"CHANNEL TOOL\"\n#define CHANNEL_TOOL_TEXT \"Do you want to change or\\nupdate your SD Card\\nchannel?\"\n\n#define CHANNEL_CHOOSER \"CHANNEL CHOOSER\"\n#define CHANNEL_CHOOSER_PROMPT \"Change channel ?\"\n#define CHANNEL_CHOOSER_TEXT \"You are about to change\\nyour SD Card channel.\\n\\n    Are you sure ?\"\n\n#define CHANNEL_DOWNLOADER \"CHANNEL DOWNLOADER\"\n#define CHANNEL_DOWNLOADER_PROMPT \"Download channel ?\"\n#define CHANNEL_DOWNLOADER_TEXT \"You are about to overwrite\\nyour SD Card channel.\\n\\n    Are you sure ?\"\n\n#define DOWNLOADER_MODAL_NAME \"Update binaries ?\"\n#define DOWNLOADER_MODAL_TITLE \"This action will:\"\n#define DOWNLOADER_MODAL_ENDED \"Synchronization complete\"\n#define DOWNLOADER_MODAL_TITLE_ERRORS_OCCURED \"Some errors occured. \"\n\n#define OVERALL_PROGRESS_TITLE \"Overall progress\"\n#define TAR_PROGRESS_TITLE  \"Downloading Registry\"\n#define NOT_IN_REGISTRY \"NOT IN REGISTRY\"\n\n#define WGET_SKIPPING \" [Checksum OK]\"\n#define WGET_UPDATING \" [Outdated]\"\n#define WGET_CREATING \" [New file]\"\n#define SYNC_FINISHED \"Synch finished\"\n#define CLEANDIR_REMOVED \"Removed %s\\n\"\n#define DOWNLOAD_FAIL \" [DOWNLOAD FAIL]\"\n#define SHASHUM_FAIL \" [SHASUM FAIL]\"\n#define UPDATE_SUCCESS \"UPDATE SUCCESS\"\n\n#define WIFI_MSG_WAITING \"Enabling WiFi...\"\n#define WIFI_MSG_CONNECTING \"Connecting WiFi..\"\n#define WIFI_TITLE_TIMEOUT \"WiFi Timeout\"\n#define WIFI_MSG_TIMEOUT \"Timed out, will try again\"\n#define WIFI_TITLE_CONNECTED \"WiFi OK\"\n#define WIFI_MSG_CONNECTED \"Connected to wifi :-)\"\n\n#define NTP_TITLE_SETUP \"NTP Setup\"\n#define NTP_MSG_SETUP \"Contacting NTP Server\"\n#define NTP_TITLE_FAIL \"NTP Down\"\n#define NTP_MSG_FAIL \"Can't enable NTP\"\n\n\n#define DL_FSCLEANUP_TITLE \"Cleanup\"\n#define DL_FSCLEANUP_MSG \"Removing previous\\nbackup\"\n#define DL_FSBACKUP_TITLE \"Backup\"\n#define DL_FSBACKUP_MSG \"Backing up registry\"\n#define DL_FSRESTORE_MSG \"Restoring backup\"\n\n#define DL_TLSFETCH_TITLE \"TLS\"\n#define DL_TLSFETCH_MSG \"Fetching TLS Cert\"\n\n#define DL_TLSFAIL_TITLE \"TLS Error\"\n#define DL_TLSFAIL_MSG \"Could not init TLS\"\n\n#define DL_HTTPINIT_TITLE \"HTTP Init\"\n#define DL_HTTPINIT_MSG \"Contacting catalog\\nendpoint\"\n\n#define DL_HTTPFAIL_TITLE \"HTTP Error\"\n#define DL_FSFAIL_TITLE \"Filesystem Error\"\n#define DL_SHAFAIL_TITLE \"SHA256 Error\"\n\n#define DL_AWAITING_TITLE \"Awaiting response\"\n\n#define DL_SUCCESS_TITLE \"Success\"\n#define DL_SUCCESS_MSG \"Registry fetched!\"\n#define DL_FAIL_MSG \"Registry could not\\nbe reached.\"\n\n\n#define WGET_MSG_FAIL \"Please check the remote server\"\n#define FS_MSG_FAIL \"Please check the filesystem\"\n\n#define MODAL_TLSCERT_INSTALLFAILED_MSG \"Certificate fetching\\nOK but TLS Install\\nfailed\"\n#define MODAL_TLSCERT_FETCHINGFAILED_MSG \"Unable to wget()\\ncertificate\"\n#define NEW_TLS_CERTIFICATE_TITLE \"New TLS certificate installed\"\n#define NEW_TLS_CERTIFICATE_TEXT \"A new certificate\\nwas fetched and\\ninstalled successfully.\"\n#define MODAL_RESTART_REQUIRED \"Restarting is\\noptional.\\n\\nReboot anyway?\"\n#define MODAL_SAME_PLAYER_SHOOT_AGAIN \"Please try again.\\n\\nReboot now?\"\n#define MODAL_REGISTRY_UPDATED \"New Registry file\\nhas been updated\"\n#define MODAL_REGISTRY_DAMAGED \"New Registry file\\nmay be damaged\"\n#define MODAL_REBOOT_REGISTRY_UPDATED \"Please reboot and\\nchoose a channel.\\n\\nReboot now?\"\n\n\n#define DEBUG_DIROPEN_FAILED \"Failed to open directory\"\n#define DEBUG_EMPTY_FS \"Empty SD Card, falling back to root menu\"\n#define DEBUG_NOTADIR \"Not a directory\"\n#define DEBUG_DIRLABEL \"  DIR : \"\n#define DEBUG_IGNORED \"  IGNORED FILE: \"\n#define DEBUG_CLEANED \"  CLEANED FILE: \"\n#define DEBUG_ABORTLISTING \"  ***Max files reached for Menu, please adjust LIST_MAX_COUNT for more (maximum is 255, sorry :-)\"\n#define DEBUG_FILELABEL \"  FILE: \"\n\n#define DEBUG_FILECOPY \"Starting File Copy for \"\n#define DEBUG_FILECOPY_DONE \"Transfer finished\"\n#define DEBUG_WILL_RESTART \"Binary removed from SPIFFS, will now restart\"\n#define DEBUG_NOTHING_TODO \"No binary to transfer\"\n#define DEBUG_KEYPAD_NOTFOUND \"Keypad not installed\"\n#define DEBUG_KEYPAD_FOUND \"Keypad detected!\"\n#define DEBUG_JOYPAD_NOTFOUND \"No Joypad detected, disabling\"\n#define DEBUG_JOYPAD_FOUND \"Joypad detected!\"\n\n#define DEBUG_TIMESTAMP_GUESS \"%s has %s time set (%04d-%02d-%02d %02d:%02d:%02d), will use %s date to set the clock\"\n\n"
  },
  {
    "path": "examples/AppStore/modules/misc/lang/i18n.kr.h",
    "content": "#define WELCOME_MESSAGE \"Welcome to the \" PLATFORM_NAME \" App Store!\"\n#define INIT_MESSAGE  PLATFORM_NAME \" App Store initializing...\"\n#define MENU_SETTINGS \"AppStoreUI loaded with %d labels per page, max %d items\\n\"\n#define GOTOSLEEP_MESSAGE \"Will go to sleep\"\n\n#define MENU_BTN_INFO     \"SELECT\"\n#define MENU_BTN_UPDATE   \"UPDATE\"\n#define MENU_BTN_BACK     \"Back\"\n#define MENU_BTN_PREV     \"<\"\n#define MENU_BTN_NEXT     \">\"\n#define MENU_BTN_YES      \"YES\"\n#define MENU_BTN_NO       \"NO\"\n#define MENU_BTN_GO       \"GO\"\n#define MENU_BTN_CANCEL   \"Cancel\"\n#define MENU_BTN_REBOOT   \"Reboot\"\n#define MENU_BTN_RESTART  \"Restart\"\n#define MENU_BTN_CONTINUE \"Continue\"\n#define MENU_BTN_RETRY    \"RETRY\"\n\n\n#define MENUTITLE_DEFAULT PLATFORM_NAME \" App Store\"\n#define MENUTITLE_MYAPPS \"My Applications\"\n#define MENUTITLE_APPSTORE \"Applications Store\"\n#define MENUTITLE_TOGGLEAPPS \"Unhide Apps\"\n#define MENUTITLE_DOWNLOADER \"Apps Downloader\"\n#define MENUTITLE_MANAGEAPPS \"Manage Applications\"\n#define MENUTITLE_MAINMENU   \"Main Menu\"\n\n#define MENUACTION_BACKTOAPPS \"Back to Browse Apps\"\n#define MENUACTION_APPINSTALL \"Install\"\n#define MENUACTION_APPUPDATE  \"Update\"\n#define MENUACTION_APPVERIFY  \"Verify\"\n#define MENUACTION_VIEW       \"View\"\n#define MENUACTION_APPHIDE    \"Hide\"\n#define MENUACTION_APPDELETE  \"Delete\"\n#define MENUACTION_BACK       \"Back\"\n#define MENUACTION_UNHIDE     \"Unhide\"\n#define MENUACTION_REFRESH    \"Refresh\"\n\n#define MENUACTION_NTP        \"NTP Server\"\n#define MENUACTION_MYAPPS     \"*My Applications\"\n#define MENUACTION_APPTOGGLE  \"Manage Hidden Apps\"\n#define MENUACTION_DOWNLOADER \"*Apps Downloader\"\n#define MENUACTION_BROWSEAPP  \"Browse App Store\"\n#define MENUACTION_DEL_APPS   \"Manage My Apps\"\n\n#define ROOTACTION_DOWNLOAD  \"Download Catalog\"\n#define ROOTACTION_REFRESH   \"Refresh Catalog\"\n#define ROOTACTION_GET       \"Install Applications\"\n\n#define ROOTACTION_SWITCH    \"Switch Channel\"\n#define ROOTACTION_NTP_PICK  \"Change NTP Server\"\n#define ROOTACTION_CLEAR_ALL \"Clear ALL\"\n#define ROOTACTION_CLEAR_TLS \"Clear TLS cache\"\n#define ROOTACTION_SLEEP     \"Sleep\"\n\n#define MODAL_DELETEALL_TITLE \"DELETE EVERYTHING\"\n#define MODAL_DELETEALL_MSG \"CAUTION! This will remove apps, assets, registries\\nand databases, even those\\noutside the scope of this\\nApplication Manager!\"\n\n#define MODAL_DOWNLOADFAIL_TITLE \"HTTP FAILED\"\n#define MODAL_DOWNLOADFAIL_MSG \"An HTTP error occured\\nwhile downloading\\nthe registry\\narchive.\\nTry again?\"\n\n#define MODAL_WIFI_NOCONN_MSG \"No connection\"\n\n#define DOWNLOADER_MODAL_CHANGE \"CHANGE\"\n\n#define MODAL_CANCELED_TITLE \"OPERATION CANCELED\"\n#define MODAL_SUCCESS_TITLE \"OPERATION SUCCESSFUL\"\n\n\n#define CHANNEL_TOOL \"CHANNEL TOOL\"\n#define CHANNEL_TOOL_TEXT \"Do you want to change or\\nupdate your SD Card channel?\"\n\n#define CHANNEL_CHOOSER \"CHANNEL CHOOSER\"\n#define CHANNEL_CHOOSER_PROMPT \"Change channel ?\"\n#define CHANNEL_CHOOSER_TEXT \"You are about to change\\nyour SD Card channel.\\n\\n    Are you sure ?\"\n\n#define CHANNEL_DOWNLOADER \"CHANNEL DOWNLOADER\"\n#define CHANNEL_DOWNLOADER_PROMPT \"Download channel ?\"\n#define CHANNEL_DOWNLOADER_TEXT \"You are about to overwrite\\nyour SD Card channel.\\r\\n    Are you sure ?\"\n\n#define DOWNLOADER_MODAL_NAME \"Update binaries ?\"\n#define DOWNLOADER_MODAL_TITLE \"This action will:\"\n#define DOWNLOADER_MODAL_ENDED \"Synchronization complete\"\n#define DOWNLOADER_MODAL_TITLE_ERRORS_OCCURED \"Some errors occured. \"\n\n#define OVERALL_PROGRESS_TITLE \"Overall progress\"\n#define TAR_PROGRESS_TITLE  \"Downloading Registry\"\n#define NOT_IN_REGISTRY \"NOT IN REGISTRY\"\n\n#define WGET_SKIPPING \" [Checksum OK]\"\n#define WGET_UPDATING \" [Outdated]\"\n#define WGET_CREATING \" [New file]\"\n#define SYNC_FINISHED \"Synch finished\"\n#define CLEANDIR_REMOVED \"Removed %s\\n\"\n#define DOWNLOAD_FAIL \" [DOWNLOAD FAIL]\"\n#define SHASHUM_FAIL \" [SHASUM FAIL]\"\n#define UPDATE_SUCCESS \"UPDATE SUCCESS\"\n\n#define WIFI_MSG_WAITING \"Enabling WiFi...\"\n#define WIFI_MSG_CONNECTING \"Connecting WiFi..\"\n#define WIFI_TITLE_TIMEOUT \"WiFi Timeout\"\n#define WIFI_MSG_TIMEOUT \"Timed out, will try again\"\n#define WIFI_TITLE_CONNECTED \"WiFi OK\"\n#define WIFI_MSG_CONNECTED \"Connected to wifi :-)\"\n\n#define NTP_TITLE_SETUP \"NTP Setup\"\n#define NTP_MSG_SETUP \"Contacting NTP Server\"\n#define NTP_TITLE_FAIL \"NTP Down\"\n#define NTP_MSG_FAIL \"Can't enable NTP\"\n\n\n#define DL_FSCLEANUP_TITLE \"Cleanup\"\n#define DL_FSCLEANUP_MSG \"Removing previous\\nbackup\"\n#define DL_FSBACKUP_TITLE \"Backup\"\n#define DL_FSBACKUP_MSG \"Backing up registry\"\n#define DL_FSRESTORE_MSG \"Restoring backup\"\n\n#define DL_TLSFETCH_TITLE \"TLS\"\n#define DL_TLSFETCH_MSG \"Fetching TLS Cert\"\n\n#define DL_TLSFAIL_TITLE \"TLS Error\"\n#define DL_TLSFAIL_MSG \"Could not init TLS\"\n\n#define DL_HTTPINIT_TITLE \"HTTP Init\"\n#define DL_HTTPINIT_MSG \"Contacting catalog\\nendpoint\"\n\n#define DL_HTTPFAIL_TITLE \"HTTP Error\"\n#define DL_FSFAIL_TITLE \"Filesystem Error\"\n#define DL_SHAFAIL_TITLE \"SHA256 Error\"\n\n#define DL_AWAITING_TITLE \"Awaiting response\"\n\n#define DL_SUCCESS_TITLE \"Success\"\n#define DL_SUCCESS_MSG \"Registry fetched!\"\n#define DL_FAIL_MSG \"Registry could not\\nbe reached.\"\n\n\n#define WGET_MSG_FAIL \"Please check the remote server\"\n#define FS_MSG_FAIL \"Please check the filesystem\"\n\n#define MODAL_TLSCERT_INSTALLFAILED_MSG \"Certificate fetching\\nOK but TLS Install\\nfailed\"\n#define MODAL_TLSCERT_FETCHINGFAILED_MSG \"Unable to wget()\\ncertificate\"\n#define NEW_TLS_CERTIFICATE_TITLE \"New TLS certificate installed\"\n#define NEW_TLS_CERTIFICATE_TEXT \"A new certificate\\nwas fetched and\\ninstalled successfully.\"\n#define MODAL_RESTART_REQUIRED \"Restarting is\\noptional.\\n\\nReboot anyway?\"\n#define MODAL_SAME_PLAYER_SHOOT_AGAIN \"Please try again.\\n\\nReboot now?\"\n#define MODAL_REGISTRY_UPDATED \"New Registry file\\nhas been updated\"\n#define MODAL_REGISTRY_DAMAGED \"New Registry file\\nmay be damaged\"\n#define MODAL_REBOOT_REGISTRY_UPDATED \"Please reboot and\\nchoose a channel.\\n\\nReboot now?\"\n\n\n#define DEBUG_DIROPEN_FAILED \"Failed to open directory\"\n#define DEBUG_EMPTY_FS \"Empty SD Card, falling back to root menu\"\n#define DEBUG_NOTADIR \"Not a directory\"\n#define DEBUG_DIRLABEL \"  DIR : \"\n#define DEBUG_IGNORED \"  IGNORED FILE: \"\n#define DEBUG_CLEANED \"  CLEANED FILE: \"\n#define DEBUG_ABORTLISTING \"  ***Max files reached for Menu, please adjust LIST_MAX_COUNT for more (maximum is 255, sorry :-)\"\n#define DEBUG_FILELABEL \"  FILE: \"\n\n#define DEBUG_FILECOPY \"Starting File Copy for \"\n#define DEBUG_FILECOPY_DONE \"Transfer finished\"\n#define DEBUG_WILL_RESTART \"Binary removed from SPIFFS, will now restart\"\n#define DEBUG_NOTHING_TODO \"No binary to transfer\"\n#define DEBUG_KEYPAD_NOTFOUND \"Keypad not installed\"\n#define DEBUG_KEYPAD_FOUND \"Keypad detected!\"\n#define DEBUG_JOYPAD_NOTFOUND \"No Joypad detected, disabling\"\n#define DEBUG_JOYPAD_FOUND \"Joypad detected!\"\n\n#define DEBUG_TIMESTAMP_GUESS \"%s has %s time set (%04d-%02d-%02d %02d:%02d:%02d), will use %s date to set the clock\"\n\n"
  },
  {
    "path": "examples/AppStore/platformio.ini",
    "content": "[platformio]\nsrc_dir = main\n;default_envs = m5stack-fire\n;default_envs = m5stack-core-esp32\ndefault_envs = m5stack-core-esp32\n;default_envs = odroid_esp32\n\n[env]\n;platform = espressif32@3.3.2\n;platform = espressif32\n;platform = https://github.com/platformio/platform-espressif32.git\nplatform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream\n;platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.0\n;platform = espressif32@3.4.0\nplatform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.2\nframework = arduino\nupload_speed = 921600\nmonitor_speed = 115200\nbuild_flags =\n  -DCORE_DEBUG_LEVEL=4\nlib_extra_dirs = ../../../M5Stack-SD-Updater\nlib_deps =\n  FS\n  SPI\n  Wire\n  LovyanGFX\n  ESP32-Chimera-Core\n  ESP32-targz\n;  M5Stack-SD-Updater\n  WiFi\n  HTTPClient\n  WiFiClientSecure\n  Preferences\n  Update\n  bblanchon/ArduinoJson\n\n\n\n[env:m5stack-fire]\nboard = m5stack-fire\nboard_build.partitions = default_16MB.csv\nlib_deps =\n  ${env.lib_deps}\n  fastled/FastLED@3.4.0\n\n[env:m5stack-core-esp32]\nboard = m5stack-core-esp32\nbuild_flags =\n  -DCORE_DEBUG_LEVEL=0\n;debug_build_flags = -Os\n;board_build.partitions = min_spiffs.csv\n;board_build.partitions = default.csv\nlib_deps =\n  ${env.lib_deps}\n\n[env:m5stack-core2]\nboard = m5stack-core2\nboard_build.partitions = default_16MB.csv\nlib_deps =\n  ${env.lib_deps}\n\n[env:odroid_esp32]\nboard = odroid_esp32\nboard_build.partitions = min_spiffs.csv\nlib_deps =\n  ${env.lib_deps}\n\n"
  },
  {
    "path": "examples/CopySketchToFS/CopySketchToFS.ino",
    "content": "#include <ESP32-Chimera-Core.h>\n#define tft M5.Lcd\n\n#define SDU_APP_PATH \"/1_test_binary.bin\" // path to file on the SD\n#define SDU_APP_NAME \"saveSketchToFS() Example\" // title for SD-Updater UI\n\n#include <M5StackUpdater.h>\n\n\n\nvoid centerMessage( const char* message, uint16_t color )\n{\n  tft.clear();\n  tft.setTextDatum( MC_DATUM );\n  tft.setTextColor( color );\n  tft.drawString( message, tft.width()/2, tft.height()/2 );\n}\n\n\nvoid setup()\n{\n  M5.begin();\n  Serial.println(\"Welcome to the SD-Updater minimal example!\");\n  Serial.println(\"Now checking if a button was pushed during boot ...\");\n  // checkSDUpdater( SD 1);\n  checkSDUpdater(\n    SD,           // filesystem (default=SD)\n    MENU_BIN,     // path to binary (default=/menu.bin, empty string=rollback only)\n    30000,         // wait delay, (default=0, will be forced to 2000 upon ESP.restart() )\n    TFCARD_CS_PIN // (usually default=4 but your mileage may vary)\n  );\n\n  Serial.println(\"Nope, will run the sketch normally\");\n  tft.print(\"Sketch Copy Utility\\n\");\n  tft.print(\"-------------------\\n\\n\");\n  tft.print(\"Press Button B to start\\n\\n\");\n\n}\n\nvoid loop()\n{\n\n  M5.update();\n  if( M5.BtnB.pressedFor( 1000 ) ) {\n    tft.print(\"Please release Button B...\\n\\n\");\n    while( M5.BtnB.isPressed() ) { // wait for release\n      M5.update();\n      vTaskDelay(100);\n    }\n    tft.print(\"Will copy sketch to SD...\\n\\n\");\n    if( saveSketchToFS( SD, SDU_APP_PATH, TFCARD_CS_PIN ) ) {\n      centerMessage( \"Copy successful !\\n\", TFT_GREEN );\n    } else {\n      centerMessage( \"Copy failed !\\n\", TFT_RED );\n    }\n  }\n\n}\n\n"
  },
  {
    "path": "examples/FactoryLauncher/FactoryLauncher.ino",
    "content": "#include <SD.h>\n#include <SPIFFS.h>\n#include <LittleFS.h>\n#include <FFat.h>\n#include <ESP32-targz.h>\n\n#include <M5Unified.h>\n#include <M5StackUpdater.h>\n\n#define tft M5.Lcd\n\n\nstruct digest_t {\n  String str{\"0000000000000000000000000000000000000000000000000000000000000000\"};\n  const char* toString( uint8_t dig[32] )\n  {\n    str = \"\";\n    char hex[3] = {0};\n    for(int i=0;i<32;i++) {\n      snprintf( hex, 3, \"%02x\", dig[i] );\n      str += String(hex);\n    }\n    return str.c_str();\n  }\n};\n\n\nvoid lsPart()\n{\n  esp_partition_iterator_t pi = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);\n  log_w(\"Partition  Type   Subtype    Address   PartSize   ImgSize    Info    Digest\");\n  log_w(\"---------+------+---------+----------+----------+---------+--------+--------\");\n  while(pi != NULL) {\n    const esp_partition_t* part = esp_partition_get(pi);\n    esp_image_metadata_t meta = esp_image_metadata_t();\n    digest_t digest;\n    bool isFactory = part->type==ESP_PARTITION_TYPE_APP && part->subtype==ESP_PARTITION_SUBTYPE_APP_FACTORY;\n    bool isOta = part->type==ESP_PARTITION_TYPE_APP && (part->subtype>=ESP_PARTITION_SUBTYPE_APP_OTA_MIN && part->subtype<ESP_PARTITION_SUBTYPE_APP_OTA_MAX);\n    //bool isOta = (part->label[3]=='1' || part->label[3] == '0');\n    String OTAName = \"OTA\";\n    if( isOta || isFactory ) {\n      meta  = SDUpdater::getSketchMeta( part );\n      OTAName += String( part->subtype-ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n    }\n\n    int digest_total = 0;\n    for( int i=0;i<sizeof(meta.image_digest);i++ ) {\n      digest_total+=meta.image_digest[i];\n    }\n\n    if( isOta && digest_total > 0 ) {\n      tft.println( digest.toString(meta.image_digest) );\n    }\n\n    log_w(\"%-8s   0x%02x      0x%02x   0x%06x   %8d  %8s %8s %8s\",\n      String( part->label ),\n      part->type,\n      part->subtype,\n      part->address,\n      part->size,\n      meta.image_len>0 ? String(meta.image_len) : \"n/a\",\n      isOta ? OTAName.c_str() : isFactory ? \"Factory\" : \"n/a\",\n      digest_total>0 ? digest.toString(meta.image_digest) : \"\"\n    );\n    pi = esp_partition_next( pi );\n  }\n  esp_partition_iterator_release(pi);\n}\n\n\n\n\nvoid setup()\n{\n\n  M5.begin();\n\n  lsPart();\n\n  if( SDUpdater::saveSketchToFactory() ) {\n    // sketch was just saved to factory partition, this is only triggered once\n    SDUpdater::loadFactory(); // will restart on success\n    log_e(\"Switching to factory app failed :-(\");\n  }\n\n\n// Menu:\n//   - Detect filesystems and pick a filesystem:\n//    a) SD/littlefs/spiffs/fatfs -> fsLoader\n//    b) factory partition -> factoryLoader\n\n// fsLoader:\n//   - list current binaries/gz\n//   - sd-update from bin/gz\n//   - pick destination partition\n\n// factoryLoader:\n//   - list current partitions\n//   - load from partition\n//   - dump partition to filesystem\n\n}\n\n\nvoid loop()\n{\n\n}\n"
  },
  {
    "path": "examples/FactoryLauncher/partitions.csv",
    "content": "# 6 Apps + Factory\n# Name,   Type, SubType,    Offset,     Size\nnvs,      data, nvs,        0x9000,   0x5000,\notadata,  data, ota,        0xe000,   0x2000,\nota_0,    0,    ota_0,     0x10000, 0x200000,\nota_1,    0,    ota_1,    0x210000, 0x200000,\nota_2,    0,    ota_2,    0x410000, 0x200000,\nota_3,    0,    ota_3,    0x610000, 0x200000,\nota_4,    0,    ota_4,    0x810000, 0x200000,\nota_5,    0,    ota_5,    0xA10000, 0x200000,\nfirmware, app,  factory,  0xC10000, 0x0F0000,\nspiffs,   data, spiffs,   0xD00000, 0x2F0000,\ncoredump, data, coredump, 0xFF0000,  0x10000,\n\n## 4 Apps + Factory\n## Name,   Type, SubType,    Offset,     Size\n#nvs,      data, nvs,        0x9000,   0x5000\n#otadata,  data, ota,        0xe000,   0x2000\n#ota_0,    0,    ota_0,     0x10000, 0x300000\n#ota_1,    0,    ota_1,    0x310000, 0x300000\n#ota_2,    0,    ota_2,    0x610000, 0x300000\n#ota_3,    0,    ota_3,    0x910000, 0x300000\n#firmware, app,  factory,  0xC10000, 0x0F0000\n#spiffs,   data, spiffs,   0xD00000, 0x2F0000\n#coredump, data, coredump, 0xFF0000,  0x10000\n\n\n\n"
  },
  {
    "path": "examples/GzLauncher/GzLauncher.ino",
    "content": "#include <ESP32-Chimera-Core.h> // https://github.com/tobozo/ESP32-Chimera-Core\n#define tft M5.Lcd\n\n#define DEST_FS_USES_SD\n#include <ESP32-targz.h>\n\n#define SDU_APP_NAME \"Application GzLauncher\"\n#define SDU_APP_PATH \"/menu.bin\" // path to file on the SD\n#include <M5StackUpdater.h>  // https://github.com/tobozo/M5Stack-SD-Updater\n\n\nSDUpdater *sdUpdater;\n\n\nvoid byteCountSI(int64_t b, char* dest )\n{\n  const int64_t unit = 1000;\n  const char* units = \"kMGTPE\";\n  if( b < unit ) {\n    sprintf(dest, \"%d B\", b);\n  }\n  int64_t div = unit;\n  int64_t exp = 0;\n  for (int64_t n = b / unit; n >= unit; n /= unit) {\n    div *= unit;\n    exp++;\n  }\n  sprintf(dest, \"%.1f %cB\", float(b)/float(div), units[exp]);\n}\n\nvoid byteCountIEC( int64_t b, char* dest )\n{\n  const int64_t unit = 1024;\n  const char* units = \"KMGTPE\";\n  if( b < unit ) {\n    sprintf(dest, \"%d B\", b);\n  }\n  int64_t div = unit;\n  int64_t exp = 0;\n  for (int64_t n = b/unit; n >= unit; n /= unit) {\n    div *= unit;\n    exp++;\n  }\n  sprintf(dest, \"%.1f %ciB\", float(b)/float(div), units[exp]);\n}\n\n\nstatic void getPartitions()\n{\n\n  esp_partition_iterator_t pi;\n  const esp_partition_t *partition;\n\n  pi = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);\n\n  if (pi) {\n    printf(\"%s - %s - %s - %s - %s - %s\\r\\n\", \"type\", \"subtype\", \"  address \", \"  size  \", \"  label \", \"encrypted\");\n    do {\n      partition = esp_partition_get(pi);\n      char bytesStr[16] = {0};\n      byteCountIEC( partition->size, bytesStr );\n      printf(\" %2d  -  %3d    - 0x%08x - %8s - %8s - %i\\r\\n\", partition->type, partition->subtype, partition->address, bytesStr, partition->label, partition->encrypted);\n    } while ((pi = esp_partition_next(pi)));\n  }\n\n  esp_partition_iterator_release(pi);\n\n}\n\nvoid centerMessage( const char* message, uint16_t color )\n{\n  tft.clear();\n  tft.setTextDatum( MC_DATUM );\n  tft.setTextColor( color );\n  tft.drawString( message, tft.width()/2, tft.height()/2 );\n}\n\n\n\nvoid setup()\n{\n  M5.begin();\n\n  Serial.printf(\"Welcome to %s\\n\", SDU_APP_NAME);\n\n  getPartitions();\n\n  /*\n  SDUCfg.setCSPin( TFCARD_CS_PIN );\n  SDUCfg.setFS( &SD );\n\n  sdUpdater = new SDUpdater( &SDUCfg );\n  */\n\n}\n\nvoid loop()\n{\n  /*\n  M5.update();\n  if( M5.BtnB.pressedFor( 1000 ) ) {\n    tft.print(\"Please release Button B...\\n\\n\");\n    while( M5.BtnB.isPressed() ) { // wait for release\n      M5.update();\n      vTaskDelay(100);\n    }\n    tft.print(\"Will copy sketch to SD...\\n\\n\");\n    if( sdUpdater->saveSketchToFS( SD, SDU_APP_PATH ) ) {\n      centerMessage( \"Copy successful !\\n\", TFT_GREEN );\n    } else {\n      centerMessage( \"Copy failed !\\n\", TFT_RED );\n    }\n  }\n  */\n}\n"
  },
  {
    "path": "examples/Headless/Headless.ino",
    "content": "#define SDU_APP_NAME \"Headless Example\"\n#define SDU_APP_PATH \"/Headless_Example.bin\"\n#define SDU_NO_AUTODETECT // don't load gfx clutter (but implement my own action trigger)\n#define SDU_NO_PRAGMAS    // don't print pragma messages when compiling\n#define TFCARD_CS_PIN 4   // this is needed by SD.begin()\n#include <SD.h>           // /!\\ headless mode skips autodetect and may require to include the filesystem library **before** the M5StackUpdater library\n#include <ESP32-targz.h>  // optional: https://github.com/tobozo/ESP32-targz\n#include <M5StackUpdater.h>\n\n\nstatic int myActionTrigger( char* labelLoad,  char* labelSkip, char* labelSave, unsigned long waitdelay )\n{\n  int64_t msec = millis();\n  do {\n    if( Serial.available() ) {\n      String out = Serial.readStringUntil('\\n');\n      if(      out == \"update\" )  return  1; // load \"/menu.bin\"\n      else if( out == \"rollback\") return  0; // rollback to other OTA partition\n      else if( out == \"skip\" )    return -1; // do nothing\n      else if( out == \"save\" ) return 2;\n      else Serial.printf(\"Ignored command: %s\\n\", out.c_str() );\n    }\n  } while( msec > int64_t( millis() ) - int64_t( waitdelay ) );\n  return -1;\n}\n\n\nvoid setup()\n{\n  Serial.begin( 115200 );\n  Serial.println(\"Welcome to the SD-Updater Headless example!\");\n  Serial.println(\"Now waiting 30 seconds for user input in Serial console...\");\n\n  SDUCfg.setWaitForActionCb( myActionTrigger );\n\n  checkSDUpdater(\n    SD,           // filesystem (default=SD)\n    MENU_BIN,     // path to binary (default=/menu.bin, empty string=rollback only)\n    30000,        // wait delay in milliseconds (default=0, e,g, 30000 will be forced to 30 seconds upon ESP.restart() )\n    TFCARD_CS_PIN // (usually default=4 but your mileage may vary)\n  );\n\n  Serial.println(\"Starting application\");\n}\n\nvoid loop()\n{\n\n}\n"
  },
  {
    "path": "examples/LGFX-SDLoader-Snippet/LGFX-SDLoader-Snippet.ino",
    "content": "// M5Stack classic buttons/SD pinout\n#define SDU_BUTTON_A_PIN 39\n#define SDU_BUTTON_B_PIN 38\n#define SDU_BUTTON_C_PIN 37\n#define TFCARD_CS_PIN 4\n\n// usual SD+TFT stack\n#define LGFX_AUTODETECT\n#define LGFX_USE_V1\n#include <SD.h>\n#include <LovyanGFX.h>\n\n// the display object\nstatic LGFX tft;\n\n// #include <M5GFX.h>\n// #define LGFX M5GFX // just alias to LGFX for SD-Updater\n\n// A GPIO button library mocking M5.BtnX Buttons API, for the sakes of this demo\n#include \"M5Stack_Buttons.h\" // stolen from M5Stack Core\n//#define USE_TOUCH_BUTTONS // uncomment this use Touch UI\n\n#define SDU_NO_AUTODETECT           // Disable autodetect (only works with <M5xxx.h> and <Chimera> cores)\n#define SDU_USE_DISPLAY             // Enable display functionalities (lobby, buttons, progress loader)\n#define HAS_LGFX                    // Display UI will use LGFX API (without this it will be tft_eSPI API)\n#define SDU_TouchButton LGFX_Button // Set button renderer\n#define SDU_Sprite LGFX_Sprite      // Set sprite type\n#define SDU_DISPLAY_TYPE LGFX*      // Set display driver type\n#define SDU_DISPLAY_OBJ_PTR &tft    // Set display driver pointer\n\n#if defined ARDUINO_M5STACK_Core2 || defined USE_TOUCH_BUTTONS\n  #define SDU_HAS_TOUCH             // Enable touch buttons\n  #define SDU_TRIGGER_SOURCE_DEFAULT TriggerSource::SDU_TRIGGER_TOUCHBUTTON // Attach Touch buttons as trigger source\n#else\n   // Use any type of push button\n  #define SDU_TRIGGER_SOURCE_DEFAULT TriggerSource::SDU_TRIGGER_PUSHBUTTON // Attach push buttons as trigger source\n#endif\n\n// #define _CLK  1\n// #define _MISO 2\n// #define _MOSI 3\n\n#define SDU_APP_NAME \"LGFX Loader Snippet\"\n#include <ESP32-targz.h> // optional: https://github.com/tobozo/ESP32-targz\n#include <M5StackUpdater.h>\n\n\n\nstatic SDUButton *BtnA = new SDUButton(SDU_BUTTON_A_PIN, true, 10);\nstatic SDUButton *BtnB = new SDUButton(SDU_BUTTON_B_PIN, true, 10);\nstatic SDUButton *BtnC = new SDUButton(SDU_BUTTON_C_PIN, true, 10);\n\nstatic void ButtonUpdate()\n{\n  if( BtnA ) BtnA->read();\n  if( BtnB ) BtnB->read();\n  if( BtnC ) BtnC->read();\n}\n\n\nvoid setup()\n{\n  Serial.begin(115200);\n  Serial.println(\"LGFX Example\");\n\n  tft.init();\n\n  // SDUCfg.setDisplay( &tft ); // attach LGFX to SD-Updater, only mandatory if SDU_DISPLAY_OBJ_PTR isn't defined earlier\n\n  if( tft.touch() ) {\n\n    log_d(\"Display has touch\");\n\n    SDUCfg.useBuiltinTouchButton();\n\n  } else {\n\n    log_d(\"Display has buttons\");\n\n    //BtnA = new SDUButton(SDU_BUTTON_A_PIN, true, 10);\n    //BtnB = new SDUButton(SDU_BUTTON_B_PIN, true, 10);\n    //BtnC = new SDUButton(SDU_BUTTON_C_PIN, true, 10);\n\n    ButtonUpdate();\n\n    SDUCfg.setBtnPoller( [](){ ButtonUpdate(); } );\n    SDUCfg.setBtnA( []() -> bool{ return BtnA->isPressed(); } );\n    SDUCfg.setBtnB( []() -> bool{ return BtnB->isPressed(); } );\n    SDUCfg.setBtnC( []() -> bool{ return BtnC->isPressed(); } );\n\n  }\n\n  // SDUCfg.setProgressCb  ( myProgress );       // void (*onProgress)( int state, int size )\n  // SDUCfg.setMessageCb   ( myDrawMsg );        // void (*onMessage)( const String& label )\n  // SDUCfg.setErrorCb     ( myErrorMsg );       // void (*onError)( const String& message, unsigned long delay )\n  // SDUCfg.setBeforeCb    ( myBeforeCb );       // void (*onBefore)()\n  // SDUCfg.setAfterCb     ( myAfterCb );        // void (*onAfter)()\n  // SDUCfg.setSplashPageCb( myDrawSplashPage ); // void (*onSplashPage)( const char* msg )\n  // SDUCfg.setButtonDrawCb( myDrawPushButton ); // void (*onButtonDraw)( const char* label, uint8_t position, uint16_t outlinecolor, uint16_t fillcolor, uint16_t textcolor )\n  // SDUCfg.setWaitForActionCb( myActionTrigger );  // int  (*onWaitForAction)( char* labelLoad, char* labelSkip, unsigned long waitdelay )\n\n  checkSDUpdater(\n    SD,           // filesystem (default=SD)\n    MENU_BIN,     // path to binary (default=/menu.bin, empty string=rollback only)\n    10000,         // wait delay, (default=0, will be forced to 2000 upon ESP.restart() )\n    TFCARD_CS_PIN // (usually default=4 but your mileage may vary)\n  );\n\n\n  tft.setTextSize(2);\n  tft.println( SDU_APP_NAME );\n\n}\n\n\n\nvoid loop()\n{\n\n  if( ! tft.touch() ) {\n    ButtonUpdate();\n    if( BtnA->wasPressed() ) {\n      tft.println(\"BtnA pressed !\");\n    }\n    if( BtnB->wasPressed() ) {\n      tft.println(\"BtnB pressed !\");\n    }\n    if( BtnC->wasPressed() ) {\n      tft.println(\"BtnC pressed !\");\n    }\n  }\n\n}\n"
  },
  {
    "path": "examples/LGFX-SDLoader-Snippet/M5Stack_Buttons.h",
    "content": "#ifndef SDUButton_h\n#define SDUButton_h\n\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wunused-variable\"\n\n#include <Arduino.h>\n\nclass SDUButton {\n  public:\n    SDUButton(uint8_t pin, uint8_t invert, uint32_t dbTime);\n    uint8_t read();\n    uint8_t setState(uint8_t);\n    uint8_t isPressed();\n    uint8_t isReleased();\n    uint8_t wasPressed();\n    uint8_t wasReleased();\n    uint8_t pressedFor(uint32_t ms);\n    uint8_t releasedFor(uint32_t ms);\n    uint8_t wasReleasefor(uint32_t ms);\n    uint32_t lastChange();\n\n  private:\n    uint8_t _pin;           //arduino pin number\n    uint8_t _puEnable;      //internal pullup resistor enabled\n    uint8_t _invert;        //if 0, interpret high state as pressed, else interpret low state as pressed\n    uint8_t _state;         //current button state\n    uint8_t _lastState;     //previous button state\n    uint8_t _changed;       //state changed since last read\n    uint32_t _time;         //time of current state (all times are in ms)\n    uint32_t _lastTime;     //time of previous state\n    uint32_t _lastChange;   //time of last state change\n    uint32_t _dbTime;       //debounce time\n    uint32_t _pressTime;    //press time\n    uint32_t _hold_time;    //hold time call wasreleasefor\n\n    uint8_t _axis;          //state changed since last read\n};\n\n\nSDUButton::SDUButton(uint8_t pin, uint8_t invert, uint32_t dbTime) {\n  _pin = pin;\n  _invert = invert;\n  _dbTime = dbTime;\n  pinMode(_pin, INPUT_PULLUP);\n  _state = digitalRead(_pin);\n  if (_invert != 0) _state = !_state;\n  _time = millis();\n  _lastState = _state;\n  _changed = 0;\n  _hold_time = -1;\n  _lastTime = _time;\n  _lastChange = _time;\n  _pressTime = _time;\n}\n\nuint8_t SDUButton::read(void) {\n  static uint8_t pinVal;\n  pinVal = digitalRead(_pin);\n  if (_invert != 0) pinVal = !pinVal;\n  return setState(pinVal);\n}\n\nuint8_t SDUButton::setState(uint8_t pinVal)\n{\n  static uint32_t ms;\n\n  ms = millis();\n  if (ms - _lastChange < _dbTime) {\n    _lastTime = _time;\n    _time = ms;\n    _changed = 0;\n    return _state;\n  }\n  else {\n    _lastTime = _time;\n    _time = ms;\n    _lastState = _state;\n    _state = pinVal;\n    if (_state != _lastState) {\n      _lastChange = ms;\n      _changed = 1;\n      if (_state) { _pressTime = _time; }\n    }\n    else {\n      _changed = 0;\n    }\n    return _state;\n  }\n}\n\nuint8_t SDUButton::isPressed(void) {\n  return _state == 0 ? 0 : 1;\n}\n\nuint8_t SDUButton::isReleased(void) {\n  return _state == 0 ? 1 : 0;\n}\n\nuint8_t SDUButton::wasPressed(void) {\n  return _state && _changed;\n}\n\nuint8_t SDUButton::wasReleased(void) {\n  return !_state && _changed && millis() - _pressTime < _hold_time;\n}\n\nuint8_t SDUButton::wasReleasefor(uint32_t ms) {\n  _hold_time = ms;\n  return !_state && _changed && millis() - _pressTime >= ms;\n}\n\n\nuint8_t SDUButton::pressedFor(uint32_t ms) {\n  return (_state == 1 && _time - _lastChange >= ms) ? 1 : 0;\n}\n\nuint8_t SDUButton::releasedFor(uint32_t ms) {\n  return (_state == 0 && _time - _lastChange >= ms) ? 1 : 0;\n}\n\nuint32_t SDUButton::lastChange(void) {\n  return _lastChange;\n}\n\n#pragma GCC diagnostic pop\n\n#endif\n"
  },
  {
    "path": "examples/M5Core2-SDLoader-Snippet/M5Core2-SDLoader-Snippet.ino",
    "content": "#pragma GCC diagnostic ignored \"-Wmissing-field-initializers\"\n\n#include <M5Core2.h>\n#define SDU_APP_NAME \"M5Core2 SDLoader Snippet\"\n#include <ESP32-targz.h> // optional: https://github.com/tobozo/ESP32-targz\n#include <M5StackUpdater.h>\n\nvoid setup()\n{\n  M5.begin();\n\n  // checkSDUpdater();\n  checkSDUpdater(\n    SD,           // filesystem (default=SD)\n    MENU_BIN,     // path to binary (default=/menu.bin, empty string=rollback only)\n    2000,         // wait delay, (default=0, will be forced to 2000 upon ESP.restart() )\n    TFCARD_CS_PIN // (usually default=4 but your mileage may vary)\n  );\n}\n\n\nvoid loop()\n{\n\n}\n"
  },
  {
    "path": "examples/M5Stack-FW-Menu/M5Stack-FW-Menu.ino",
    "content": "\n// dummy file to satisfy both Arduino IDE and platformio requirements\n// code is in src/main.cpp file\n"
  },
  {
    "path": "examples/M5Stack-FW-Menu/ReadMe.md",
    "content": "## Factory Partition Firmware Launcher\n\n/!\\ This app launcher depends on special partition schemes that include the factory type and at least 4 OTA (app) partitions.\n\nThe intent of this launcher is to provide an application manager to hanle firmwares directly from the flash.\n\nThis is useful in the following situations:\n- Some firmwares on the SD are frequently flashed with updateFromFS()\n- updateFromFS() can be slow with big binaries\n- Multiple apps needed but filesystem is empty of no filesystem is available\n- Filesystem is variable, it can be one of: SPIFFS, LittleFS, FFat, SD\n\n\n### Requirements\n\n- M5Unified supported device (typically M5Stack, M5Fire, M5Core2, M5CoreS3)\n- 16MB Flash\n- Display module\n- Buttons and/or touch\n\n\n### Launcher Usage\n\nOn first run, the launcher firmware will copy itself to the factory partition and restart from there, it will take\nsome time but it only happens once.\n\nThe app launcher UI comes with a set of tools:\n- Firmware launcher (from flash)\n- Firmware launcher (from filesystem, like M5Stack-SD-Launcher but without the decorations)\n- Partitions Manager\n  - Add firmware (copy from filesystem to flash)\n  - Backup firmware (copy from flash to filesystem)\n  - Remove firmware (a.k.a. erase ota partition)\n\n\n### Application Usage\n\n#### Direct implementation of the factory loader in an application:\n\n```cpp\nif( Flash::hasFactory() ) {\n   Flash::loadFactory()\n}\n```\n\n#### Indirect implementation with `checkSDUpdater()`:\n\n/!\\ This is temporary as it uses a side effect of M5StackUpdater's legacy behaviour instead of a clean and separate\nlogic statement. This will change in the future.\n\nUsing this app launcher instead of M5Stack-SD-Menu launcher will require a couple of modifications in the call to `checkSDUpdater()`:\n\n\n```cpp\n    SDUCfg.rollBackToFactory = true; // ignore M5Stack-SD-Menu (menu.bin) hot-loading\n    SDUCfg.setLabelMenu(\"FW Menu\");  // Change the launcher button label\n    checkSDUpdater( SD, \"\", 5000, TFCARD_CS_PIN ); // second argument must be empty\n```\n\n"
  },
  {
    "path": "examples/M5Stack-FW-Menu/partitions/partitions-16MB-factory-4-apps.csv",
    "content": "## 4 Apps + Factory\n## Name,   Type, SubType,    Offset,     Size\nnvs,      data, nvs,        0x9000,   0x5000\notadata,  data, ota,        0xe000,   0x2000\nota_0,    0,    ota_0,     0x10000, 0x300000\nota_1,    0,    ota_1,    0x310000, 0x300000\nota_2,    0,    ota_2,    0x610000, 0x300000\nota_3,    0,    ota_3,    0x910000, 0x300000\nfirmware, app,  factory,  0xC10000, 0x0F0000\nspiffs,   data, spiffs,   0xD00000, 0x2F0000\ncoredump, data, coredump, 0xFF0000,  0x10000\n"
  },
  {
    "path": "examples/M5Stack-FW-Menu/partitions/partitions-16MB-factory-6-apps.csv",
    "content": "# 6 Apps + Factory\n# Name,   Type, SubType,    Offset,     Size\nnvs,      data, nvs,        0x9000,   0x5000\notadata,  data, ota,        0xe000,   0x2000\nota_0,    0,    ota_0,     0x10000, 0x200000\nota_1,    0,    ota_1,    0x210000, 0x200000\nota_2,    0,    ota_2,    0x410000, 0x200000\nota_3,    0,    ota_3,    0x610000, 0x200000\nota_4,    0,    ota_4,    0x810000, 0x200000\nota_5,    0,    ota_5,    0xA10000, 0x200000\nfirmware, app,  factory,  0xC10000, 0x0F0000\nspiffs,   data, spiffs,   0xD00000, 0x2F0000\ncoredump, data, coredump, 0xFF0000,  0x10000\n\n"
  },
  {
    "path": "examples/M5Stack-FW-Menu/platformio.ini",
    "content": "[platformio]\ndefault_envs      = m5stack-cores3\nsrc_dir           = src\n\n\n[env]\nframework         = arduino\nboard             = m5stack-core-esp32\nbuild_type        = debug\nlib_ldf_mode      = deep\nplatform          = https://github.com/tasmota/platform-espressif32\nplatform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32/releases/download/2.0.9/esp32-2.0.9.zip\nboard_build.partitions = partitions/partitions-16MB-factory-4-apps.csv\nboard_upload.flash_size = 16MB\nlib_deps          =\n  SPI\n  SD\n  M5Stack-SD-Updater\n  M5GFX\n  M5Unified\nupload_speed = 1500000\nupload_port = /dev/ttyACM0\nmonitor_speed = 115200\n\n\n[CoreS3]\nboard = esp32-s3-devkitc-1\n;board_upload.maximum_size = 2097152\n;board_upload.maximum_size = 3145728\nboard_build.arduino.memory_type = qio_qspi\nbuild_flags =\n    ${env.build_flags}\n    -DARDUINO_M5STACK_CORES3\n    -DBOARD_HAS_PSRAM\n    -DARDUINO_UDB_MODE=1\n\n\n[env:m5stack-cores3]\nextends = CoreS3\n\n\n\n[env:m5stack-cores3-dev]\nextends = CoreS3\nlib_deps          =\n  git+https://github.com/M5Stack/M5GFX#develop\n  git+https://github.com/M5Stack/M5Unified#develop\n  git+https://github.com/tobozo/M5Stack-SD-Updater#1.2.8\n"
  },
  {
    "path": "examples/M5Stack-FW-Menu/src/main.cpp",
    "content": "// load all supported filesystems\n#define SDU_USE_SD\n#define SDU_USE_SPIFFS\n#define SDU_USE_FFAT\n#define SDU_USE_LITTLEFS\n//#define SDU_ENABLE_GZ\n\n#include <SD.h>\n#include <SPIFFS.h>\n#include <FFat.h>\n#include <LittleFS.h>\n//#include <ESP32-targz.h>\n\n//#include <M5Unified.h>\n#include <ESP32-Chimera-Core.h>\n#include <M5StackUpdater.h>\n\n#include <rom/rtc.h>\n#define resetReason (int)rtc_get_reset_reason(0)\n\n\nenum M5Btns_t\n{\n  M5_NOBTN = -1,\n  M5_BTNA  =  0,\n  M5_BTNB  =  1,\n  M5_BTNC  =  2\n};\n\n\nstruct sdu_filesystem_t\n{\n  fs::FS* fs;\n  const char* name;\n  bool enabled;\n};\n\n\nstruct Item_t\n{\n  std::string name;\n  void *item;\n};\n\n\nstruct scrollClip_t\n{\n  int32_t x;\n  int32_t y;\n  uint32_t w;\n  uint32_t h;\n};\n\n\ntypedef void* resultGetter_t_type;\ntypedef resultGetter_t_type (*resultGetter_t)(std::vector<Item_t> items, uint8_t idx);\ntypedef void(*listRenderer_t)( std::vector<Item_t> items, uint8_t selected_idx );\ntypedef void(*cb_t)();\n\nvoid lsFlashPartitions();\nvoid lsNVSpartitions();\n\n\nstd::vector<sdu_filesystem_t> filesystems_enabled;\nconst int colors[] = { TFT_WHITE, TFT_CYAN, TFT_RED, TFT_YELLOW, TFT_BLUE, TFT_GREEN };\nconst char* const names[] = { \"none\", \"wasHold\", \"wasClicked\", \"wasPressed\", \"wasReleased\", \"wasDeciedCount\" };\n\nstd::string scrollableTitle = \"\";\nscrollClip_t scrollClip = {0,0,0,0};\nbool do_scroll = false;\n\n\nnamespace AppTheme\n{\n  using namespace SDU_UI;\n\n  fontInfo_t Font0Size1 = {&Font0, 1};\n  fontInfo_t Font0Size2 = {&Font0, 2};\n  fontInfo_t Font2Size1 = {&Font2, 1};\n  fontInfo_t DejaVu12Size1 = {&DejaVu12, 1};\n  fontInfo_t DejaVu18Size1 = {&DejaVu18, 1};\n  fontInfo_t Font8x8C64Size1 = {&Font8x8C64, 1};\n  fontInfo_t FreeMono9pt7bSize1 = {&FreeMono9pt7b,1};\n  fontInfo_t FreeMono12pt7bSize1 = {&FreeMono12pt7b,1};\n  fontInfo_t FreeSans9pt7bSize1 = {&FreeSans9pt7b, 1};\n  fontInfo_t FreeSans12pt7bSize1 = {&FreeSans12pt7b, 1};\n\n  const uint16_t ListFgColor = TFT_WHITE;\n  const uint16_t ListBgColor = M5.Lcd.color565( 0x20, 0x20, 0x20);\n\n  const int32_t ListOffsetX=10;\n  const int32_t ListOffsetY=40;\n  int32_t ListPadding=6;\n\n\n  const BtnStyle_t UpBtn     = { 0x73AE, 0x630C, TFT_WHITE, TFT_BLACK };\n  const BtnStyle_t SelectBtn = { 0x73AE, 0x630C, TFT_WHITE, TFT_BLACK };\n  const BtnStyle_t DownBtn   = { 0x73AE, 0x630C, TFT_WHITE, TFT_BLACK };\n  const uint16_t MsgFontColors[2] = { ListFgColor, ListBgColor };\n  const BtnStyles_t BtnStyles( UpBtn, SelectBtn, DownBtn, BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HWIDTH, &FreeSans9pt7bSize1, &Font0Size2, MsgFontColors );\n  SplashPageElementStyle_t TitleStyle      = { TFT_BLACK,     TFT_WHITE, &Font0Size2, MC_DATUM, TFT_LIGHTGREY, TFT_DARKGREY };\n  SplashPageElementStyle_t AppNameStyle    = { TFT_LIGHTGREY, TFT_BLACK, &Font0Size2, BC_DATUM, 0, 0 };\n  SplashPageElementStyle_t AuthorNameStyle = { TFT_LIGHTGREY, TFT_BLACK, &Font0Size2, BC_DATUM, 0, 0 };\n  SplashPageElementStyle_t AppPathStyle    = { TFT_DARKGREY,  TFT_BLACK, &Font0Size1, BC_DATUM, 0, 0 };\n  Theme_t Theme = { &BtnStyles, &TitleStyle, &AppNameStyle, &AuthorNameStyle, &AppPathStyle, &ProgressStyle };\n};\n\n\n// ersatz for M5.update(), modified to also work with CoreS3's touch\nvoid HIDUpdate()\n{\n  #if defined __M5UNIFIED_HPP__\n  if( M5.getBoard()==lgfx::boards::board_M5StackCoreS3 && M5.Touch.isEnabled() ) {\n    // M5Unified doesn't handle Touch->M5.BtnX translation for CoreS3, give it a little help\n    uint8_t edge = 220;\n    auto ms = m5gfx::millis();\n    M5.Touch.update(ms);\n    uint_fast8_t btn_bits = 0;\n    int i = M5.Touch.getCount();\n    if( i>0 ) {\n      while (--i >= 0) {\n        auto raw = M5.Touch.getTouchPointRaw(i);\n        if (raw.y > edge) {\n          auto det = M5.Touch.getDetail(i);\n          if (det.state & m5::touch_state_t::touch) {\n            if (M5.BtnA.isPressed()) { btn_bits |= 1 << 0; }\n            if (M5.BtnB.isPressed()) { btn_bits |= 1 << 1; }\n            if (M5.BtnC.isPressed()) { btn_bits |= 1 << 2; }\n            if (btn_bits || !(det.state & m5::touch_state_t::mask_moving)) {\n              btn_bits |= 1 << ((raw.x - 2) / 107);\n            }\n          }\n        }\n      }\n    }\n    M5.BtnA.setRawState(ms, btn_bits & 1);\n    M5.BtnB.setRawState(ms, btn_bits & 2);\n    M5.BtnC.setRawState(ms, btn_bits & 4);\n  } else\n  #endif\n  { // trust M5Unified\n    M5.update();\n  }\n}\n\n\nM5Btns_t getPressedButton(cb_t cb=nullptr)\n{\n  using namespace AppTheme;\n  SDUCfg.onButtonDraw( \"Up\",     0, BtnStyles.Load.BorderColor, BtnStyles.Load.FillColor, BtnStyles.Load.TextColor, BtnStyles.Load.ShadowColor );\n  SDUCfg.onButtonDraw( \"Select\", 1, BtnStyles.Skip.BorderColor, BtnStyles.Skip.FillColor, BtnStyles.Skip.TextColor, BtnStyles.Skip.ShadowColor );\n  SDUCfg.onButtonDraw( \"Down\",   2, BtnStyles.Save.BorderColor, BtnStyles.Save.FillColor, BtnStyles.Save.TextColor, BtnStyles.Save.ShadowColor );\n\n  while(true) {\n    HIDUpdate();\n    if( M5.BtnA.wasPressed() ) return M5_BTNA;\n    if( M5.BtnB.wasPressed() ) return M5_BTNB;\n    if( M5.BtnC.wasPressed() ) return M5_BTNC;\n    if( cb ) cb();\n  }\n  return M5_NOBTN;\n}\n\n\nvoid scrollTitle()\n{\n  using namespace AppTheme;\n  const uint32_t delay_ms = 120;\n  static uint32_t last_scroll_ms = millis();\n  uint32_t now = millis();\n  if( last_scroll_ms+delay_ms>now ) return;\n  std::rotate(scrollableTitle.begin(), scrollableTitle.begin() + 1, scrollableTitle.end());\n  M5.Lcd.setTextDatum( TL_DATUM ); // text coords are top-left based\n  setFontInfo( SDU_GFX, &DejaVu18Size1 );\n  M5.Lcd.setTextColor( MsgFontColors[1], MsgFontColors[0] );\n  //M5.Lcd.setClipRect(scrollClip.x, scrollClip.y, scrollClip.w, scrollClip.h);\n  M5.Lcd.drawString( scrollableTitle.c_str(), scrollClip.x, scrollClip.y );\n  //M5.Lcd.clearClipRect();\n  last_scroll_ms = now;\n}\n\n\nvoid renderList( std::vector<Item_t> items, uint8_t selected_idx )\n{\n  using namespace AppTheme;\n  if( selected_idx >= items.size() ) return;\n\n  M5.Lcd.setTextDatum( TL_DATUM ); // text coords are top-left based\n  setFontInfo( SDU_GFX, &DejaVu18Size1 );\n\n  // figure out pagination variables\n  uint16_t max_avail_width = M5.Lcd.width()-ListOffsetX;\n  uint8_t box_height = M5.Lcd.height() - (ListOffsetY + ListPadding + BtnStyles.height);\n  uint8_t line_height = M5.Lcd.fontHeight()*1.5;\n  uint8_t max_items_per_page = box_height / line_height;\n  uint8_t page_num = selected_idx/max_items_per_page;\n  uint8_t item_idx = selected_idx%max_items_per_page;\n  uint8_t item_start = page_num*max_items_per_page;\n  uint8_t item_end = item_start+(max_items_per_page);\n  if( item_end >items.size() ) item_end = items.size();\n  uint8_t items_visible = item_end-item_start;\n\n  M5.Lcd.setClipRect( 0, ListOffsetY-ListPadding, M5.Lcd.width(), max_items_per_page*line_height+ListPadding  );\n  M5.Lcd.fillRoundRect( 0, ListOffsetY-ListPadding, M5.Lcd.width(), max_items_per_page*line_height+ListPadding, ListPadding*2, ListBgColor );\n\n  for( int i=item_start; i<item_end; i++ ) {\n    int32_t ypos = i%max_items_per_page * line_height;\n    if( selected_idx == i ) { // highlighted\n      M5.Lcd.setTextColor( MsgFontColors[1], MsgFontColors[0] );\n      uint32_t tw = M5.Lcd.textWidth(items[i].name.c_str());\n      if( tw > max_avail_width ) {\n        scrollClip = { ListOffsetX, ListOffsetY+ypos, max_avail_width, line_height };\n        scrollableTitle = \" \" + items[i].name + \" \";\n        do_scroll = true;\n      } else {\n        do_scroll = false;\n      }\n    } else { // regular\n      M5.Lcd.setTextColor( MsgFontColors[0], MsgFontColors[1] );\n    }\n\n    M5.Lcd.drawString( items[i].name.c_str(), ListOffsetX, ListOffsetY+ypos );\n  }\n  M5.Lcd.clearClipRect();\n}\n\n\nvoid* paginateLoop( uint8_t& menu_idx, std::vector<Item_t> items, resultGetter_t getter, listRenderer_t renderList )\n{\n  while(1) {\n    renderList( items, menu_idx ); // render paginated list\n    auto btn_idx = getPressedButton( do_scroll?scrollTitle:nullptr ); // poll BtnA/B/C\n    switch( btn_idx ) { // paginate\n      case M5_BTNA: menu_idx = (menu_idx>0) ? menu_idx-1: items.size()-1; break; // btn up\n      case M5_BTNB: return getter( items, menu_idx ); break; // btn action\n      case M5_BTNC: menu_idx = (menu_idx<items.size()-1) ? menu_idx+1: 0; break; // btn down\n      default: return nullptr;\n    }\n  }\n}\n\n\nvoid *fsGetter(std::vector<Item_t> items, uint8_t idx)\n{\n  if( idx==0 || idx >= filesystems_enabled.size() ) return nullptr;\n  auto fsitem = (sdu_filesystem_t*) &filesystems_enabled[idx];\n  return (void*)fsitem->fs;\n}\n\n\nfs::FS* fsPicker()\n{\n  using namespace AppTheme;\n  using namespace SDU_UI;\n  M5.Lcd.setClipRect( 0, 0, 0, 0 ); // ignore SdUpdater UI messages as it is only a scan\n  SDUpdater sdUpdater;\n  M5.Lcd.clearClipRect();\n  SDUpdater::_message(\"Scanning filesystems...\");\n  M5.Lcd.setClipRect( 0, 0, 0, 0 );\n\n  sdu_filesystem_t filesystems[] =\n  {\n    { nullptr,   \"..\",       true                                         },\n    { &SD,       \"SD\",       ConfigManager::hasFS( &sdUpdater, SD )       },\n    { &LittleFS, \"LittleFS\", ConfigManager::hasFS( &sdUpdater, LittleFS ) },\n    { &SPIFFS,   \"SPIFFS\",   ConfigManager::hasFS( &sdUpdater, SPIFFS )   },\n    { &FFat,     \"FFat\",     ConfigManager::hasFS( &sdUpdater, FFat )     }\n  };\n\n  M5.Lcd.clearClipRect();\n\n  filesystems_enabled.clear();\n  //std::vector<std::string> filesystems_names;\n  std::vector<Item_t> filesystems_vect;\n  size_t fscount = sizeof(filesystems)/sizeof(sdu_filesystem_t);\n  uint8_t menu_idx = 0;\n\n  for( int i=0; i<fscount; i++ ) {\n    if( filesystems[i].enabled ) {\n      filesystems_enabled.push_back( filesystems[i] );\n      //filesystems_names.push_back( std::string(filesystems[i].name) );\n      filesystems_vect.push_back( { std::string(filesystems[i].name), filesystems[i].fs } );\n    }\n  }\n\n  if( filesystems_vect.size() == 1 ) return nullptr; // no readable FS found\n  if( filesystems_vect.size() == 2 ) return (fs::FS*)filesystems_vect[1].item; // only one real fs found, no need to pick\n\n  drawSDUSplashElement( \"Choose a filesystem\", M5.Lcd.width()/2, 0, &TitleStyle );\n\n  fs::FS* ret = (fs::FS*)paginateLoop( menu_idx, filesystems_vect, fsGetter, renderList );\n  return ret;\n}\n\n\nvoid *fileGetter(std::vector<Item_t> items, uint8_t idx)\n{\n  if( idx == 0 ) return nullptr;\n  static String outFile = \"\";\n  outFile = String(items[idx].name.c_str());\n  return (void*)outFile.c_str();\n}\n\n\nconst char* filePicker( fs::FS *fs )\n{\n  using namespace AppTheme;\n  using namespace SDU_UI;\n  SDUpdater::_message(\"Scanning files...\");\n\n  static String outFile = \"\";\n  File root = fs->open( \"/\" );\n  if( !root ){\n    SDUpdater::_error( \"Opendir '/' failed\" );\n    return nullptr;\n  }\n  auto file = root.openNextFile();\n\n  std::vector<Item_t> files_vec;\n  files_vec.push_back({\"..\", nullptr});\n  uint8_t menu_idx = 0;\n\n  while( file ){\n    auto fname = String( SDUpdater::fs_file_path(&file) );\n    if( fname.endsWith(\".bin\") || fname.endsWith(\".gz\") ) {\n      files_vec.push_back( { std::string( fname.c_str() ), nullptr } );\n    }\n    file = root.openNextFile();\n  }\n  root.close();\n\n  drawSDUSplashElement( \"Choose a binary file\", M5.Lcd.width()/2, 0, &TitleStyle );\n\n  if( files_vec.size() == 1 ) return nullptr; // no readable FS found\n\n  const char* ret = (const char*)paginateLoop( menu_idx, files_vec, fileGetter, renderList );\n\n  return ret;\n}\n\n\nstd::vector<Item_t> GetSlotItems( bool show_hidden )\n{\n  std::vector<Item_t> slots;\n  slots.push_back({\"..\",nullptr});\n  for( int i=0; i<NVS::Partitions.size(); i++ ) {\n    auto nvs_part = &NVS::Partitions[i];\n    if( nvs_part->bin_size > 0 ) {\n      String slotNameStr = String(nvs_part->name);\n      if( slotNameStr.endsWith(\".bin\") ) {\n        slotNameStr.replace(\".bin\", \"\"); // lose the trailing extension\n      }\n      if( slotNameStr[0] == '/' ) {\n        slotNameStr = slotNameStr.substring( 1, slotNameStr.length());  // lose the leading slash\n      }\n      slotNameStr = \"[\" + String(nvs_part->ota_num) + \"] \" + slotNameStr; // prepend partition number\n      std::string slotname = std::string( slotNameStr.c_str() );\n      slots.push_back( {slotname,nvs_part} );\n    } else {\n      if( show_hidden && nvs_part->ota_num!=0 ) { // slot 0 must always be available\n        std::string slotname = std::string(\"Slot \") + std::to_string( nvs_part->ota_num ) + std::string(\" available\");\n        slots.push_back( {slotname,nvs_part} );\n      }\n    }\n  }\n  return slots;\n}\n\n\nvoid *slotGetter(std::vector<Item_t> items, uint8_t idx)\n{\n  static int8_t ret;\n  if( idx>=items.size() ) return nullptr;\n  return items[idx].item;\n}\n\n\nint8_t slotPicker( const char* label,  bool show_hidden=true )\n{\n  using namespace AppTheme;\n  using namespace SDU_UI;\n  uint8_t menu_idx = 0;\n  std::vector<Item_t> slots_vec;\n  slots_vec = GetSlotItems( show_hidden );\n  drawSDUSplashElement( label, M5.Lcd.width()/2, 0, &TitleStyle );\n  if( slots_vec.size() == 1 ) {\n    if( show_hidden ) {\n      SDUpdater::_error(\"No OTA slots found :-(\");\n    } else {\n      SDUpdater::_message(\"All OTA slots are free\");\n      delay(1000);\n    }\n    return -1;\n  }\n  auto nvs_part = (NVS::PartitionDesc_t*)paginateLoop( menu_idx, slots_vec, slotGetter, renderList );\n  return nvs_part ? nvs_part->ota_num : -1;\n}\n\n\nvoid menuItemLoadFW()\n{\n  auto ota_num = slotPicker(\"Run Flash App\", false); // load apps\n  if( ota_num > 0 ) {\n    String msg = \"Booting partition \" + String(ota_num);\n    SDUpdater::_message(msg);\n    if( !Flash::bootPartition( ota_num ) ) {\n      SDUpdater::_error(\"Partition unbootable :(\");\n    }\n  }\n}\n\n\nvoid menuItemLoadFS()\n{\n  auto fs = fsPicker();\n  if( !fs ) return;\n  auto file = filePicker(fs);\n  if( !file ) return;\n  String fsBin = String(file);\n  log_w(\"Will overwrite next OTA slot\");\n  SDUCfg.use_rollback = false;\n  updateFromFS( *fs, fsBin );\n}\n\n\nvoid menuItemPartitionsInfo()\n{\n  using namespace AppTheme;\n  using namespace SDU_UI;\n  while(1) {\n    auto ota_num = slotPicker(\"Show Partition Info\", false); // load apps\n    if( ota_num < 0 ) {\n      log_d(\"leaving menuItemPartitionsInfo\");\n      return;\n    }\n\n    drawSDUSplashElement( \"Partition Info\", M5.Lcd.width()/2, 0, &TitleStyle );\n    uint16_t box_top_y = ListOffsetY;\n    uint16_t box_height = M5.Lcd.height() - (box_top_y + BtnStyles.height + ListPadding*2);\n    uint16_t box_hmiddle = M5.Lcd.width()/2;\n    uint16_t box_vmiddle = M5.Lcd.height()/2;\n    M5.Lcd.fillRoundRect( 0, ListOffsetY-ListPadding, M5.Lcd.width(), box_height, ListPadding*2, ListBgColor );\n    M5.Lcd.setTextColor( MsgFontColors[0], MsgFontColors[1] );\n\n    auto nvs_part = NVS::findPartition( ota_num );\n    auto sdu_part = Flash::findPartition( ota_num );\n    auto part     = &sdu_part.part;\n    auto meta     = sdu_part.meta;\n    if(!part) continue;\n\n    String AppName = \"n/a\";\n\n    if( Flash::partitionIsApp( part ) ) {\n      if( Flash::partitionIsFactory( part ) ) {\n        AppName = \"Factory\";\n      } else {\n        // TODO: match digest with application meta (binary name, initial path, picture, description)\n        AppName = \"OTA\" + String( part->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n      }\n    }\n\n    M5.Lcd.setTextDatum( TL_DATUM ); // text coords are top-left based\n    setFontInfo( SDU_GFX, &FreeMono9pt7bSize1 );\n\n    M5.Lcd.setClipRect( ListPadding, box_top_y, M5.Lcd.width(), box_height );\n    M5.Lcd.setCursor( ListPadding, box_top_y );\n\n    M5.Lcd.printf(\"Name:  %s\\n\", nvs_part->name);\n    M5.Lcd.printf(\"Slot:  %d (%s)\\n\", nvs_part->ota_num, AppName.c_str());\n    M5.Lcd.printf(\"Type:  0x%02x\\n\", part->type);\n    M5.Lcd.printf(\"SType: 0x%02x\\n\", part->subtype);\n    M5.Lcd.printf(\"Addr:  0x%06lx\\n\", part->address);\n    M5.Lcd.printf(\"Size:  %lu\\n\", part->size);\n    M5.Lcd.printf(\"Used:  %s\\n\", meta.image_len>0 ? String(meta.image_len).c_str() : \"n/a\");\n    //M5.Lcd.printf(\"Desc:  %s\\n\", nvs_part->desc[0]!=0?nvs_part->desc:\"none\");\n\n    M5.Lcd.clearClipRect();\n\n    if( Flash::partitionIsApp(part)&&Flash::metadataHasDigest(&meta) ) {\n\n      setFontInfo( SDU_GFX, &Font8x8C64Size1 );\n\n      uint16_t *metaBytes16 = (uint16_t*)meta.image_digest;\n      uint16_t squareSize = M5.Lcd.fontWidth()*12;\n      uint16_t clipLeft = M5.Lcd.width()-squareSize;\n\n      M5.Lcd.drawString( \"digest\", clipLeft, box_top_y+30);\n      M5.Lcd.setClipRect( clipLeft, box_top_y+42, squareSize, squareSize );\n      M5.Lcd.setCursor( clipLeft, box_top_y+42);\n\n      for( int i=0;i<16;i++ ) {\n        uint8_t* bytes = (uint8_t*)&metaBytes16[i];\n        M5.Lcd.printf(\"%02x %02x \", bytes[0], bytes[1] );\n      }\n\n      M5.Lcd.clearClipRect();\n\n      // draw a 4x4@16bpp matrix of the digest, directly from the partition metadata buffer :)\n      M5.Lcd.pushImageRotateZoom( M5.Lcd.width()-(16+ListPadding), box_vmiddle+60, 2, 2, 0.0, 8.0, 8.0, 4, 4, metaBytes16, lgfx::rgb565_2Byte );\n    }\n\n    getPressedButton();\n  }\n}\n\n\nvoid handleResult( bool res, const char* msg )\n{\n  String message = String(msg) + \" \";\n  if(!res ) {\n    message +=\"Fail\";\n    SDUpdater::_error(message.c_str());\n  } else {\n    message +=\"Success\";\n    SDUpdater::_message(message.c_str());\n  }\n  delay(1000);\n}\n\n\nvoid menuItemPartitionFlash()\n{\n  auto ret = PartitionManager::flash( slotPicker(\"Flash Slot\", true), fsPicker, filePicker );\n  handleResult( ret, \"Flash\");\n}\n\n\nvoid menuItemPartitionBackup()\n{\n  auto ret = PartitionManager::backup( slotPicker(\"Backup Slot\", true), fsPicker );\n  handleResult( ret, \"Backup\");\n}\n\n\nvoid menuItemPartitionErase()\n{\n  auto ret = PartitionManager::erase(slotPicker(\"Erase Slot\"));\n  handleResult( ret, \"Erase\");\n}\n\n\nvoid menuItemPartitionVerify()\n{\n  auto ret = PartitionManager::verify(slotPicker(\"Verify Slot\"));\n  handleResult( ret, \"Verify\");\n}\n\n\nvoid *menuItemGetter(std::vector<Item_t> items, uint8_t idx)\n{\n  if( idx >= items.size() ) return nullptr; // first menu item is always null\n  static int8_t ret;\n  ret = idx;\n  return (void*)&ret;\n}\n\n\n\nvoid snoozeUI()\n{\n  using namespace AppTheme;\n  using namespace SDU_UI;\n  drawSDUSplashElement( \"Snooze settings\", M5.Lcd.width()/2, 0, &TitleStyle );\n  drawSDUSplashElement( \"Disabled\", M5.Lcd.width()/2, M5.Lcd.height()/3, &TitleStyle );\n\n  uint8_t delay_mn = 10;\n\n  while( 1 ) {\n    auto btnId = getPressedButton();\n    switch( btnId ) {\n      case M5_BTNA: delay_mn++; break;\n      case M5_BTNC: delay_mn--; break;\n      default: return; break; // TODO: save delay_mn in NVS\n    }\n    drawSDUSplashElement( String(\"Sleep after \" + String(delay_mn)+\"mn\").c_str(), M5.Lcd.width()/2, M5.Lcd.height()/2, &TitleStyle );\n  }\n}\n\n\nvoid brightnessUI()\n{\n  using namespace AppTheme;\n  using namespace SDU_UI;\n  drawSDUSplashElement( \"Brightness\", M5.Lcd.width()/2, 0, &TitleStyle );\n  drawSDUSplashElement( String(M5.Lcd.getBrightness()).c_str(), M5.Lcd.width()/2, M5.Lcd.height()/2, &TitleStyle );\n\n  while( 1 ) {\n    uint8_t brightness = M5.Lcd.getBrightness();\n    auto btnId = getPressedButton();\n    switch( btnId ) {\n      case M5_BTNA: brightness+=8; break;\n      case M5_BTNC: brightness-=8; break;\n      default: return; break; // TODO: save brightness in NVS\n    }\n    M5.Lcd.setBrightness( brightness );\n    drawSDUSplashElement( String(brightness).c_str(), M5.Lcd.width()/2, M5.Lcd.height()/2, &TitleStyle );\n  }\n}\n\n\n\n\nvoid toolsPicker()\n{\n  using namespace AppTheme;\n  using namespace SDU_UI;\n  std::vector<Item_t> labels_vec;\n  labels_vec.push_back({\"..\", nullptr});\n  labels_vec.push_back({\"Print Partition Info\", nullptr});\n  labels_vec.push_back({\"Add firmware\", nullptr});\n  labels_vec.push_back({\"Backup firmware\", nullptr});\n  labels_vec.push_back({\"Remove firmware\", nullptr});\n  labels_vec.push_back({\"Verify firmware\", nullptr});\n  //labels_vec.push_back({\"Clear NVS\", nullptr});\n  //labels_vec.push_back({\"List NVS key/value pairs\", nullptr});\n\n  while(1) {\n    drawSDUSplashElement( \"Partition Tools\", M5.Lcd.width()/2, 0, &TitleStyle );\n    uint8_t menu_idx = 0;\n    int8_t* ret = (int8_t*)paginateLoop( menu_idx, labels_vec, menuItemGetter, renderList );\n\n    if( ret ) {\n      switch( *ret ) {\n        case 1: menuItemPartitionsInfo() ; break;\n        case 2: menuItemPartitionFlash() ; break;\n        case 3: menuItemPartitionBackup(); break;\n        case 4: menuItemPartitionErase() ; break;\n        case 5: menuItemPartitionVerify(); break;\n        default: log_d(\"Leaving toolsPicker\"); return; break;\n      }\n    }\n  }\n}\n\n\nvoid preferencesPicker()\n{\n  using namespace AppTheme;\n  using namespace SDU_UI;\n  std::vector<Item_t> labels_vec;\n  labels_vec.push_back({\"..\", nullptr});\n  labels_vec.push_back({\"Change Bightness\", nullptr});\n  labels_vec.push_back({\"Set snooze delay\", nullptr});\n  labels_vec.push_back({\"Restart\", nullptr});\n  #if defined __M5UNIFIED_HPP__\n    labels_vec.push_back({\"Power Off\", nullptr});\n  #endif\n\n  while(1) {\n    drawSDUSplashElement( \"Misc Tools\", M5.Lcd.width()/2, 0, &TitleStyle );\n    uint8_t menu_idx = 0;\n    int8_t* ret = (int8_t*)paginateLoop( menu_idx, labels_vec, menuItemGetter, renderList );\n\n    if( ret ) {\n      switch( *ret ) {\n        case 1: brightnessUI(); break;\n        case 2: snoozeUI(); break;\n        case 3: ESP.restart() ; break;\n        #if defined __M5UNIFIED_HPP__\n          case 4: M5.Power.powerOff(); break;\n        #endif\n        default: log_d(\"Leaving Tools\"); return; break;\n      }\n    }\n  }\n}\n\n\n\nvoid launcherPicker()\n{\n  using namespace AppTheme;\n  using namespace SDU_UI;\n\n  std::vector<Item_t> labels_vec;\n  labels_vec.push_back({\"..\", nullptr});\n  labels_vec.push_back({\"Load Firmware (Flash)\", nullptr});\n  labels_vec.push_back({\"Load Firmware (Filesystem)\", nullptr});\n  labels_vec.push_back({\"Manage Partitions\", nullptr});\n  labels_vec.push_back({\"Tools\", nullptr});\n  labels_vec.push_back({\"Full Flash Backup\", nullptr});\n  if( Update.canRollBack() ) {\n    labels_vec.push_back({\"Rollback\", nullptr});\n  }\n\n  while(1) {\n    drawSDUSplashElement( \"Firmware Launcher\", M5.Lcd.width()/2, 0, &TitleStyle );\n    uint8_t menu_idx = 0;\n    int8_t* ret = (int8_t*)paginateLoop( menu_idx, labels_vec, menuItemGetter, renderList );\n    if( ret ) {\n      switch( *ret ) {\n        case 1: menuItemLoadFW(); break;\n        case 2: menuItemLoadFS(); break;\n        case 3: toolsPicker(); break;\n        case 4: preferencesPicker(); break;\n        case 5: {\n          SDUpdater sdUpdater;\n          if( ConfigManager::hasFS( &sdUpdater, SD ) ) {\n            PartitionManager::backupFlash( &SD, \"/full_dump.fw\" );\n          }\n        } break;\n        case 6: Update.rollBack(); ESP.restart(); break;\n        default: log_d(\"Leaving launcherPicker\"); return; break;\n      }\n    }\n  }\n}\n\n\n// decorator for serial output\nvoid printFlashPartition( Flash::Partition_t* sdu_partition )\n{\n  Flash::digest_t digests = Flash::digest_t();\n\n  auto part = sdu_partition->part;\n  auto meta = sdu_partition->meta;\n\n  String AppName = \"n/a\";\n\n  if( Flash::partitionIsApp( &part ) ) {\n    if( Flash::partitionIsFactory( &part ) ) {\n      AppName = \"Factory\";\n    } else {\n      AppName = \"OTA\" + String( part.subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n    }\n  }\n\n  Serial.printf(\"%-8s   0x%02x      0x%02x   0x%06lx   %8lu  %8s %8s %8s\\n\",\n    String( part.label ).c_str(),\n    part.type,\n    part.subtype,\n    part.address,\n    part.size,\n    meta.image_len>0 ? String(meta.image_len).c_str() : \"n/a\",\n    AppName.c_str(),\n    Flash::partitionIsApp(&part)&&Flash::metadataHasDigest(&meta) ? digests.toString(meta.image_digest) : \"n/a\"\n  );\n}\n\n\nvoid lsFlashPartitions()\n{\n  Serial.println(\"Partition  Type   Subtype    Address   PartSize   ImgSize    Info    Digest\");\n  Serial.println(\"---------+------+---------+----------+----------+---------+--------+--------\");\n  for( int i=0; i<Flash::Partitions.size(); i++ ) {\n    printFlashPartition( &Flash::Partitions[i] );\n  }\n}\n\n#include \"base64.h\"\n\nvoid lsNVSpartitions()\n{\n  Flash::digest_t digests = Flash::digest_t();\n  for(int i=0;i<NVS::Partitions.size();i++ ) {\n    auto nvs_part = &NVS::Partitions[i];\n    if( nvs_part->bin_size > 0 ) {\n      Serial.printf(\"[%d] %s %s\\n\", nvs_part->ota_num, nvs_part->name, digests.toString( nvs_part->digest ) );\n    } else {\n      Serial.printf(\"[%d] %s slot\\n\", nvs_part->ota_num, i==0?\"Reserved\":\"Available\" );\n    }\n  }\n  Serial.println(\"\\nPartitions as CSV:\");\n\n\n  size_t blob_size = (sizeof(NVS::PartitionDesc_t)*NVS::Partitions.size());\n  NVS::blob_partition_t *bPart = new NVS::blob_partition_t(blob_size);\n\n  if( !bPart->blob) {\n    log_e(\"Can't allocate %d bytes for blob\", blob_size );\n    return;\n  }\n  size_t idx = 0;\n  for( int i=0; i<NVS::Partitions.size(); i++ ) {\n    idx = i*sizeof(NVS::PartitionDesc_t);\n    auto part = &NVS::Partitions[i];\n    memcpy( &bPart->blob[idx], part, sizeof(NVS::PartitionDesc_t) );\n  }\n\n\n  // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/nvs_partition_gen.html#csv-file-format\n  String b64 = base64::encode( (const uint8_t*)bPart->blob, blob_size );\n  Serial.printf(\"Sizeof PartitionDesc_t: %d bytes\\n\", sizeof(NVS::PartitionDesc_t) );\n  Serial.printf(\"Sizeof NVS blob: %d bytes\\n\", blob_size );\n  Serial.printf(\"Sizeof Base64: %d bytes\\n\", b64.length() );\n  Serial.println();\n  Serial.println(\"key,type,encoding,value\");//     <-- column header\n  Serial.printf(\"%s,namespace,,\\n\", NVS::PARTITION_NS ); // <-- First entry should be of type \"namespace\"\n  Serial.printf(\"%s,data,base64,%s\\n\", NVS::PARTITION_KEY, b64.c_str() );\n  Serial.println();\n  // key1,data,u8,1\n  // key2,file,string,/path/to/file\n}\n\n\nbool checkFactoryStickyPartition()\n{\n  Flash::scan();\n\n  if( Flash::Partitions.size() == 0 ) {\n    log_e(\"No flash partitions found\");\n    return false;\n  }\n\n  if( Flash::FactoryPartition == nullptr ) {\n    log_e(\"No factory partition found\");\n    return false;\n  }\n\n  SDUpdater::_message(\"Checking factory...\");\n  // will compare running partition with factory, and update if necessary\n  if( SDUpdater::saveSketchToFactory() ) {\n    // sketch was just saved to factory partition, mark it as bootable and restart\n    Flash::loadFactory(); // will trigger a restart on success\n    log_e(\"Switching to factory app failed :-(\");\n    return false;\n  }\n  SDUpdater::_message(\"Checking partitions...\");\n  if( !NVS::getPartitions() ) {\n    log_w(\"Partitions not found on NVS, creating\"); // first visit!\n    PartitionManager::createPartitions();\n  } else {\n    PartitionManager::updatePartitions(); // refresh\n  }\n\n  if( NVS::Partitions.size() == 0 ) {\n    log_e(\"Failed to create shadow copy of partitions table in NVS\");\n    return false;\n  }\n\n  // cleanup/update shadow copy of partitions table in NVS\n  PartitionManager::processPartitions();\n  return Flash::FactoryPartition != nullptr;\n}\n\n\n\n\nbool checkOTAStickyPartition()\n{\n  if( Flash::Partitions.size() == 0 ) {\n    log_e(\"No flash partitions found\");\n    return false;\n  }\n\n  const esp_partition_t* last_partition = esp_ota_get_next_update_partition(NULL);\n\n  if( !last_partition ) {\n    return false;\n  }\n\n  const esp_partition_t* next_partition = Flash::getNextAvailPartition( last_partition->type, last_partition->subtype );\n\n  bool check_for_migration = false;\n  Flash::digest_t digests = Flash::digest_t();\n\n  if( last_partition && next_partition ) {\n    Flash::Partition_t fPartLast = { *last_partition, Flash::getSketchMeta( last_partition ) };\n    Flash::Partition_t fPartNext = { *next_partition, Flash::getSketchMeta( next_partition ) };\n    if( ! digests.match(fPartLast.meta.image_digest, fPartNext.meta.image_digest) ) {\n      printFlashPartition( &fPartLast );\n      printFlashPartition( &fPartNext );\n      check_for_migration = true;\n    }\n  }\n\n  return check_for_migration;\n}\n\n\n\n\n\nvoid setup()\n{\n  using namespace AppTheme;\n  using namespace SDU_UI;\n\n  if( resetReason == 12 /*RTC_SW_CPU_RST*/ ) {\n    log_d(\"software reset detected, delaying\");\n    delay(1000);\n  }\n\n  M5.begin();\n  Serial.setTimeout(100);\n\n  SDUCfg.display = &M5.Lcd;\n  SDUCfg.setErrorCb( DisplayErrorUI );\n  SDUCfg.setMessageCb( DisplayUpdateUI );\n  SDUCfg.setButtonDrawCb( drawSDUPushButton );\n  SDUCfg.setButtonsTheme( &Theme );\n\n  SDU_UI::resetScroll();\n\n  //SDUpdater::_message(\"Booting factory...\");\n\n  if( ! checkFactoryStickyPartition() ) {\n    // print partitions for debug\n    lsFlashPartitions();\n    // NVSUtil::Dump(); // more debug\n    SDUpdater::_error(\"No factory context\");\n    SDUpdater::_error(\"Halting\"); // TODO: load SDUpdater lobby ?\n    while(1);\n  }\n\n  SDUpdater::_message(\"Checking for OTA migration...\");\n  checkOTAStickyPartition();\n\n  SDUpdater::_message(\"Enumerating NVS partition...\");\n  lsNVSpartitions();\n  SDUpdater::_message(\"Enumerating flash partition...\");\n  lsFlashPartitions();\n}\n\n\nvoid loop()\n{\n  menuItemLoadFW();\n  launcherPicker();\n}\n"
  },
  {
    "path": "examples/M5Stack-LittleFS-Snippet/M5Stack-LittleFS-Snippet.ino",
    "content": "/*\n *\n * M5Stack LittleFS Loader Snippet\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2019 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n *\n * To be used with M5Stack SD Menu https://github.com/tobozo/M5Stack-SD-Updater\n * This sketch is useless without a precompiled \"menu.bin\" saved on the SD Card.\n * You may compile menu.bin from the M5Stack-SD-Menu sketch.\n *\n * Just use this sketch as your boilerplate and code your app over it, then\n * compile it and put the binary on the sdcard. See M5Stack-SD-Menu for more\n * info on the acceptable file formats.\n *\n * When this app is in memory, booting the M5StickC with the Button A pushed will\n * flash back the menu.bin into memory.\n *\n */\n#include <M5Stack.h>\n#include <LittleFS.h>\n#include <ESP32-targz.h> // optional: https://github.com/tobozo/ESP32-targz\n\n#define SDU_APP_NAME \"M5Stack SDLoader Snippet\"\n#define SDU_APP_PATH \"/MY_SKETCH.bin\"\n#include <M5StackUpdater.h>\n\n\nvoid setup() {\n  Serial.begin(115200);\n  Serial.println(\"Welcome to the LittleFS-Updater minimal example!\");\n  Serial.println(\"Now checking if a button was pushed during boot ...\");\n\n  SDUCfg.setLabelMenu(\"<< Menu\");        // BtnA label: load menu.bin\n  SDUCfg.setLabelSkip(\"Launch\");         // BtnB label: skip the lobby countdown and run the app\n  SDUCfg.setLabelSave(\"Save\");           // BtnC label: save the sketch to the SD\n  SDUCfg.setAppName( SDU_APP_NAME );     // Lobby screen label: application name\n  SDUCfg.setBinFileName( SDU_APP_PATH ); // If file path to bin is set for this app, it will be checked at boot and created if not exist\n\n  // checkSDUpdater( SD );\n  checkSDUpdater(\n    LittleFS,     // filesystem (default=SD)\n    MENU_BIN,     // path to binary (default=/menu.bin, empty string=rollback only)\n    2000,         // wait delay, (default=0, will be forced to 2000 upon ESP.restart() )\n    TFCARD_CS_PIN // (usually default=4 but your mileage may vary)\n  );\n  Serial.println(\"Nope, will run the sketch normally\");\n\n\n}\n\nvoid loop() {\n\n}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/M5Stack-SD-Menu.ino",
    "content": "/*\n *\n * M5Stack SD Menu\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2019 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n *\n * This sketch is the menu application. It must be compiled once\n * (sketch / export compiled binary) and saved on the SD Card as\n * \"menu.bin\" for persistence, and initially flashed on the M5Stack.\n *\n * If the SD is blank, or no \"menu.bin\" exists, it will attempt to\n * self-replicate on the filesystem and create the minimal necessary\n * directory structure.\n *\n * The very insecure YOLO Downloader is now part of this menu and can\n * take care of downloading the lastest binaries from the registry.\n *\n * As SD Card mounting can be a hassle, using the ESP32 Sketch data\n * uploader is also possible. Any file sent using this method will\n * automatically be copied onto the SD Card on next restart.\n * This includes .bin, .json, .jpg and .mod files.\n * To enable this feature, set \"migrateSPIFFS\" to false\n *\n * The menu will list all available apps on the sdcard and load them\n * on demand.\n *\n * Once you're finished with the loaded app, push reset+BTN_A and it\n * loads the menu again. Rinse and repeat.\n *\n * Most of those apps will not embed this launcher, instead they should\n * include and implement the M5Stack SD Loader Snippet, a lighter version\n * of the loader dedicated to load and launch the menu.\n *\n * Usage: Push BTN_A on boot calls the menu (in app) or powers off the\n * M5Stack (in menu)\n *\n * Accepted file types on the SD:\n *   - [sketch name].bin the Arduino binary\n *   - [sketch name].jpg an image (max 200x100 but smaller is better)\n *   - [sketch name].json file with dimensions descriptions: {\"width\":xxx,\"height\":xxx,\"authorName\":\"tobozo\", \"projectURL\":\"http://blah\"}\n *\n * The file names must be the same (case matters!) and left int heir relevant folders.\n * For this you will need to create two folders on the root of the SD:\n *   /jpg\n *   /json\n * jpg and json are optional but must both be set if provided.\n *\n *\n * Persistence and fast loading:\n * - To speed up things when reloading the menu, an Update.canRollback() test is done first\n * - Loader partition information (digest, size) is saved into NVS for additional consistency\n * - Any binary named \"xxxLauncher\" (e.g. LovyanLauncher) will become the default launcher if selected and loaded from the menu\n *\n *\n */\n\n#include \"main/main.cpp\"\n\n\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SAM.h",
    "content": "#pragma once\n//#pragma GCC diagnostic ignored \"-Wdeprecated-declarations\"\n\n#include <vector>\n\n#if !defined(ARDUINO_M5STACK_ATOM_AND_TFCARD)\n#include \"core.h\"\n#endif\n\n#define M5SAM_MENU_TITLE_MAX_SIZE 24\n#define M5SAM_BTN_TITLE_MAX_SIZE 6\n#define M5SAM_MAX_SUBMENUS 8\n\n#ifndef M5SAM_LIST_MAX_COUNT\n#define M5SAM_LIST_MAX_COUNT 96\n#endif\n\n#define M5SAM_LIST_MAX_LABEL_SIZE 36 // list labels will be trimmed\n#define M5SAM_LIST_PAGE_LABELS 6\n\nvolatile static uint8_t _keyboardIRQRcvd;\nvolatile static uint8_t _keyboardChar;\n\n#ifdef ARDUINO_ODROID_ESP32\n  #define BUTTONS_COUNT 4\n#else\n  #define BUTTONS_COUNT 3\n#endif\n\nclass M5SAM\n{\n  public:\n    M5SAM();\n    void up();\n    void down();\n    void execute();\n    void windowClr();\n    void setColorSchema(uint16_t inmenucolor, uint16_t inwindowcolor, uint16_t intextcolor);\n    void drawAppMenu(String inmenuttl, String inbtnAttl, String inbtnBttl, String inbtnCttl);\n    #ifdef ARDUINO_ODROID_ESP32\n    void drawAppMenu(String inmenuttl, String inbtnAttl, String intSpeakerttl, String inbtnBttl, String inbtnCttl);\n    #endif\n    void GoToLevel(byte inlevel);\n    uint16_t getrgb(byte inred, byte ingrn, byte inblue);\n    void addMenuItem(byte levelID, const char *menu_title,const char *btnA_title,const char *btnB_title,const char *btnC_title, signed char goto_level, void(*function)());\n    void show();\n    void showList();\n    void clearList();\n    byte getListID();\n    void setListID(byte idx);\n    //String getListString();\n    void nextList( bool renderAfter = true );\n    void addList(String inLabel);\n    void setListCaption(String inCaption);\n    String keyboardGetString();\n    String lastBtnTittle[BUTTONS_COUNT];\n    uint8_t listMaxLabelSize = M5SAM_LIST_MAX_LABEL_SIZE; // list labels will be trimmed\n    uint8_t listPagination = M5SAM_LIST_PAGE_LABELS;\n    uint8_t listPageLabelsOffset = 80; // initially 80, pixels offset from top screen for list items\n    uint8_t listCaptionDatum = TC_DATUM; // initially TC_DATUM=top centered, TL_DATUM=top left (default), top/right/bottom/left\n    uint16_t listCaptionXPos = 160; // initially tft.width()/2, text cursor position-x for list caption\n    uint16_t listCaptionYPos = 45; // initially 45, text cursor position-x for list caption\n\n  private:\n    String listCaption;\n    void drawListItem(byte inIDX, byte postIDX);\n\n    void btnRestore();\n    void keyboardEnable();\n    void keyboardDisable();\n    static void keyboardIRQ();\n    void drawMenu(String inmenuttl, String inbtnAttl, String inbtnBttl, String inbtnCttl, uint16_t inmenucolor, uint16_t inwindowcolor, uint16_t intxtcolor);\n    #ifdef ARDUINO_ODROID_ESP32\n    void drawMenu(String inmenuttl, String inbtnAttl, String intSpeakerttl, String inbtnBttl, String inbtnCttl, uint16_t inmenucolor, uint16_t inwindowcolor, uint16_t intxtcolor);\n    #endif\n    struct MenuCommandCallback\n    {\n      char title[M5SAM_MENU_TITLE_MAX_SIZE + 1];\n      char btnAtitle[M5SAM_BTN_TITLE_MAX_SIZE + 1];\n      char btnBtitle[M5SAM_BTN_TITLE_MAX_SIZE + 1];\n      char btnCtitle[M5SAM_BTN_TITLE_MAX_SIZE + 1];\n      signed char gotoLevel;\n      void (*function)();\n    };\n    //String list_labels[M5SAM_LIST_MAX_COUNT];\n    std::vector<std::string> list_labels_str;\n    byte list_lastpagelines;\n    byte list_count;\n    byte list_pages;\n    byte list_page;\n    byte list_idx;\n    byte list_lines;\n\n    MenuCommandCallback *menuList[M5SAM_MAX_SUBMENUS];\n    byte menuIDX;\n    byte levelIDX;\n    byte menuCount[M5SAM_MAX_SUBMENUS];\n    uint16_t menucolor;\n    uint16_t windowcolor;\n    uint16_t menutextcolor;\n};\n\n\n\n\n\nM5SAM::M5SAM()\n//  : menuList(NULL),\n//    menuIDX(0),\n//    subMenuIDX(0),\n//    menuCount(0)\n{\n\n  _keyboardIRQRcvd = LOW;\n\n  levelIDX = 0;\n  menuCount[levelIDX] = 0;\n  menuIDX = 0;\n  menucolor     = tft.color565( 0,0,128 );\n  windowcolor   = tft.color565( 128,128,128 );\n  menutextcolor = tft.color565( 255,255,255 );\n  clearList();\n}\n\nvoid M5SAM::setListCaption(String inCaption)\n{\n  listCaption = inCaption;\n}\n\n\nvoid M5SAM::clearList()\n{\n  list_count = 0;\n  list_pages = 0;\n  list_page = 0;\n  list_lastpagelines = 0;\n  list_idx = 0;\n  list_labels_str.clear();\n  // for(byte x = 0; x<M5SAM_LIST_MAX_COUNT;x++){\n  //   list_labels[x] = \"\";\n  // }\n  listCaption = \"\";\n}\n\n\nvoid M5SAM::addList(String inStr)\n{\n  if(inStr.length()<=listMaxLabelSize && inStr.length()>0 && list_count<M5SAM_LIST_MAX_COUNT){\n    list_labels_str.push_back( inStr.c_str() );\n    list_count++;\n  }\n  if(list_count>0){\n    if(list_count > listPagination){\n      list_lastpagelines = list_count % listPagination;\n      if(list_lastpagelines>0) {\n        list_pages = (list_count - list_lastpagelines) / listPagination;\n        list_pages++;\n      }else{\n        list_pages = list_count / listPagination;\n      }\n    }else{\n      list_pages = 1;\n    }\n  }\n}\n\n\n\nbyte M5SAM::getListID()\n{\n  return list_idx;\n}\n\n\nvoid M5SAM::setListID(byte idx)\n{\n  if(idx< list_page * listPagination + list_lines){\n    list_idx = idx;\n  }\n  list_page = list_idx / listPagination;\n}\n\n\n\n// String M5SAM::getListString()\n// {\n//   return String( list_labels_str[list_idx] );\n//   //return list_labels[list_idx];\n// }\n\n\n\nvoid M5SAM::nextList( bool renderAfter )\n{\n  if(list_idx< list_page * listPagination + list_lines - 1){\n    list_idx++;\n  }else{\n    if(list_page<list_pages - 1){\n      list_page++;\n    }else{\n      list_page = 0;\n    }\n    list_idx = list_page * listPagination;\n  }\n  if( renderAfter ) showList();\n}\n\n\n\nvoid M5SAM::drawListItem(byte inIDX, byte postIDX)\n{\n  tft.setFont( &Font2 );\n  if(inIDX==list_idx){\n    tft.setFont( &Font2 );\n    //tft.drawString(list_labels[inIDX],15,listPageLabelsOffset+(postIDX*20));\n    tft.drawString(list_labels_str[inIDX].c_str(), 15, listPageLabelsOffset+(postIDX*20) );\n    tft.drawString(\">\",3,listPageLabelsOffset+(postIDX*20),&Font2);\n  }else{\n    tft.drawString(list_labels_str[inIDX].c_str(), 15, listPageLabelsOffset+(postIDX*20) );\n  }\n}\n\n\n\nvoid M5SAM::showList()\n{\n    windowClr();\n    byte labelid = 0;\n    tft.setTextDatum( listCaptionDatum );\n    //tft.drawCentreString(listCaption,tft.width()/2,45,2);\n    tft.drawString(listCaption, listCaptionXPos, listCaptionYPos, &Font2);\n    tft.setTextDatum( TL_DATUM );\n    if((list_page + 1) == list_pages){\n      if(list_lastpagelines == 0 and list_count >= listPagination){\n        list_lines = listPagination;\n        for(byte i = 0;i<listPagination;i++){\n          labelid = i+(list_page*listPagination);\n          drawListItem(labelid,i);\n        }\n      }else{\n        if(list_pages>1){\n          list_lines = list_lastpagelines;\n          for(byte i = 0;i<list_lastpagelines;i++){\n            labelid = i+(list_page*listPagination);\n            drawListItem(labelid,i);\n          }\n        }else{\n          list_lines = list_count;\n          for(byte i = 0;i<list_count;i++){\n            labelid = i+(list_page*listPagination);\n            drawListItem(labelid,i);\n          }\n        }\n      }\n    }else{\n        list_lines = listPagination;\n        for(byte i = 0;i<listPagination;i++){\n            labelid = i+(list_page*listPagination);\n            drawListItem(labelid,i);\n        }\n    }\n}\n\n\n\nvoid M5SAM::up()\n{\n  if(menuIDX<menuCount[levelIDX]-1){\n    menuIDX++;\n    show();\n  }\n}\n\n\n\nvoid M5SAM::down()\n{\n  if(menuIDX>0){\n    menuIDX--;\n    show();\n  }\n}\n\n\n\nvoid M5SAM::GoToLevel(byte inlevel)\n{\n  levelIDX = inlevel;\n  menuIDX = 0;\n  show();\n}\n\n\n\nvoid M5SAM::execute()\n{\n  if(menuList[levelIDX][menuIDX].gotoLevel==-1){\n    (*menuList[levelIDX][menuIDX].function)();\n  }else{\n    GoToLevel(menuList[levelIDX][menuIDX].gotoLevel);\n  }\n}\n\n\n\nvoid M5SAM::addMenuItem(byte levelID, const char *menu_title,const char *btnA_title,const char *btnB_title,const char *btnC_title, signed char goto_level,  void(*function)())\n{\n  byte mCnt = menuCount[levelID];\n  menuList[levelID] = (MenuCommandCallback *) realloc(menuList[levelID], (mCnt + 1) * sizeof(MenuCommandCallback));\n  strncpy(menuList[levelID][mCnt].title, menu_title, M5SAM_MENU_TITLE_MAX_SIZE);\n  strncpy(menuList[levelID][mCnt].btnAtitle, btnA_title, M5SAM_BTN_TITLE_MAX_SIZE);\n  strncpy(menuList[levelID][mCnt].btnBtitle, btnB_title, M5SAM_BTN_TITLE_MAX_SIZE);\n  strncpy(menuList[levelID][mCnt].btnCtitle, btnC_title, M5SAM_BTN_TITLE_MAX_SIZE);\n  menuList[levelID][mCnt].gotoLevel = goto_level;\n  menuList[levelID][mCnt].function = function;\n  menuCount[levelID]++;\n}\n\n\n\nvoid M5SAM::show()\n{\n  drawMenu(menuList[levelIDX][menuIDX].title, menuList[levelIDX][menuIDX].btnAtitle, menuList[levelIDX][menuIDX].btnBtitle, menuList[levelIDX][menuIDX].btnCtitle, menucolor, windowcolor, menutextcolor);\n}\n\n\n\nvoid M5SAM::windowClr()\n{\n  tft.fillRoundRect(0,32,tft.width(),tft.height()-32-32,3,windowcolor);\n}\n\n\n\nuint16_t M5SAM::getrgb(byte inred, byte ingrn, byte inblue)\n{\n  inred = map(inred,0,255,0,31);\n  ingrn = map(ingrn,0,255,0,63);\n  inblue = map(inblue,0,255,0,31);\n  return inred << 11 | ingrn << 5 | inblue;\n}\n\n\n\nvoid M5SAM::drawAppMenu(String inmenuttl, String inbtnAttl, String inbtnBttl, String inbtnCttl)\n{\n  drawMenu(inmenuttl, inbtnAttl, inbtnBttl, inbtnCttl, menucolor, windowcolor, menutextcolor);\n  tft.setTextColor(menutextcolor,windowcolor);\n}\n\n\n\nvoid M5SAM::setColorSchema(uint16_t inmenucolor, uint16_t inwindowcolor, uint16_t intextcolor)\n{\n  menucolor = inmenucolor;\n  windowcolor = inwindowcolor;\n  menutextcolor = intextcolor;\n}\n\n\n\nString M5SAM::keyboardGetString()\n{\n  String tmp_str = \"\";\n  boolean tmp_klock = HIGH;\n  keyboardEnable();\n  tft.fillRoundRect(0,tft.height()-28,tft.width(),28,3,windowcolor);\n  tft.drawString(\">\"+tmp_str,5,tft.height()-28+6,&Font2);\n  while(tmp_klock==HIGH){\n    if(_keyboardIRQRcvd==HIGH){\n      if(_keyboardChar == 0x08){\n        tmp_str = tmp_str.substring(0,tmp_str.length()-1);\n      }else if(_keyboardChar == 0x0D){\n        tmp_klock = LOW;\n      }else{\n        tmp_str = tmp_str + char(_keyboardChar);\n      }\n      tft.fillRoundRect(0,tft.height()-28,tft.width(),28,3,windowcolor);\n      tft.drawString(\">\"+tmp_str,5,tft.height()-28+6,&Font2);\n      _keyboardIRQRcvd = LOW;\n    }\n  }\n  keyboardDisable();\n  btnRestore();\n  return tmp_str;\n}\n\n\n/*\nvoid M5SAM::keyboardEnable(){\n  pinMode(5, INPUT);\n  attachInterrupt(digitalPinToInterrupt(5), keyboardIRQ, FALLING);\n  while(!digitalRead(5)){\n    Wire.requestFrom(0x08,1,true);\n    Wire.read();\n  }\n}\n\nvoid M5SAM::keyboardDisable(){\n  detachInterrupt(5);\n}\n\nvoid M5SAM::keyboardIRQ(){\n  while(!digitalRead(5)){\n    Wire.requestFrom(0x08,1,true);\n    _keyboardChar = Wire.read();\n  }\n  _keyboardIRQRcvd = HIGH;\n}\n*/\n\n#ifdef ARDUINO_ODROID_ESP32\n\n  #define BUTTON_WIDTH 60\n  #define BUTTON_HWIDTH BUTTON_WIDTH/2 // 30\n  #define BUTTON_HEIGHT 28\n  uint16_t buttonsXOffset[4] = {\n    1, 72, 188, 260\n  };\n\n  void M5SAM::drawMenu(String inmenuttl, String inbtnAttl, String intSpeakerttl, String inbtnBttl, String inbtnCttl, uint16_t inmenucolor, uint16_t inwindowcolor, uint16_t intxtcolor)\n  {\n    lastBtnTittle[1] = intSpeakerttl;\n    drawMenu(inmenuttl, inbtnAttl, inbtnBttl, inbtnCttl,  inmenucolor, inwindowcolor, intxtcolor);\n  }\n\n\n  void M5SAM::drawAppMenu(String inmenuttl, String inbtnAttl, String intSpeakerttl, String inbtnBttl, String inbtnCttl)\n  {\n    drawMenu(inmenuttl, inbtnAttl, intSpeakerttl, inbtnBttl, inbtnCttl, menucolor, windowcolor, menutextcolor);\n    tft.setTextColor(menutextcolor,windowcolor);\n  }\n\n#else\n\n  #define BUTTON_WIDTH 60\n  #define BUTTON_HWIDTH BUTTON_WIDTH/2 // 30\n  #define BUTTON_HEIGHT 28\n  uint16_t buttonsXOffset[3] =\n  {\n    31, 126, 221\n  };\n\n#endif\n\nvoid M5SAM::btnRestore()\n{\n  //using namespace SDU_UI;\n  tft.setTextColor(menutextcolor);\n  tft.setFont( &Font2 );\n  tft.fillRoundRect(0,tft.height()-BUTTON_HEIGHT,tft.width(),BUTTON_HEIGHT,3,0x00);\n  for( byte i=0; i<BUTTONS_COUNT; i++ ) {\n    tft.fillRoundRect(buttonsXOffset[i],tft.height()-BUTTON_HEIGHT,BUTTON_WIDTH,BUTTON_HEIGHT,3,menucolor);\n    if( lastBtnTittle[i] != \"\" ) {\n      tft.drawCentreString( lastBtnTittle[i], buttonsXOffset[i]+BUTTON_HWIDTH, tft.height()-BUTTON_HEIGHT+6 );\n\n    }\n  }\n  tft.setTextColor(menutextcolor,windowcolor);\n}\n\n\nvoid M5SAM::drawMenu(String inmenuttl, String inbtnAttl, String inbtnBttl, String inbtnCttl, uint16_t inmenucolor, uint16_t inwindowcolor, uint16_t intxtcolor)\n{\n  #ifdef ARDUINO_ODROID_ESP32\n    lastBtnTittle[0] = inbtnAttl;\n    //lastBtnTittle[1] = \"\";\n    lastBtnTittle[2] = inbtnBttl;\n    lastBtnTittle[3] = inbtnCttl;\n  #else\n    lastBtnTittle[0] = inbtnAttl;\n    lastBtnTittle[1] = inbtnBttl;\n    lastBtnTittle[2] = inbtnCttl;\n  #endif\n  for( byte i=0; i<BUTTONS_COUNT; i++ ) {\n    tft.fillRoundRect(buttonsXOffset[i],tft.height()-BUTTON_HEIGHT,BUTTON_WIDTH,BUTTON_HEIGHT,3,inmenucolor);\n  }\n  tft.fillRoundRect(0,0,tft.width(),BUTTON_HEIGHT,3,inmenucolor);\n  tft.fillRoundRect(0,32,tft.width(),tft.height()-32-32,3,inwindowcolor);\n\n  tft.setTextColor(intxtcolor);\n  tft.setFont( &Font2 );\n  tft.drawCentreString(inmenuttl,tft.width()/2,6);\n  for( byte i=0; i<BUTTONS_COUNT; i++ ) {\n    if( lastBtnTittle[i] != \"\" ) {\n      tft.drawCentreString(lastBtnTittle[i],buttonsXOffset[i]+BUTTON_HWIDTH,tft.height()-BUTTON_HEIGHT+6 );\n    }\n  }\n}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Colours_Demo.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"@Kongduino\", \"projectURL\": \"https://git.io/vptwc\",\"credits\":\"https://github.com/Kongduino/M5-Colours-Demo\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/CrackScreen.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"nomolk\",\"projectURL\":\"https://git.io/vptXq\",\"credits\":\"https://github.com/nomolk/M5Stack_CrackScreen\",\"credits\":\"\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Downloader.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"tobozo\",\"projectURL\":\"https://git.io/fhQ3F\",\"credits\":\"https://github.com/tobozo/M5Stack-SD-Updater/\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/FlappyBird.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"Domenico Ponticelli\", \"projectURL\": \"https://git.io/vptPB\",\"credits\":\"https://github.com/pcelli85/M5Stack_FlappyBird_game\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Lora_Frequency_Hopping.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"@Kongduino\", \"projectURL\": \"https://git.io/vptVQ\",\"credits\":\"https://github.com/Kongduino/M5_LoRa_Frequency_Hopping\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/LovyanLauncher.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"lovyan03\",\"projectURL\":\"https://git.io/fhdJV\",\"credits\":\"https://github.com/lovyan03\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/MultiApps-Adv.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"botofancalin\",\"projectURL\":\"https://git.io/vpu1r\",\"credits\":\"https://github.com/botofancalin/M5Stack-MultiApp-Advanced\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/NyanCat.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"tobozo\",\"projectURL\":\"https://git.io/vptXa\",\"credits\":\"https://github.com/tobozo/M5Stack-NyanCat\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/NyanCat_Ext.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"xisai\",\"projectURL\":\"https://git.io/vpQqA\",\"credits\":\"https://github.com/xisai/M5Stack_NyanCat\",\"assets\":[{\"name\":\"NyanCat.mp3\",\"path\":\"\\/mp3\\/\"}]}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Oscilloscope.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"@botofancalin\", \"projectURL\": \"https://git.io/vptVE\",\"credits\":\"https://github.com/botofancalin/M5Stack-ESP32-Oscilloscope\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/PacketMonitor.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"Spacehuhn\", \"projectURL\":\"https://git.io/vptPd\",\"credits\":\"https://github.com/spacehuhn/PacketMonitor32\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Pacman-JoyPSP.json",
    "content": "{\"width\":110, \"height\":110, \"authorName\":\"Masahage\", \"projectURL\": \"https://git.io/vptX1\",\"credits\":\"Macsbug https://macsbug.wordpress.com/2018/03/07/pacman-with-m5stack/\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Pixel-Fun.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"Harsh Talpada\", \"projectURL\": \"https://git.io/vptiz\",\"credits\":\"https://twitter.com/neoharsh\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Raytracer.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"tobozo\",\"projectURL\":\"https://git.io/fhQ35\",\"credits\":\"https://github.com/tobozo/M5Stack-Raytracer\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Rickroll.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"tobozo\",\"projectURL\":\"https://git.io/vptXS\",\"credits\":\"https://github.com/tobozo/M5Stack-Rickroll  http://www.buildlog.net/blog/2018/02/game-audio-for-the-esp32/\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/RotateyCube.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"tobozo\",\"projectURL\":\"https://git.io/vptXN\",\"credits\":\"https://github.com/tobozo/Rotatey_Balls https://www.reddit.com/user/GoblinJuicer\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/SWRasterizer.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"gyabo\",\"projectURL\":\"https://git.io/fjL8E\",\"credits\":\"https://github.com/kumaashi  https://twitter.com/gyabo\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Sokoban.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"Robo8080\", \"projectURL\": \"https://git.io/vpti5\",\"credits\":\"Masahage https://github.com/MhageGH/esp32_ILI9328_Sokoban\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/SpaceDefense.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"Dmitry Dimi\", \"projectURL\": \"https://git.io/vptXp\",\"credits\":\"https://github.com/dsiberia9s/SpaceDefense-m5stack\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/SpaceShooter.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"HailTheBDFL\", \"projectURL\": \"https://git.io/vpt1e\",\"credits\":\"https://github.com/HailTheBDFL/esp32-spaceShooter\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Tetris.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"Masahage\", \"projectURL\": \"https://git.io/vpt1f\",\"credits\":\"https://github.com/MhageGH/esp32_ST7735_Tetris\",\"assets\":[{\"name\":\"tetrisbg.jpg\",\"path\":\"\\/jpg\\/\"},{\"name\":\"Tetris_gh.jpg\",\"path\":\"\\/jpg\\/\"},{\"name\":\"Tetris.jpg\",\"path\":\"\\/jpg\\/\"}]}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Thermal-Camera.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"Offer DIYER\", \"projectURL\": \"https://git.io/vpjkL\",\"credits\":\"https://github.com/hkoffer/M5Stack-Thermal-Camera-\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/TobozoLauncher.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"tobozo\",\"projectURL\":\"https://git.io/vptwX\",\"credits\":\" github.com/reaper7  * * github.com/tobozo/M5Stack-SD-Updater * * \"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/Tube.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"tobozo\",\"projectURL\":\"https://git.io/fhQ37\",\"credits\":\"https://github.com/tobozo/M5Tube\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/WiFiScanner.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"Dimi\",\"projectURL\":\"https://github.com/PartsandCircuits/M5Stack-WiFiScanner\",\"credits\":\"https://github.com/PartsandCircuits/M5Stack-WiFiScanner\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/arduinomegachess.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"ususovsv@gmail.com\",\"projectURL\":\"https://bit.ly/2F1l9Cm\",\"credits\":\"https://www.hackster.io/Sergey_Urusov/arduino-mega-chess-for-m5stack-7feafb\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/d_invader.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"Ohishi Nobuaki\", \"projectURL\": \"https://git.io/vpt6X\",\"credits\":\"https://github.com/NibblesLab/d_invader\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/drawNumber.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"Kazuhiro Sasao\", \"projectURL\": \"https://git.io/vptVI\",\"credits\":\"https://gist.github.com/ksasao\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/menu.json",
    "content": "{\"width\":110,\"height\":110,\"authorName\":\"tobozo\",\"projectURL\":\"https://git.io/vptwX\",\"credits\":\" github.com/reaper7  * * github.com/tobozo/M5Stack-SD-Updater * * \"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/SD-Content/json/mp3-player.json",
    "content": "{\"width\":110,\"height\":110, \"authorName\":\"Dimi Siberian\", \"projectURL\": \"https://git.io/vptoK\",\"credits\":\"https://github.com/dsiberia9s\"}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/assets.h",
    "content": "/*\n * Image Source: https://i.imgur.com/llemDHF.gif\n *\n * - ImageMagick:\n *    #> convert -strip disk.gif disk%02d.jpg\n *\n * - Crop visual to 30x30 in an image editor\n *\n * - Export to C uchar format:\n *    #> xxd -i disk00.jpg >> assets.h\n *    #> xxd -i disk01.jpg >> assets.h\n *\n *\n */\n\nconst unsigned char disk00_jpg[1775] = {\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x1e, 0x00, 0x1e, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n  0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,\n  0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n  0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,\n  0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n  0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25,\n  0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n  0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56,\n  0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86,\n  0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n  0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n  0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n  0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n  0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00,\n  0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n  0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,\n  0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n  0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08,\n  0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n  0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18,\n  0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n  0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55,\n  0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n  0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84,\n  0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n  0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa,\n  0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n  0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n  0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00,\n  0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfe,\n  0xc7, 0x7f, 0x6f, 0xc0, 0x7f, 0xe1, 0x45, 0x78, 0x04, 0x16, 0x04, 0xaf,\n  0xed, 0xaf, 0xff, 0x00, 0x04, 0xd4, 0x0d, 0xb7, 0x20, 0x93, 0xff, 0x00,\n  0x0f, 0x16, 0xfd, 0x95, 0xf2, 0x06, 0x64, 0x24, 0x65, 0x8a, 0xb9, 0x0e,\n  0x64, 0x25, 0x41, 0x03, 0x2c, 0x55, 0xc7, 0xa9, 0xfe, 0xd7, 0x7a, 0xff,\n  0x00, 0xc5, 0x4f, 0x09, 0xfe, 0xc9, 0xff, 0x00, 0xb4, 0xff, 0x00, 0x8a,\n  0xbe, 0x05, 0x43, 0xad, 0xdc, 0xfc, 0x6f, 0xf0, 0xcf, 0xec, 0xf1, 0xf1,\n  0xab, 0xc4, 0x1f, 0x07, 0x2d, 0xfc, 0x37, 0xe1, 0xa8, 0xfc, 0x6d, 0xe2,\n  0x29, 0xfe, 0x2a, 0xe8, 0xdf, 0x0d, 0xbc, 0x4b, 0xa8, 0xfc, 0x3d, 0x87,\n  0x40, 0xf0, 0x64, 0xfa, 0x66, 0xb7, 0x07, 0x8b, 0xb5, 0xa9, 0x7c, 0x5d,\n  0x6d, 0xa4, 0x26, 0x97, 0xe1, 0x89, 0xb4, 0x5d, 0x5e, 0x2d, 0x7e, 0xf9,\n  0xa0, 0xd2, 0xa4, 0xd2, 0xef, 0xd2, 0xec, 0xda, 0x4b, 0xe4, 0xff, 0x00,\n  0xb4, 0xaf, 0xc5, 0x3f, 0x0c, 0xeb, 0xba, 0xb6, 0xbb, 0xfb, 0x33, 0xc7,\n  0xfb, 0x34, 0xfc, 0x6c, 0xfd, 0xaa, 0x35, 0xcf, 0xf8, 0x42, 0xbc, 0x0f,\n  0xf1, 0x2f, 0xe2, 0x27, 0x86, 0x3e, 0x0c, 0x6b, 0xff, 0x00, 0x07, 0xbc,\n  0x0d, 0xaa, 0x7c, 0x2a, 0xd0, 0xbc, 0x4b, 0xe3, 0x2f, 0x14, 0xc5, 0xf0,\n  0x4b, 0xc7, 0xb0, 0xf8, 0xf3, 0xe3, 0x37, 0xc7, 0xdf, 0xd9, 0xda, 0xeb,\n  0x43, 0xf1, 0x9d, 0xd7, 0xc4, 0x0f, 0x83, 0x1e, 0x39, 0xf1, 0x17, 0xc2,\n  0xef, 0x15, 0xfc, 0x12, 0xf1, 0x4e, 0xa7, 0xf1, 0x1b, 0xe1, 0x6f, 0x8d,\n  0x7e, 0x13, 0x58, 0xf8, 0xde, 0xe2, 0xf3, 0xe1, 0xb6, 0xb3, 0x27, 0xc3,\n  0x6d, 0x6f, 0x5d, 0xf8, 0xbc, 0xfc, 0x2c, 0x18, 0xc0, 0xfd, 0x84, 0x7f,\n  0xe0, 0xb5, 0xb9, 0x20, 0x95, 0xcf, 0xfc, 0x15, 0x95, 0x1b, 0x3d, 0x00,\n  0x3b, 0x5f, 0xfe, 0x0b, 0x48, 0xe8, 0xc3, 0x25, 0x78, 0x64, 0x64, 0x39,\n  0x01, 0x95, 0x81, 0xc1, 0x00, 0xf2, 0x9f, 0x12, 0x78, 0xbb, 0xc0, 0x1a,\n  0xdd, 0xa7, 0xc4, 0xaf, 0x0f, 0xfe, 0xce, 0x5f, 0x1a, 0xf5, 0xbf, 0x8f,\n  0xbf, 0xb2, 0x8e, 0x83, 0xfb, 0x43, 0xff, 0x00, 0xc1, 0x12, 0xb5, 0x9f,\n  0x0a, 0x78, 0xe6, 0x6f, 0xda, 0x27, 0xc6, 0xdf, 0xb5, 0x9f, 0x87, 0x2c,\n  0xbf, 0x68, 0x5f, 0x12, 0xff, 0x00, 0xc1, 0x50, 0xb4, 0xab, 0x2f, 0x8e,\n  0x3e, 0x0e, 0xd2, 0xbe, 0x3f, 0x78, 0xf7, 0xc7, 0xbf, 0x16, 0x3c, 0x48,\n  0x75, 0x8d, 0x1f, 0xe1, 0xb7, 0x85, 0xff, 0x00, 0x66, 0x6d, 0x6b, 0x5e,\n  0xf8, 0x41, 0x0f, 0xc4, 0x09, 0x74, 0x8f, 0x00, 0xd8, 0xf8, 0x8b, 0xc3,\n  0x3e, 0x30, 0xd3, 0xfc, 0x1f, 0xe1, 0xeb, 0x8f, 0x8b, 0x5a, 0xae, 0xb5,\n  0xe3, 0x5f, 0xaf, 0x3f, 0xe0, 0xa2, 0x9f, 0xf0, 0x48, 0x7f, 0xd9, 0x0b,\n  0xfe, 0x0a, 0x6e, 0xde, 0x04, 0xd5, 0x7e, 0x3d, 0x69, 0xbe, 0x33, 0xf0,\n  0x9f, 0xc4, 0x1f, 0x87, 0x62, 0x5d, 0x3b, 0x42, 0xf8, 0xb3, 0xf0, 0x87,\n  0x51, 0xf0, 0xaf, 0x86, 0xfe, 0x21, 0xde, 0x78, 0x36, 0x56, 0xd4, 0xee,\n  0x26, 0xf8, 0x73, 0xe2, 0x3d, 0x43, 0xc5, 0x9e, 0x0b, 0xf1, 0xe6, 0x87,\n  0xe2, 0x2f, 0x06, 0xdb, 0xeb, 0x9a, 0x94, 0x9e, 0x24, 0xd2, 0x2d, 0x35,\n  0x6f, 0x0f, 0xdc, 0xea, 0xbe, 0x17, 0xd7, 0x24, 0xd5, 0x6e, 0x7c, 0x17,\n  0xad, 0x78, 0x7a, 0xcb, 0xc6, 0x3e, 0x3f, 0xb0, 0xf1, 0x57, 0x98, 0x78,\n  0x6f, 0xc0, 0xb0, 0x78, 0x6b, 0xc4, 0x9e, 0x1f, 0xf1, 0x29, 0xff, 0x00,\n  0x82, 0x71, 0xff, 0x00, 0xc1, 0x56, 0xfc, 0x79, 0x71, 0xe1, 0xbd, 0x63,\n  0x4a, 0xf1, 0x36, 0x8f, 0xa0, 0xfc, 0x61, 0xff, 0x00, 0x82, 0x82, 0xfc,\n  0x2a, 0xf8, 0xed, 0xf0, 0xfe, 0x3f, 0x10, 0xe8, 0x57, 0xf1, 0xeb, 0x7e,\n  0x17, 0xf1, 0x0e, 0xa1, 0xf0, 0xc3, 0xe3, 0x5f, 0xfc, 0x15, 0xcf, 0xc7,\n  0xbf, 0x0e, 0x35, 0x6d, 0x73, 0xc2, 0x3e, 0x23, 0xb0, 0xd3, 0x7c, 0x5b,\n  0xe0, 0xad, 0x73, 0x57, 0xf0, 0xa6, 0xa3, 0xa9, 0x78, 0x3b, 0xc6, 0x9a,\n  0x2f, 0x87, 0xfc, 0x69, 0xe1, 0x7b, 0x8d, 0x27, 0xc5, 0xbe, 0x1c, 0xd0,\n  0xf5, 0xad, 0x3f, 0xf4, 0x97, 0xe0, 0x6f, 0xc6, 0xdd, 0x33, 0xe3, 0x5e,\n  0x99, 0xe3, 0x5f, 0xf8, 0xa3, 0x3c, 0x71, 0xf0, 0xd3, 0xc6, 0x7f, 0x0c,\n  0x7c, 0x70, 0xdf, 0x0e, 0xbe, 0x28, 0xfc, 0x2e, 0xf8, 0x88, 0x3c, 0x1f,\n  0x37, 0x8b, 0xfe, 0x1e, 0xf8, 0xce, 0x6f, 0x07, 0xf8, 0x3f, 0xe2, 0x46,\n  0x91, 0xa3, 0xea, 0x9a, 0xb7, 0xc3, 0x6f, 0x1a, 0x7c, 0x46, 0xf8, 0x6f,\n  0xad, 0x26, 0xbd, 0xf0, 0xc7, 0xe2, 0x27, 0xc3, 0xcf, 0x88, 0x1a, 0x75,\n  0xe7, 0x81, 0xbe, 0x20, 0x78, 0xbf, 0x4a, 0xb3, 0xd2, 0x7c, 0x69, 0xa7,\n  0xe8, 0x7a, 0xce, 0xa1, 0xa4, 0x78, 0xdb, 0x47, 0xf1, 0x67, 0x84, 0xbc,\n  0x36, 0x01, 0xe3, 0x5e, 0x06, 0xe3, 0xfe, 0x0a, 0x2f, 0xfb, 0x56, 0x31,\n  0x19, 0x51, 0xfb, 0x13, 0x7e, 0xc0, 0x60, 0xe7, 0x1b, 0x49, 0x5f, 0x8e,\n  0x7f, 0xf0, 0x52, 0xb6, 0x65, 0x25, 0x8a, 0xae, 0x42, 0xb2, 0x92, 0x0b,\n  0x00, 0x03, 0x02, 0x48, 0x07, 0x35, 0xf9, 0xd9, 0xff, 0x00, 0x05, 0x69,\n  0xfd, 0x8c, 0xff, 0x00, 0x63, 0x9d, 0x7f, 0xc5, 0x1e, 0x21, 0xff, 0x00,\n  0x82, 0x87, 0x7f, 0xc1, 0x46, 0x3c, 0x4d, 0xe3, 0x6f, 0x89, 0xff, 0x00,\n  0xb2, 0x37, 0xec, 0xd3, 0xfb, 0x2f, 0xb7, 0xc3, 0xad, 0x1f, 0xf6, 0x5b,\n  0xf0, 0xf9, 0xf8, 0xa9, 0xa6, 0x3d, 0x87, 0xc6, 0x2f, 0x15, 0xfc, 0x65,\n  0xd3, 0x6e, 0x2c, 0xbe, 0x2b, 0x78, 0x37, 0xc4, 0x7f, 0x0f, 0x7e, 0x2e,\n  0x78, 0x2a, 0x41, 0xe3, 0x8f, 0x89, 0x4f, 0xab, 0x78, 0x3f, 0xe0, 0x9d,\n  0xce, 0x83, 0xaa, 0x69, 0x7a, 0x2f, 0x85, 0xb5, 0x13, 0xff, 0x00, 0x08,\n  0xbf, 0x8a, 0x7e, 0x22, 0x78, 0xfb, 0x4d, 0xf0, 0xe7, 0x83, 0xed, 0x2e,\n  0xbc, 0x3d, 0xfa, 0x1f, 0xe0, 0x40, 0x47, 0xfc, 0x14, 0x5f, 0xf6, 0xa7,\n  0x7c, 0x2e, 0x5b, 0xf6, 0x2a, 0xfd, 0x81, 0x11, 0x8e, 0x46, 0xe0, 0x63,\n  0xf8, 0xe7, 0xff, 0x00, 0x05, 0x27, 0x3b, 0x72, 0x50, 0xef, 0x54, 0xcc,\n  0x92, 0x46, 0xc7, 0x6b, 0x79, 0x8c, 0xc1, 0xbe, 0x56, 0x1e, 0x5f, 0x69,\n  0xfb, 0x65, 0xfe, 0xcf, 0x9a, 0xe7, 0xed, 0x51, 0xfb, 0x37, 0xfc, 0x47,\n  0xf8, 0x0b, 0xe1, 0x7f, 0x1a, 0xf8, 0x27, 0xe1, 0xbe, 0xb9, 0xe3, 0xa5,\n  0xf0, 0x7a, 0xd9, 0xf8, 0xc7, 0xe2, 0x17, 0xc0, 0x7f, 0x87, 0x5f, 0xb4,\n  0xc7, 0x83, 0xb4, 0x65, 0xf0, 0xaf, 0x8e, 0xfc, 0x27, 0xe3, 0x4b, 0x84,\n  0xd5, 0x7e, 0x0a, 0x7c, 0x57, 0x56, 0xf0, 0x07, 0x8d, 0x45, 0xf5, 0xae,\n  0x89, 0x3e, 0x97, 0xa6, 0xff, 0x00, 0xc2, 0x43, 0x13, 0x8f, 0x0d, 0x6a,\n  0x77, 0xf6, 0xbe, 0x2b, 0xd2, 0x16, 0x3d, 0x6b, 0x42, 0xd3, 0x98, 0x80,\n  0x7c, 0x1d, 0xff, 0x00, 0x04, 0x34, 0xfd, 0x9f, 0x7e, 0x34, 0x7e, 0xcf,\n  0x1f, 0xb1, 0xa7, 0x88, 0xb4, 0xaf, 0x8b, 0xff, 0x00, 0x0e, 0x75, 0x8f,\n  0x81, 0x3a, 0x17, 0xc5, 0x3f, 0xda, 0x1b, 0xe2, 0xb7, 0xc7, 0x2f, 0x80,\n  0x5f, 0xb3, 0x3f, 0x89, 0x7e, 0x20, 0xf8, 0xaf, 0xe2, 0x27, 0x88, 0xff,\n  0x00, 0x66, 0x1f, 0xd9, 0xf7, 0xe2, 0x52, 0xf8, 0x53, 0x50, 0xf0, 0x27,\n  0xc1, 0x3d, 0x7a, 0xff, 0x00, 0xc6, 0x76, 0x36, 0x7a, 0x9e, 0x8d, 0xad,\n  0x69, 0x3a, 0xbd, 0xaf, 0x8a, 0x3c, 0x57, 0xab, 0xe8, 0xb7, 0x09, 0x0e,\n  0xbc, 0xda, 0x8f, 0x8b, 0xae, 0x35, 0xdf, 0x88, 0x1a, 0x6e, 0x83, 0xf1,\n  0x6b, 0x5e, 0xf1, 0xef, 0x86, 0xb4, 0x2f, 0xb0, 0xbf, 0x65, 0x60, 0x47,\n  0xc7, 0x5f, 0xf8, 0x29, 0x5e, 0x46, 0x33, 0xfb, 0x6b, 0xf8, 0x0d, 0x86,\n  0x7b, 0xaf, 0xfc, 0x3b, 0x9b, 0xf6, 0x02, 0x5c, 0x8f, 0x51, 0xb9, 0x58,\n  0x67, 0xa6, 0x54, 0x8e, 0xa0, 0xd7, 0xd9, 0xcb, 0x8c, 0x90, 0x15, 0x59,\n  0xc3, 0x64, 0xfc, 0xa1, 0x14, 0xbf, 0xef, 0x21, 0xdc, 0x3f, 0xd6, 0x1f,\n  0x98, 0x47, 0xe5, 0xf2, 0x58, 0xa4, 0x47, 0x0a, 0x71, 0xfb, 0xb1, 0xf1,\n  0x67, 0xec, 0xae, 0xad, 0xff, 0x00, 0x0b, 0xcf, 0xfe, 0x0a, 0x50, 0x54,\n  0x2e, 0xe9, 0x3f, 0x6d, 0x5f, 0x02, 0x49, 0xf7, 0xca, 0x64, 0x0f, 0xf8,\n  0x27, 0x4f, 0xec, 0x0a, 0xa1, 0xdd, 0xd6, 0x32, 0x4c, 0x85, 0x42, 0x29,\n  0x50, 0xbb, 0x02, 0x22, 0x00, 0x49, 0x52, 0xce, 0x01, 0xff, 0xd9\n};\n\nconst unsigned char disk01_jpg[1486] = {\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x1e, 0x00, 0x1e, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n  0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,\n  0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n  0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,\n  0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n  0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25,\n  0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n  0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56,\n  0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86,\n  0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n  0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n  0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n  0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n  0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00,\n  0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n  0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,\n  0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n  0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08,\n  0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n  0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18,\n  0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n  0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55,\n  0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n  0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84,\n  0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n  0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa,\n  0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n  0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n  0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00,\n  0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfe,\n  0xc7, 0x7f, 0x6f, 0xc0, 0x7f, 0xe1, 0x45, 0x78, 0x04, 0x16, 0x04, 0xaf,\n  0xed, 0xaf, 0xff, 0x00, 0x04, 0xd4, 0x0d, 0xb7, 0x20, 0x93, 0xff, 0x00,\n  0x0f, 0x16, 0xfd, 0x95, 0xf2, 0x06, 0x64, 0x24, 0x65, 0x8a, 0xb9, 0x0e,\n  0x64, 0x25, 0x41, 0x03, 0x2c, 0x55, 0xc7, 0xa9, 0xfe, 0xd7, 0x7a, 0xff,\n  0x00, 0xc5, 0x4f, 0x09, 0xfe, 0xc9, 0xff, 0x00, 0xb4, 0xff, 0x00, 0x8a,\n  0xbe, 0x05, 0x43, 0xad, 0xdc, 0xfc, 0x6f, 0xf0, 0xcf, 0xec, 0xf1, 0xf1,\n  0xab, 0xc4, 0x1f, 0x07, 0x2d, 0xfc, 0x37, 0xe1, 0xa8, 0xfc, 0x6d, 0xe2,\n  0x29, 0xfe, 0x2a, 0xe8, 0xdf, 0x0d, 0xbc, 0x4b, 0xa8, 0xfc, 0x3d, 0x87,\n  0x40, 0xf0, 0x64, 0xfa, 0x66, 0xb7, 0x07, 0x8b, 0xb5, 0xa9, 0x7c, 0x5d,\n  0x6d, 0xa4, 0x26, 0x97, 0xe1, 0x89, 0xb4, 0x5d, 0x5e, 0x2d, 0x7e, 0xf9,\n  0xa0, 0xd2, 0xa4, 0xd2, 0xef, 0xd2, 0xec, 0xda, 0x4b, 0xe4, 0xff, 0x00,\n  0xb4, 0xaf, 0xc5, 0x3f, 0x0c, 0xeb, 0xba, 0xb6, 0xbb, 0xfb, 0x33, 0xc7,\n  0xfb, 0x34, 0xfc, 0x6c, 0xfd, 0xaa, 0x35, 0xcf, 0xf8, 0x42, 0xbc, 0x0f,\n  0xf1, 0x2f, 0xe2, 0x27, 0x86, 0x3e, 0x0c, 0x6b, 0xff, 0x00, 0x07, 0xbc,\n  0x0d, 0xaa, 0x7c, 0x2a, 0xd0, 0xbc, 0x4b, 0xe3, 0x2f, 0x14, 0xc5, 0xf0,\n  0x4b, 0xc7, 0xb0, 0xf8, 0xf3, 0xe3, 0x37, 0xc7, 0xdf, 0xd9, 0xda, 0xeb,\n  0x43, 0xf1, 0x9d, 0xd7, 0xc4, 0x0f, 0x83, 0x1e, 0x39, 0xf1, 0x17, 0xc2,\n  0xef, 0x15, 0xfc, 0x12, 0xf1, 0x4e, 0xa7, 0xf1, 0x1b, 0xe1, 0x6f, 0x8d,\n  0x7e, 0x13, 0x58, 0xf8, 0xde, 0xe2, 0xf3, 0xe1, 0xb6, 0xb3, 0x27, 0xc3,\n  0x6d, 0x6f, 0x5d, 0xf8, 0xc0, 0xfc, 0x2b, 0x38, 0x38, 0xfd, 0x84, 0x3f,\n  0xe0, 0xb5, 0xd9, 0xc1, 0xc6, 0x7f, 0xe0, 0xac, 0x8a, 0xc3, 0x3d, 0xbe,\n  0x57, 0xff, 0x00, 0x82, 0xd2, 0x3a, 0x37, 0xd1, 0xd1, 0x94, 0xf4, 0x65,\n  0x61, 0x90, 0x40, 0x3c, 0xa3, 0xc4, 0x9e, 0x2e, 0xf0, 0x06, 0xb7, 0x69,\n  0xf1, 0x2b, 0xc3, 0xff, 0x00, 0xb3, 0x97, 0xc6, 0xbd, 0x6f, 0xe3, 0xef,\n  0xec, 0xa3, 0xa0, 0xfe, 0xd0, 0xff, 0x00, 0xf0, 0x44, 0xad, 0x67, 0xc2,\n  0x9e, 0x39, 0x9b, 0xf6, 0x89, 0xf1, 0xb7, 0xed, 0x67, 0xe1, 0xcb, 0x2f,\n  0xda, 0x17, 0xc4, 0xbf, 0xf0, 0x54, 0x2d, 0x2a, 0xcb, 0xe3, 0x8f, 0x83,\n  0xb4, 0xaf, 0x8f, 0xde, 0x3d, 0xf1, 0xef, 0xc5, 0x8f, 0x12, 0x1d, 0x63,\n  0x47, 0xf8, 0x6d, 0xe1, 0x7f, 0xd9, 0x9b, 0x5a, 0xd7, 0xbe, 0x10, 0x43,\n  0xf1, 0x02, 0x5d, 0x23, 0xc0, 0x36, 0x3e, 0x22, 0xf0, 0xcf, 0x8c, 0x34,\n  0xff, 0x00, 0x07, 0xf8, 0x7a, 0xe3, 0xe2, 0xd6, 0xab, 0xad, 0x78, 0xd7,\n  0xeb, 0xcf, 0xf8, 0x28, 0xa7, 0xfc, 0x12, 0x1f, 0xf6, 0x42, 0xff, 0x00,\n  0x82, 0x9b, 0xb7, 0x81, 0x35, 0x5f, 0x8f, 0x5a, 0x6f, 0x8c, 0xfc, 0x27,\n  0xf1, 0x07, 0xe1, 0xd8, 0x97, 0x4e, 0xd0, 0xbe, 0x2c, 0xfc, 0x21, 0xd4,\n  0x7c, 0x2b, 0xe1, 0xbf, 0x88, 0x77, 0x9e, 0x0d, 0x95, 0xb5, 0x3b, 0x89,\n  0xbe, 0x1c, 0xf8, 0x8f, 0x50, 0xf1, 0x67, 0x82, 0xfc, 0x79, 0xa1, 0xf8,\n  0x8b, 0xc1, 0xb6, 0xfa, 0xe6, 0xa5, 0x27, 0x89, 0x34, 0x8b, 0x4d, 0x5b,\n  0xc3, 0xf7, 0x3a, 0xaf, 0x85, 0xf5, 0xc9, 0x35, 0x5b, 0x9f, 0x05, 0xeb,\n  0x5e, 0x1e, 0xb2, 0xf1, 0x8f, 0x8f, 0xec, 0x3c, 0x55, 0xe6, 0x3e, 0x1c,\n  0xf0, 0x1c, 0x5e, 0x18, 0xf1, 0x26, 0x81, 0xe2, 0x67, 0xff, 0x00, 0x82,\n  0x70, 0xff, 0x00, 0xc1, 0x57, 0x3c, 0x75, 0x71, 0xe1, 0xad, 0x67, 0x4b,\n  0xf1, 0x26, 0x8f, 0xa0, 0xfc, 0x62, 0xff, 0x00, 0x82, 0x82, 0x7c, 0x2b,\n  0xf8, 0xeb, 0xf0, 0xfe, 0x3f, 0x11, 0xe8, 0x17, 0xf1, 0x6b, 0x7e, 0x18,\n  0xd7, 0xf5, 0x0f, 0x86, 0x1f, 0x1a, 0xff, 0x00, 0xe0, 0xae, 0x7e, 0x3d,\n  0xf8, 0x6f, 0xab, 0x6b, 0xbe, 0x10, 0xf1, 0x1d, 0x8e, 0x97, 0xe2, 0xdf,\n  0x05, 0xeb, 0x7a, 0xbf, 0x85, 0x35, 0x1d, 0x4f, 0xc1, 0xbe, 0x34, 0xd1,\n  0xbc, 0x3d, 0xe3, 0x4f, 0x0c, 0x5c, 0x69, 0x3e, 0x2d, 0xf0, 0xee, 0x85,\n  0xad, 0x69, 0xff, 0x00, 0xa4, 0x9f, 0x03, 0x7e, 0x36, 0xe9, 0x9f, 0x1a,\n  0xf4, 0xcf, 0x1a, 0xff, 0x00, 0xc5, 0x19, 0xe3, 0x8f, 0x86, 0x9e, 0x33,\n  0xf8, 0x63, 0xe3, 0x86, 0xf8, 0x75, 0xf1, 0x47, 0xe1, 0x77, 0xc4, 0x41,\n  0xe0, 0xf9, 0xbc, 0x5f, 0xf0, 0xf7, 0xc6, 0x73, 0x78, 0x3f, 0xc1, 0xff,\n  0x00, 0x12, 0x34, 0x8d, 0x1f, 0x54, 0xd5, 0xbe, 0x1b, 0x78, 0xd3, 0xe2,\n  0x37, 0xc3, 0x7d, 0x69, 0x35, 0xef, 0x86, 0x3f, 0x11, 0x3e, 0x1e, 0x7c,\n  0x40, 0xd3, 0xaf, 0x3c, 0x0d, 0xf1, 0x03, 0xc5, 0xfa, 0x55, 0x9e, 0x93,\n  0xe3, 0x4d, 0x3f, 0x43, 0xd6, 0x75, 0x0d, 0x23, 0xc6, 0xda, 0x3f, 0x8b,\n  0x3c, 0x25, 0xe1, 0xb0, 0x0f, 0x1b, 0xf0, 0x18, 0x3f, 0xf0, 0xf1, 0xaf,\n  0xda, 0xa8, 0xe3, 0x8f, 0xf8, 0x62, 0x8f, 0xd8, 0x09, 0x73, 0xdb, 0x70,\n  0xf8, 0xe9, 0xff, 0x00, 0x05, 0x2a, 0x24, 0x67, 0xd4, 0x06, 0x52, 0x47,\n  0x50, 0x19, 0x4f, 0x42, 0x2b, 0xed, 0x8a, 0xf8, 0x8f, 0xc0, 0x60, 0xff,\n  0x00, 0xc3, 0xc6, 0x3f, 0x6a, 0x77, 0xda, 0xa4, 0xb7, 0xec, 0x55, 0xfb,\n  0x02, 0x23, 0x64, 0xf3, 0x94, 0xf8, 0xe7, 0xff, 0x00, 0x05, 0x27, 0x3b,\n  0x4b, 0x6c, 0x25, 0xd1, 0x09, 0x92, 0x48, 0xd8, 0xed, 0x7d, 0xee, 0xc1,\n  0xbe, 0x56, 0x1e, 0x5f, 0xdb, 0x7c, 0xfa, 0x0f, 0xcc, 0xff, 0x00, 0x85,\n  0x00, 0x2d, 0x7c, 0x4f, 0xfb, 0x2b, 0x02, 0x3e, 0x3a, 0xff, 0x00, 0xc1,\n  0x4a, 0xf2, 0x31, 0x9f, 0xdb, 0x5f, 0xc0, 0x6c, 0x33, 0xdd, 0x7f, 0xe1,\n  0xdc, 0xdf, 0xb0, 0x12, 0xe4, 0x7a, 0x8d, 0xca, 0xc3, 0x3d, 0x32, 0xa4,\n  0x75, 0x06, 0xbe, 0xd6, 0x3b, 0xb0, 0x70, 0xaa, 0x4e, 0x0f, 0x05, 0x88,\n  0x07, 0xd8, 0x9d, 0xa7, 0x00, 0xf7, 0xe0, 0xfd, 0x0d, 0x7c, 0x49, 0xfb,\n  0x2b, 0xab, 0x7f, 0xc2, 0xf3, 0xff, 0x00, 0x82, 0x94, 0x15, 0x0b, 0xba,\n  0x4f, 0xdb, 0x57, 0xc0, 0x92, 0x7d, 0xf2, 0x99, 0x03, 0xfe, 0x09, 0xd3,\n  0xfb, 0x02, 0xa8, 0x77, 0x75, 0x8c, 0x93, 0x21, 0x50, 0x8a, 0x54, 0x2e,\n  0xc0, 0x88, 0x80, 0x12, 0x54, 0xb3, 0x80, 0x7f, 0xff, 0xd9\n};\n\nconst unsigned char joyicon_jpeg[1070] = /* 32 x 32 */{\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x05, 0x03, 0x04, 0x04, 0x04, 0x03, 0x05, 0x04, 0x04, 0x04, 0x05,\n  0x05, 0x05, 0x06, 0x07, 0x0c, 0x08, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x0b,\n  0x0b, 0x09, 0x0c, 0x11, 0x0f, 0x12, 0x12, 0x11, 0x0f, 0x11, 0x11, 0x13,\n  0x16, 0x1c, 0x17, 0x13, 0x14, 0x1a, 0x15, 0x11, 0x11, 0x18, 0x21, 0x18,\n  0x1a, 0x1d, 0x1d, 0x1f, 0x1f, 0x1f, 0x13, 0x17, 0x22, 0x24, 0x22, 0x1e,\n  0x24, 0x1c, 0x1e, 0x1f, 0x1e, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x05, 0x05,\n  0x05, 0x07, 0x06, 0x07, 0x0e, 0x08, 0x08, 0x0e, 0x1e, 0x14, 0x11, 0x14,\n  0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,\n  0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,\n  0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,\n  0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,\n  0x1e, 0x1e, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x20, 0x00, 0x20, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n  0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,\n  0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n  0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,\n  0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n  0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25,\n  0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n  0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56,\n  0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86,\n  0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n  0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n  0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n  0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n  0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00,\n  0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n  0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,\n  0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n  0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08,\n  0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n  0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18,\n  0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n  0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55,\n  0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n  0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84,\n  0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n  0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa,\n  0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n  0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n  0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00,\n  0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9,\n  0x67, 0xc1, 0x3e, 0x17, 0x7d, 0x6a, 0x43, 0x75, 0x74, 0x5a, 0x3b, 0x08,\n  0xce, 0x09, 0x1d, 0x64, 0x3e, 0x83, 0xfa, 0x9a, 0xfa, 0xbb, 0xf6, 0x5f,\n  0xf0, 0x27, 0x84, 0x75, 0x34, 0xd5, 0x45, 0xe6, 0x89, 0x6d, 0x30, 0xb5,\n  0x11, 0x08, 0xb2, 0x0e, 0x46, 0xed, 0xd9, 0x24, 0x8e, 0x49, 0xf9, 0x47,\n  0x5a, 0x7f, 0xc3, 0x5f, 0x80, 0xeb, 0x7d, 0xe0, 0x4d, 0x26, 0xe7, 0xfb,\n  0x55, 0x6d, 0xbc, 0xdb, 0x70, 0xde, 0x58, 0x8f, 0x24, 0x1e, 0x72, 0x4f,\n  0xb9, 0x3c, 0xfe, 0x35, 0xd1, 0xe9, 0x3a, 0x5d, 0xef, 0xc1, 0x1d, 0x4d,\n  0xef, 0xee, 0xe5, 0xfe, 0xd2, 0xf0, 0xee, 0xa1, 0xb2, 0x2b, 0xa9, 0xa3,\n  0x4c, 0x49, 0x6f, 0x20, 0xce, 0xd3, 0x8e, 0xe3, 0x93, 0xfe, 0x71, 0x90,\n  0x0e, 0x53, 0xf6, 0xa5, 0xf0, 0x27, 0x84, 0x34, 0xb9, 0x34, 0x43, 0x65,\n  0xa1, 0xda, 0xc1, 0xf6, 0xc5, 0x9c, 0x4d, 0xb4, 0x1c, 0xb6, 0xdf, 0x2f,\n  0x69, 0xcf, 0x50, 0x7e, 0x63, 0xd3, 0xd6, 0xbe, 0x55, 0xf1, 0xaf, 0x86,\n  0x1f, 0x45, 0x90, 0x5c, 0xdb, 0x16, 0x96, 0xc2, 0x43, 0x85, 0x63, 0xcb,\n  0x21, 0xfe, 0xe9, 0xfe, 0x86, 0xbe, 0xd7, 0xd5, 0x74, 0x7b, 0xaf, 0x8d,\n  0xb7, 0xa9, 0xab, 0x24, 0xc7, 0x4f, 0xd0, 0xac, 0x37, 0xc5, 0x64, 0xcc,\n  0x99, 0x79, 0x98, 0x91, 0xbc, 0x91, 0xdb, 0xee, 0x8f, 0xcb, 0xeb, 0x8e,\n  0x53, 0xe2, 0xc7, 0xc0, 0xc8, 0xb4, 0xaf, 0x87, 0x5a, 0xc5, 0xfa, 0xea,\n  0xcb, 0x38, 0x82, 0x0d, 0xe6, 0x36, 0x4c, 0x67, 0x91, 0x8e, 0x7b, 0x10,\n  0x70, 0x7f, 0x0a, 0x00, 0xb9, 0xf0, 0xcb, 0xe3, 0x94, 0x76, 0x7f, 0x0f,\n  0xf4, 0x8b, 0x76, 0xd2, 0xd6, 0xe5, 0xa2, 0xb7, 0x0b, 0xe6, 0x2c, 0xbb,\n  0x72, 0x79, 0xc8, 0x23, 0x1d, 0x41, 0xc8, 0xfc, 0x2b, 0xa5, 0xd0, 0xef,\n  0x24, 0xf8, 0xd3, 0x79, 0x3c, 0x1a, 0x8a, 0x1b, 0x3d, 0x0b, 0x4e, 0xd8,\n  0xd2, 0xdb, 0x23, 0x92, 0xd3, 0xc8, 0xc0, 0xed, 0xc9, 0xf4, 0xe0, 0xff,\n  0x00, 0x93, 0x91, 0xf1, 0x2f, 0x81, 0xbc, 0x57, 0x26, 0x87, 0x21, 0xb4,\n  0xba, 0x0d, 0x2d, 0x84, 0x87, 0x25, 0x47, 0x58, 0xcf, 0xf7, 0x87, 0xf5,\n  0x15, 0xf5, 0x8f, 0xec, 0xbf, 0xe3, 0xaf, 0x08, 0xd8, 0x5b, 0xea, 0xe2,\n  0xf3, 0x5c, 0xb4, 0x83, 0xed, 0x1e, 0x53, 0xc6, 0x5d, 0xb1, 0x9c, 0x6e,\n  0xc8, 0x23, 0xa8, 0x23, 0x70, 0xe0, 0xd0, 0x06, 0xfe, 0xab, 0xa9, 0x5d,\n  0xfc, 0x11, 0xd5, 0x63, 0xb0, 0xd3, 0xd4, 0x6a, 0x1a, 0x06, 0xa7, 0xbe,\n  0x6b, 0x7b, 0x69, 0x9c, 0x87, 0xb7, 0x91, 0x76, 0xee, 0xc3, 0x7a, 0x1d,\n  0xc3, 0xfc, 0x8c, 0x9e, 0x6b, 0xe2, 0xcf, 0xc6, 0xf3, 0xaa, 0xfc, 0x3b,\n  0xd5, 0xec, 0x8e, 0x97, 0x15, 0xaa, 0xcf, 0x0e, 0xd2, 0xfe, 0x61, 0x63,\n  0xd4, 0x60, 0x01, 0xea, 0x4e, 0x07, 0xe3, 0x51, 0xfe, 0xd4, 0x1e, 0x39,\n  0xf0, 0x9e, 0xa5, 0x71, 0xa4, 0x35, 0xa6, 0xb7, 0x6b, 0x32, 0xda, 0xc5,\n  0x29, 0x91, 0x91, 0xb3, 0xf7, 0x8a, 0xe0, 0x01, 0xd4, 0x9f, 0x94, 0xf4,\n  0xaf, 0x95, 0xbc, 0x71, 0xe2, 0x99, 0x35, 0xc9, 0x45, 0xad, 0xb0, 0x68,\n  0xec, 0x23, 0x39, 0x55, 0x3c, 0x34, 0x87, 0xfb, 0xc7, 0xfa, 0x0a, 0x00,\n  0xff, 0xd9\n};\n\nconst unsigned char caution_jpg[] /* 64x46 */ = {\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x14, 0x0e, 0x0f, 0x12, 0x0f, 0x0d, 0x14, 0x12, 0x10, 0x12, 0x17,\n  0x15, 0x14, 0x18, 0x1e, 0x32, 0x21, 0x1e, 0x1c, 0x1c, 0x1e, 0x3d, 0x2c,\n  0x2e, 0x24, 0x32, 0x49, 0x40, 0x4c, 0x4b, 0x47, 0x40, 0x46, 0x45, 0x50,\n  0x5a, 0x73, 0x62, 0x50, 0x55, 0x6d, 0x56, 0x45, 0x46, 0x64, 0x88, 0x65,\n  0x6d, 0x77, 0x7b, 0x81, 0x82, 0x81, 0x4e, 0x60, 0x8d, 0x97, 0x8c, 0x7d,\n  0x96, 0x73, 0x7e, 0x81, 0x7c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x15, 0x17,\n  0x17, 0x1e, 0x1a, 0x1e, 0x3b, 0x21, 0x21, 0x3b, 0x7c, 0x53, 0x46, 0x53,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x2e, 0x00, 0x40, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n  0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,\n  0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n  0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,\n  0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n  0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25,\n  0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n  0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56,\n  0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86,\n  0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n  0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n  0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n  0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n  0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00,\n  0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n  0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,\n  0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n  0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08,\n  0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n  0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18,\n  0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n  0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55,\n  0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n  0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84,\n  0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n  0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa,\n  0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n  0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n  0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00,\n  0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0x6e,\n  0xa1, 0xa8, 0xdd, 0x47, 0x7d, 0x14, 0x16, 0xf0, 0xc7, 0x34, 0x92, 0xc6,\n  0x1c, 0x97, 0x5d, 0xc4, 0x93, 0x9f, 0xd2, 0xb5, 0x12, 0x19, 0x16, 0x10,\n  0xd7, 0x0d, 0x02, 0xb0, 0x19, 0x6c, 0x47, 0xf2, 0x8f, 0xcc, 0xd2, 0x40,\n  0xb1, 0x24, 0x8d, 0x3b, 0xed, 0x56, 0x58, 0x50, 0x17, 0x3d, 0x86, 0x09,\n  0xae, 0x73, 0x5b, 0xd6, 0x9a, 0xf5, 0x8c, 0x10, 0x12, 0xb6, 0xe0, 0xf2,\n  0x7b, 0xbf, 0xff, 0x00, 0x5a, 0xbc, 0x64, 0xa5, 0x5a, 0x5c, 0xb1, 0xd2,\n  0xdb, 0xb3, 0x7d, 0x12, 0x25, 0xbb, 0xd7, 0xd9, 0x27, 0x65, 0xb6, 0x8e,\n  0x07, 0x8c, 0x70, 0x19, 0xa3, 0xeb, 0xfa, 0xd6, 0xae, 0x99, 0xf6, 0xcb,\n  0xb8, 0x3c, 0xeb, 0x98, 0xe0, 0x89, 0x5b, 0xee, 0x01, 0x17, 0x24, 0x7a,\n  0xf5, 0xac, 0xfd, 0x0b, 0x44, 0x0d, 0xb6, 0xea, 0xf1, 0x78, 0xea, 0x91,\n  0x9e, 0xfe, 0xe6, 0xae, 0x6b, 0x7a, 0xd2, 0xd9, 0xa9, 0x82, 0xd8, 0x83,\n  0x39, 0x1c, 0x9e, 0xc9, 0xff, 0x00, 0xd7, 0xaa, 0xa9, 0x69, 0x4b, 0xd9,\n  0x52, 0x5a, 0xf7, 0x05, 0xdd, 0x91, 0x6a, 0xfa, 0x9a, 0xe9, 0xec, 0x22,\n  0x8b, 0xc9, 0x96, 0x6e, 0xe0, 0xc7, 0xc2, 0x8f, 0x7e, 0x6a, 0x2d, 0x2e,\n  0xfe, 0xff, 0x00, 0x51, 0x9b, 0x6a, 0xc1, 0x6c, 0xb1, 0x2f, 0xdf, 0x7f,\n  0x2f, 0xa7, 0xeb, 0xd6, 0xb2, 0xb4, 0xcd, 0x3a, 0x6d, 0x52, 0xe0, 0x92,\n  0xc4, 0x46, 0x0e, 0x64, 0x90, 0xff, 0x00, 0x9e, 0x4d, 0x75, 0x53, 0x4f,\n  0x69, 0xa3, 0x58, 0x80, 0x00, 0x54, 0x5e, 0x15, 0x07, 0x56, 0x34, 0xea,\n  0x72, 0xd3, 0x4a, 0x9c, 0x75, 0x90, 0x96, 0xba, 0xf4, 0x1b, 0x7d, 0x2c,\n  0x76, 0x16, 0xe6, 0x59, 0x9e, 0x3c, 0x7f, 0x0a, 0x88, 0x86, 0x58, 0xfa,\n  0x0e, 0x6b, 0x33, 0x4b, 0xd6, 0x26, 0xbb, 0xb9, 0x91, 0x5a, 0x38, 0x93,\n  0x64, 0x6d, 0x22, 0xb2, 0x2e, 0x08, 0x23, 0xa7, 0x35, 0x8f, 0x73, 0x71,\n  0x73, 0xab, 0x5e, 0x8c, 0x82, 0xce, 0xc7, 0x08, 0x83, 0xa2, 0x8a, 0xe8,\n  0x2d, 0x34, 0xb8, 0xf4, 0xe8, 0x09, 0x24, 0x3c, 0xef, 0x1b, 0xef, 0x6f,\n  0x4e, 0x3a, 0x0a, 0x6e, 0x0a, 0x8c, 0x3d, 0xe7, 0xef, 0x30, 0xdd, 0xe8,\n  0x53, 0xbf, 0xb4, 0x17, 0xda, 0xdd, 0xa4, 0x0c, 0xc5, 0x55, 0xa0, 0x5c,\n  0x91, 0xd7, 0x03, 0x26, 0xa8, 0x5c, 0x43, 0x63, 0x71, 0x24, 0x51, 0xd8,\n  0x09, 0x52, 0x56, 0x93, 0x61, 0x57, 0xe4, 0x11, 0xeb, 0x9a, 0xd4, 0xbd,\n  0x82, 0xf7, 0xfb, 0x4e, 0xde, 0xe6, 0xca, 0x2f, 0x30, 0xc5, 0x0a, 0x67,\n  0x91, 0x8e, 0xfc, 0x55, 0x39, 0xb5, 0x56, 0xb5, 0xba, 0x45, 0xfb, 0x0c,\n  0x51, 0x79, 0x2e, 0x5c, 0xa0, 0x7c, 0xe5, 0x88, 0xc6, 0x73, 0xfd, 0x2b,\n  0x48, 0x39, 0x3b, 0x72, 0xeb, 0xa7, 0x7f, 0xcf, 0xf0, 0x13, 0xf3, 0x27,\n  0x93, 0x47, 0xb5, 0xfe, 0xd2, 0xb5, 0x8e, 0x22, 0xed, 0x6f, 0x2e, 0xf5,\n  0x6f, 0x9b, 0x9d, 0xcb, 0x9c, 0xd5, 0x48, 0xad, 0x2d, 0x12, 0xc0, 0xdd,\n  0x5c, 0x2c, 0x8c, 0x16, 0xe0, 0xc6, 0x42, 0x1e, 0x76, 0xe2, 0xad, 0x5a,\n  0xdf, 0xea, 0x97, 0xb1, 0x23, 0xc7, 0x6c, 0x26, 0x68, 0xa5, 0xdc, 0x24,\n  0xe1, 0x47, 0x4c, 0x11, 0x8e, 0xfd, 0x4d, 0x41, 0xac, 0x49, 0x72, 0xb6,\n  0xc9, 0x0c, 0x96, 0x49, 0x69, 0x13, 0x39, 0x7c, 0x07, 0x04, 0xb3, 0x7a,\n  0xd1, 0x1f, 0x69, 0xcc, 0xa1, 0x27, 0xf8, 0xfa, 0x86, 0x9b, 0x93, 0xcd,\n  0x61, 0xa7, 0x2d, 0xed, 0xad, 0xac, 0x69, 0x30, 0x69, 0xb0, 0xe4, 0x96,\n  0xe3, 0x69, 0x07, 0x8f, 0xaf, 0x15, 0x1a, 0x58, 0x58, 0xc1, 0x18, 0x92,\n  0xec, 0x4a, 0xfe, 0x74, 0xed, 0x14, 0x61, 0x4f, 0xdd, 0x00, 0xe3, 0x26,\n  0xa0, 0x8e, 0xfa, 0x7b, 0xbd, 0x4e, 0xd6, 0x58, 0x6d, 0xf7, 0xc9, 0x12,\n  0x05, 0x08, 0x0f, 0x5c, 0x67, 0x9c, 0xf6, 0xeb, 0x57, 0x90, 0xdf, 0xdb,\n  0xc4, 0xe6, 0xe7, 0x4e, 0x49, 0x23, 0x59, 0x0c, 0xca, 0x5d, 0xc0, 0xf2,\n  0xc9, 0x39, 0xeb, 0x4a, 0x5c, 0xf1, 0xb2, 0x6f, 0xf1, 0xf5, 0x1e, 0x8c,\n  0x67, 0xf6, 0x0a, 0x1b, 0x7b, 0xc5, 0x8d, 0x98, 0xcf, 0x14, 0x84, 0x47,\n  0xcf, 0xde, 0x18, 0x07, 0x1f, 0x5c, 0x53, 0xa0, 0xb5, 0x8e, 0xcf, 0x5b,\n  0xb8, 0x86, 0x1c, 0xec, 0x16, 0xcc, 0x79, 0x39, 0xea, 0x05, 0x67, 0xff,\n  0x00, 0x6c, 0xcf, 0xb5, 0xbe, 0x51, 0xe6, 0x34, 0xe2, 0x6d, 0xe0, 0xf7,\n  0xc6, 0x31, 0x8f, 0x4a, 0xd4, 0xb7, 0x5b, 0xa9, 0xb5, 0x19, 0x6f, 0x6e,\n  0x2d, 0x8c, 0x2b, 0x25, 0xbb, 0x00, 0x33, 0x9e, 0xc2, 0x89, 0x2a, 0x91,\n  0x4f, 0x9d, 0xe9, 0x6f, 0xf2, 0x0d, 0x3a, 0x06, 0xaf, 0xaa, 0x49, 0x64,\n  0xa2, 0x08, 0x3e, 0x59, 0x24, 0x89, 0x0e, 0xff, 0x00, 0xee, 0x8c, 0x76,\n  0xf7, 0xac, 0xdd, 0x23, 0x49, 0x93, 0x52, 0x97, 0x7b, 0xe5, 0x60, 0x53,\n  0xf3, 0x37, 0x76, 0x3e, 0x82, 0xb5, 0x75, 0x6b, 0x2b, 0x59, 0x2e, 0xc2,\n  0xdc, 0xbc, 0xc2, 0x48, 0x94, 0x21, 0xf2, 0xc0, 0xc1, 0x03, 0xeb, 0x56,\n  0x62, 0xbf, 0x86, 0x18, 0xd6, 0x38, 0xb7, 0xaa, 0x28, 0xc0, 0x02, 0x31,\n  0xc7, 0xeb, 0x50, 0xea, 0x7b, 0x38, 0x72, 0xc1, 0x7b, 0xdd, 0x42, 0xd7,\n  0x7a, 0x93, 0xde, 0xde, 0x5b, 0x68, 0xf6, 0x6a, 0xa1, 0x40, 0xc0, 0xc4,\n  0x71, 0xaf, 0x7f, 0xf3, 0xeb, 0x5c, 0x9b, 0x35, 0xd6, 0xaf, 0x7d, 0xde,\n  0x49, 0x5f, 0xa0, 0xec, 0xa3, 0xfa, 0x0a, 0xd4, 0xba, 0xb6, 0xb4, 0xbc,\n  0x9d, 0xa6, 0x9e, 0x7b, 0xb6, 0x73, 0xfe, 0xca, 0xe0, 0x0f, 0x41, 0x56,\n  0x6c, 0x1e, 0xd3, 0x4f, 0x46, 0x58, 0x0c, 0xb9, 0x6e, 0xac, 0xc8, 0xa4,\n  0x9f, 0xd6, 0xa6, 0x9c, 0xa1, 0x4a, 0x2d, 0xad, 0x64, 0xc6, 0xee, 0xdf,\n  0x91, 0x7b, 0x4f, 0xb0, 0xb7, 0xd2, 0x2d, 0x59, 0x99, 0x86, 0xec, 0x66,\n  0x49, 0x4f, 0xf9, 0xe9, 0x5c, 0xee, 0xb3, 0xac, 0x3e, 0xa0, 0xfe, 0x54,\n  0x59, 0x5b, 0x75, 0x3c, 0x0e, 0xed, 0xee, 0x6b, 0x52, 0xfe, 0x48, 0x35,\n  0x04, 0x54, 0x9a, 0x7b, 0x95, 0x41, 0xce, 0xd4, 0x55, 0x00, 0x9f, 0x7e,\n  0x6a, 0xb5, 0xad, 0xa5, 0x85, 0xac, 0xeb, 0x2a, 0xb4, 0xee, 0xcb, 0xd0,\n  0x3a, 0xa9, 0x00, 0xfa, 0xf5, 0xa5, 0x49, 0xc6, 0x2d, 0xd4, 0x9e, 0xb2,\n  0x07, 0x7d, 0x91, 0x36, 0x85, 0xa1, 0xec, 0xdb, 0x75, 0x78, 0xbf, 0x37,\n  0x54, 0x8c, 0xf6, 0xf7, 0x3e, 0xf5, 0x7e, 0xe6, 0xfa, 0x19, 0x6e, 0xde,\n  0xd6, 0x33, 0xb9, 0xd2, 0x27, 0x66, 0x23, 0xa0, 0xe3, 0xa7, 0xd6, 0xa3,\n  0x97, 0x51, 0x59, 0x23, 0x64, 0x12, 0xcc, 0x9b, 0x86, 0x37, 0x2a, 0x2e,\n  0x47, 0xeb, 0x55, 0x74, 0xbb, 0x1b, 0x54, 0xbb, 0x22, 0x09, 0x27, 0x69,\n  0x65, 0x52, 0x99, 0x90, 0x0c, 0x0c, 0xf5, 0x3c, 0x54, 0xab, 0xd5, 0x93,\n  0x94, 0xde, 0xbd, 0x10, 0x6c, 0xb4, 0x3f, 0xff, 0xd9\n};\nconst unsigned int caution_jpg_len = 1557;\n\nconst unsigned char checksum_jpg[] /* 22 x32 */ = {\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x14, 0x0e, 0x0f, 0x12, 0x0f, 0x0d, 0x14, 0x12, 0x10, 0x12, 0x17,\n  0x15, 0x14, 0x18, 0x1e, 0x32, 0x21, 0x1e, 0x1c, 0x1c, 0x1e, 0x3d, 0x2c,\n  0x2e, 0x24, 0x32, 0x49, 0x40, 0x4c, 0x4b, 0x47, 0x40, 0x46, 0x45, 0x50,\n  0x5a, 0x73, 0x62, 0x50, 0x55, 0x6d, 0x56, 0x45, 0x46, 0x64, 0x88, 0x65,\n  0x6d, 0x77, 0x7b, 0x81, 0x82, 0x81, 0x4e, 0x60, 0x8d, 0x97, 0x8c, 0x7d,\n  0x96, 0x73, 0x7e, 0x81, 0x7c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x15, 0x17,\n  0x17, 0x1e, 0x1a, 0x1e, 0x3b, 0x21, 0x21, 0x3b, 0x7c, 0x53, 0x46, 0x53,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x20, 0x00, 0x16, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n  0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,\n  0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n  0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,\n  0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n  0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25,\n  0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n  0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56,\n  0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86,\n  0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n  0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n  0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n  0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n  0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00,\n  0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n  0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,\n  0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n  0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08,\n  0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n  0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18,\n  0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n  0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55,\n  0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n  0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84,\n  0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n  0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa,\n  0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n  0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n  0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00,\n  0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xca,\n  0xd6, 0x75, 0x9d, 0x46, 0x0d, 0x5a, 0xee, 0x28, 0xaf, 0x66, 0x44, 0x49,\n  0x08, 0x55, 0x0f, 0xc0, 0x15, 0x26, 0x97, 0xaa, 0x5f, 0x5d, 0x5a, 0xdf,\n  0x8b, 0x86, 0x7b, 0xe5, 0x58, 0x94, 0xf9, 0x32, 0x65, 0x81, 0xf9, 0xd4,\n  0x1f, 0xd3, 0x3c, 0xd4, 0x37, 0x76, 0x42, 0xf3, 0xc4, 0x77, 0xe2, 0x42,\n  0xc2, 0x28, 0xdd, 0x9e, 0x42, 0xa3, 0x2c, 0x46, 0x70, 0x00, 0xf7, 0x24,\n  0x81, 0xf8, 0xd5, 0xc3, 0x67, 0x6f, 0x6e, 0x92, 0xf9, 0x71, 0x98, 0x19,\n  0x14, 0x97, 0x36, 0xd7, 0x0e, 0x64, 0x40, 0x3a, 0x93, 0x90, 0x15, 0xf0,\n  0x7a, 0xe0, 0xf1, 0x40, 0x19, 0x97, 0xba, 0x34, 0xca, 0x8b, 0x71, 0x65,\n  0x04, 0xef, 0x6f, 0x21, 0xc6, 0xc6, 0x43, 0xbe, 0x33, 0xe8, 0x47, 0x7f,\n  0xad, 0x15, 0xaf, 0xe0, 0xab, 0x99, 0xe7, 0xbf, 0xb8, 0x13, 0x4d, 0x24,\n  0x80, 0x45, 0x90, 0x1d, 0xc9, 0xee, 0x28, 0xa0, 0x0a, 0xeb, 0xff, 0x00,\n  0x23, 0x5d, 0xd9, 0x0e, 0xde, 0x62, 0x4d, 0xbd, 0x23, 0x07, 0x02, 0x52,\n  0xac, 0x0e, 0xdf, 0xae, 0x01, 0xc7, 0xb8, 0x15, 0xa0, 0xb3, 0x21, 0x78,\n  0x14, 0x48, 0x59, 0x2d, 0xdd, 0x19, 0x8b, 0x2b, 0x0f, 0x2e, 0x35, 0x04,\n  0x39, 0x6c, 0x81, 0x82, 0x41, 0x03, 0x19, 0x39, 0x35, 0x46, 0xf2, 0xcb,\n  0x54, 0x87, 0x5b, 0xbc, 0x9a, 0x0d, 0x3e, 0x49, 0xe1, 0x95, 0xc8, 0x65,\n  0x64, 0x25, 0x5d, 0x73, 0x9f, 0xf2, 0x45, 0x3a, 0xe5, 0x35, 0x29, 0xad,\n  0xcc, 0x43, 0x47, 0xbc, 0x6c, 0x8c, 0x01, 0x34, 0xcf, 0x22, 0xaf, 0xb8,\n  0x5e, 0x39, 0xfa, 0xe6, 0x80, 0x17, 0xc0, 0xf8, 0xfe, 0xd2, 0xba, 0xdb,\n  0xf7, 0x7c, 0xae, 0x3f, 0xef, 0xa1, 0x45, 0x58, 0xf0, 0x7e, 0x9d, 0x79,\n  0x65, 0x7b, 0x3b, 0xdd, 0x5b, 0x49, 0x0a, 0xb4, 0x78, 0x05, 0xd7, 0x19,\n  0x39, 0x14, 0x50, 0x07, 0xff, 0xd9\n};\nconst unsigned int checksum_jpg_len = 882;\n\nconst unsigned char download_jpg[] = /* 26 x 32 */ {\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x14, 0x0e, 0x0f, 0x12, 0x0f, 0x0d, 0x14, 0x12, 0x10, 0x12, 0x17,\n  0x15, 0x14, 0x18, 0x1e, 0x32, 0x21, 0x1e, 0x1c, 0x1c, 0x1e, 0x3d, 0x2c,\n  0x2e, 0x24, 0x32, 0x49, 0x40, 0x4c, 0x4b, 0x47, 0x40, 0x46, 0x45, 0x50,\n  0x5a, 0x73, 0x62, 0x50, 0x55, 0x6d, 0x56, 0x45, 0x46, 0x64, 0x88, 0x65,\n  0x6d, 0x77, 0x7b, 0x81, 0x82, 0x81, 0x4e, 0x60, 0x8d, 0x97, 0x8c, 0x7d,\n  0x96, 0x73, 0x7e, 0x81, 0x7c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x15, 0x17,\n  0x17, 0x1e, 0x1a, 0x1e, 0x3b, 0x21, 0x21, 0x3b, 0x7c, 0x53, 0x46, 0x53,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x20, 0x00, 0x1a, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n  0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,\n  0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n  0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,\n  0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n  0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25,\n  0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n  0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56,\n  0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86,\n  0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n  0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n  0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n  0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n  0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00,\n  0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n  0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,\n  0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n  0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08,\n  0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n  0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18,\n  0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n  0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55,\n  0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n  0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84,\n  0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n  0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa,\n  0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n  0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n  0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00,\n  0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xaf,\n  0xe2, 0x0d, 0x7b, 0x52, 0xd3, 0xb5, 0x99, 0xed, 0x2c, 0xee, 0x04, 0x56,\n  0xf1, 0x04, 0x08, 0x82, 0x34, 0x21, 0x46, 0xd0, 0x7b, 0x8f, 0x7a, 0xcd,\n  0xff, 0x00, 0x84, 0xb3, 0x5a, 0xff, 0x00, 0x9f, 0xdf, 0xfc, 0x84, 0x9f,\n  0xe1, 0x47, 0x8b, 0xff, 0x00, 0xe4, 0x64, 0xbb, 0xff, 0x00, 0x80, 0x7f,\n  0xe8, 0x0b, 0x4c, 0xd3, 0xa1, 0x8d, 0xfc, 0x3f, 0xac, 0x48, 0xf1, 0xa3,\n  0x49, 0x1f, 0x91, 0xb1, 0x8a, 0x82, 0x57, 0x2e, 0x73, 0x83, 0xdb, 0x34,\n  0x01, 0xd0, 0x59, 0xdd, 0xdb, 0x6b, 0x9a, 0x2a, 0x47, 0xac, 0xde, 0x5a,\n  0xf9, 0xa5, 0xdb, 0x2f, 0x24, 0x8b, 0x1c, 0x89, 0xe8, 0x54, 0x62, 0xb3,\n  0xdf, 0xc0, 0xfa, 0x88, 0x76, 0x09, 0x35, 0xbb, 0x28, 0x3c, 0x12, 0xc4,\n  0x12, 0x3e, 0x98, 0xae, 0x66, 0xbd, 0x9a, 0x80, 0x3c, 0xcb, 0xc5, 0xff,\n  0x00, 0xf2, 0x32, 0x5d, 0xff, 0x00, 0xc0, 0x3f, 0xf4, 0x05, 0xab, 0xf6,\n  0x9e, 0x25, 0xb3, 0xfe, 0xce, 0x9d, 0x6f, 0xac, 0x96, 0x4b, 0xa6, 0x08,\n  0x0e, 0xd1, 0x85, 0x9b, 0x69, 0xca, 0xee, 0xf4, 0xc7, 0xeb, 0x57, 0x75,\n  0x1b, 0x1d, 0x70, 0x6b, 0xf7, 0x3a, 0x8e, 0x8c, 0x80, 0xc7, 0x32, 0x2a,\n  0xac, 0x81, 0xa3, 0x21, 0x97, 0x6a, 0xe7, 0x86, 0x3e, 0xa2, 0x9b, 0xff,\n  0x00, 0x15, 0xa7, 0xf9, 0xf2, 0x68, 0x03, 0x92, 0xbc, 0xba, 0x92, 0xf6,\n  0xea, 0x4b, 0x89, 0xb6, 0xf9, 0x92, 0x1c, 0x9d, 0xa3, 0x02, 0xbd, 0x82,\n  0xb8, 0x2b, 0xfd, 0x3f, 0xc5, 0x7a, 0x94, 0x2b, 0x0d, 0xe4, 0x5e, 0x6c,\n  0x6a, 0xdb, 0x80, 0xdd, 0x10, 0xe7, 0x04, 0x76, 0x3e, 0xe6, 0xbb, 0x46,\n  0xd4, 0x2c, 0xd1, 0x8a, 0xb5, 0xd4, 0x21, 0x81, 0xc1, 0x05, 0xc7, 0x06,\n  0x80, 0x3f, 0xff, 0xd9\n};\nconst unsigned int download_jpg_len = 868;\n\nconst unsigned char ntp_jpeg[] /* 32x32 */= {\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x14, 0x0e, 0x0f, 0x12, 0x0f, 0x0d, 0x14, 0x12, 0x10, 0x12, 0x17,\n  0x15, 0x14, 0x18, 0x1e, 0x32, 0x21, 0x1e, 0x1c, 0x1c, 0x1e, 0x3d, 0x2c,\n  0x2e, 0x24, 0x32, 0x49, 0x40, 0x4c, 0x4b, 0x47, 0x40, 0x46, 0x45, 0x50,\n  0x5a, 0x73, 0x62, 0x50, 0x55, 0x6d, 0x56, 0x45, 0x46, 0x64, 0x88, 0x65,\n  0x6d, 0x77, 0x7b, 0x81, 0x82, 0x81, 0x4e, 0x60, 0x8d, 0x97, 0x8c, 0x7d,\n  0x96, 0x73, 0x7e, 0x81, 0x7c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x15, 0x17,\n  0x17, 0x1e, 0x1a, 0x1e, 0x3b, 0x21, 0x21, 0x3b, 0x7c, 0x53, 0x46, 0x53,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c,\n  0x7c, 0x7c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x20, 0x00, 0x20, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n  0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,\n  0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n  0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,\n  0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n  0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25,\n  0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n  0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56,\n  0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86,\n  0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n  0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n  0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n  0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n  0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00,\n  0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n  0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,\n  0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n  0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08,\n  0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n  0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18,\n  0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n  0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55,\n  0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n  0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84,\n  0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n  0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa,\n  0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n  0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n  0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00,\n  0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0x6c,\n  0x8e, 0x11, 0x73, 0x82, 0x49, 0x38, 0x00, 0x75, 0x27, 0xd2, 0x9e, 0xd6,\n  0x8e, 0xb6, 0xef, 0x34, 0xf2, 0x36, 0xe0, 0xb9, 0x08, 0x87, 0x00, 0x7e,\n  0x3d, 0x4d, 0x24, 0x20, 0x35, 0xf4, 0x00, 0xf4, 0x01, 0x98, 0x7d, 0x71,\n  0xff, 0x00, 0xd7, 0x35, 0x7a, 0xf1, 0x59, 0xad, 0x25, 0x08, 0xa5, 0x98,\n  0xa9, 0xc0, 0x1d, 0xe8, 0x02, 0x9a, 0x59, 0xbb, 0xdb, 0xa4, 0xb0, 0x48,\n  0xdb, 0x8a, 0xe7, 0x63, 0x9c, 0x83, 0xf8, 0xf5, 0x15, 0x1c, 0x6f, 0xbd,\n  0x73, 0x82, 0xa4, 0x1c, 0x10, 0x7a, 0x83, 0xe9, 0x56, 0x2c, 0x2e, 0xd3,\n  0xcb, 0x48, 0x24, 0x06, 0x29, 0x54, 0x63, 0x6b, 0x71, 0x9a, 0x8a, 0x70,\n  0x16, 0xfe, 0x60, 0x3a, 0x15, 0x56, 0x3f, 0x5e, 0x47, 0xf4, 0x14, 0x00,\n  0xc7, 0xdc, 0xac, 0x92, 0x47, 0xf7, 0xe3, 0x39, 0x00, 0xf7, 0xf5, 0x15,\n  0x7e, 0x0b, 0xb8, 0xa7, 0x1f, 0x2b, 0x61, 0xbb, 0xa3, 0x70, 0xc3, 0xf0,\n  0xac, 0xd9, 0x1e, 0x55, 0x6c, 0x47, 0x08, 0x71, 0x8e, 0xbb, 0xf1, 0x55,\n  0xee, 0x3c, 0xe9, 0x63, 0x21, 0xad, 0x57, 0x38, 0xeb, 0xb8, 0x31, 0x1f,\n  0x4a, 0x00, 0xd7, 0xbb, 0x92, 0xd5, 0x36, 0x99, 0x82, 0xb3, 0xa9, 0xf9,\n  0x54, 0x72, 0xc4, 0xfb, 0x55, 0x34, 0xde, 0xcc, 0xf2, 0xc9, 0xc3, 0xc8,\n  0x72, 0x47, 0xa0, 0xec, 0x2a, 0xa5, 0xbf, 0x9d, 0x1c, 0x63, 0x6d, 0xaa,\n  0xe7, 0x1d, 0x77, 0x05, 0x26, 0xac, 0x46, 0xf3, 0x33, 0x62, 0x48, 0x42,\n  0x0f, 0x5d, 0xf9, 0xa0, 0x0f, 0xff, 0xd9\n};\nconst unsigned int ntp_jpeg_len = 835;\n\nconst unsigned char bluefork_jpg[] /* 10x14 */  = {\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x0e, 0x00, 0x0a, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n  0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,\n  0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n  0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,\n  0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n  0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25,\n  0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n  0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56,\n  0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86,\n  0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n  0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n  0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n  0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n  0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00,\n  0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n  0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,\n  0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n  0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08,\n  0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n  0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18,\n  0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n  0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55,\n  0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n  0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84,\n  0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n  0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa,\n  0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n  0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n  0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00,\n  0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xe2,\n  0xbc, 0x35, 0xf0, 0x5b, 0xe2, 0x4f, 0xed, 0xa9, 0xf1, 0x2f, 0xfe, 0x09,\n  0x99, 0xfb, 0x63, 0x7e, 0xce, 0xde, 0x31, 0x7f, 0x88, 0x3f, 0xb2, 0xf4,\n  0xfa, 0x1f, 0xc6, 0x6d, 0x7b, 0xf6, 0xcc, 0xf8, 0xe7, 0xab, 0x7c, 0x51,\n  0xd1, 0xf4, 0x2f, 0x10, 0xff, 0x00, 0xc1, 0x34, 0x3e, 0x20, 0x7c, 0x5c,\n  0xf8, 0x73, 0x67, 0xa1, 0xfe, 0xd3, 0xdf, 0x0b, 0x3c, 0x07, 0xaa, 0xf8,\n  0x92, 0xcb, 0x47, 0x7f, 0xd9, 0xd7, 0xe0, 0xa7, 0xec, 0x7b, 0xf0, 0xff,\n  0x00, 0x4b, 0x1f, 0x14, 0x3f, 0xe0, 0x9e, 0x76, 0x5a, 0x95, 0xae, 0xbf,\n  0xa7, 0x78, 0x4b, 0xe2, 0x4d, 0xdd, 0xd7, 0x89, 0x6c, 0x75, 0x4d, 0x6b,\n  0x4f, 0xf1, 0x07, 0x87, 0x25, 0xd6, 0x3e, 0x06, 0xf8, 0x97, 0xff, 0x00,\n  0x05, 0xe1, 0x97, 0xe1, 0x8f, 0xc4, 0x6f, 0x1f, 0xfc, 0x35, 0xf8, 0x53,\n  0xfb, 0x31, 0xfe, 0xc8, 0x5f, 0xb4, 0x77, 0xc2, 0xef, 0x87, 0x9e, 0x36,\n  0xf1, 0x57, 0x81, 0xbe, 0x1b, 0x7e, 0xd0, 0xdf, 0x1c, 0x3e, 0x16, 0x6a,\n  0x3e, 0x2b, 0xf8, 0xd3, 0xf1, 0xe3, 0xc0, 0x3e, 0x13, 0xd7, 0x6f, 0xf4,\n  0x0f, 0x07, 0x7c, 0x66, 0xf8, 0xbf, 0xe2, 0x8d, 0x42, 0x5d, 0x06, 0xff,\n  0x00, 0xc4, 0x9f, 0x14, 0x7e, 0x28, 0x78, 0x7b, 0x4f, 0xd3, 0xbc, 0x6f,\n  0xe3, 0xfd, 0x7e, 0xfb, 0x43, 0xd1, 0xaf, 0x35, 0x8f, 0x16, 0x6b, 0x9a,\n  0xb6, 0xa3, 0x73, 0xa4, 0xe9, 0xd3, 0x5c, 0xbd, 0x9c, 0x3e, 0x7b, 0xff,\n  0x00, 0x04, 0xf3, 0xf0, 0xdf, 0x8a, 0x2e, 0xff, 0x00, 0xe0, 0x90, 0x1f,\n  0xf0, 0x54, 0x49, 0xfc, 0x3d, 0x71, 0xa2, 0x41, 0xf0, 0xa7, 0xc5, 0x31,\n  0xc1, 0x17, 0xed, 0x69, 0xfd, 0xa5, 0x73, 0xae, 0xc3, 0xf1, 0x15, 0xa2,\n  0xf8, 0x4d, 0xe1, 0x3d, 0x3f, 0xc7, 0x7f, 0xb0, 0xdc, 0x5f, 0xb3, 0xf9,\n  0xd3, 0x26, 0x8f, 0xc3, 0x51, 0x47, 0x71, 0xfb, 0x47, 0xeb, 0x3e, 0x32,\n  0xb7, 0xfd, 0xaa, 0x93, 0xe2, 0x02, 0xed, 0xd4, 0xbe, 0x08, 0xbe, 0x85,\n  0x65, 0xf0, 0xe8, 0x4f, 0xe2, 0x91, 0x7f, 0x15, 0x7f, 0x3d, 0xf5, 0xfe,\n  0xca, 0xf0, 0x9f, 0x87, 0x19, 0x1e, 0x71, 0x9c, 0xf1, 0x2e, 0x5d, 0xc4,\n  0x30, 0xa3, 0x9c, 0xe5, 0x5c, 0x23, 0x8a, 0xff, 0x00, 0x57, 0xf8, 0x6f,\n  0x0a, 0xe8, 0xd7, 0xc0, 0xd5, 0x8e, 0x5f, 0x88, 0xc5, 0xe3, 0x73, 0xda,\n  0x98, 0xbc, 0xd7, 0x11, 0x86, 0xc6, 0x45, 0xe2, 0x73, 0x1a, 0x39, 0x86,\n  0x69, 0x8f, 0xca, 0x68, 0x47, 0x0d, 0x0c, 0x36, 0x12, 0x59, 0x7e, 0x06,\n  0x86, 0x61, 0x5a, 0x94, 0xb1, 0x79, 0x95, 0x4c, 0x3e, 0x5f, 0xf9, 0xf5,\n  0x7c, 0x5d, 0x5a, 0x74, 0xe8, 0xce, 0x95, 0xe9, 0xce, 0xbc, 0x7d, 0xad,\n  0x69, 0x5d, 0x49, 0x39, 0xa8, 0xc2, 0x9a, 0x8c, 0x13, 0x8e, 0x90, 0x70,\n  0x84, 0x2a, 0x3e, 0x6e, 0x69, 0x73, 0xce, 0x50, 0x4f, 0x96, 0x37, 0x9f,\n  0xff, 0xd9\n};\nconst unsigned int bluefork_jpg_len = 1022;\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/certificates.h",
    "content": "#pragma once\n\n#if defined USE_DOWNLOADER\n\n/*\n\n  ssl_host=\"phpsecure.info\" && \\\n    prefix=\"const char* ${ssl_host//[^a-zA-Z0-9]/_}_ca =\\\\\" && \\\n    suffix=\"\\\"\\\";\" && \\\n    echo $prefix > $ssl_host.txt && \\\n    openssl s_client -showcerts -connect $ssl_host:443 </dev/null | \\\n    sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | \\\n    awk '{print \"\\\"\" $0 \"\\\\n\\\"\\\\\"}' >> $ssl_host.txt && \\\n    echo $suffix >> $ssl_host.txt &&\n    cat $ssl_host.txt\n\n\nCertificate chain\n 0 s:/businessCategory=Private Organization/jurisdictionC=US/jurisdictionST=Delaware/serialNumber=5157550/C=US/ST=California/L=San Francisco/O=GitHub, Inc./CN=github.com\n   i:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 Extended Validation Server CA\n\n*/\n\nconst char* github_ca =\\\n\"-----BEGIN CERTIFICATE-----\\n\"\\\n\"MIIHQjCCBiqgAwIBAgIQCgYwQn9bvO1pVzllk7ZFHzANBgkqhkiG9w0BAQsFADB1\\n\"\\\n\"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\\n\"\\\n\"d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk\\n\"\\\n\"IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE4MDUwODAwMDAwMFoXDTIwMDYwMzEy\\n\"\\\n\"MDAwMFowgccxHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB\\n\"\\\n\"BAGCNzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMRAwDgYDVQQF\\n\"\\\n\"Ewc1MTU3NTUwMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQG\\n\"\\\n\"A1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRMwEQYD\\n\"\\\n\"VQQDEwpnaXRodWIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\\n\"\\\n\"xjyq8jyXDDrBTyitcnB90865tWBzpHSbindG/XqYQkzFMBlXmqkzC+FdTRBYyneZ\\n\"\\\n\"w5Pz+XWQvL+74JW6LsWNc2EF0xCEqLOJuC9zjPAqbr7uroNLghGxYf13YdqbG5oj\\n\"\\\n\"/4x+ogEG3dF/U5YIwVr658DKyESMV6eoYV9mDVfTuJastkqcwero+5ZAKfYVMLUE\\n\"\\\n\"sMwFtoTDJFmVf6JlkOWwsxp1WcQ/MRQK1cyqOoUFUgYylgdh3yeCDPeF22Ax8AlQ\\n\"\\\n\"xbcaI+GwfQL1FB7Jy+h+KjME9lE/UpgV6Qt2R1xNSmvFCBWu+NFX6epwFP/JRbkM\\n\"\\\n\"fLz0beYFUvmMgLtwVpEPSwIDAQABo4IDeTCCA3UwHwYDVR0jBBgwFoAUPdNQpdag\\n\"\\\n\"re7zSmAKZdMh1Pj41g8wHQYDVR0OBBYEFMnCU2FmnV+rJfQmzQ84mqhJ6kipMCUG\\n\"\\\n\"A1UdEQQeMByCCmdpdGh1Yi5jb22CDnd3dy5naXRodWIuY29tMA4GA1UdDwEB/wQE\\n\"\\\n\"AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdQYDVR0fBG4wbDA0\\n\"\\\n\"oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVyLWcy\\n\"\\\n\"LmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItZXYtc2Vy\\n\"\\\n\"dmVyLWcyLmNybDBLBgNVHSAERDBCMDcGCWCGSAGG/WwCATAqMCgGCCsGAQUFBwIB\\n\"\\\n\"FhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAcGBWeBDAEBMIGIBggrBgEF\\n\"\\\n\"BQcBAQR8MHowJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBS\\n\"\\\n\"BggrBgEFBQcwAoZGaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0\\n\"\\\n\"U0hBMkV4dGVuZGVkVmFsaWRhdGlvblNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAA\\n\"\\\n\"MIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgCkuQmQtBhYFIe7E6LMZ3AKPDWY\\n\"\\\n\"BPkb37jjd80OyA3cEAAAAWNBYm0KAAAEAwBHMEUCIQDRZp38cTWsWH2GdBpe/uPT\\n\"\\\n\"Wnsu/m4BEC2+dIcvSykZYgIgCP5gGv6yzaazxBK2NwGdmmyuEFNSg2pARbMJlUFg\\n\"\\\n\"U5UAdgBWFAaaL9fC7NP14b1Esj7HRna5vJkRXMDvlJhV1onQ3QAAAWNBYm0tAAAE\\n\"\\\n\"AwBHMEUCIQCi7omUvYLm0b2LobtEeRAYnlIo7n6JxbYdrtYdmPUWJQIgVgw1AZ51\\n\"\\\n\"vK9ENinBg22FPxb82TvNDO05T17hxXRC2IYAdgC72d+8H4pxtZOUI5eqkntHOFeV\\n\"\\\n\"CqtS6BqQlmQ2jh7RhQAAAWNBYm3fAAAEAwBHMEUCIQChzdTKUU2N+XcqcK0OJYrN\\n\"\\\n\"8EYynloVxho4yPk6Dq3EPgIgdNH5u8rC3UcslQV4B9o0a0w204omDREGKTVuEpxG\\n\"\\\n\"eOQwDQYJKoZIhvcNAQELBQADggEBAHAPWpanWOW/ip2oJ5grAH8mqQfaunuCVE+v\\n\"\\\n\"ac+88lkDK/LVdFgl2B6kIHZiYClzKtfczG93hWvKbST4NRNHP9LiaQqdNC17e5vN\\n\"\\\n\"HnXVUGw+yxyjMLGqkgepOnZ2Rb14kcTOGp4i5AuJuuaMwXmCo7jUwPwfLe1NUlVB\\n\"\\\n\"Kqg6LK0Hcq4K0sZnxE8HFxiZ92WpV2AVWjRMEc/2z2shNoDvxvFUYyY1Oe67xINk\\n\"\\\n\"myQKc+ygSBZzyLnXSFVWmHr3u5dcaaQGGAR42v6Ydr4iL38Hd4dOiBma+FXsXBIq\\n\"\\\n\"WUjbST4VXmdaol7uzFMojA4zkxQDZAvF5XgJlAFadfySna/teik=\\n\"\\\n\"-----END CERTIFICATE-----\\n\"\\\n/*\n 1 s:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 Extended Validation Server CA\n   i:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance EV Root CA\n*/\n\"-----BEGIN CERTIFICATE-----\\n\"\\\n\"MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs\\n\"\\\n\"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\\n\"\\\n\"d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\\n\"\\\n\"ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL\\n\"\\\n\"MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\\n\"\\\n\"LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW\\n\"\\\n\"YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\\n\"\\\n\"ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY\\n\"\\\n\"uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/\\n\"\\\n\"LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy\\n\"\\\n\"/Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh\\n\"\\\n\"cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k\\n\"\\\n\"8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB\\n\"\\\n\"Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF\\n\"\\\n\"BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp\\n\"\\\n\"Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy\\n\"\\\n\"dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2\\n\"\\\n\"MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j\\n\"\\\n\"b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW\\n\"\\\n\"gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh\\n\"\\\n\"hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg\\n\"\\\n\"4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa\\n\"\\\n\"2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs\\n\"\\\n\"1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1\\n\"\\\n\"oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn\\n\"\\\n\"8TUoE6smftX3eg==\\n\"\\\n\"-----END CERTIFICATE-----\\n\"\\\n\n\"\";\n\n\n/*\n\n  openssl s_client -showcerts -connect phpsecu.re:443 </dev/null\n\nCertificate chain\n 0 s:/CN=phpsecure.info\n   i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3\n*/\n\n\nconst char* phpsecu_re_ca =\\\n\"-----BEGIN CERTIFICATE-----\\n\"\\\n\"MIIFmTCCBIGgAwIBAgISA07S9G1tcSqoXPt3V5tkg/0/MA0GCSqGSIb3DQEBCwUA\\n\"\\\n\"MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD\\n\"\\\n\"ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTAyMDQxODAwMTdaFw0x\\n\"\\\n\"OTA1MDUxODAwMTdaMBkxFzAVBgNVBAMTDnBocHNlY3VyZS5pbmZvMIIBIjANBgkq\\n\"\\\n\"hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArIn36ItK3gloILk29t/Edg199/jS/JVh\\n\"\\\n\"8yDSKap6ArptW50IoJQm3wgrDdVzdXDWIhUpx9vGedmHlh/4vzhlL4ESkZ/PEdPr\\n\"\\\n\"STl96pbVCg2XZfI0BFSCM/J4O+6VvTf3LyhdyssVXyQ69r5r9UNCScBuqtt2WxyH\\n\"\\\n\"u4EQmFAVhpXlC+Q1AxulXD/LAwd0eAUdRyGHUxwv9mX6i/f1wRhg+0GaS+fHHcGm\\n\"\\\n\"IlP2Lql+7p1/fvLhgvq7pAdpV120OqpingluFKDIgh97ZJbrIPkyWk8gv/WqMa7t\\n\"\\\n\"uPxN2j7feCEQ9pK8V3ijhr4iuUBUcH9PWClma8G6RfQ+KkK/ltveCwIDAQABo4IC\\n\"\\\n\"qDCCAqQwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF\\n\"\\\n\"BQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBR7tgXuR4+QJ/bDoxOKNyOiWO7X\\n\"\\\n\"gTAfBgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBvBggrBgEFBQcBAQRj\\n\"\\\n\"MGEwLgYIKwYBBQUHMAGGImh0dHA6Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5v\\n\"\\\n\"cmcwLwYIKwYBBQUHMAKGI2h0dHA6Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5v\\n\"\\\n\"cmcvMF4GA1UdEQRXMFWCE21haWwucGhwc2VjdXJlLmluZm+CCnBocHNlY3UucmWC\\n\"\\\n\"DnBocHNlY3VyZS5pbmZvgg53d3cucGhwc2VjdS5yZYISd3d3LnBocHNlY3VyZS5p\\n\"\\\n\"bmZvMEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYB\\n\"\\\n\"BQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIIBBAYKKwYBBAHWeQIE\\n\"\\\n\"AgSB9QSB8gDwAHYAdH7agzGtMxCRIZzOJU9CcMK//V5CIAjGNzV55hB7zFYAAAFo\\n\"\\\n\"ueHsDgAABAMARzBFAiEAizC//cJkynkF8+GDEjKE8KqfeMcIhgFliO4EPG1zw2IC\\n\"\\\n\"IEMre4WA8O6+Mqv4J8MU04zzDZ3lFLlwscDs5b2V0OF9AHYAY/Lbzeg7zCzPC3KE\\n\"\\\n\"J1drM6SNYXePvXWmOLHHaFRL2I0AAAFoueHsCQAABAMARzBFAiACIjUK2/fpfFsd\\n\"\\\n\"n8pDvDtBEdhnt2L4MLANpXTD+INK0QIhAMpSKmoOnLnIXJHdr7iVbg7rYs7iP8yF\\n\"\\\n\"5HsMzggWhGgRMA0GCSqGSIb3DQEBCwUAA4IBAQBIrLITsR5pP9b7YVics3x4BB0N\\n\"\\\n\"IxUZMFZfA1+on23ZkRnWVzENIbmIhlCbwpz2qUIY6nZgm13OsqZ3XL7lRIDnyfSa\\n\"\\\n\"z3JJQmedPL4Mh5QYok02jPZWj5n+RmOrZOI57TquGojFSqPWigUpS5cFbr4I6TWO\\n\"\\\n\"f8D30tw5npbjTfIP4+Tcn9xiivx75kpp4wnhtWwH+4WrTM4zmiFij/GhC66tW0ZV\\n\"\\\n\"nXynRxI/RjfPNd43aL9ox9F+R/xumay+QTBVakEh0Y0VMXzZ3ShA6LXZGER0eHJG\\n\"\\\n\"g+Rd8B0E9Z1Cxi/HYxxJHewzh0jtduCVuIoIRFe/aDu/EHE1B3wJMDw3Wqhw\\n\"\\\n\"-----END CERTIFICATE-----\\n\"\\\n\"-----BEGIN CERTIFICATE-----\\n\"\\\n\"MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/\\n\"\\\n\"MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\\n\"\\\n\"DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow\\n\"\\\n\"SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT\\n\"\\\n\"GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC\\n\"\\\n\"AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF\\n\"\\\n\"q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8\\n\"\\\n\"SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0\\n\"\\\n\"Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA\\n\"\\\n\"a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj\\n\"\\\n\"/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T\\n\"\\\n\"AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG\\n\"\\\n\"CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv\\n\"\\\n\"bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k\\n\"\\\n\"c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw\\n\"\\\n\"VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC\\n\"\\\n\"ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz\\n\"\\\n\"MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu\\n\"\\\n\"Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF\\n\"\\\n\"AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo\\n\"\\\n\"uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/\\n\"\\\n\"wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu\\n\"\\\n\"X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG\\n\"\\\n\"PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6\\n\"\\\n\"KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==\\n\"\\\n\"-----END CERTIFICATE-----\\n\"\\\n\"\";\n\n#endif\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/compile_time.h",
    "content": "/*\n * compile_time.h\n *\n * Created: 30.05.2017 20:57:58\n *  Author: Dennis (instructable.com/member/nqtronix)\n *\n * This code provides the macro __TIME_UNIX__ which returns the current time in UNIX format. It can\n * be used to identify a version of code on an embedded device, to initialize its RTC and much more.\n * Along that several more constants for seconds, minutes, etc. are provided\n *\n * The macro is based on __TIME__ and __DATE__, which are assumed to be formatted \"HH:MM:SS\" and\n * \"MMM DD YYYY\", respectively. The actual value can be calculated by the C compiler at compile time\n * as all inputs are literals. MAKE SURE TO ENABLE OPTIMISATION!\n */\n\n\n#ifndef COMPILE_TIME_H_\n#define COMPILE_TIME_H_\n\n// extracts 1..4 characters from a string and interprets it as a decimal value\n#define CONV_STR2DEC_1(str, i)  (str[i]>'0'?str[i]-'0':0)\n#define CONV_STR2DEC_2(str, i)  (CONV_STR2DEC_1(str, i)*10 + str[i+1]-'0')\n#define CONV_STR2DEC_3(str, i)  (CONV_STR2DEC_2(str, i)*10 + str[i+2]-'0')\n#define CONV_STR2DEC_4(str, i)  (CONV_STR2DEC_3(str, i)*10 + str[i+3]-'0')\n\n// Some definitions for calculation\n#define SEC_PER_MIN             60UL\n#define SEC_PER_HOUR            3600UL\n#define SEC_PER_DAY             86400UL\n#define SEC_PER_YEAR            (SEC_PER_DAY*365)\n#define UNIX_START_YEAR         1970UL\n\n// Custom \"glue logic\" to convert the month name to a usable number\n#define GET_MONTH(str, i)      (str[i]=='J' && str[i+1]=='a' && str[i+2]=='n' ? 1 :     \\\n                                str[i]=='F' && str[i+1]=='e' && str[i+2]=='b' ? 2 :     \\\n                                str[i]=='M' && str[i+1]=='a' && str[i+2]=='r' ? 3 :     \\\n                                str[i]=='A' && str[i+1]=='p' && str[i+2]=='r' ? 4 :     \\\n                                str[i]=='M' && str[i+1]=='a' && str[i+2]=='y' ? 5 :     \\\n                                str[i]=='J' && str[i+1]=='u' && str[i+2]=='n' ? 6 :     \\\n                                str[i]=='J' && str[i+1]=='u' && str[i+2]=='l' ? 7 :     \\\n                                str[i]=='A' && str[i+1]=='u' && str[i+2]=='g' ? 8 :     \\\n                                str[i]=='S' && str[i+1]=='e' && str[i+2]=='p' ? 9 :     \\\n                                str[i]=='O' && str[i+1]=='c' && str[i+2]=='t' ? 10 :    \\\n                                str[i]=='N' && str[i+1]=='o' && str[i+2]=='v' ? 11 :    \\\n                                str[i]=='D' && str[i+1]=='e' && str[i+2]=='c' ? 12 : 0)\n\n#define GET_MONTH2DAYS(month)  ((month == 1 ? 0 : 31 +                      \\\n                                (month == 2 ? 0 : 28 +                      \\\n                                (month == 3 ? 0 : 31 +                      \\\n                                (month == 4 ? 0 : 30 +                      \\\n                                (month == 5 ? 0 : 31 +                      \\\n                                (month == 6 ? 0 : 30 +                      \\\n                                (month == 7 ? 0 : 31 +                      \\\n                                (month == 8 ? 0 : 31 +                      \\\n                                (month == 9 ? 0 : 30 +                      \\\n                                (month == 10 ? 0 : 31 +                     \\\n                                (month == 11 ? 0 : 30))))))))))))           \\\n\n\n#define GET_LEAP_DAYS           ((__TIME_YEARS__-1968)/4 - (__TIME_MONTH__ <=2 ? 1 : 0))\n\n\n\n#define __TIME_SECONDS__        CONV_STR2DEC_2(__TIME__, 6)\n#define __TIME_MINUTES__        CONV_STR2DEC_2(__TIME__, 3)\n#define __TIME_HOURS__          CONV_STR2DEC_2(__TIME__, 0)\n#define __TIME_DAYS__           CONV_STR2DEC_2(__DATE__, 4)\n#define __TIME_MONTH__          GET_MONTH(__DATE__, 0)\n#define __TIME_YEARS__          CONV_STR2DEC_4(__DATE__, 7)\n\n#define __TIME_UNIX__         ((__TIME_YEARS__-UNIX_START_YEAR)*SEC_PER_YEAR+       \\\n                                GET_LEAP_DAYS*SEC_PER_DAY+                          \\\n                                GET_MONTH2DAYS(__TIME_MONTH__)*SEC_PER_DAY+         \\\n                                __TIME_DAYS__*SEC_PER_DAY-SEC_PER_DAY+              \\\n                                __TIME_HOURS__*SEC_PER_HOUR+                        \\\n                                __TIME_MINUTES__*SEC_PER_MIN+                       \\\n                                __TIME_SECONDS__)\n\n#endif /* COMPILE_TIME_H_ */\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/controls.h",
    "content": "#ifndef __CONTROLS_H\n#define __CONTROLS_H\n\n/*\n * Mandatory and optional controls for the menu to be usable\n */\nenum HIDSignal\n{\n  HID_INERT      = 0, // when nothing happens\n  HID_UP         = 1, // optional\n  HID_DOWN       = 2,\n  HID_SELECT     = 3,\n  HID_PAGE_DOWN  = 4,\n  HID_PAGE_UP    = 5,\n  HID_SCREENSHOT = 6\n};\n\n#define FAST_REPEAT_DELAY 50 // ms, push delay\n#define SLOW_REPEAT_DELAY 500 // ms, must be higher than FAST_REPEAT_DELAY and smaller than LONG_DELAY_BEFORE_REPEAT\n#define LONG_DELAY_BEFORE_REPEAT 1000 // ms, delay before slow repeat enables\n\tunsigned long fastRepeatDelay = FAST_REPEAT_DELAY;\nunsigned long beforeRepeatDelay = LONG_DELAY_BEFORE_REPEAT;\n#if defined(ARDUINO_ODROID_ESP32) && defined(_CHIMERA_CORE_)\n  bool JOY_Y_pressed = false;\n  bool JOY_X_pressed = false;\n#endif\n\n#if !defined __M5UNIFIED_HPP__ && (defined ARDUINO_M5Stack_Core_ESP32 || ARDUINO_M5STACK_CORE_ESP32 || defined ARDUINO_M5STACK_FIRE)\n  #define CAN_I_HAZ_M5FACES\n#endif\n\n\n#if defined ARDUINO_M5STACK_Core2 && defined _CHIMERA_CORE_ && defined HAS_AXP192\n  // enable M5Core2's haptic feedback !\n  static bool isVibrating = false;\n\n  static void vibrateTask( void * param )\n  {\n    if( !isVibrating ) {\n      isVibrating = true;\n      int ms = *((int*)param); // dafuq\n      M5.Axp.SetLDOEnable( 3,1 );\n      delay( ms );\n      M5.Axp.SetLDOEnable( 3,0 );\n      isVibrating = false;\n    }\n    vTaskDelete( NULL );\n  }\n\n  static void HIDFeedback( int ms )\n  {\n    // xTaskCreatePinnedToCore( vibrateTask, \"vibrateTask\", 2048, (void*)&ms, 1, NULL , 1 );\n  }\n\n#else\n\n  static void HIDFeedback( int ms ) { ;  }\n\n#endif\n\nstatic bool M5FacesEnabled = false;\n\n#if defined CAN_I_HAZ_M5FACES\n  #if defined(_CHIMERA_CORE_)\n    #include \"drivers/M5Stack/M5Faces.h\"\n  #else\n    #include <M5Faces.h>\n  #endif\n  #define GAMEBOY_KEY_NONE        0x00\n  #define GAMEBOY_KEY_RELEASED    0xFF\n  #define GAMEBOY_KEY_START       0x7F\n  #define GAMEBOY_KEY_SELECT      0xBF\n  #define GAMEBOY_KEY_A           0xEF\n  #define GAMEBOY_KEY_B           0xDF\n  #define GAMEBOY_KEY_UP          0xFE\n  #define GAMEBOY_KEY_DOWN        0xFD\n  #define GAMEBOY_KEY_LEFT        0xFB\n  #define GAMEBOY_KEY_RIGHT       0xF7\n\n  M5Faces Faces;\n\n  uint8_t M5FacesLastReleasedKey = 0x00;\n  unsigned long M5FacesLastI2CQuery = millis();\n  unsigned long M5FacesI2CQueryDelay = 50; // some debounce\n  static bool M5FacesPressed = false;\n\n  void IRAM_ATTR M5FacesIsr()\n  {\n    M5FacesPressed = true;\n  }\n\n  HIDSignal M5FacesOnKeyPushed( HIDSignal signal )\n  {\n    log_d(\"Key %d pushed\", signal );\n    M5FacesLastReleasedKey = GAMEBOY_KEY_NONE;\n    return signal;\n  }\n\n  HIDSignal extKey()\n  {\n    if( ! M5FacesPressed ) {\n      // interrupt wasn't called\n      return HID_INERT;\n    }\n    M5FacesPressed = false;\n    uint8_t keypadState = Faces.getch();\n    // look for \"released\" signal\n    if( keypadState == GAMEBOY_KEY_RELEASED ) {\n      keypadState = M5FacesLastReleasedKey;\n    } else {\n      M5FacesLastReleasedKey = keypadState;\n      keypadState = GAMEBOY_KEY_NONE;\n    }\n    switch( keypadState ) {\n      case GAMEBOY_KEY_UP :\n        return M5FacesOnKeyPushed( HID_UP );\n      break;\n      case GAMEBOY_KEY_DOWN :\n        return M5FacesOnKeyPushed( HID_DOWN );\n      break;\n      case GAMEBOY_KEY_SELECT :\n        return M5FacesOnKeyPushed( HID_SELECT );\n      break;\n      case GAMEBOY_KEY_RIGHT :\n      case GAMEBOY_KEY_LEFT :\n      case GAMEBOY_KEY_START :\n      case GAMEBOY_KEY_A :\n      case GAMEBOY_KEY_B :\n        return M5FacesOnKeyPushed( HID_INERT );\n      break;\n      default:\n        return HID_INERT;\n    }\n  }\n\n#else\n\n  HIDSignal extKey() { return HID_INERT; }\n\n#endif\n\nHIDSignal HIDFeedback( HIDSignal signal, int ms = 50 )\n{\n  if( signal != HID_INERT ) {\n    HIDFeedback( ms );\n  }\n  return signal;\n}\n\n\n#if defined ARDUINO_M5STACK_ATOM_AND_TFCARD\n\n  #include <Button2.h>\n  Button2 button;//for G39\n\n  static HIDSignal _button;\n\n  void handler(Button2 &btn)\n  {\n    switch (btn.getType())\n    {\n      case clickType::single_click:\n      Serial.print(\"single \");\n      _button = HIDFeedback(HID_DOWN);\n      break;\n      case clickType::double_click:\n      Serial.print(\"double \");\n      _button = HIDFeedback(HID_PAGE_DOWN);\n      break;\n      case clickType::triple_click:\n      Serial.print(\"triple \");\n      _button = HIDFeedback(HID_PAGE_UP);\n      break;\n      case clickType::long_click:\n      Serial.print(\"long \");\n      _button = HIDFeedback(HID_SELECT);\n      break;\n      case clickType::empty:\n      break;\n      default:\n      break;\n    }\n\n    Serial.print(\"click\");\n    Serial.print(\" (\");\n    Serial.print(btn.getNumberOfClicks());\n    Serial.println(\")\");\n  }\n\n#endif\n\nvoid HIDInit()\n{\n  #if defined CAN_I_HAZ_M5FACES\n    Wire.begin(SDA, SCL);\n    M5FacesEnabled = Faces.canControlFaces();\n    if( M5FacesEnabled ) {\n      // set the interrupt\n      attachInterrupt(5, M5FacesIsr, FALLING); // 5 = KEYBOARD_INT from M5Faces.cpp\n      Serial.println(\"M5Faces enabled and listening\");\n    }\n  #endif\n\n  #if defined ARDUINO_M5STACK_ATOM_AND_TFCARD\n    // G39 button\n    button.setClickHandler(handler);\n    button.setDoubleClickHandler(handler);\n    button.setTripleClickHandler(handler);\n    button.setLongClickHandler(handler);\n    button.begin(39);\n  #endif\n}\n\n\nHIDSignal getControls()\n{\n  // no buttons? no problemo! (c) Arnold S.\n  if( Serial.available() ) {\n    char command = Serial.read(); // read one char\n    Serial.flush();\n    switch(command) {\n      case 'a':Serial.println(\"Sending HID_DOWN Signal\");      return HIDFeedback( HID_DOWN );\n      case 'b':Serial.println(\"Sending HID_UP Signal\");        return HIDFeedback( HID_UP );\n      case 'c':Serial.println(\"Sending HID_PAGE_DOWN Signal\"); return HIDFeedback( HID_PAGE_DOWN );\n      case 'd':Serial.println(\"Sending HID_PAGE_UP Signal\");   return HIDFeedback( HID_PAGE_UP );\n      case 'e':Serial.println(\"Sending HID_SCREENSHOT Signal\");return HIDFeedback( HID_SCREENSHOT );\n      case 'f':Serial.println(\"Sending HID_SELECT Signal\");    return HIDFeedback( HID_SELECT );\n      default: Serial.print(\"Ignoring serial input: \");Serial.println( String(command) );\n    }\n  }\n\n  #if defined(ARDUINO_ODROID_ESP32) && defined(_CHIMERA_CORE_)\n    M5.update();\n\n    // Odroid-Go buttons support ** with repeat delay **\n    if( M5.JOY_Y.pressedFor( fastRepeatDelay ) ) {\n      uint8_t updown = M5.JOY_Y.isAxisPressed();\n      if( JOY_Y_pressed == false || M5.JOY_Y.pressedFor( beforeRepeatDelay ) ) {\n        beforeRepeatDelay += FAST_REPEAT_DELAY;\n        JOY_Y_pressed = true;\n        switch( updown ) {\n          case 1: return HIDFeedback( HID_DOWN );\n          case 2: return HIDFeedback( HID_UP );\n          break;\n        }\n      }\n      return HID_INERT;\n    } else {\n      if( JOY_Y_pressed == true && M5.JOY_Y.releasedFor( FAST_REPEAT_DELAY ) ) {\n        // button released, reset timers\n        fastRepeatDelay = FAST_REPEAT_DELAY;\n        beforeRepeatDelay = LONG_DELAY_BEFORE_REPEAT;\n        JOY_Y_pressed = false;\n      }\n    }\n\n    if( M5.JOY_X.pressedFor( fastRepeatDelay ) ) {\n      uint8_t leftright = M5.JOY_X.isAxisPressed();\n      if( JOY_X_pressed == false || M5.JOY_X.pressedFor( beforeRepeatDelay ) ) {\n        beforeRepeatDelay += FAST_REPEAT_DELAY;\n        JOY_X_pressed = true;\n        switch( leftright ) {\n          case 1: return HIDFeedback( HID_PAGE_DOWN );\n          case 2: return HIDFeedback( HID_PAGE_UP );\n          break;\n        }\n      }\n      return HID_INERT;\n    } else {\n      if( JOY_X_pressed == true && M5.JOY_X.releasedFor( FAST_REPEAT_DELAY ) ) {\n        // button released, reset timers\n        fastRepeatDelay = FAST_REPEAT_DELAY;\n        beforeRepeatDelay = LONG_DELAY_BEFORE_REPEAT;\n        JOY_X_pressed = false;\n      }\n    }\n\n    bool a = M5.BtnMenu.wasPressed() || M5.BtnA.wasPressed(); // acts as \"BntA\" on M5Stack, leftmost button, alias of \"(A)\" gameboy button\n    bool b = M5.BtnSelect.wasPressed() || M5.BtnB.wasPressed(); // acts as \"BntB\" on M5Stack, middle button, alias of \"(B)\" gameboy button\n    bool c = M5.BtnStart.wasPressed(); // acts as \"BntC\" on M5Stack, rightmost button, no alias\n    bool d = M5.BtnVolume.wasPressed();\n\n    if( d ) return HIDFeedback( HID_SCREENSHOT );\n    if( b ) return HIDFeedback( HID_PAGE_DOWN );\n    if( c ) return HIDFeedback( HID_DOWN );\n    if( a ) return HIDFeedback( HID_SELECT );\n\n  #else\n\n    // M5Faces support\n    if( M5FacesEnabled ) {\n      HIDSignal M5FacesSignal = extKey();\n      if( M5FacesSignal != HID_INERT ) {\n        return HIDFeedback( M5FacesSignal );\n      }\n    }\n\n    #if defined ARDUINO_M5Stack_Core_ESP32 || defined ARDUINO_M5STACK_FIRE || defined ARDUINO_M5STACK_Core2 || defined ARDUINO_ESP32_S3_BOX || defined ARDUINO_M5STACK_CORES3\n\n      M5.update();\n\n      // legacy buttons support\n      bool a = M5.BtnA.wasPressed();\n      bool b = M5.BtnB.wasPressed() && !M5.BtnC.isPressed();\n      bool c = M5.BtnC.wasPressed() && !M5.BtnB.isPressed();\n      bool d = ( M5.BtnB.wasPressed() && M5.BtnC.isPressed() );\n      bool e = ( M5.BtnB.isPressed() && M5.BtnC.wasPressed() );\n\n      if( d || e ) return HIDFeedback( HID_PAGE_UP ); // multiple push, suggested by https://github.com/mongonta0716\n      if( b ) return HIDFeedback( HID_PAGE_DOWN );\n      if( c ) return HIDFeedback( HID_DOWN );\n      if( a ) return HIDFeedback( HID_SELECT );\n\n    #elif defined(ARDUINO_M5STACK_ATOM_AND_TFCARD)\n\n      button.loop();\n      HIDSignal temp = _button;\n      _button = HID_INERT;\n      return temp;\n\n    #endif\n\n  #endif\n\n  //HIDSignal padValue = HID_INERT;\n\n  return HID_INERT;\n}\n\n#endif\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/core.h",
    "content": "#pragma once\n\n//#include <FFat.h>\n#include <SD.h>\n\n#define ECC_NO_PRAGMAS // turn ESP32-Chimera-Core's pragma messages off\n#define ECC_NO_SCREENSHOT // comment this out to take screenshots\n#define ECC_NO_SPEAKER // comment this out to use audio\n#define ECC_NO_NVSUTILS\n#define ECC_NO_POWER\n#define ECC_NO_MPU\n#define ECC_NO_RTC\n#include <ESP32-Chimera-Core.h> // use LGFX display autodetect\n\n//#include <M5Unified.h>\n\n#include <ESP32-targz.h> // optional: https://github.com/tobozo/ESP32-targz\n#define SDU_NO_PRAGMAS // turn M5StackUpdater's pragma messages off\n#define SDU_APP_NAME \"Application Launcher\"\n\n#if defined(ARDUINO_M5STACK_ATOM_AND_TFCARD)\n\n  #if defined _CLK && defined _MISO && defined _MOSI\n    #if !defined SDU_SPI_MODE\n      #define SDU_SPI_MODE SPI_MODE3\n    #endif\n    #if !defined SDU_SPI_FREQ\n      #define SDU_SPI_FREQ 80000000\n    #endif\n    #define SDU_SD_BEGIN [](int csPin)->bool{ SPI.begin(_CLK, _MISO, _MOSI, csPin); SPI.setDataMode(SDU_SPI_MODE); return SD.begin(csPin, SPI, SDU_SPI_FREQ); }\n  #endif\n\n  class LGFX_8BIT_CVBS : public lgfx::LGFX_Device\n  {\n    public:\n      lgfx::Panel_CVBS _panel_instance;\n      LGFX_8BIT_CVBS(void)\n      {\n        {\n          auto cfg = _panel_instance.config();\n          cfg.memory_width  = 320;\n          cfg.memory_height = 240;\n          cfg.panel_width   = 320 - 8;\n          cfg.panel_height  = 240 - 16;\n          cfg.offset_x      = 4;\n          cfg.offset_y      = 8;\n          _panel_instance.config(cfg);\n        }\n        {\n          auto cfg = _panel_instance.config_detail();\n          cfg.signal_type  = cfg.signal_type_t::NTSC_J;\n          cfg.pin_dac      = 26;\n          cfg.use_psram    = 0;\n          cfg.output_level = 128;\n          cfg.chroma_level = 128;\n          _panel_instance.config_detail(cfg);\n        }\n        setPanel(&_panel_instance);\n      }\n  };\n\n  static LGFX_8BIT_CVBS tft;\n\n#elif defined __M5UNIFIED_HPP__\n\n  M5GFX &tft( M5.Lcd );\n\n#else\n\n  M5Display &tft( M5.Lcd );\n\n#endif\n\n#include <M5StackUpdater.h>  // https://github.com/tobozo/M5Stack-SD-Updater\n\nstatic SDU_Sprite sprite = SDU_Sprite( &tft );\nfs::SDFS &M5_FS(SD);\n\nvoid progressBar( SDU_DISPLAY_TYPE tft, int x, int y, int w, int h, uint8_t val, uint16_t color = 0x09F1, uint16_t bgcolor = 0x0000 );\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/downloader.h",
    "content": "/*\n *\n * M5Stack SD Menu\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2019 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n */\n\n//\n\n\n\n\n\n#include \"certificates.h\"\n//#include <WiFi.h>\n#include <HTTPClient.h>\n//#include <WiFi.h>\n#include <WiFiClientSecure.h>\n\n//#define USE_WIFI_MANAGER\n\n#if defined USE_WIFI_MANAGER\n  #include \"wifi_manager.h\"\n#endif\n\n//#define USE_SODIUM // as of 2.0.1-rc1 this produces a bigger binary (+4Kb) + occasional crashes\n#define USE_MBEDTLS // old mbedtls still more stable and produces a smaller binary\n\n#ifdef USE_SODIUM\n  #include \"sodium/crypto_hash_sha256.h\"\n  crypto_hash_sha256_state ctx;\n  #define SHA_START() [](){}\n  #define SHA_INIT crypto_hash_sha256_init\n  #define SHA_UPDATE crypto_hash_sha256_update\n  #define SHA_FINAL crypto_hash_sha256_final\n#elif defined USE_MBEDTLS\n  #define MBEDTLS_SHA256_ALT\n  #define MBEDTLS_ERROR_C\n  #include \"mbedtls/sha256.h\"\n  mbedtls_sha256_context ctx;\n  #define SHA_START() mbedtls_sha256_starts(&ctx,0)\n  #define SHA_INIT mbedtls_sha256_init\n  #define SHA_UPDATE mbedtls_sha256_update\n  #define SHA_FINAL mbedtls_sha256_finish\n#endif\n\n\n// registry this launcher is tied to\n#include \"registry.h\"\n\n#ifndef M5_LIB_VERSION\n  #define M5_LIB_VERSION \"unknown\"\n#endif\n\n// inherit progress bar from SD-Updater library\n#define M5SDMenuProgress SDUCfg.onProgress\n\nlong timezone = 0; // UTC\nbyte daysavetime = 1; // UTC + 1\n\nHTTPClient http;\n\n// tiny buffer shared by HTTP and sha256 sum\nsize_t sizeOfTinyBuff = 512; // smaller is better because sha256 hashing happen between reads\nuint8_t *tinyBuff = nullptr;\n\nextern M5SAM M5Menu;\nextern uint16_t appsCount;\nextern uint16_t MenuID;\nextern AppRegistry Registry;\n\n#define SD_CERT_PATH      \"/cert/\" // Filesystem (SD) temporary path where certificates are stored, needs a trailing slash !!\nString UserAgent;\n\nbool wifisetup = false;\nbool ntpsetup  = false;\nbool done      = false;\nuint8_t progress = 0;\nfloat progress_modulo = 0;\nconst uint16_t M5MENU_GREY = M5Menu.getrgb( 128, 128, 128 );\nconst uint16_t M5MENU_BLUE = M5Menu.getrgb(   0,   0, 128 );\n\n//mbedtls_md_context_t ctx;\n//mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;\nbyte shaResult[32];\nstatic String shaResultStr = \"f7ff9bcd52fee13ae7ebd6b4e3650a4d9d16f8f23cab370d5cdea291e5b6bba6\"; // cheap malloc: any string is good as long as it's 64 chars\n\nint tlserrors = 0;\nint jsonerrors = 0;\nint downloadererrors = 0;\nint updatedfiles = 0;\nint newfiles = 0;\nint checkedfiles = 0;\n\ntypedef struct {\n  const char* host;\n  const char* ca;\n} TLSCert;\n\nconst char* nullHost;\nconst char* nullCa;\n\nTLSCert GithubCert = { \"github.com\", github_ca };\nTLSCert PHPSecureCert = { \"phpsecu.re\", phpsecu_re_ca };\nTLSCert NULLCert = { nullHost, nullCa };\n\nTLSCert TLSWallet[8] = {NULLCert, NULLCert, NULLCert, NULLCert, NULLCert, NULLCert, NULLCert, NULLCert };\n\nbool wget( String bin_url, String outputFile );\nbool wget( String bin_url, const char* outputFile );\nbool wget( const char* bin_url, String outputFile );\nbool wget( const char* bin_url, const char* outputFile );\nint modalConfirm( const char* modalName, const char* question, const char* title, const char* body, const char* labelA, const char* labelB, const char* labelC );\nbool wifiSetupWorked();\nbool init_tls_or_die( String host );\nstatic String heapState();\nvoid WiFiEvent(WiFiEvent_t event);\n\nstruct URLParts {\n  String url;\n  String protocol;\n  String host;\n  String port;\n  String auth;\n  String uri;\n};\n\n\n\n\nbool tinyBuffInit()\n{\n  if( tinyBuff == nullptr ) {\n    tinyBuff = (uint8_t *)heap_caps_malloc(sizeOfTinyBuff, MALLOC_CAP_8BIT);\n    if( tinyBuff == NULL ) {\n      return false;\n    } else {\n      log_d(\"Allocated %d bytes for wget buffer\", sizeOfTinyBuff );\n    }\n  } else {\n    log_d(\"Reusing wget buffer\");\n  }\n  return true;\n}\n\n\n\nURLParts parseURL( String url ) { // logic stolen from HTTPClient::beginInternal()\n  URLParts urlParts;\n  int index = url.indexOf(':');\n  if(index < 0) {\n    log_e(\"failed to parse protocol\");\n    return urlParts;\n  }\n  urlParts.url = \"\" + url;\n  urlParts.protocol = url.substring(0, index);\n  url.remove(0, (index + 3)); // remove http:// or https://\n  index = url.indexOf('/');\n  String host = url.substring(0, index);\n  url.remove(0, index); // remove host part\n  index = host.indexOf('@'); // get Authorization\n  if(index >= 0) { // auth info\n    urlParts.auth = host.substring(0, index);\n    host.remove(0, index + 1); // remove auth part including @\n  }\n  index = host.indexOf(':'); // get port\n  if(index >= 0) {\n    urlParts.host = host.substring(0, index); // hostname\n    host.remove(0, (index + 1)); // remove hostname + :\n    urlParts.port = host.toInt(); // get port\n  } else {\n    urlParts.host = host;\n  }\n  urlParts.uri = url;\n  return urlParts;\n}\n\nURLParts parseURL( const char* url ) {\n  return parseURL( String( url ) );\n}\n\n\n\n\n\nvoid registrySave( AppRegistry registry, String appRegistryLocalFile = \"\" ) {\n  URLParts urlParts = parseURL( registry.url );\n  if( appRegistryLocalFile == \"\" ) {\n    log_d(\"Will attempt to create/save %s\", appRegistryLocalFile.c_str() );\n    appRegistryLocalFile = String( appRegistryFolder + \"/\" + urlParts.host + \".json\" );\n  }\n\n  DynamicJsonDocument jsonRegistryBuffer(2048);\n  if( jsonRegistryBuffer.capacity() == 0 ) {\n    log_e(\"ArduinoJSON failed to allocate 2kb\");\n    return;\n  }\n\n  if( M5_FS.exists( appRegistryLocalFile ) ) {\n    log_d(\"Removing %s before writing\", appRegistryLocalFile.c_str());\n    M5_FS.remove( appRegistryLocalFile );\n  }\n  // Open file for writing\n  File file = M5_FS.open( appRegistryLocalFile, FILE_WRITE );\n  if (!file) {\n    log_e(\"Failed to create file %s\", appRegistryLocalFile.c_str());\n    return;\n  }\n\n  JsonObject channels            = jsonRegistryBuffer.createNestedObject(\"channels\");\n  JsonObject masterChannelJson   = channels.createNestedObject(\"master\");\n  JsonObject unstableChannelJson = channels.createNestedObject(\"unstable\");\n\n  masterChannelJson[\"name\"]         = registry.masterChannel.name;\n  masterChannelJson[\"description\"]  = registry.masterChannel.description;\n  masterChannelJson[\"url\"]          = registry.masterChannel.url;\n  masterChannelJson[\"api_host\"]     = registry.masterChannel.api_host;\n  masterChannelJson[\"api_path\"]     = registry.masterChannel.api_path;\n  masterChannelJson[\"cert_path\"]    = registry.masterChannel.api_cert_path;\n  masterChannelJson[\"updater_path\"] = registry.masterChannel.updater_path;\n  masterChannelJson[\"endpoint\"]     = registry.masterChannel.catalog_endpoint;\n\n  unstableChannelJson[\"name\"]         = registry.unstableChannel.name;\n  unstableChannelJson[\"description\"]  = registry.unstableChannel.description;\n  unstableChannelJson[\"url\"]          = registry.unstableChannel.url;\n  unstableChannelJson[\"api_host\"]     = registry.unstableChannel.api_host;\n  unstableChannelJson[\"api_path\"]     = registry.unstableChannel.api_path;\n  unstableChannelJson[\"cert_path\"]    = registry.unstableChannel.api_cert_path;\n  unstableChannelJson[\"updater_path\"] = registry.unstableChannel.updater_path;\n  unstableChannelJson[\"endpoint\"]     = registry.unstableChannel.catalog_endpoint;\n\n  jsonRegistryBuffer[\"name\"]                 = registry.name;\n  jsonRegistryBuffer[\"description\"]          = registry.description;\n  jsonRegistryBuffer[\"url\"]                  = registry.url;\n  jsonRegistryBuffer[\"pref_default_channel\"] = registry.pref_default_channel;\n\n  log_i(\"Created json:\");\n  serializeJsonPretty(jsonRegistryBuffer, Serial);\n  //Serial.println();\n\n  if (serializeJson(jsonRegistryBuffer, file) == 0) {\n    log_e( \"Failed to write to file %s\", appRegistryLocalFile.c_str() );\n  } else {\n    log_i (\"Successfully created %s\", appRegistryLocalFile.c_str() );\n  }\n  file.close();\n\n}\n\n\nvoid registryFetch( AppRegistry registry, String appRegistryLocalFile = \"\" ) {\n  if( !wifiSetupWorked() ) {\n    modalConfirm( \"wififail\", MENU_BTN_CANCELED, \"    No connexion available\", MODAL_SAME_PLAYER_SHOOT_AGAIN, DOWNLOADER_MODAL_REBOOT, DOWNLOADER_MODAL_RESTART, MENU_BTN_WFT );\n    ESP.restart();\n  }\n  URLParts urlParts = parseURL( registry.url );\n\n  init_tls_or_die( urlParts.host );\n\n  if( appRegistryLocalFile == \"\" ) {\n    appRegistryLocalFile = appRegistryFolder + \"/\" + appRegistryDefaultName;\n  } else {\n    appRegistryLocalFile = appRegistryFolder + \"/\" + urlParts.host + \".json\";\n  }\n  if( !wget( registry.url , appRegistryLocalFile ) ) {\n    modalConfirm( \"regdead\", MENU_BTN_CANCELED, MODAL_REGISTRY_DAMAGED, MODAL_SAME_PLAYER_SHOOT_AGAIN, DOWNLOADER_MODAL_REBOOT, DOWNLOADER_MODAL_RESTART, MENU_BTN_WFT );\n  } else {\n    String appRegistryDefaultFile = appRegistryFolder + \"/\" + appRegistryDefaultName;\n    File sourceFile = M5_FS.open( appRegistryLocalFile );\n    if( M5_FS.exists( appRegistryDefaultFile ) ) {\n      M5_FS.remove( appRegistryDefaultFile );\n    }\n    File destFile   = M5_FS.open( appRegistryDefaultFile, FILE_WRITE );\n    static uint8_t buf[512];\n    size_t packets = 0;\n    while( (packets = sourceFile.read( buf, sizeof(buf))) > 0 ) {\n      destFile.write( buf, packets );\n    }\n    destFile.close();\n    sourceFile.close();\n    modalConfirm( \"regupd\", UPDATE_SUCCESS, MODAL_REGISTRY_UPDATED, MODAL_REBOOT_REGISTRY_UPDATED, DOWNLOADER_MODAL_REBOOT, DOWNLOADER_MODAL_RESTART, MENU_BTN_WFT );\n  }\n  ESP.restart();\n}\n\n\n\n\nAppRegistry registryInit( String appRegistryLocalFile = \"\" ) {\n  if( appRegistryLocalFile == \"\" ) {\n    appRegistryLocalFile = appRegistryFolder + \"/\" + appRegistryDefaultName;\n  }\n  log_i(\"Opening channel file: %s\", appRegistryLocalFile.c_str());\n\n  if( !M5_FS.exists( appRegistryLocalFile ) ) {\n    // create file\n    log_i(\"Registry file %s does not exist, creating from firmware defaults\", appRegistryLocalFile.c_str() );\n    registrySave( defaultAppRegistry, appRegistryFolder + \"/\" + appRegistryDefaultName );\n    defaultAppRegistry.init();\n    return defaultAppRegistry;\n  }\n  // load from file\n\n  File file = M5_FS.open( appRegistryLocalFile );\n\n  AppRegistryItem masterChannel;\n  AppRegistryItem unstableChannel;\n\n  //DynamicJsonDocument jsonRegistryBuffer(2048);\n  StaticJsonDocument<2048> jsonRegistryBuffer;\n  DeserializationError error = deserializeJson( jsonRegistryBuffer, file );\n  if (error) {\n    log_e(\"JSON Error while reading registry file %s\", appRegistryLocalFile.c_str() );\n    defaultAppRegistry.init();\n    return defaultAppRegistry;\n  }\n  JsonObject root = jsonRegistryBuffer.as<JsonObject>();\n  if ( root.isNull() ) {\n    log_w(\"Registry file %s has empty JSON\", appRegistryLocalFile.c_str() );\n    defaultAppRegistry.init();\n    return defaultAppRegistry;\n  } else  {\n    if( root[\"channels\"][\"master\"][\"name\"].as<String>() ==\"\"\n     || root[\"channels\"][\"master\"][\"description\"].as<String>() ==\"\"\n     || root[\"channels\"][\"master\"][\"url\"].as<String>() ==\"\"\n     || root[\"channels\"][\"master\"][\"api_host\"].as<String>() ==\"\"\n     || root[\"channels\"][\"master\"][\"api_path\"].as<String>() ==\"\"\n     || root[\"channels\"][\"master\"][\"cert_path\"].as<String>() ==\"\"\n     || root[\"channels\"][\"master\"][\"updater_path\"].as<String>() ==\"\"\n     || root[\"channels\"][\"master\"][\"endpoint\"].as<String>() ==\"\" ) {\n     // bad master item\n     log_w(\"%s\", \"Bad master channel in JSON file\");\n     defaultAppRegistry.init();\n     return defaultAppRegistry;\n    } else {\n      masterChannel = {\n        \"master\",\n        root[\"channels\"][\"master\"][\"description\"].as<String>(),\n        root[\"channels\"][\"master\"][\"url\"].as<String>(),\n        root[\"channels\"][\"master\"][\"api_host\"].as<String>(),\n        root[\"channels\"][\"master\"][\"api_path\"].as<String>(),\n        root[\"channels\"][\"master\"][\"cert_path\"].as<String>(),\n        root[\"channels\"][\"master\"][\"updater_path\"].as<String>(),\n        root[\"channels\"][\"master\"][\"endpoint\"].as<String>()\n      };\n    }\n\n    if( root[\"channels\"][\"unstable\"][\"name\"].as<String>() ==\"\"\n     || root[\"channels\"][\"unstable\"][\"description\"].as<String>() ==\"\"\n     || root[\"channels\"][\"unstable\"][\"url\"].as<String>() ==\"\"\n     || root[\"channels\"][\"unstable\"][\"api_host\"].as<String>() ==\"\"\n     || root[\"channels\"][\"unstable\"][\"api_path\"].as<String>() ==\"\"\n     || root[\"channels\"][\"unstable\"][\"cert_path\"].as<String>() ==\"\"\n     || root[\"channels\"][\"unstable\"][\"updater_path\"].as<String>() ==\"\"\n     || root[\"channels\"][\"unstable\"][\"endpoint\"].as<String>() ==\"\" ) {\n     // bad master item\n     log_w(\"%s\", \"Bad unstable channel in JSON file\");\n     defaultAppRegistry.init();\n     return defaultAppRegistry;\n    } else {\n      unstableChannel = {\n        \"unstable\",\n        root[\"channels\"][\"unstable\"][\"description\"].as<String>(),\n        root[\"channels\"][\"unstable\"][\"url\"].as<String>(),\n        root[\"channels\"][\"unstable\"][\"api_host\"].as<String>(),\n        root[\"channels\"][\"unstable\"][\"api_path\"].as<String>(),\n        root[\"channels\"][\"unstable\"][\"cert_path\"].as<String>(),\n        root[\"channels\"][\"unstable\"][\"updater_path\"].as<String>(),\n        root[\"channels\"][\"unstable\"][\"endpoint\"].as<String>()\n      };\n    }\n\n    if( root[\"name\"].as<String>() == \"\"\n     || root[\"description\"].as<String>() == \"\"\n     || root[\"url\"].as<String>() == \"\" ) {\n      log_w(\"%s\", \"Bad channel meta in JSON file\");\n      defaultAppRegistry.init();\n      return defaultAppRegistry;\n    } else {\n      String SDUpdaterChannelNameStr    = \"\";\n      if( !root[\"pref_default_channel\"].isNull() && root[\"pref_default_channel\"].as<String>() != \"\" ) {\n        // inherit from json\n        SDUpdaterChannelNameStr = root[\"pref_default_channel\"].as<String>();\n      } else {\n        // assign default\n        SDUpdaterChannelNameStr = \"master\";\n      }\n      AppRegistry appRegistry = {\n        root[\"name\"].as<String>(),\n        root[\"description\"].as<String>(),\n        root[\"url\"].as<String>(),\n        SDUpdaterChannelNameStr, // default channel\n        masterChannel,\n        unstableChannel\n      };\n      appRegistry.init();\n      return appRegistry;\n    }\n  }\n\n};\n\n\nint modalConfirm( const char* modalName, 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 ) {\n  tft.clear();\n  M5Menu.drawAppMenu( question, labelA, labelB, labelC);\n  tft.setTextSize( 1 );\n  tft.setTextColor( TFT_WHITE );\n  tft.drawCentreString( title, 160, 50, 1 );\n  tft.setCursor( 0, 72 );\n  tft.print( body );\n\n  tft.drawJpg(caution_jpg, caution_jpg_len, 224, 136, 64, 46 );\n  HIDSignal hidState = HID_INERT;\n\n  while( hidState==HID_INERT ) {\n    delay( 100 );\n    M5.update();\n    hidState = getControls();\n    #ifdef _CHIMERA_CORE_\n      if( hidState == HID_SCREENSHOT ) {\n        M5.ScreenShot->snap( modalName );\n        hidState = HID_INERT;\n      }\n    #endif\n  }\n  return hidState;\n}\n\n\nvoid printProgress(uint16_t progress) {\n  uint16_t x = tft.getCursorX();\n  uint16_t y = tft.getCursorX();\n  tft.setTextColor( TFT_WHITE, M5MENU_GREY );\n  tft.setCursor( 10, 194 );\n  tft.print( String( OVERALL_PROGRESS_TITLE ) + String(progress) + \"%\" );\n  tft.setCursor( 260, 194 );\n  tft.print( String(downloadererrors) + \" errors\" );\n  tft.setCursor( x, y );\n}\n\n\nvoid renderDownloadIcon(uint16_t color=TFT_GREEN, int16_t x=272, int16_t y=7, float size=2.0 ) {\n  float halfsize = size/2;\n  tft.fillTriangle(x,      y+2*size,   x+4*size, y+2*size,   x+2*size, y+5*size, color);\n  tft.fillTriangle(x+size, y,          x+3*size, y,          x+2*size, y+5*size, color);\n  tft.fillRect( x, -halfsize+y+6*size, 1+4*size, size, color);\n}\n\n\nvoid drawRSSIBar(int16_t x, int16_t y, int16_t rssi, uint16_t bgcolor, float size=1.0) {\n  uint16_t barColors[4] = { bgcolor, bgcolor, bgcolor, bgcolor };\n  switch(rssi%6) {\n   case 5:\n      barColors[0] = TFT_GREEN;\n      barColors[1] = TFT_GREEN;\n      barColors[2] = TFT_GREEN;\n      barColors[3] = TFT_GREEN;\n    break;\n    case 4:\n      barColors[0] = TFT_GREEN;\n      barColors[1] = TFT_GREEN;\n      barColors[2] = TFT_GREEN;\n    break;\n    case 3:\n      barColors[0] =  TFT_YELLOW;\n      barColors[1] =  TFT_YELLOW;\n      barColors[2] =  TFT_YELLOW;\n    break;\n    case 2:\n      barColors[0] =  TFT_ORANGE;\n      barColors[1] =  TFT_ORANGE;\n    break;\n    case 1:\n      barColors[0] = TFT_RED;\n    break;\n    default:\n    case 0:\n      barColors[0] = TFT_RED; // want: RAINBOW\n    break;\n  }\n  tft.fillRect(x,          y + 4*size, 2*size, 4*size, barColors[0]);\n  tft.fillRect(x + 3*size, y + 3*size, 2*size, 5*size, barColors[1]);\n  tft.fillRect(x + 6*size, y + 2*size, 2*size, 6*size, barColors[2]);\n  tft.fillRect(x + 9*size, y + 1*size, 2*size, 7*size, barColors[3]);\n}\n\n\nvoid drawSDUpdaterChannel() {\n  tft.setTextColor(TFT_WHITE, M5MENU_BLUE );\n  tft.setTextDatum( ML_DATUM );\n  tft.drawJpg(bluefork_jpg, bluefork_jpg_len, 2, 8 );\n  tft.drawString( Registry.defaultChannel.name, 18, 14 );\n  tft.setTextColor(TFT_WHITE, M5MENU_GREY );\n}\n\n\nvoid drawAppMenu() {\n  M5Menu.windowClr();\n  #if defined(ARDUINO_ODROID_ESP32) && defined(_CHIMERA_CORE_)\n    M5Menu.drawAppMenu( APP_DOWNLOADER_MENUTITLE, \"\", \"\", \"\", \"\");\n  #else\n    M5Menu.drawAppMenu( APP_DOWNLOADER_MENUTITLE, \"\", \"\", \"\");\n  #endif\n  drawSDUpdaterChannel();\n  if( wifisetup ) {\n    drawRSSIBar( 290, 4, 5, M5MENU_BLUE, 2.0 );\n  }\n  if( ntpsetup ) {\n    // TODO: draw something\n  }\n}\n\n\nvoid cleanDir( const char* dir) {\n\n  tft.fillRoundRect( 0, 32, M5.Lcd.width(), M5.Lcd.height()-32-32, 3, M5MENU_GREY );\n  tft.setCursor( 8, 36 );\n  tft.setTextColor( TFT_WHITE, M5MENU_GREY );\n\n  String dirToOpen = String( dir );\n\n  // trim last slash if any, except for rootdir\n  if( dirToOpen != \"/\" && dirToOpen.endsWith(\"/\" ) ) {\n    dirToOpen = dirToOpen.substring(0, dirToOpen.length()-1);\n  }\n\n  File root = M5_FS.open( dirToOpen );\n  if(!root){\n    log_e(\"%s\",  DEBUG_DIROPEN_FAILED );\n    return;\n  }\n  if(!root.isDirectory()){\n    log_e(\"%s\",  DEBUG_NOTADIR );\n    return;\n  }\n\n  File file = root.openNextFile();\n  while(file) {\n    if(file.isDirectory()){\n      // don't delete net-yet-emptied folders\n      file = root.openNextFile();\n      continue;\n    }\n    if( tft.getCursorY() > tft.height()-32-16 ) {\n      tft.fillRoundRect( 0, 32, M5.Lcd.width(), M5.Lcd.height()-32-32, 3, M5MENU_GREY );\n      tft.setCursor( 8, 36 );\n    }\n    Serial.printf( CLEANDIR_REMOVED, SDUpdater::fs_file_path( &file ) /*file.name()*/ );\n    tft.setCursor( 8, tft.getCursorY() );\n    tft.printf( CLEANDIR_REMOVED, SDUpdater::fs_file_path( &file ) /*file.name()*/ );\n    M5_FS.remove( SDUpdater::fs_file_path( &file ) /*file.name()*/ );\n    file = root.openNextFile();\n  }\n\n  //drawAppMenu();\n}\n\n\n/* this is to avoid using GOTO statements */\ntypedef struct {\n  bool deleteclient = true;\n  bool endhttp = false;\n  bool dismiss( WiFiClientSecure *client, bool error = false ) {\n    if( error ) {\n      renderDownloadIcon( TFT_RED );\n      downloadererrors++;\n    }\n    if( endhttp ) {\n      log_d(\"[HEAP before http.end(): %d]\", ESP.getFreeHeap() );\n      if( http.connected() ) {\n        http.end();\n      }\n      endhttp = false;\n      log_d(\"[HEAP after http.end(): %d]\", ESP.getFreeHeap() );\n    }\n    if( deleteclient ) {\n      log_d(\"[Deleting WiFiClientSecure client: %d]\", ESP.getFreeHeap() );\n      delete client;\n      deleteclient = false;\n      log_d(\"[Deleted WiFiClientSecure client: %d]\", ESP.getFreeHeap() );\n    }\n    return !error;\n  }\n} HTTPRouter;\n\n\nvoid sha_sum_to_str() {\n  shaResultStr = \"\";\n  char str[3];\n  for(int i= 0; i< sizeof(shaResult); i++) {\n    sprintf(str, \"%02x\", (int)shaResult[i]);\n    shaResultStr += String( str );\n  }\n}\n\n\nstatic void sha256_sum(const char* fileName) {\n  log_d(\"SHA256: checking file %s\\n\", fileName);\n  File checkFile = M5_FS.open( fileName );\n  size_t fileSize = checkFile.size();\n  size_t len = fileSize;\n  if( !checkFile || fileSize==0 ) {\n    downloadererrors++;\n    log_e(\"  [ERROR] Can't open %s file for reading, aborting\\n\", fileName);\n    return;\n  }\n  tft.drawJpg( checksum_jpg, checksum_jpg_len, 288, 125, 22, 32 );\n\n  tinyBuffInit();\n  *shaResult = {0};\n\n  SHA_INIT(&ctx);\n  SHA_START();\n\n  size_t n;\n  while ((n = checkFile.read(tinyBuff, sizeOfTinyBuff)) > 0) {\n    SHA_UPDATE(&ctx, (const unsigned char *) tinyBuff, n);\n    if( fileSize/10 > sizeOfTinyBuff && fileSize != len ) {\n      M5SDMenuProgress(fileSize-len, fileSize);\n    }\n    len -= n;\n    delay(1);\n  }\n  tft.fillRect( 288, 125, 22, 32, M5MENU_GREY );\n  checkFile.close();\n\n  SHA_FINAL(&ctx, shaResult);\n  sha_sum_to_str();\n}\n\nstatic void sha256_sum( String fileName ) {\n  return sha256_sum( fileName.c_str() );\n}\n\n\n\nconst char* updateWallet( String host, const char* ca) {\n  int8_t idx = -1;\n  uint8_t sizeOfWallet = sizeof( TLSWallet ) / sizeof( TLSWallet[0] );\n  for(uint8_t i=0; i<sizeOfWallet; i++) {\n    if( TLSWallet[i].host==NULL ) {\n      if( idx == -1 ) {\n        idx = i;\n      }\n      continue;\n    }\n    if( String( TLSWallet[i].host ) == host ) {\n      log_d(\"[WALLET SKIP UPDATE] Wallet #%d exists ( %s )\\n\", i, TLSWallet[i].host );\n      return TLSWallet[i].ca;\n    }\n  }\n  if( idx > -1 ) {\n    int hostlen = host.length() + 1;\n    int certlen = String(ca).length() + 1;\n    char *newhost = (char*)malloc( hostlen );\n    char *newcert = (char*)malloc( certlen );\n    memcpy( newhost, host.c_str(), hostlen );\n    memcpy( newcert, ca, certlen);\n    TLSWallet[idx] = { (const char*)newhost , (const char*)newcert };\n    log_d(\"[WALLET UPDATE] Wallet #%d loaded ( %s )\\n\", idx, TLSWallet[idx].host );\n    return TLSWallet[idx].ca;\n  }\n  return ca;\n}\n\n\nconst char* fetchLocalCert( String host ) {\n  String certPath = String( SD_CERT_PATH ) + host;\n  File certFile = M5_FS.open( certPath );\n  if(! certFile ) { // failed to open the cert file\n    log_w(\"[WARNING] Failed to open the cert file %s, TLS cert checking therefore disabled\", certPath.c_str() );\n    return NULL;\n  }\n  String certStr = \"\";\n  while( certFile.available() ) {\n    certStr += certFile.readStringUntil('\\n') + \"\\n\";\n  }\n  certFile.close();\n  log_v(\"\\n%s\\n\", certStr.c_str() );\n  const char* certChar = updateWallet( host, certStr.c_str() );\n  return certChar;\n}\n\nbool isInWallet( String host ) {\n  uint8_t sizeOfWallet = sizeof( TLSWallet ) / sizeof( TLSWallet[0] );\n  for(uint8_t i=0; i<sizeOfWallet; i++) {\n    if( TLSWallet[i].host==NULL ) continue;\n    if( String( TLSWallet[i].host ) == host ) {\n      return true;\n    }\n  }\n  return false;\n}\n\nconst char* getWalletCert( String host ) {\n  uint8_t sizeOfWallet = sizeof( TLSWallet ) / sizeof( TLSWallet[0] );\n  log_d(\"\\nChecking wallet (%d items)\",  sizeOfWallet );\n  for(uint8_t i=0; i<sizeOfWallet; i++) {\n    if( TLSWallet[i].host==NULL ) continue;\n    if( String( TLSWallet[i].host ) == host ) {\n      log_d(\"Wallet #%d ( %s ) : [OK]\", i, TLSWallet[i].host );\n      //log_d(\" [OK]\");\n      return TLSWallet[i].ca;\n    } else {\n      log_d(\"Wallet #%d ( %s ) : [KO]\", i, TLSWallet[i].host );\n      //log_d(\" [KO]\");\n    }\n  }\n  const char* nullcert = NULL;\n  return nullcert;\n}\n\n\nconst char* fetchCert( String host, bool checkWallet = true, bool checkFS = true ) {\n  //const char* nullcert = NULL;\n  if( checkWallet ) {\n    const char* walletCert = getWalletCert( host );\n    if( walletCert != NULL ) {\n      log_d(\"[FETCHED WALLET CERT] -> %s\", host.c_str() );\n      return walletCert;\n    } else {\n      //\n    }\n  }\n  String certPath = String( SD_CERT_PATH ) + host;\n  String certURL = Registry.defaultChannel.api_cert_provider_url_https + host;\n  if( !checkFS || !M5_FS.exists( certPath ) ) {\n    //log_d(\"[FETCHING REMOTE CERT] -> \");\n    //wget(certURL , certPath );\n    return fetchLocalCert( host );\n  } else {\n    log_d(\"[FETCHING LOCAL (SD) CERT] -> %s\", certPath.c_str() );\n  }\n  return fetchLocalCert( host );\n}\n\n\n\nbool syncConnect(WiFiClientSecure *client, HTTPRouter &router, URLParts urlParts, const char* sender=\"none\", bool enforceTLS=false) {\n  if(!client) {\n    log_e( \"[%s:ERROR] Attempt to syncConnect to %s without a proper WiFiClientSecure client, aborting!\", sender, urlParts.url.c_str() );\n    return false; // router.dismiss( client, true );\n  } else {\n    log_d( \"[%s:INFO] Synconnect to %s [%d]\", sender, urlParts.url.c_str(), ESP.getFreeHeap() );\n  }\n\n  http.setConnectTimeout( 10000 ); // 10s timeout = 10000\n  //if( urlParts.protocol == \"https\" ) {\n    log_d( \"[%s:INFO] Synconnect protocol is %s [%d]\", sender, urlParts.protocol.c_str(), ESP.getFreeHeap() );\n    if( isInWallet( urlParts.host ) ) {\n      //client->setCACert( fetchCert( urlParts.host ) );\n      client->setCACert( fetchCert( urlParts.host ) );\n    } else {\n//       if( !enforceTLS ) {\n//         log_e(\" [%s:WARNING] An HTTPS URL was called (%s) but no certificate was provided\", sender, urlParts.url.c_str() );\n//         client->setCACert( NULL ); // disable TLS check\n//       } else {\n        log_e(\" [%s:ERROR] An HTTPS URL was called (%s) but no certificate was provided, trying without cert\", sender, urlParts.url.c_str() );\n        client->setInsecure();\n        //return false;\n//      }\n    }\n    //client->setTimeout( 10 ); // in seconds\n    router.endhttp = false;\n    log_d( \"[%s:INFO] http.begin( TLS ) to %s [%d]\", sender, urlParts.url.c_str(), ESP.getFreeHeap() );\n    if ( ! http.begin(*client, urlParts.url ) ) {\n      tlserrors++;\n      log_e(\" [%s:ERROR] HTTPS failed\", sender);\n      router.endhttp = false;\n      router.dismiss( client, true );\n      return false;\n    }\n    log_d( \"[%s:INFO] TLS SUCCESS [%d]\", sender, ESP.getFreeHeap() );\n//   } else {\n//     //client->setInsecure();\n//     log_d(\" [%s:INFO] An HTTP (NO TLS) URL was called (%s)\", sender, urlParts.url.c_str() );\n//     router.endhttp = false;\n//     http.begin(*client, urlParts.url );\n//   }\n  log_d( \"[%s:INFO] Running http.GET( %s ) [%d]\", sender, urlParts.url.c_str(), ESP.getFreeHeap() );\n  int httpCode = http.GET();\n  log_d( \"[%s:INFO] http.GET() SENT [%d]\", sender, ESP.getFreeHeap() );\n\n  if(httpCode <= 0) {\n    log_e(\"\\n[%s:ERROR] HTTP GET %s failed [%d]\", sender, urlParts.url.c_str(), ESP.getFreeHeap() );\n    router.dismiss( client, true );\n    return false;\n  }\n  if(httpCode != HTTP_CODE_OK) {\n    log_e(\"\\n[%s:ERROR %d] HTTP GET %s failed: %s [%d]\", sender, httpCode, urlParts.url.c_str(), http.errorToString(httpCode).c_str(), ESP.getFreeHeap() );\n    router.dismiss( client, true );\n    return false;\n  }\n  return true;\n}\n\n\n\n\n\nbool wget( const char* bin_url, const char* outputFile ) {\n  log_d(\"[HEAP Before: %d]\", ESP.getFreeHeap() );\n  Serial.printf(\"#> wget %s --output-document=%s \", bin_url, outputFile );\n  renderDownloadIcon( TFT_GREEN );\n  WiFiClientSecure *client = new WiFiClientSecure;\n  //int httpCode;\n\n  HTTPRouter wgetRouter;\n  URLParts urlParts = parseURL( bin_url );\n\n  if( ! syncConnect(client, wgetRouter, urlParts, \"wget()\") ) {\n    wgetRouter.dismiss( client, true );\n    return false;\n  }\n\n  int len = http.getSize();\n\n  if(len<=0) {\n    log_e(\"  [ERROR] %s has zero Content-Lenght, aborting\\n\", bin_url );\n    wgetRouter.dismiss( client, true );\n    return false;\n  }\n  int httpSize = len;\n\n  if( !tinyBuffInit() ) {\n    log_e(\"Failed to allocate memory for download buffer, aborting\");\n    wgetRouter.dismiss( client, true );\n  }\n\n  File myFile = M5_FS.open( outputFile, FILE_WRITE);\n  if(!myFile) {\n    myFile.close();\n    log_e(\"  [ERROR] Failed to open %s for writing, aborting\\n\", outputFile );\n    wgetRouter.dismiss( client, true );\n    return false;\n  }\n  unsigned long downwloadstart = millis();\n  WiFiClient *stream = http.getStreamPtr();\n  *shaResult = {0};\n\n  SHA_INIT( &ctx );\n  SHA_START();\n\n  tft.drawJpg( checksum_jpg, checksum_jpg_len, 288, 125, 22, 32 );\n  tft.drawJpg( download_jpg, download_jpg_len, 252, 125, 26, 32 );\n  Serial.print(\"[Download+SHA256 Sum -->][..\");\n  while(http.connected() && (len > 0 || len == -1)) {\n    int c = stream->readBytes(tinyBuff, sizeOfTinyBuff );\n    if( c > 0 ) {\n      // write it to SD\n      myFile.write(tinyBuff, c);\n      delay(1);\n      // calculate hash sum (multipart mode)\n      SHA_UPDATE(&ctx, (const unsigned char *)tinyBuff, c);\n      delay(1);\n    } else {\n      log_d(\"Got empty buffer from last read while %d bytes remaning\", len );\n    }\n    if(len > 0) {\n      len -= c;\n    }\n    if( httpSize/10 > sizeOfTinyBuff && httpSize!=len ) {\n      M5SDMenuProgress(httpSize-len, httpSize);\n    }\n    vTaskDelay(1); // feed the watchdog to prevent beacon timeouts\n  }\n  SHA_FINAL(&ctx, shaResult);\n  myFile.close();\n  Serial.print(\"]\");\n  sha_sum_to_str();\n  tft.fillRect( 252, 125, 70, 32, M5MENU_GREY );\n  renderDownloadIcon( M5MENU_GREY );\n  unsigned long dl_duration;\n  float bytespermillis;\n  dl_duration = ( millis() - downwloadstart );\n  if( dl_duration > 0 ) {\n    bytespermillis = httpSize / dl_duration;\n    Serial.printf(\"  [OK][Downloaded %d KB at %d KB/s]\\n\", (httpSize/1024), (int)( bytespermillis / 1024.0 * 1000.0 ) );\n  } else {\n    Serial.printf(\"  [OK] Copy done...\\n\");\n  }\n  //free(tinyBuff);\n  delete stream;\n  wgetRouter.endhttp = false;\n  wgetRouter.deleteclient = false;\n  wgetRouter.dismiss( client, false );\n  return true;\n}\n// aliases\nbool wget( String bin_url, String outputFile ) {\n  return wget( bin_url.c_str(), outputFile.c_str() );\n}\nbool wget( String bin_url, const char* outputFile ) {\n  return wget( bin_url.c_str(), outputFile );\n}\nbool wget( const char* bin_url, String outputFile ) {\n  return wget( bin_url, outputFile.c_str() );\n}\n\n\nstatic void countDownReboot( void * param ) {\n  unsigned long wait = 10000;\n  unsigned long startCountDown = millis();\n  while( startCountDown + wait > millis() ) {\n    delay( 100 );\n  }\n  ESP.restart();\n  vTaskDelete( NULL );\n}\n\n\nvoid syncFinished( bool restart=true ) {\n  M5Menu.windowClr();\n  tft.setCursor(10,60);\n  tft.setTextColor( TFT_WHITE, M5MENU_GREY );\n  tft.println( SYNC_FINISHED );\n  Serial.printf(\"\\n\\n## Download Finished  ##\\n   Errors: %d\\n\\n\", downloadererrors );\n  xTaskCreatePinnedToCore( countDownReboot, \"countDownReboot\", 2048, NULL, 5, NULL, 0 );\n  char modalBody[256];\n  sprintf( modalBody, DOWNLOADER_MODAL_BODY_ERRORS_OCCURED, downloadererrors, checkedfiles, updatedfiles, newfiles);\n  modalConfirm( \"syncend\", DOWNLOADER_MODAL_ENDED, DOWNLOADER_MODAL_TITLE_ERRORS_OCCURED, modalBody, DOWNLOADER_MODAL_REBOOT, DOWNLOADER_MODAL_RETRY, MENU_BTN_BACK );\n  if( restart ) {\n    ESP.restart();\n    delay(1000);\n  }\n}\n\n\nvoid syncStart() {\n  downloadererrors = 0;\n  updatedfiles = 0;\n  newfiles = 0;\n  checkedfiles = 0;\n  drawAppMenu();\n  renderDownloadIcon( TFT_ORANGE, 140, 80, 10.0 );\n  // TODO: add M5 model detection\n  UserAgent = \"ESP32HTTPClient (SDU-\" + String(M5_SD_UPDATER_VERSION)+\"-M5Core-\"+String(M5_LIB_VERSION)+\"-\"+String(__DATE__)+\"@\"+String(__TIME__)+\")\";\n  http.setUserAgent( UserAgent );\n}\n\n\nvoid printVerifyProgress( const char* msg, uint16_t textcolor, uint16_t bgcolor, uint16_t restorecolor) {\n  Serial.println( msg );\n  tft.setTextColor(textcolor, bgcolor);\n  tft.print( msg );\n  tft.setTextColor(restorecolor, bgcolor);\n}\n\n\nbool getApp( String appURL ) {\n  renderDownloadIcon( TFT_GREEN );\n\n  HTTPRouter getAppRouter;\n  URLParts urlParts = parseURL( appURL );\n  WiFiClientSecure *client = new WiFiClientSecure;\n\n  if( ! syncConnect(client, getAppRouter, urlParts, \"getApp()\") ) {\n    getAppRouter.dismiss( client, true );\n    return false;\n  }\n\n  renderDownloadIcon( M5MENU_GREY );\n\n  DynamicJsonDocument jsonAppBuffer( 4096 );\n\n  if( jsonAppBuffer.capacity() == 0 ) {\n    log_e(\"ArduinoJSON failed to allocate 4Kb\");\n    return false;\n  }\n\n  DeserializationError error = deserializeJson(jsonAppBuffer, http.getString() );\n\n  getAppRouter.dismiss( client, false );\n\n  if (error) {\n    downloadererrors++;\n    jsonerrors++;\n    log_e(\"\\n[ERROR] JSON Parsing failed on %s\\n\",  appURL.c_str());\n    delay(10000);\n    return false;\n  }\n  JsonObject root = jsonAppBuffer.as<JsonObject>();\n\n  uint16_t appsCount = root[\"apps_count\"].as<uint16_t>();\n  if(appsCount!=1) {\n    downloadererrors++;\n    log_e(\"\\n%s\\n\", \"[ERROR] AppsCount misenumeration\");\n    return false;\n  }\n\n  String base_url = root[\"base_url\"].as<String>();\n  String sha_sum, filePath, fileName, finalName, tempFileName;\n  sha_sum.reserve(65);\n  filePath.reserve(32);\n  fileName.reserve(32);\n  finalName.reserve(32);\n  tempFileName.reserve(32);\n\n  uint16_t assets_count = root[\"apps\"][0][\"json_meta\"][\"assets_count\"].as<uint16_t>();\n  for(uint16_t i=0;i<assets_count;i++) {\n    // TODO: properly verify/sanitize this + error handling\n    filePath            = root[\"apps\"][0][\"json_meta\"][\"assets\"][i][\"path\"].as<String>();\n    fileName            = root[\"apps\"][0][\"json_meta\"][\"assets\"][i][\"name\"].as<String>();\n    //uint32_t remoteTime = root[\"apps\"][0][\"json_meta\"][\"assets\"][i][\"created_at\"].as<uint32_t>();\n    sha_sum             = root[\"apps\"][0][\"json_meta\"][\"assets\"][i][\"sha256_sum\"].as<String>();\n    //size_t appSize      = root[\"apps\"][0][\"json_meta\"][\"assets\"][i][\"size\"].as<size_t>();\n    finalName = filePath + fileName;\n    tempFileName = finalName + String(\".tmp\");\n    tft.setCursor(10, 54+i*10);\n    tft.print( fileName );\n    Serial.printf( \"  [%s]\", fileName.c_str() );\n    if(M5_FS.exists( finalName.c_str() )) {\n      Serial.print(\"[SD:SHA256 Sum ->][....\");\n      // check the sha sum of the *local* file\n      sha256_sum( finalName );\n      Serial.print(\"]\");\n      if( shaResultStr.equals( sha_sum ) ) {\n        log_d(\"[checksums match]\");\n        checkedfiles++;\n        printVerifyProgress( WGET_SKIPPING, TFT_GREEN, M5MENU_GREY, TFT_WHITE);\n        continue;\n      }\n      log_d(\"[checksums differ]\");\n      printVerifyProgress( WGET_UPDATING, TFT_ORANGE, M5MENU_GREY, TFT_WHITE);\n    } else {\n      printVerifyProgress( WGET_CREATING, TFT_WHITE, M5MENU_GREY, TFT_WHITE);\n    }\n\n    appURL = base_url + filePath + fileName;\n    if( !wget( appURL, tempFileName ) ) {\n      // uh-oh\n      log_e(\"\\n%s\\n\", \"[ERROR] could not download %s to %s\", appURL.c_str(), tempFileName.c_str() );\n      modalConfirm( \"wgeterr\", \"DOWNLOAD ERROR\", \"Failed to fetch some file\", String(\"    - \" + appURL + \"\\n    - \" + tempFileName).c_str(), \"SHIT\", \"BUMMER\", MENU_BTN_WFT );\n      downloadererrors++;\n      continue;\n    }\n\n    if( shaResultStr.equals( sha_sum ) ) {\n      checkedfiles++;\n      if( M5_FS.exists( tempFileName ) ) {\n        if( M5_FS.exists( finalName ) ) {\n          M5_FS.remove( finalName );\n          updatedfiles++;\n        } else {\n          newfiles++;\n        }\n        M5_FS.rename( tempFileName, finalName );\n      } else {\n        // download failed, error was previously disclosed\n        printVerifyProgress( DOWNLOAD_FAIL, TFT_RED, M5MENU_GREY, TFT_WHITE);\n      }\n    } else {\n      downloadererrors++;\n      Serial.printf(\"  [SHA256 SUM ERROR] Remote hash: %s, Local hash: %s ### keeping local file and removing temp file ###\\n\", String(sha_sum).c_str(), shaResultStr.c_str() );\n      printVerifyProgress( SHASHUM_FAIL, TFT_RED, M5MENU_GREY, TFT_WHITE);\n      M5_FS.remove( tempFileName );\n    }\n    uint16_t myprogress = progress + (i* float(progress_modulo/assets_count));\n    printProgress(myprogress);\n  }\n  return true;\n}\n\n\nbool init_tls_or_die( String host ) {\n  if( fetchLocalCert( host ) == NULL ) {\n    String certPath = String( SD_CERT_PATH ) + host;\n    String certURL = Registry.defaultChannel.api_cert_provider_url_https + host;\n    if( wget( certURL , certPath ) ) {\n      if( fetchLocalCert( host ) != NULL ) {\n        log_w( NEW_TLS_CERTIFICATE_INSTALLED );\n        modalConfirm( \"tlsnew\", MENU_BTN_CANCELED, NEW_TLS_CERTIFICATE_INSTALLED, MODAL_RESTART_REQUIRED, DOWNLOADER_MODAL_REBOOT, DOWNLOADER_MODAL_RESTART, MENU_BTN_WFT );\n      } else {\n        log_e( \"Certificate fetching OK but TLS Install failed\" );\n        modalConfirm( \"tlsfail\", MENU_BTN_CANCELED, \"Certificate fetching OK but TLS Install failed\", \"Please check the remote server\", DOWNLOADER_MODAL_REBOOT, DOWNLOADER_MODAL_RESTART, MENU_BTN_WFT );\n      }\n    } else {\n      log_e( \"Unable to wget() certificate\" );\n      modalConfirm( \"tlsfail\", MENU_BTN_CANCELED, \"Unable to wget() certificate\", \"Please check the remote server\", DOWNLOADER_MODAL_REBOOT, DOWNLOADER_MODAL_RESTART, MENU_BTN_WFT );\n    }\n    ESP.restart();\n  }\n  return true;\n}\n\n\nbool syncAppRegistry( String BASE_URL ) {\n  syncStart();\n  String appURL = BASE_URL + Registry.defaultChannel.catalog_endpoint;\n  String payload = \"\";\n  URLParts urlParts = parseURL( appURL );\n\n  init_tls_or_die( urlParts.host );\n\n  HTTPRouter syncAppRouter;\n  WiFiClientSecure *client = new WiFiClientSecure;\n\n  if( ! syncConnect(client, syncAppRouter, urlParts, \"syncAppRegistry()\", /*enforceTLS=*/true) ) {\n    log_e( \"Could not connect to registry at %s\", appURL.c_str() );\n    syncAppRouter.dismiss( client, true );\n    return false;\n  }\n\n  renderDownloadIcon( TFT_GREEN, 140, 80, 10.0 );\n\n  log_d(\"Heap free before collecting https payload: %d\", ESP.getFreeHeap() );\n  payload = http.getString();\n  syncAppRouter.dismiss( client, false );\n\n  log_d(\"Heap free before allocating 8K to DynamicJsonDocument: %d\", ESP.getFreeHeap() );\n  DynamicJsonDocument jsonAppBuffer(8192);\n  log_d(\"jsonAppBuffer.capacity() = %d\", jsonAppBuffer.capacity() );\n  log_d(\"Heap free before JSON deserialization: %d\", ESP.getFreeHeap() );\n\n  DeserializationError error = deserializeJson(jsonAppBuffer, payload );\n  //DeserializationError error = deserializeJson(jsonAppBuffer, payload.c_str(), payload.length() );\n\n  if (error) {\n    downloadererrors++;\n    log_e(\"\\nJSON Parsing failed! (err=%s)\\n\", error.c_str() );\n    Serial.println(payload);\n    return false;\n  }\n  JsonObject root = jsonAppBuffer.as<JsonObject>();\n\n  appsCount = root[\"apps_count\"].as<uint16_t>();\n  if( appsCount==0 ) {\n    downloadererrors++;\n    log_e(\"%s\", \"No apps found, aborting\");\n    return false;\n  }\n  progress_modulo = 100/appsCount;\n  String base_url = root[\"base_url\"].as<String>();\n  Serial.printf(\"\\nFound %s apps at %s\\n\", String(appsCount).c_str(), appURL.c_str() );\n  for(uint16_t i=0;i<appsCount;i++) {\n    String appName = root[\"apps\"][i][\"name\"].as<String>();\n    //if( appName == \"Downloader\" ) continue;\n    String appURL  = BASE_URL + \"/\" + appName + \".json\";\n    progress = float(i*progress_modulo);\n    M5Menu.windowClr();\n    printProgress(progress);\n    Serial.printf(\"\\n[%s] :\\n\", appName.c_str());\n    tft.setTextColor( TFT_WHITE, M5MENU_GREY );\n    tft.setCursor(10, 36);\n    tft.print( appName );\n    getApp( appURL );\n    delay(150);\n    heapState();\n  }\n\n  syncFinished();\n\n  return true;\n}\n\n\nvoid WiFiEvent(WiFiEvent_t event) {\n  log_w(\"[WiFi-event] event: %d\\n\", event);\n\n  switch (event) {\n    case SYSTEM_EVENT_WIFI_READY:\n        log_d(\"WiFi interface ready\");\n        break;\n    case SYSTEM_EVENT_SCAN_DONE:\n        log_d(\"Completed scan for access points\");\n        break;\n    case SYSTEM_EVENT_STA_START:\n        log_d(\"WiFi client started\");\n        break;\n    case SYSTEM_EVENT_STA_STOP:\n        log_d(\"WiFi clients stopped\");\n        break;\n    case SYSTEM_EVENT_STA_CONNECTED:\n        log_d(\"Connected to access point\");\n        break;\n    case SYSTEM_EVENT_STA_DISCONNECTED:\n        log_i( \"STA Disconnected, reconnecting\");\n        WiFi.begin();\n        break;\n    case SYSTEM_EVENT_STA_AUTHMODE_CHANGE:\n        log_w(\"Authentication mode of access point has changed\");\n        break;\n    case SYSTEM_EVENT_STA_GOT_IP:\n        log_w(\"Obtained IP address: %s\", WiFi.localIP().toString().c_str() );\n        break;\n    case SYSTEM_EVENT_STA_LOST_IP:\n        log_w(\"Lost IP address and IP address is reset to 0\");\n        break;\n    case SYSTEM_EVENT_STA_WPS_ER_SUCCESS:\n        log_w(\"WiFi Protected Setup (WPS): succeeded in enrollee mode\");\n        break;\n    case SYSTEM_EVENT_STA_WPS_ER_FAILED:\n        log_w(\"WiFi Protected Setup (WPS): failed in enrollee mode\");\n        break;\n    case SYSTEM_EVENT_STA_WPS_ER_TIMEOUT:\n        log_w(\"WiFi Protected Setup (WPS): timeout in enrollee mode\");\n        break;\n    case SYSTEM_EVENT_STA_WPS_ER_PIN:\n        log_w(\"WiFi Protected Setup (WPS): pin code in enrollee mode\");\n        break;\n    case SYSTEM_EVENT_AP_START:\n        log_w(\"WiFi access point started\");\n        break;\n    case SYSTEM_EVENT_AP_STOP:\n        log_w(\"WiFi access point  stopped\");\n        break;\n    case SYSTEM_EVENT_AP_STACONNECTED:\n        log_w(\"Client connected\");\n        break;\n    case SYSTEM_EVENT_AP_STADISCONNECTED:\n        log_w(\"Client disconnected\");\n        break;\n    case SYSTEM_EVENT_AP_STAIPASSIGNED:\n        log_w(\"Assigned IP address to client\");\n        break;\n    case SYSTEM_EVENT_AP_PROBEREQRECVED:\n        log_w(\"Received probe request\");\n        break;\n    case SYSTEM_EVENT_GOT_IP6:\n        log_w(\"IPv6 is preferred\");\n        break;\n    case SYSTEM_EVENT_ETH_START:\n        log_w(\"Ethernet started\");\n        break;\n    case SYSTEM_EVENT_ETH_STOP:\n        log_w(\"Ethernet stopped\");\n        break;\n    case SYSTEM_EVENT_ETH_CONNECTED:\n        log_w(\"Ethernet connected\");\n        break;\n    case SYSTEM_EVENT_ETH_DISCONNECTED:\n        log_w(\"Ethernet disconnected\");\n        break;\n    case SYSTEM_EVENT_ETH_GOT_IP:\n        log_w(\"Ethernet obtained IP address\");\n        break;\n    default: break;\n  }\n}\n\n\n\nvoid enableWiFi() {\n  //WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector\n  WiFi.mode(WIFI_OFF);\n  delay(500);\n  WiFi.mode(WIFI_STA);\n  Serial.println(WiFi.macAddress());\n  WiFi.onEvent(WiFiEvent);\n  WiFi.begin(); // set SSID/PASS from another app (i.e. WiFi Manager) and reload this app\n  unsigned long startup = millis();\n\n  tft.clear();\n  drawAppMenu();\n  tft.setCursor(10, 50);\n  tft.setTextColor( TFT_WHITE, M5MENU_GREY );\n  tft.println(WIFI_MSG_WAITING);\n  size_t rssi = 0;\n\n  while (WiFi.status() != WL_CONNECTED) {\n    drawRSSIBar( 122, 100, rssi++, M5MENU_GREY, 4.0 );\n    delay(500);\n    if(rssi%3==0) {\n      Serial.println(WIFI_MSG_CONNECTING);\n    }\n    if(startup + 30000 < millis()) {\n      Serial.println(WIFI_MSG_TIMEOUT);\n      tft.println(WIFI_MSG_TIMEOUT);\n      delay(1000);\n      tft.clear();\n      return;\n    }\n  }\n  tft.println( WIFI_MSG_CONNECTED );\n  Serial.println( WIFI_MSG_CONNECTED );\n  wifisetup = true;\n}\n\n\nvoid enableNTP() {\n  Serial.println(\"Contacting Time Server\");\n  tft.clear();\n  drawAppMenu();\n  tft.setCursor(10, 50);\n  tft.setTextColor( TFT_WHITE, M5MENU_GREY );\n  tft.println(\"Contacting NTP Server\");\n  tft.drawJpg( ntp_jpeg, ntp_jpeg_len, 144, 104, 32, 32 );\n  configTime(timezone*3600, daysavetime*3600, \"pool.ntp.org\", \"asia.pool.ntp.org\", \"europe.pool.ntp.org\");\n  struct tm tmstruct ;\n  tmstruct.tm_year = 0;\n  getLocalTime(&tmstruct, 5000);\n  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);\n  Serial.println(\"\");\n  ntpsetup = true;\n  // TODO: modal-confirm date\n  delay(500);\n  tft.clear();\n}\n\nbool wifiSetupWorked() {\n  int16_t maxAttempts = 5;\n\n  while( !wifisetup ) {\n    enableWiFi();\n    maxAttempts--;\n    if( maxAttempts < 0 ) {\n      WiFi.mode(WIFI_OFF);\n      break;\n    }\n  }\n  if( wifisetup ) {\n    enableNTP();\n    drawAppMenu();\n  }\n  return wifisetup;\n}\n\n\n\nvoid updateOne(String appName) {\n  syncStart();\n  uint16_t oldAppsCount = appsCount;\n  String AppEndpointURLStr = \"/\" + appName + \".json\";\n  appsCount = 1;\n  if( wifiSetupWorked() ) {\n    Serial.printf(\"Will update app : %s\\n\", AppEndpointURLStr.c_str());\n    String appURL  = Registry.defaultChannel.api_url_https + AppEndpointURLStr;\n\n    URLParts urlParts = parseURL( appURL );\n    init_tls_or_die( urlParts.host );\n\n    M5Menu.windowClr();\n    Serial.printf( \"\\n[%s] :\\n\", AppEndpointURLStr.c_str() );\n    tft.setTextColor( TFT_WHITE, M5MENU_GREY );\n    tft.setCursor( 10, 36 );\n    tft.print( AppEndpointURLStr );\n    if( ! getApp( appURL ) ) { // no cert, invalid cert, invalid TLS host or JSON parsin failed ?\n      modalConfirm( \"getAppFail\", MENU_BTN_CANCELED, \"Download failed\", \"Restart?\",  DOWNLOADER_MODAL_REBOOT, DOWNLOADER_MODAL_RESTART, MENU_BTN_WFT );\n      ESP.restart();\n      // if( resp == HID_SELECT ) ESP.restart();\n      // else if( resp == HID_PAGE_DOWN ) continue;\n      // else cleanDir( SD_CERT_PATH ); // cleanup cached certs\n      /*\n      if( tlserrors > 0 ) {\n        cleanDir( SD_CERT_PATH ); // cleanup cached certs\n        URLParts urlParts = parseURL( appURL );\n        String certPath = String( SD_CERT_PATH ) + urlParts.host;\n        String certURL = Registry.defaultChannel.api_cert_provider_url_https + urlParts.host;\n        if( wget( certURL , certPath ) ) {\n          modalConfirm( \"tlsnew\", MENU_BTN_CANCELED, NEW_TLS_CERTIFICATE_INSTALLED, MODAL_RESTART_REQUIRED,  DOWNLOADER_MODAL_REBOOT, DOWNLOADER_MODAL_RESTART, MENU_BTN_WFT );\n          ESP.restart();\n          //getApp( appURL );\n        } else {\n          // failed\n          log_e(\"Failed to negotiate certificate for appURL %s\\n\", appURL.c_str() );\n        }\n      } else {\n        log_e( \"Failed to get app %s, probably a JSON error ?\", appName.c_str() );\n      }*/\n    }\n    delay(300);\n    M5Menu.windowClr();\n    tft.setCursor(10,60);\n    tft.setTextColor( TFT_WHITE, M5MENU_GREY );\n    tft.println( SYNC_FINISHED );\n    Serial.printf(\"\\n\\n## Download Finished  ##\\n   Errors: %d\\n\\n\", downloadererrors );\n    WiFi.mode( WIFI_OFF );\n    wifisetup = false;\n    syncFinished(false); // throw a modal\n  } else {\n    // tried all attempts and gave up\n    #if defined USE_WIFI_MANAGER\n      if( modalConfirm( \"WiFi Fail\", MENU_BTN_CANCELED, \"You may need to run a WiFi Manager\", \"Config WiFi ?\",  DOWNLOADER_MODAL_CHANGE, MENU_BTN_WFT, MENU_BTN_WFT ) == HID_SELECT ) {\n        // run WiFi Manager\n        wifiManagerSetup();\n        wifiManagerLoop();\n        ESP.restart();\n      }\n    #endif\n  }\n  appsCount = oldAppsCount;\n}\n\n\nvoid updateAll() {\n  if( wifiSetupWorked() ) {\n    // TODO: cleanup heap memory before doing some greedy HTTP stuff ?\n    /*\n    for( uint16_t i=0; i<appsCount;i++) {\n      delete &fileInfo[i].jsonMeta;\n      delete &fileInfo[i];\n    }\n    log_e(\"[HEAP after delete fileInfo[i]: %d]\", ESP.getFreeHeap() );\n    */\n\n    //if( wget(\"http://my.site/my_binary_package.tar.gz\", \"/my_binary_package.tar.gz\") ) {\n    //  tft.drawString( \"  Untarring ... \", tft.width()/2, tft.height()/2 );\n    //  gzExpander(M5_FS, \"/my_binary_package.tar.gz\", M5_FS, \"/tmp/tmp.tar\");\n    //  tft.drawString( \"  Gunzipping ... \", tft.width()/2, tft.height()/2 );\n    //  tarExpander(M5_FS, \"/tmp/tmp.tar\", M5_FS, \"/\");\n    //  return;\n    //}\n\n    while(!done && downloadererrors==0 ) {\n      syncAppRegistry( Registry.defaultChannel.api_url_https );\n      if( downloadererrors > 0 ) {\n        syncFinished();\n        break;\n      }\n    }\n  } else {\n    // tried all attempts and gave up\n  }\n}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/fsformat.h",
    "content": "/*\n *\n * M5Stack SD Menu\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2019 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n */\n\n\n//extern SDUpdater *sdUpdater; // used for menu progress\n//static const char* sduFSFilePath( fs::File *file );\n\n\n/*\n *\n * /!\\ When set to true, files with those extensions\n * will be transferred to the SD Card if found on SPIFFS.\n * Directory is automatically created.\n *\n */\nbool migrateSPIFFS = false;\n\nconst uint8_t extensionsCount = 6; // change this if you add / remove an extension\nString allowedExtensions[extensionsCount] =\n{\n    // do NOT remove jpg and json or the menu will crash !!!\n    \"jpg\", \"bmp\", \"json\", \"mod\", \"mp3\", \"cert\"\n};\n\nconst String appDataFolder = \"/data\"; // if an app needs spiffs data, it's stored here\nconst String launcherSignature = \"Launcher.bin\"; // app with name ending like this can overwrite menu.bin\n\nconst String appRegistryFolder = \"/.registry\";\nconst String appRegistryDefaultName = \"default.json\";\n\n\nbool isBinFile( const char* fileName )\n{\n  return String( fileName ).endsWith( \".bin\" ) || String( fileName ).endsWith( \".BIN\" )\n  #if defined SDU_HAS_TARGZ\n      || String( fileName ).endsWith( \".gz\" ) // || String( fileName ).endsWith( \".tar.gz\" )\n  #endif\n  ;\n}\n\nbool isValidAppName( const char* fileName )\n{\n  if( String( fileName )!=MENU_BIN // ignore menu\n     && ( isBinFile( fileName ) ) // ignore files not ending in \".bin\"\n     #if !defined USE_DOWNLOADER\n     && String( DOWNLOADER_BIN ) != String( fileName )  // ignore downloader if no download means are available\n     #endif\n     && !String( fileName ).startsWith( \"/.\" ) ) { // ignore dotfiles (thanks to https://twitter.com/micutil)\n    return true;\n  }\n  return false;\n}\n\n\nbool iFile_exists( fs::FS *fs, String &fname )\n{\n  if( fs->exists( fname.c_str() ) ) {\n    return true;\n  }\n  String locasename = fname;\n  String hicasename = fname;\n  locasename.toLowerCase();\n  hicasename.toUpperCase();\n  if( fs->exists( locasename.c_str() ) ) {\n    fname = locasename;\n    return true;\n  }\n  if( fs->exists( hicasename.c_str() ) ) {\n    fname = hicasename;\n    return true;\n  }\n  return false;\n}\n\n\n\nenum FileSystem_src_t\n{\n  SRC_NONE,\n  SRC_SD,\n  SRC_SPIFFS,\n  SRC_LITTLEFS,\n  SRC_FLASH\n};\n\nstruct FileSystem_fs_t\n{\n  FileSystem_src_t src{SRC_NONE};\n  fs::FS* fs{nullptr};\n};\n\n\n/* Storing json meta file information r */\nstruct JSONMeta\n{\n  int width; // app image width\n  int height; // app image height\n  String authorName = \"\";\n  String projectURL = \"\";\n  String credits = \"\"; // scroll this ?\n  // TODO: add more interesting properties\n};\n\n/* filenames cache structure */\nstruct FileInfo\n{\n  String fileName;  // path to the binary file\n  String metaName;  // a json file with all meta info on the binary\n  String iconName;  // a jpeg image representing the binary\n  String faceName;  // a jpeg image representing the author\n  uint32_t fileSize;\n  FileSystem_fs_t srcfs; // meta is implicitely on default FS but firmware can be on flash!\n\n  String displayName()\n  {\n    String out = fileName.substring(1);\n    out.replace( \".bin\", \"\" );\n    out.replace( \".BIN\", \"\" );; // the binary name, without the file extension and the leading slash\n    return out;\n  }\n  String shortName()\n  {\n    String out = displayName();\n    if( out.startsWith(\"--\") ) {\n      out.replace(\"--\", \"\");\n    }\n    return out;\n  }\n  bool hasIcon()\n  {\n    String currentIconFile = shortName();\n    currentIconFile = \"/jpg/\" + shortName() + \".jpg\";\n    if( iFile_exists( &M5_FS, currentIconFile ) ) {\n      iconName = currentIconFile;\n      return true;\n    }\n    log_d(\"[JSON]: no currentIconFile %s\", currentIconFile.c_str() );\n    return false;\n  }\n  bool hasFace()\n  {\n    String currentIconFile = shortName();\n    currentIconFile = \"/jpg/\" + shortName() + \"_gh.jpg\";\n    if( iFile_exists( &M5_FS, currentIconFile ) ) {\n      faceName = currentIconFile;\n      return true;\n    }\n    if( hasIcon() ) {\n      faceName = iconName;\n      return true;\n    }\n    log_d(\"[JSON]: no currentIconFile %s\", currentIconFile.c_str() );\n    return false;\n  }\n  bool hasMeta()\n  {\n    String currentMetaFile = shortName();\n    if( currentMetaFile.startsWith(\"/--\") ) {\n      currentMetaFile.replace(\"--\", \"\");\n    }\n    currentMetaFile = \"/json/\" + shortName() + \".json\";\n    if( iFile_exists( &M5_FS, currentMetaFile ) ) {\n      metaName = currentMetaFile;\n      return true;\n    }\n    log_d(\"[JSON]: no currentMetaFile %s\", currentMetaFile.c_str() );\n    return false;\n  }\n  bool hasData = false; // app requires a spiffs /data folder\n  JSONMeta jsonMeta;\n};\n\n\nvoid getMeta( FileInfo *fileInfo )\n{\n\n  fs::File file = M5_FS.open( fileInfo->metaName );\n  if( !file ) {\n    log_e(\"Can't open %s\", fileInfo->metaName  );\n    return;\n  }\n  log_d(\"Fetching meta for %s (%d bytes)\", fileInfo->metaName.c_str(), file.size() );\n  DynamicJsonDocument root(2048);\n  if( root.capacity() == 0 ) {\n    log_e(\"ArduinoJSON failed to allocate 2kb\");\n    return;\n  }\n\n  DeserializationError error = deserializeJson( root, file );\n\n  if (error) {\n    log_e(\"JSON ERROR #%d : %s\", error, error.c_str() );\n    file.close();\n    return;\n  }\n  // serializeJsonPretty(root, Serial);\n  if ( !root.isNull() ) {\n    JsonObject meta;\n    if( !root[\"json_meta\"].isNull() ) {\n      meta = root[\"json_meta\"].as<JsonObject>(); // new format\n    } else {\n      meta = root.as<JsonObject>();\n    }\n    fileInfo->jsonMeta.width      = meta[\"width\"].as<size_t>();\n    fileInfo->jsonMeta.height     = meta[\"height\"].as<size_t>();\n    fileInfo->jsonMeta.authorName = meta[\"authorName\"].as<String>();\n    fileInfo->jsonMeta.projectURL = meta[\"projectURL\"].as<String>();\n    fileInfo->jsonMeta.credits    = meta[\"credits\"].as<String>();\n    log_d(\"Fetched values: w=%d, h=%d\", fileInfo->jsonMeta.width, fileInfo->jsonMeta.height );\n  } else {\n    log_e(\"Unparsable JSON\");\n  }\n  file.close();\n}\n\n\nvoid getFileInfo( FileInfo &fileInfo, File *file, const char* binext=\".bin\" )\n{\n  String BINEXT = binext;\n  BINEXT.toUpperCase();\n  String fileName   = SDUpdater::fs_file_path( file ); //.name();\n  uint32_t fileSize = file->size();\n  time_t lastWrite = file->getLastWrite();\n  struct tm * tmstruct = localtime(&lastWrite);\n  char fileDate[64] = \"1980-01-01 00:07:20\";\n  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);\n  if( (tmstruct->tm_year)+1900 < 2000 ) {\n    // time is not set\n  }\n  Serial.println( \"[\" + String(fileDate) + \"]\" + String( DEBUG_FILELABEL ) + fileName );\n  fileInfo.fileName = fileName;\n\n  fileInfo.fileSize = fileSize;\n  if( fileName.startsWith(\"/--\") ) {\n    fileName.replace(\"--\", \"\");\n  }\n\n  if( fileInfo.hasIcon() && !fileInfo.hasFace() ) {\n    fileInfo.faceName = fileInfo.iconName;\n  }\n\n  fileName.replace( binext, \"\" );\n  fileName.replace( BINEXT, \"\" );\n\n  #if defined SDU_HAS_TARGZ\n    fileName.replace( \".gz\", \"\" );\n    //fileName.replace( \".tar.gz\", \"\" );\n  #endif\n\n  String currentDataFolder = appDataFolder + fileName;\n\n  if( M5_FS.exists( currentDataFolder.c_str() ) ) {\n    fileInfo.hasData = true; // TODO: actually use this feature\n  }\n\n  if( fileInfo.hasMeta() == true ) {\n    getMeta( &fileInfo );\n  }\n}\n\n\n\nvoid scanDataFolder()\n{\n  // check if mandatory folders exists and create if necessary\n  if( !M5_FS.exists( appDataFolder ) ) {\n    M5_FS.mkdir( appDataFolder );\n  }\n  if( !M5_FS.exists( appRegistryFolder ) ) {\n    M5_FS.mkdir( appRegistryFolder );\n  }\n  for( uint8_t i=0; i<extensionsCount; i++ ) {\n    String dir = \"/\" + allowedExtensions[i];\n    if( !M5_FS.exists( dir ) ) {\n      M5_FS.mkdir( dir );\n    }\n  }\n  if( !migrateSPIFFS ) {\n    return;\n  }\n#if 0\n  log_i( \"%s\", DEBUG_SPIFFS_SCAN );\n  if( !SPIFFS.begin() ){\n    log_e( \"%s\", DEBUG_SPIFFS_MOUNTFAILED );\n  } else {\n    File root = SPIFFS.open( \"/\" );\n    if( !root ){\n      log_e( \"%s\", DEBUG_DIROPEN_FAILED );\n    } else {\n      if( !root.isDirectory() ){\n        log_i( \"%s\", DEBUG_NOTADIR );\n      } else {\n        File file = root.openNextFile();\n        log_i( \"%s\", file.name() );\n        String fileName = file.name();\n        String destName = \"\";\n        if( isBinFile( fileName.c_str() ) ) {\n          destName = fileName;\n        }\n        // move allowed file types to their own folders\n        for( uint8_t i=0; i<extensionsCount; i++)  {\n          String ext = \".\" + allowedExtensions[i];\n          if( fileName.endsWith( ext ) ) {\n            destName = \"/\" + allowedExtensions[i] + fileName;\n          }\n        }\n        if( destName!=\"\" ) {\n          sdUpdater.displayUpdateUI( String( MOVINGFILE_MESSAGE ) + fileName );\n          size_t fileSize = file.size();\n          File destFile = M5_FS.open( destName, FILE_WRITE );\n          if( !destFile ){\n            log_e( \"%s\", DEBUG_SPIFFS_WRITEFAILED) ;\n          } else {\n            static uint8_t buf[512];\n            size_t packets = 0;\n            log_i( \"%s%s\", DEBUG_FILECOPY, fileName.c_str() );\n\n            while( file.read( buf, 512) ) {\n              destFile.write( buf, 512 );\n              packets++;\n              /*sdUpdater.*/SDMenuProgress( (packets*512)-511, fileSize );\n            }\n            destFile.close();\n            Serial.println();\n            log_i( \"%s\", DEBUG_FILECOPY_DONE );\n            SPIFFS.remove( fileName );\n            log_i( \"%s\", DEBUG_WILL_RESTART );\n            delay( 500 );\n            ESP.restart();\n          }\n        } else {\n          log_i( \"%s\", DEBUG_NOTHING_TODO );\n        } // aa\n      } // aaaaa\n    } // aaaaaaaaa\n  } // aaaaaaaaaaaaah!\n#endif\n} // nooooooooooooooes!!\n\n\nbool replaceItem( fs::FS &fs, String SourceName, String  DestName)\n{\n  if( !fs.exists( SourceName ) ) {\n    log_e(\"Source file %s does not exists !\\n\", SourceName.c_str() );\n    return false;\n  }\n  fs.remove( DestName );\n  fs::File source = fs.open( SourceName );\n  if( !source ) {\n    log_e(\"Failed to open source file %s\\n\", SourceName.c_str() );\n    return false;\n  }\n  fs::File dest = fs.open( DestName, FILE_WRITE );\n  if( !dest ) {\n    log_e(\"Failed to open dest file %s\\n\", DestName.c_str() );\n    return false;\n  }\n  uint8_t buf[4096]; // 4K buffer should be enough to fast-copy the file\n  uint8_t dot = 0;\n  size_t fileSize = source.size();\n  size_t n;\n  while ((n = source.read(buf, sizeof(buf))) > 0) {\n    Serial.print(\".\");\n    if(dot++%64==0) {\n      Serial.println();\n      if( SDUCfg.onProgress ) SDUCfg.onProgress( (dot*4096)-4095, fileSize );\n    }\n    dest.write(buf, n);\n  }\n  dest.close();\n  source.close();\n  return true;\n}\n\n\nbool replaceLauncher( fs::FS &fs, FileInfo &info)\n{\n  if(!replaceItem( fs, info.fileName, String(MENU_BIN) ) ) {\n    return false;\n  }\n  if( info.hasIcon() ) {\n    replaceItem( fs, info.iconName, \"/jpg/\" MENU_BIN \".jpg\" );\n  }\n  if( info.hasFace() ) {\n    replaceItem( fs, info.faceName, \"/jpg/\" MENU_BIN \"_gh.jpg\" );\n  }\n  if( info.hasMeta() ) {\n    replaceItem( fs, info.metaName, \"/json/\" MENU_BIN \".json\" );\n  }\n  return true;\n}\n\n\n\nstd::vector<FileInfo> fileInfo;\n\n//FileInfo *fileInfo = nullptr;\n\nvoid initFileInfo()\n{\n  // if( psramInit() ) {\n  //   fileInfo = (FileInfo *)ps_calloc( M5SAM_LIST_MAX_COUNT, sizeof(FileInfo) );\n  // } else {\n  //   fileInfo = (FileInfo *)calloc( M5SAM_LIST_MAX_COUNT, sizeof(FileInfo) );\n  // }\n  // if( fileInfo == NULL ) {\n  //   log_n(\"[CRITICAL] Failed to allocate %d bytes!! Set a lower value to M5SAM_LIST_MAX_COUNT in SAM.h to prevent this. Halting...\", sizeof(FileInfo)*M5SAM_LIST_MAX_COUNT );\n  //   while(1) vTaskDelay(1);\n  // }\n}\n\n\n\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/i18n.h",
    "content": "#ifndef __I18N_H\n#define __I18N_H\n\n\n#define WELCOME_MESSAGE F(\"Welcome to the \" PLATFORM_NAME \" SD Menu Loader!\")\n#define DOWNLOADER_BIN \"/--Downloader--.bin\" // Fixme/Hack: a dummy file will be created so it appears in the menu as an app\n#define DOWNLOADER_BIN_VIRTUAL \"/Downloader.bin\" // old bin name, will be renamed, kept for backwards compat\n#define INIT_MESSAGE F( PLATFORM_NAME \" SD Updater initializing...\")\n#define M5_SAM_MENU_SETTINGS \"M5StackSam loaded with %d labels per page, max %d items\\n\"\n#define SD_LOADING_MESSAGE F(\"Checking SD Card...\")\n#define INSERTSD_MESSAGE F(\"Insert SD\")\n#define GOTOSLEEP_MESSAGE F(\"Will go to sleep\")\n#define MOVINGFILE_MESSAGE F(\"Moving \")\n#define FILESIZE_UNITS F(\" bytes\")\n\n#define MENU_TITLE F( PLATFORM_NAME \" SD/FW LAUNCHER\")\n#define MENU_SUBTITLE F(\"Applications\")\n#define MENU_BTN_INFO F(\"SELECT\")\n#define MENU_BTN_SET F(\"SET\")\n#define MENU_BTN_LOAD F(\"LOAD\")\n#define MENU_BTN_LAUNCH F(\"LAUNCH\")\n#define MENU_BTN_UPDATE \"UPDATE\"\n#define MENU_BTN_SOURCE \"SOURCE\"\n#define MENU_BTN_BACK \"BACK\"\n#define MENU_BTN_PAGE F(\">>\")\n#define MENU_BTN_NEXT F(\">\")\n#define MENU_BTN_WFT \"UUH?\"\n#define MENU_BTN_YES \"YES\"\n#define MENU_BTN_NO \"NO\"\n#define MENU_BTN_CANCEL \"CANCEL\"\n\n//#ifdef TFT_SDA_READ\n#define MENU_SCREENSHOT \"SNAP\"\n//#endif\n\n#define ABOUT_THIS_MENU F(\"--About This Launcher--\")\n\n#define AUTHOR_PREFIX F(\"By \")\n#define AUTHOR_SUFFIX F(\" **\")\n\n#define APP_DOWNLOADER_MENUTITLE PLATFORM_NAME \" Apps Downloader\"\n\n#define CHANNEL_TOOL \"CHANNEL TOOL\"\n#define CHANNEL_TOOL_PROMPT \"    Change or Update channel ?\"\n#define CHANNEL_TOOL_TEXT \"    Do you want to change or update\\n\\n    your SD Card channel?\\n\\n\\n    Please choose.\"\n\n#define CHANNEL_CHOOSER \"CHANNEL CHOOSER\"\n#define CHANNEL_CHOOSER_PROMPT \"    Change channel ?\"\n#define CHANNEL_CHOOSER_TEXT \"    You are about to change your SD Card channel.\\n\\n    Are you sure ?\"\n\n#define CHANNEL_DOWNLOADER \"CHANNEL DOWNLOADER\"\n#define CHANNEL_DOWNLOADER_PROMPT \"    Download channel ?\"\n#define CHANNEL_DOWNLOADER_TEXT \"    You are about to overwrite your SD Card channel.\\r\\n    Are you sure ?\"\n\n#define DOWNLOADER_MODAL_NAME \"Update binaries ?\"\n#define DOWNLOADER_MODAL_TITLE \"This action will:\"\n#define DOWNLOADER_MODAL_BODY \"  - Connect to WiFi\\n\\n  - Get app list from remote registry\\n\\n  - Download/overwrite files\\n\\n  - Restart this menu\\n\\n\\n\\n  THIS OPERATION IS INSECURE!!\\n\\n  YOU DO THIS AT YOUR OWN RISK!!\"\n#define DOWNLOADER_MODAL_ENDED \"Synchronization complete\"\n#define DOWNLOADER_MODAL_TITLE_ERRORS_OCCURED \"Some errors occured. \"\n#define DOWNLOADER_MODAL_BODY_ERRORS_OCCURED \"  %d errors occured during the download\\n\\n  %d files were verified\\n\\n  %d files were updated\\n\\n  %d files were created\\n\\n\\n\\n  \" PLATFORM_NAME \" will reboot in 10s\"\n#define DOWNLOADER_MODAL_REBOOT \"REBOOT\"\n#define DOWNLOADER_MODAL_RESTART \"RESTART\"\n\n#define DOWNLOADER_MODAL_RETRY \"RETRY\"\n#define DOWNLOADER_MODAL_CHANGE \"CHANGE\"\n#define MENU_BTN_CANCELED \"OPERATION CANCELED\"\n\n#define OVERALL_PROGRESS_TITLE \"Overall progress: \"\n#define WGET_SKIPPING \" [Checksum OK]\"\n#define WGET_UPDATING \" [Outdated]\"\n#define WGET_CREATING \" [New file]\"\n#define SYNC_FINISHED \"Synch finished\"\n#define CLEANDIR_REMOVED \"Removed %s\\n\"\n#define DOWNLOAD_FAIL \" [DOWNLOAD FAIL]\"\n#define SHASHUM_FAIL \" [SHASUM FAIL]\"\n#define UPDATE_SUCCESS \"UPDATE SUCCESS\"\n\n#define WIFI_MSG_WAITING \"Waiting for WiFi to connect\"\n#define WIFI_MSG_CONNECTING \"Establishing connection to WiFi..\"\n#define WIFI_MSG_TIMEOUT \"Timed out, will try again\"\n#define WIFI_MSG_CONNECTED \"Connected to wifi :-)\"\n\n#define NEW_TLS_CERTIFICATE_INSTALLED \"    New TLS certificate installed\"\n#define MODAL_RESTART_REQUIRED \"    This will require a restart.\\r\\n\\r\\n    Reboot now?\"\n#define MODAL_SAME_PLAYER_SHOOT_AGAIN \"    Please reboot and try again.\\r\\n\\r\\n    Reboot now?\"\n#define MODAL_REGISTRY_UPDATED \"    New Registry file has been updated\"\n#define MODAL_REGISTRY_DAMAGED \"    New Registry file may be damaged\"\n#define MODAL_REBOOT_REGISTRY_UPDATED \"    Please reboot and choose a channel.\\r\\n\\r\\n    Reboot now?\"\n\n#define DEBUG_DIRNAME \"Listing directory: %s\\n\"\n#define DEBUG_DIROPEN_FAILED F(\"Failed to open directory\")\n#define DEBUG_NOTADIR F(\"Not a directory\")\n#define DEBUG_DIRLABEL F(\"  DIR : \")\n#define DEBUG_IGNORED F(\"  IGNORED FILE: \")\n#define DEBUG_CLEANED F(\"  CLEANED FILE: \")\n#define DEBUG_ABORTLISTING F(\"  ***Max files reached for M5StackSam Menu, please adjust M5SAM_LIST_MAX_COUNT for more (maximum is 255, sorry :-)\")\n#define DEBUG_FILELABEL F(\"  FILE: \")\n\n#define DEBUG_SPIFFS_SCAN F(\"Scanning SPIFFS for binaries\")\n#define DEBUG_SPIFFS_MOUNTFAILED F(\"SPIFFS Mount Failed\")\n#define DEBUG_SPIFFS_WRITEFAILED F(\"- failed to open file for writing\")\n#define DEBUG_FILECOPY \"Starting File Copy for \"\n#define DEBUG_FILECOPY_DONE F(\"Transfer finished\")\n#define DEBUG_WILL_RESTART F(\"Binary removed from SPIFFS, will now restart\")\n#define DEBUG_NOTHING_TODO F(\"No binary to transfer\")\n#define DEBUG_KEYPAD_NOTFOUND F(\"Keypad not installed\")\n#define DEBUG_KEYPAD_FOUND F(\"Keypad detected!\")\n#define DEBUG_JOYPAD_NOTFOUND F(\"No Joypad detected, disabling\")\n#define DEBUG_JOYPAD_FOUND F(\"Joypad detected!\")\n\n#define DEBUG_TIMESTAMP_GUESS \"Menu.bin has %s time set (%04d-%02d-%02d %02d:%02d:%02d), will use %s date to set the clock\\n\"\n\n#endif\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/main/main.cpp",
    "content": "#include \"../menu.h\"\n\n\nvoid setup()\n{\n\n  #if defined __M5UNIFIED_HPP__\n    M5.Log.setEnableColor(m5::log_target_serial, false);\n  #endif\n\n  #ifdef ARDUINO_M5STACK_FIRE\n    spicommon_periph_free( VSPI_HOST ); // fix 2.0.4 psramInit mess\n  #endif\n\n  #if defined(ARDUINO_M5STACK_ATOM_AND_TFCARD)\n    SDUCfg.setDisplay(&tft);\n    tft.init();\n  #else\n    M5.begin(); // bool LCDEnable, bool SDEnable, bool SerialEnable, bool I2CEnable, bool ScreenShotEnable\n  #endif\n\n\n  SDUCfg.setFS( &M5_FS );\n  SDUCfg.setCSPin( TFCARD_CS_PIN );\n\n  //WiFi.onEvent(WiFiEvent); // helps debugging WiFi problems with the Serial console\n  UISetup(); // UI init and check if a SD exists\n\n  doFSChecks(); // replicate on SD and app1 partition, scan data folder, load registry\n  doFSInventory(); // enumerate apps and render menu\n\n}\n\n\nvoid loop() {\n\n  HIDMenuObserve();\n  sleepTimer();\n\n}\n\n\n#if !defined ARDUINO\nextern \"C\" {\n  void loopTask(void*)\n  {\n    setup();\n    for(;;) {\n      loop();\n    }\n  }\n  void app_main()\n  {\n    xTaskCreatePinnedToCore( loopTask, \"loopTask\", 16384, NULL, 1, NULL, 1 );\n  }\n\n}\n#endif\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/menu.h",
    "content": "/*\n *\n * M5Stack SD Menu\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2019 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n */\n// #pragma GCC push_options\n// #pragma GCC optimize (\"Os\")\n#pragma once\n\n\n// TODO: moved USE_DOWNLOADER features to \"AppStore.ino\"\n// auto-select board\n#if defined( ARDUINO_M5STACK_Core2 )\n  #pragma message \"M5STACK Core2 detected\"\n  #define PLATFORM_NAME \"M5Core2\"\n  #define DEFAULT_REGISTRY_BOARD \"m5core2\"\n  //#define USE_DOWNLOADER\n#elif defined( ARDUINO_M5Stack_Core_ESP32 )\n  #pragma message \"M5STACK CLASSIC detected\"\n  #define PLATFORM_NAME \"M5Stack\"\n  #define DEFAULT_REGISTRY_BOARD \"m5stack\"\n  //#define USE_DOWNLOADER // moved to AppStore.ino\n#elif defined( ARDUINO_M5STACK_FIRE )\n  #pragma message \"M5STACK FIRE detected\"\n  #define PLATFORM_NAME \"M5Fire\"\n  #define DEFAULT_REGISTRY_BOARD \"m5fire\"\n  //#define USE_DOWNLOADER\n#elif defined( ARDUINO_ODROID_ESP32 )\n  #pragma message \"ODROID detected\"\n  #define PLATFORM_NAME \"Odroid-GO\"\n  #define DEFAULT_REGISTRY_BOARD \"odroid\"\n#elif defined ( ARDUINO_ESP32_DEV ) || defined( ARDUINO_LOLIN_D32_PRO )\n  #pragma message \"WROVER OR LOLIN_D32_PRO detected\"\n  #define DEFAULT_REGISTRY_BOARD \"esp32\"\n  #define PLATFORM_NAME \"ESP32\"\n  //#define USE_DOWNLOADER\n#elif defined( ARDUINO_ESP32_S3_BOX )\n  #pragma message \"ESP32_S3_BOX detected\"\n  #define DEFAULT_REGISTRY_BOARD \"esp32s3\"\n  #define PLATFORM_NAME \"S3Box\"\n#elif defined ARDUINO_M5STACK_CORES3\n  #pragma message \"M5Sack CoreS3 detected\"\n  #define DEFAULT_REGISTRY_BOARD \"cores3\"\n  #define PLATFORM_NAME \"CoreS3\"\n#elif defined( ARDUINO_M5STACK_ATOM_AND_TFCARD )\n  #pragma message \"M5Stack ATOM detected\"\n  #define DEFAULT_REGISTRY_BOARD \"m5atom\"\n  #define PLATFORM_NAME \"M5 ATOM(matrix/lite)\"\n#else\n  #pragma message \"NOTHING detected\"\n  #define DEFAULT_REGISTRY_BOARD \"lambda\"\n  #define PLATFORM_NAME \"LAMBDA\"\n#endif\n\n\n//#pragma GCC diagnostic ignored \"-Wdeprecated-declarations\"\n//#define M5_FS SD\n\n#include \"core.h\"\n\n#if !defined(ARDUINO_M5STACK_ATOM_AND_TFCARD)\n\n#else\n  //#include <SD.h>\n  //#include \"LGFX_8BIT_CVBS.h\"\n  //#include <Button2.h>\n  //Button2 button;//for G39\n\n  //#define USE_DISPLAY\n  //#define LGFX_ONLY\n  //#define TFCARD_CS_PIN -1\n\n  //static LGFX_8BIT_CVBS tft;\n  //#define LGFX LGFX_8BIT_CVBS\n  //#define BUTTON_WIDTH 60\n  //#define SDU_APP_NAME \"Application Launcher\"\n  // #define SDU_NO_AUTODETECT           // Disable autodetect (only works with <M5xxx.h> and <Chimera> cores)\n  // #define SDU_USE_DISPLAY             // Enable display functionalities (lobby, buttons, progress loader)\n  // #define HAS_LGFX                    // Display UI will use LGFX API (without this it will be tft_eSPI API)\n  // #define SDU_TouchButton LGFX_Button // Set button renderer\n  // #define SDU_Sprite LGFX_Sprite\n  // #define SDU_DISPLAY_TYPE M5Display*\n  // #define SDU_DISPLAY_OBJ_PTR &tft\n  // #define SDU_TRIGGER_SOURCE_DEFAULT TriggerSource::SDU_TRIGGER_PUSHBUTTON // Attach push buttons as trigger source\n\n\n  //#include <M5StackUpdater.h>\n\n  //static LGFX_Sprite sprite(&tft);\n  //fs::SDFS &M5_FS(SD);\n\n  //Button2 button;//for G39\n\n#endif\n\n#include <sys/time.h>\n#include \"compile_time.h\"\n//#include <SPIFFS.h>\n\n#if defined(_CHIMERA_CORE_) || defined(ARDUINO_M5STACK_ATOM_AND_TFCARD) || __has_include(\"lgfx/utility/lgfx_qrcode.h\")\n  #include \"lgfx/utility/lgfx_qrcode.h\"\n  #define qrcode_getBufferSize lgfx_qrcode_getBufferSize\n  #define qrcode_initText lgfx_qrcode_initText\n  #define qrcode_initBytes lgfx_qrcode_initBytes\n  #define qrcode_getModule lgfx_qrcode_getModule\n#else\n  #ifndef _QRCODE_H_\n    #include \"utility/qrcode.h\" // from M5Stack Core\n  #endif\n#endif\n\n#include \"SAM.h\" // altered version of https://github.com/tomsuch/M5StackSAM, maintained at https://github.com/tobozo/M5StackSAM/\n#include <ArduinoJson.h>     // https://github.com/bblanchon/ArduinoJson/\n#include <Preferences.h>\n#include \"i18n.h\"            // language file\n#include \"assets.h\"          // some artwork for the UI\n#include \"controls.h\"        // keypad / joypad / keyboard controls\n#include \"fsformat.h\"        // filesystem bin formats, functions, helpers\n\n\n#if defined USE_DOWNLOADER\n  #include \"downloader.h\"      // binaries downloader module A.K.A YOLO Downloader\n#endif\n\n#include \"partition_manager.h\"\n\n#define MAX_BRIGHTNESS 100\nconst unsigned long MS_BEFORE_SLEEP = 600000; // 600000 = 10mn\nuint8_t brightness = MAX_BRIGHTNESS;\n\nuint16_t appsCount = 0; // how many binary files\nuint16_t appsCountProgress = 0; // progress window\nbool inInfoMenu = false; // menu state machine\nunsigned long lastcheck = millis(); // timer check\nunsigned long lastpush = millis(); // keypad/keyboard activity\nuint16_t checkdelay = 300; // timer frequency\nuint16_t MenuID; // pointer to the current menu item selected\nuint16_t PageID;\nuint16_t Pages = 0;\nuint16_t PageIndex;\n\nint16_t scrollPointer = 0; // pointer to the scrollText position\nunsigned long lastScrollRender = micros(); // timer for scrolling\nString lastScrollMessage; // last scrolling string state\nint16_t lastScrollOffset; // last scrolling string position\n\nM5SAM M5Menu;\n#if defined USE_DOWNLOADER\n  AppRegistry Registry;\n#endif\n\n\n/* vMicro compliance, see https://github.com/tobozo/M5Stack-SD-Updater/issues/5#issuecomment-386749435 */\n//void getMeta( fs::FS &fs, String metaFileName, JSONMeta &jsonMeta );\nvoid freeAllMeta();\nvoid freeMeta();\nvoid renderIcon( FileInfo &fileInfo );\nvoid renderMeta( JSONMeta &jsonMeta );\n//void qrRender( String text, float sizeinpixels, int xOffset=-1, int yOffset=-1 );\nvoid qrRender( SDU_DISPLAY_TYPE gfx, String text, int posX, int posY, uint32_t width, uint32_t height );\n\nvoid progressBar(SDU_DISPLAY_TYPE tft, int x, int y, int w, int h, uint8_t val, uint16_t color, uint16_t bgcolor)\n{\n  tft->drawRect(x, y, w, h, color);\n  if (val > 100) val = 100;\n  if (val == 0) tft->fillRect(x + 1, y + 1, w - 2, h - 2, bgcolor);\n  else {\n    int fillw = (w * (((float)val) / 100.0)) - 2;\n    tft->fillRect(x + 1, y + 1, fillw - 2, h - 2, color);\n    tft->fillRect(x + fillw + 1, y + 1, w - fillw - 2, h - 2, bgcolor);\n  }\n}\n\nstatic String heapState()\n{\n  log_i(\"\\nRAM SIZE:\\t%.2f KB\\nFREE RAM:\\t%.2f KB\\nMAX ALLOC:\\t%.2f KB\",\n    ESP.getHeapSize() / 1024.0,\n    ESP.getFreeHeap() / 1024.0,\n    ESP.getMaxAllocHeap() / 1024.0\n  );\n  return \"\";\n}\n\nvoid gotoSleep()\n{\n  Serial.println( GOTOSLEEP_MESSAGE );\n  #ifdef ARDUINO_M5STACK_Core2\n    esp_sleep_enable_ext0_wakeup(GPIO_NUM_39, 0); // gpio39 == touch INT\n    //M5.Axp.DeepSleep();\n  #else\n    #if !defined _CHIMERA_CORE_ || defined HAS_POWER || defined HAS_IP5306\n      //M5.setWakeupButton( BUTTON_B_PIN );\n      //M5.powerOFF();\n    #else\n    #endif\n  #endif\n  delay(100);\n  //M5.Lcd.fillScreen(TFT_BLACK);\n  //M5.Lcd.sleep();\n  //M5.Lcd.waitDisplay();\n  esp_deep_sleep_start();\n}\n\nvoid renderScroll( String scrollText, uint8_t x = 0, uint8_t y = 0, uint16_t width = tft.width() )\n{\n  if( scrollText==\"\" ) return;\n\n  sprite.setTextSize( 2 ); // setup text size before it's measured\n  sprite.setTextDatum( ML_DATUM );\n  sprite.setTextWrap( false ); // lazy way to solve a wrap bug\n\n  sprite.setColorDepth( 1 );\n  sprite.createSprite( width, BUTTON_HEIGHT );\n  //sprite.fillSprite( TFT_BLACK );\n\n  if( !scrollText.endsWith( \" \" )) {\n    scrollText += \"   ***   \"; // append a space since scrolling text *will* repeat\n  }\n  while( sprite.textWidth( scrollText ) < width ) {\n    scrollText += scrollText; // grow text to desired width\n  }\n\n  String  scrollMe = \"\";\n  int16_t textWidth = sprite.textWidth( scrollText );\n  int16_t vsize = 0,\n          vpos = 0,\n          voffset = 0,\n          scrollOffset = 0;\n  uint8_t csize = 0,\n          lastcsize = 0;\n\n  scrollPointer-=1;\n  if( scrollPointer<-textWidth ) {\n    scrollPointer = 0;\n    vsize = scrollPointer;\n  }\n  while( sprite.textWidth(scrollMe) < width ) {\n    for( uint8_t i=0; i<scrollText.length(); i++ ) {\n      char thisChar[2];\n      thisChar[0] = scrollText[i];\n      thisChar[1] = '\\0';\n      csize = sprite.textWidth( thisChar );\n      vsize+=csize;\n      vpos = vsize+scrollPointer;\n      if( vpos>0 && vpos<=width ) {\n        scrollMe += scrollText[i];\n        lastcsize = csize;\n        voffset = scrollPointer%lastcsize;\n        scrollOffset = voffset;\n        if( sprite.textWidth(scrollMe) > width-voffset ) {\n          break; // break out of the loop and out of the while\n        }\n      }\n    }\n  }\n  // display trim\n  while( sprite.textWidth( scrollMe ) > width-voffset ) {\n    scrollMe.remove( scrollMe.length()-1 );\n  }\n  //scrollMe.remove( scrollMe.length()-1 ); // one last for the ride\n  // only draw if things changed\n  if( scrollOffset!=lastScrollOffset || scrollMe!=lastScrollMessage ) {\n    sprite.setTextColor( TFT_WHITE, TFT_BLACK ); // setting background color removes the flickering effect\n    sprite.setCursor( scrollOffset, BUTTON_HEIGHT/2 );\n    sprite.print( scrollMe );\n    sprite.setTextColor( TFT_WHITE );\n    sprite.pushSprite( x, y );\n  }\n  sprite.deleteSprite();\n  lastScrollMessage = scrollMe;\n  lastScrollOffset  = scrollOffset;\n  lastScrollRender  = micros();\n  //lastpush          = millis();\n}\n\n\n/* by file info */\nvoid renderIcon( FileInfo &fileInfo )\n{\n  if( !fileInfo.hasMeta() || !fileInfo.hasIcon() ) {\n    return;\n  }\n  JSONMeta jsonMeta = fileInfo.jsonMeta;\n  log_d(\"[%d] Will render icon %s at[%d:%d]\", ESP.getFreeHeap(), fileInfo.iconName.c_str(), tft.width()-jsonMeta.width-10, (tft.height()/2)-(jsonMeta.height/2)+10 );\n\n  fs::File iconFile = M5_FS.open( fileInfo.iconName.c_str()  );\n  if( !iconFile ) return;\n  tft.drawJpg( &iconFile, 190, 60, 110, 110 );\n  iconFile.close();\n  //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*/ );\n}\n\n/* by menu ID */\nvoid renderIcon( uint16_t MenuID )\n{\n  renderIcon( fileInfo[MenuID] );\n}\n\n/* by file name */\nvoid renderFace( String face )\n{\n  log_d(\"[%d] Will render face %s\", ESP.getFreeHeap(), face.c_str() );\n  fs::File iconFile = M5_FS.open( face.c_str()  );\n  if( !iconFile ) return;\n  tft.drawJpg( &iconFile, 5, 85, 120, 120 );\n  iconFile.close();\n  //tft.drawJpgFile( M5_FS, face.c_str(), 5, 85/*, 120, 120, 0, 0, JPEG_DIV_NONE*/ );\n}\n\n\nvoid renderMeta( JSONMeta &jsonMeta )\n{\n  sprite.setTextSize( 1 );\n  sprite.setTextDatum( TL_DATUM );\n  sprite.setTextColor( TFT_WHITE, TFT_BLACK );\n  sprite.setTextWrap( false );\n\n  sprite.setColorDepth( 1 );\n  sprite.createSprite( (tft.width() / 2)-20, 5*sprite.fontHeight() );\n  sprite.setCursor( 0, 0 );\n  sprite.println( fileInfo[MenuID].fileName );\n  sprite.println();\n\n  log_d(\"Rendering meta\");\n\n  if( jsonMeta.authorName!=\"\" && jsonMeta.projectURL!=\"\" ) { // both values provided\n    log_d(\"Rendering QRCode+author\");\n    sprite.print( AUTHOR_PREFIX );\n    sprite.print( jsonMeta.authorName );\n    sprite.println( AUTHOR_SUFFIX );\n    sprite.println();\n    qrRender( SDU_DISPLAY_OBJ_PTR, jsonMeta.projectURL, 155, 45, 150, 150 );\n  } else if( jsonMeta.projectURL!=\"\" ) { // only projectURL\n    log_d(\"Rendering QRCode\");\n    sprite.println( jsonMeta.projectURL );\n    sprite.println();\n    qrRender( SDU_DISPLAY_OBJ_PTR, jsonMeta.projectURL, 155, 45, 150, 150 );\n  } else { // only authorName\n    log_d(\"Rendering Authorname\");\n    sprite.println( jsonMeta.authorName );\n    sprite.println();\n  }\n\n  sprite.println( String( fileInfo[MenuID].fileSize ) + String( FILESIZE_UNITS ) );\n  sprite.pushSprite( 5, 35, TFT_BLACK );\n  sprite.deleteSprite();\n}\n\n\n/* give up on redundancy and ECC to produce less and bigger squares */\nuint8_t getLowestQRVersionFromString( String text, uint8_t ecc )\n{\n  #define QR_MAX_VERSION 9\n  if(ecc>QR_MAX_VERSION) return QR_MAX_VERSION; // fail fast\n  uint16_t len = text.length();\n  uint8_t QRMaxLenByECCLevel[4][QR_MAX_VERSION] = {\n    // https://www.qrcode.com/en/about/version.html\n    // Handling version 1-9 only since there's no point with M5Stack's 320x240 display (next version is at 271)\n    { 17, 32, 53, 78, 106, 134, 154, 192, 230 }, // L\n    { 14, 26, 45, 62, 84,  106, 122, 152, 180 }, // M\n    { 11, 20, 32, 46, 60,  74,  86,  108, 130 }, // Q\n    { 7,  14, 24, 34, 44,  58,  64,  84,  98  }  // H\n  };\n  for( uint8_t i=0; i<QR_MAX_VERSION; i++ ) {\n    if( len <= QRMaxLenByECCLevel[ecc][i] ) {\n      log_d(\"string len=%d bytes, fits in version %d / ecc %d (max=%d)\", len, i+1, ecc, QRMaxLenByECCLevel[ecc][i] );\n      return i+1;\n    }\n  }\n  log_e(\"String length exceeds output parameters\");\n  return QR_MAX_VERSION;\n}\n\n\nvoid qrRender( SDU_DISPLAY_TYPE gfx, String text, int posX, int posY, uint32_t width, uint32_t height )\n{\n  // see https://github.com/Kongduino/M5_QR_Code/blob/master/M5_QRCode_Test.ino\n  // Create the QR code\n  QRCode qrcode;\n\n  uint8_t ecc = 0; // QR on TFT can do with minimal ECC\n  uint8_t version = getLowestQRVersionFromString( text, ecc );\n\n  uint8_t qrcodeData[lgfx_qrcode_getBufferSize( version )];\n  lgfx_qrcode_initText( &qrcode, qrcodeData, version, ecc, text.c_str() );\n\n  uint32_t gridSize  = (qrcode.size + 4);   // margin: 2 dots\n  uint32_t dotWidth  = width / gridSize;    // rounded\n  uint32_t dotHeight = height / gridSize;   // rounded\n  uint32_t realWidth  = dotWidth*gridSize;  // recalculated\n  uint32_t realHeight = dotHeight*gridSize; // recalculated\n  if( realWidth > width || realHeight > height ) {\n    log_e(\"Can't fit QR with gridsize(%d),dotSize(%d) =>[%dx%d] => [%dx%d]\", gridSize, dotWidth, realWidth, realHeight, width, height );\n    return;\n  } else {\n    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 );\n  }\n\n  uint8_t marginX = (width  - qrcode.size*dotWidth)/2;\n  uint8_t marginY = (height - qrcode.size*dotHeight)/2;\n\n  gfx->fillRect( posX, posY, width, height, TFT_WHITE );\n\n  for ( uint8_t y = 0; y < qrcode.size; y++ ) {\n    // Each horizontal module\n    for ( uint8_t x = 0; x < qrcode.size; x++ ) {\n      bool q = lgfx_qrcode_getModule( &qrcode, x, y );\n      if (q) {\n        gfx->fillRect( x*dotWidth +posX+marginX, y*dotHeight +posY+marginY, dotWidth, dotHeight, TFT_BLACK );\n      }\n    }\n  }\n}\n\n\nvoid listDir( fs::FS &fs, const char * dirName, uint8_t levels, bool process )\n{\n  log_i( DEBUG_DIRNAME, dirName );\n  File root = fs.open( dirName );\n  if( !root ){\n    log_e( \"%s\", DEBUG_DIROPEN_FAILED );\n    return;\n  }\n  if( !root.isDirectory() ){\n    log_e( \"%s\", DEBUG_NOTADIR );\n    return;\n  }\n  File file = root.openNextFile();\n  tft.setFont( &Font2 );\n  while( file ){\n    if( file.isDirectory() ){\n      log_d( \"%s %s\", DEBUG_DIRLABEL, SDUpdater::fs_file_path(&file) );\n      if( levels ){\n        listDir( fs, SDUpdater::fs_file_path(&file), levels -1, process );\n      }\n    } else {\n      if( isValidAppName( SDUpdater::fs_file_path(&file) ) ) {\n        if( process ) {\n          FileInfo newFile = FileInfo();\n          fileInfo.push_back(newFile);\n          newFile.srcfs.fs = &fs;\n          getFileInfo( fileInfo[appsCount], &file );\n          if( appsCountProgress > 0 ) {\n            float progressRatio = ((((float)appsCount+1.0) / (float)appsCountProgress) * 80.00)+20.00;\n            progressBar( SDU_DISPLAY_OBJ_PTR, 110, 112, 100, 20, progressRatio);\n            tft.fillRect( 0, 140, tft.width(), 16, TFT_BLACK);\n            tft.drawString( fileInfo[appsCount].displayName(), 160, 148 );\n          }\n        }\n        appsCount++;\n        if( appsCount >= M5SAM_LIST_MAX_COUNT-1 ) {\n          //Serial.println( String( DEBUG_IGNORED ) + SDUpdater::fs_file_path(&file) );\n          log_w( \"%s\", DEBUG_ABORTLISTING );\n          break; // don't make M5Stack list explode\n        }\n      } else {\n        if( String( SDUpdater::fs_file_path(&file) ).endsWith(\".tmp\") || String( SDUpdater::fs_file_path(&file) ).endsWith(\".pcap\") ) {\n          fs.remove( SDUpdater::fs_file_path(&file) );\n          log_d( \"%s %s\", DEBUG_CLEANED, SDUpdater::fs_file_path(&file) );\n        } else {\n          log_d( \"%s %s\", DEBUG_IGNORED, SDUpdater::fs_file_path(&file) );\n        }\n      }\n    }\n    file = root.openNextFile();\n  }\n  if( fs.exists( MENU_BIN ) ) {\n    file = fs.open( MENU_BIN );\n    if( process ) {\n      FileInfo newFile = FileInfo();\n      newFile.srcfs.fs = &fs;\n      fileInfo.push_back(newFile);\n      getFileInfo( fileInfo[appsCount], &file );\n      if( appsCountProgress > 0 ) {\n        float progressRatio = ((((float)appsCount+1.0) / (float)appsCountProgress) * 80.00)+20.00;\n        progressBar( SDU_DISPLAY_OBJ_PTR, 110, 112, 100, 20, progressRatio);\n        tft.fillRect( 0, 140, tft.width(), 16, TFT_BLACK);\n        tft.drawString( fileInfo[appsCount].displayName(), 160, 148 );\n      }\n    }\n    appsCount++;\n  } else {\n    log_w( \"[WARNING] No %s file found\\n\", MENU_BIN );\n  }\n}\n\n\n/*\n *  bubble sort filenames\n *  '32' is based on SPIFFS filename limitations\n */\nvoid aSortFiles( uint8_t depth_level=32 )\n{\n  bool swapped;\n  FileInfo temp;\n  String name1, name2;\n  do {\n    swapped = false;\n    for( uint16_t i=0; i<appsCount-1; i++ ) {\n      name1 = fileInfo[i].fileName[0];\n      name2 = fileInfo[i+1].fileName[0];\n      if( name1==name2 ) {\n        uint8_t depth = 0;\n        while( depth <= depth_level ) {\n          depth++;\n          if( depth > fileInfo[i].fileName.length() || depth > fileInfo[i+1].fileName.length() ) {\n            // end of filename\n            break;\n          }\n          name1 = fileInfo[i].fileName[depth];\n          name2 = fileInfo[i+1].fileName[depth];\n          if( name1==name2 ) {\n            continue;\n          } else {\n            break;\n          }\n        }\n      }\n      if ( name1 > name2 || name1==MENU_BIN ) {\n        temp = fileInfo[i];\n        fileInfo[i] = fileInfo[i + 1];\n        fileInfo[i + 1] = temp;\n        swapped = true;\n      }\n    }\n  } while ( swapped );\n}\n\n\nvoid buildM5Menu()\n{\n  PageID = 0;\n  Pages = appsCount / M5Menu.listPagination;\n  if( appsCount % M5Menu.listPagination != 0 ) Pages++;\n  PageIndex = 0;\n  M5Menu.clearList();\n  M5Menu.setListCaption( MENU_SUBTITLE );\n  for( uint16_t i=0; i < appsCount; i++ ) {\n    if( fileInfo[i].shortName() == \"menu\" ) {\n      M5Menu.addList( ABOUT_THIS_MENU );\n    } else {\n      M5Menu.addList( fileInfo[i].displayName() );\n    }\n  }\n}\n\n\nvoid drawM5Menu( bool renderButtons = false )\n{\n  const char* paginationTpl = \"Page %d / %d\";\n  char paginationStr[64];\n  PageID = MenuID / M5Menu.listPagination;\n  PageIndex = MenuID % M5Menu.listPagination;\n  sprintf(paginationStr, paginationTpl, PageID+1, Pages);\n  M5Menu.setListCaption( paginationStr );\n  if( renderButtons ) {\n    #if defined(ARDUINO_ODROID_ESP32) && defined(_CHIMERA_CORE_)\n      M5Menu.drawAppMenu( MENU_TITLE, MENU_BTN_INFO, MENU_SCREENSHOT, MENU_BTN_PAGE, MENU_BTN_NEXT );\n    #else\n      M5Menu.drawAppMenu( MENU_TITLE, MENU_BTN_INFO, MENU_BTN_PAGE, MENU_BTN_NEXT );\n    #endif\n    tft.drawJpg(sdUpdaterIcon15x16_jpg, sdUpdaterIcon15x16_jpg_len, 296, 6, 15, 16);\n    if( factory_partition ) {\n      tft.drawJpg(flashUpdaterIcon16x16_jpg, flashUpdaterIcon16x16_jpg_len, 8, 6, 16, 16);\n    }\n    #if defined USE_DOWNLOADER\n      drawSDUpdaterChannel();\n    #endif\n  }\n  M5Menu.showList();\n  renderIcon( MenuID );\n  inInfoMenu = false;\n  lastpush = millis();\n}\n\n\nvoid pageDown()\n{\n  if( PageID < Pages -1 ) {\n    PageID++;\n    MenuID = (PageID * M5Menu.listPagination) -1;\n    M5Menu.setListID( MenuID );\n    M5Menu.nextList();\n    MenuID = M5Menu.getListID();\n  } else {\n    PageID = 0;\n    MenuID = 0;\n  }\n  M5Menu.setListID( MenuID );\n  drawM5Menu( inInfoMenu );\n}\n\n\nvoid pageUp()\n{\n  if( PageID > 0 ) {\n    PageID--;\n    MenuID -= M5Menu.listPagination;\n    M5Menu.setListID( MenuID );\n    drawM5Menu( inInfoMenu );\n  }\n}\n\n\n\nvoid menuUp()\n{\n  MenuID = M5Menu.getListID();\n  if( MenuID > 0 ) {\n    if( (MenuID - 1)%M5Menu.listPagination==0 ) {\n      MenuID += (M5Menu.listPagination-1);\n    } else {\n      MenuID--;\n    }\n  } else {\n    MenuID = appsCount-1;\n  }\n  M5Menu.setListID( MenuID );\n  drawM5Menu( inInfoMenu );\n}\n\n\nvoid menuDown( int jumpSize = 1 )\n{\n  if(MenuID<appsCount-1){\n    if( (MenuID + 1)%M5Menu.listPagination==0 ) {\n      MenuID -= (M5Menu.listPagination-1);\n    } else {\n      MenuID++;\n    }\n  } else {\n    MenuID = PageID * M5Menu.listPagination;\n  }\n  M5Menu.setListID( MenuID );\n  drawM5Menu( inInfoMenu );\n}\n\n\nvoid menuInfo()\n{\n  inInfoMenu = true;\n  M5Menu.windowClr();\n\n  #ifdef USE_DOWNLOADER\n    if( MenuID == 0 ) {\n      // downloader\n      M5Menu.drawAppMenu( \"Apps Downloader\", MENU_BTN_LAUNCH, MENU_BTN_SOURCE, MENU_BTN_BACK );\n      lastpush = millis();\n      return;\n    } else\n  #endif\n  if( fileInfo[ MenuID ].fileName.endsWith( launcherSignature ) ) {\n    M5Menu.drawAppMenu( String(MENU_TITLE), MENU_BTN_SET, MENU_BTN_UPDATE, MENU_BTN_BACK );\n  } else {\n    M5Menu.drawAppMenu( String(MENU_TITLE), MENU_BTN_LOAD, MENU_BTN_UPDATE, MENU_BTN_BACK );\n  }\n  #if defined USE_DOWNLOADER\n    drawSDUpdaterChannel();\n  #endif\n  renderMeta( fileInfo[MenuID].jsonMeta );\n  if( fileInfo[MenuID].hasFace() ) {\n    renderFace( fileInfo[MenuID].faceName );\n  }\n  lastpush = millis();\n}\n\n\nvoid checkMenuTimeStamp()\n{\n  File menu = M5_FS.open( MENU_BIN );\n  time_t lastWrite;\n  if( menu ) {\n    lastWrite = menu.getLastWrite();\n    menu.close();\n  } else {\n    lastWrite = __TIME_UNIX__;\n  }\n\n  // setting a pseudo realistic internal clock time when no NTP sync occured,\n  // and before writing to the SD Card gives unacurate but better timestamps\n  // than the default [1980-01-01 00:00:00]\n  int epoch_time = __TIME_UNIX__; // this macro is populated at compilation time\n  struct tm * tmstruct = localtime(&lastWrite);\n  String timeSource = \"this sketch build\";\n  String timeStatus;\n\n  if( (tmstruct->tm_year)+1900 < 2000 ) {\n    timeStatus = \"an unreliable\";\n  } else {\n    int tmptime = mktime(tmstruct); // epoch time ( seconds since 1st jan 1969 )\n    if( tmptime > epoch_time ) {\n      timeSource = \"menu.bin's lastWrite\";\n      timeStatus = \"a realistic\";\n      epoch_time = tmptime;\n    } else {\n      timeStatus = \"an obsolete\";\n    }\n  }\n\n  log_w(DEBUG_TIMESTAMP_GUESS, 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() );\n\n  timeval epoch = {epoch_time, 0};\n  const timeval *tv = &epoch;\n  settimeofday(tv, NULL);\n\n  struct tm now;\n  getLocalTime(&now,0);\n\n  Serial.printf(\"[Hobo style] Clock set to %s source (%s): \", timeStatus.c_str(), timeSource.c_str());\n  Serial.println(&now,\"%B %d %Y %H:%M:%S (%A)\");\n}\n\n#if defined USE_DOWNLOADER\n\nvoid downloaderMenu()\n{\n  int resp = modalConfirm( \"chantool\", CHANNEL_TOOL, CHANNEL_TOOL_PROMPT, CHANNEL_TOOL_TEXT, DOWNLOADER_MODAL_CHANGE, MENU_BTN_UPDATE, \"WiFi\" );\n  // choose between updating the JSON or changing the default channel\n  switch( resp ) {\n\n    case HID_SELECT:\n      resp = modalConfirm( \"chanpick\", CHANNEL_CHOOSER, CHANNEL_CHOOSER_PROMPT, CHANNEL_CHOOSER_TEXT, DOWNLOADER_MODAL_CHANGE, MENU_BTN_CANCEL, MENU_BTN_BACK );\n      if( resp == HID_SELECT ) {\n        if( Registry.pref_default_channel == \"master\" ) {\n          Registry.pref_default_channel = \"unstable\";\n        } else {\n          Registry.pref_default_channel = \"master\";\n        }\n        registrySave( Registry, appRegistryFolder + \"/\" + appRegistryDefaultName );\n        Serial.println(\"Will reload in 5 sec\");\n        delay(5000);\n        ESP.restart();\n      }\n    break;\n\n    case HID_PAGE_DOWN:\n      resp = modalConfirm( \"chanupd\", CHANNEL_DOWNLOADER, CHANNEL_DOWNLOADER_PROMPT, CHANNEL_DOWNLOADER_TEXT, MENU_BTN_UPDATE, MENU_BTN_CANCEL, MENU_BTN_BACK );\n      if( resp == HID_SELECT ) {\n        // TODO: WiFi connect, wget file and save to SD\n        registryFetch( Registry, appRegistryFolder + \"/\" + appRegistryDefaultName );\n      }\n    break;\n\n    default:\n      #if defined USE_WIFI_MANAGER\n        resp = modalConfirm( \"appDlChooser\", \"WiFi Setup\", \"WiFi Manager\", \"        Start WiFi Manager ?\",  \"Start\", MENU_BTN_CANCEL, MENU_BTN_BACK );\n        if( resp == HID_SELECT ) {\n          wifiManagerSetup();\n          wifiManagerLoop();\n          ESP.restart();\n        }\n      #endif\n    break;\n\n  }\n  drawM5Menu( inInfoMenu );\n}\n\n\nvoid updateApp( FileInfo &info )\n{\n  String appName = info.shortName();\n  //appName.replace(\".bin\", \"\");\n  //appName.replace(\".BIN\", \"\");\n  //appName.replace(\"/\", \"\");\n  Serial.println( appName );\n  updateOne( appName );\n  drawM5Menu( inInfoMenu );\n}\n\n#endif\n\n\nvoid launchApp( FileInfo &info )\n{\n  #if defined USE_DOWNLOADER\n    if( info.fileName == String( DOWNLOADER_BIN ) ) {\n      if( modalConfirm( \"launchapp\", DOWNLOADER_MODAL_NAME, DOWNLOADER_MODAL_TITLE, DOWNLOADER_MODAL_BODY ) == HID_SELECT ) {\n        updateAll();\n      }\n      // action cancelled or refused by user\n      drawM5Menu( inInfoMenu );\n      return;\n    }\n  #endif\n  if( info.fileName.endsWith( launcherSignature ) ) {\n    log_w(\"Will overwrite current %s with a copy of %s\\n\", MENU_BIN, info.fileName.c_str() );\n    if( replaceLauncher( M5_FS, info ) ) {\n      // fine\n    } else {\n      log_e(\"Failed to overwrite %s!\", info.fileName.c_str());\n      return;\n    }\n  }\n  SDUpdaterNS::updateFromFS( M5_FS, fileInfo[MenuID].fileName );\n  //updateFromFS( M5_FS, fileInfo[MenuID].fileName, TFCARD_CS_PIN );\n  ESP.restart();\n}\n\n\nvoid UISetup()\n{\n  initFileInfo();\n  HIDInit();\n  // make sure you're using the latest from https://github.com/tobozo/M5StackSAM/\n  M5Menu.listMaxLabelSize = 32; // list labels will be trimmed\n  M5Menu.listPagination = 8; // 8 items per page\n  M5Menu.listPageLabelsOffset = 42; // initially 80, pixels offset from top screen for list items\n  M5Menu.listCaptionDatum = TR_DATUM; // initially TC_DATUM=top centered, TL_DATUM=top left (default), top/right/bottom/left\n  M5Menu.listCaptionXPos = tft.width()-10; // initially M5.Lcd.width()/2, text cursor position-x for list caption\n  M5Menu.listCaptionYPos = 42; // initially 45, text cursor position-x for list caption\n\n  Serial.println( WELCOME_MESSAGE );\n  Serial.println( INIT_MESSAGE );\n  Serial.printf( M5_SAM_MENU_SETTINGS, M5Menu.listPagination, M5SAM_LIST_MAX_COUNT);\n  Serial.printf(\"Has PSRam: %s\\n\", psramInit() ? \"true\" : \"false\");\n\n  heapState();\n\n  //lsPart();\n\n  tft.setBrightness(100);\n\n  #if defined HAS_LGFX // reset scroll position\n    log_d(\"Resetting scroll position\");\n    tft.setScrollRect(0, 0, tft.width(), tft.height() );\n    tft.startWrite();\n    tft.writecommand(0x37); // ILI934x/ST778x VSCRSADD Vertical scrolling pointer\n    tft.writedata(0>>8);\n    tft.writedata(0);\n    tft.endWrite();\n  #endif\n\n  lastcheck = millis();\n  tft.drawJpg(disk01_jpg, 1775, (tft.width()-30)/2, 100);\n  tft.setTextDatum(MC_DATUM);\n  tft.setTextColor( TFT_WHITE, TFT_BLACK );\n  tft.setTextSize( 1 );\n  tft.setFont( &Font0 );\n  tft.drawString( SD_LOADING_MESSAGE, 160, 142 );\n\n  //M5.update();\n\n  bool toggle = true;\n#ifdef _CHIMERA_CORE_\n  while( !M5.sd_begin() )\n#else\n  while( !M5_FS.begin(4) )\n#endif\n  {\n    // TODO: make a more fancy animation\n    toggle = !toggle;\n    tft.setTextColor( toggle ? TFT_BLACK : TFT_WHITE );\n    tft.setFont( &Font2 );\n    tft.drawString( INSERTSD_MESSAGE, 160, 84 );\n    tft.drawJpg( toggle ? disk01_jpg : disk00_jpg, 1775, (tft.width()-30)/2, 100 );\n    delay( toggle ? 300 : 500 );\n    // go to sleep after a minute, no need to hammer the SD Card reader\n    if( lastcheck + 60000 < millis() ) {\n      gotoSleep();\n    }\n  }\n  tft.setTextDatum(TL_DATUM);\n\n  #if defined USE_DOWNLOADER\n\n    unsigned long longPush = 10000;\n    unsigned long shortPush = 5000;\n\n    if( M5.BtnB.isPressed() )\n    {\n      unsigned long pushStart = millis();\n      unsigned long pushDuration = 0;\n      drawAppMenu(); // render the menu\n      M5.update();\n      tft.setTextColor( TFT_WHITE, M5MENU_GREY );\n      tft.setTextDatum(MC_DATUM);\n      tft.setFont( &Font2 );\n      char remainingStr[32];\n      while( M5.BtnB.isPressed() ) {\n        pushDuration = millis() - pushStart;\n        if( pushDuration > longPush ) break;\n        if( pushDuration > shortPush ) {\n          tft.setTextColor( TFT_WHITE, TFT_RED );\n          tft.drawString( \"FULL RESET\", 160, 100 );\n          sprintf( remainingStr, \"%.2f\", (float)(longPush-pushDuration)/1000 );\n        } else {\n          tft.drawString( \"TLS RESET\", 160, 100 );\n          sprintf( remainingStr, \"%.2f\", (float)(shortPush-pushDuration)/1000 );\n        }\n        tft.drawString( remainingStr, 160, 120 );\n        delay(100);\n        M5.update();\n      }\n      tft.setTextDatum(TL_DATUM);\n\n      Serial.printf(\"Push duration : %d\\n\", (int)pushDuration );\n      if( pushDuration > shortPush ) {\n        // Short push at boot = cleanup /cert/ and /.registry/ folders\n        cleanDir( SD_CERT_PATH );\n        cleanDir( appRegistryFolder.c_str() );\n      }\n      if( pushDuration > longPush ) {\n        int resp = modalConfirm( \"cleanup\", \"DELETE APPS\", \"CAUTION! This will remove all apps and assets.\", \"    Obliviate?\",  \"DELETE\", \"CANCEL\", \"NOES!\" );\n        if( resp == HID_SELECT ) {\n          #if !defined HAS_RTC\n            checkMenuTimeStamp(); // set the time before cleaning up the folder\n          #endif\n          cleanDir( \"/\" );\n          cleanDir( \"/jpg/\" );\n          cleanDir( \"/json/\" );\n          drawAppMenu(); // render the menu\n          copyPartition(); // restore the menu.bin file\n        }\n      }\n      gotoSleep();\n    }\n  #endif\n\n      //Serial.println(\"Going to factory in 5s\");\n      //delay(5000);\n      //loadFactory();\n      //ESP.restart();\n\n\n\n}\n\n\nvoid doFSChecks()\n{\n  tft.setTextColor( TFT_WHITE );\n  tft.setTextSize( 1 );\n  tft.clear();\n\n  #if !defined HAS_RTC\n    checkMenuTimeStamp();\n  #endif\n\n  factory_partition = Flash::getFactoryPartition();\n\n  if( ! factory_partition ) {\n    // propagate to SD and OTA1\n    checkMenuStickyPartition();\n  }\n\n  tft.fillRect(110, 112, 100, 20,0);\n  progressBar( SDU_DISPLAY_OBJ_PTR, 110, 112, 100, 20, 10);\n\n  scanDataFolder(); // create necessary folders\n\n  #if defined USE_DOWNLOADER\n    Registry = registryInit(); // load registry profile\n\n    if( !M5_FS.exists( DOWNLOADER_BIN ) ) {\n      if( M5_FS.exists( DOWNLOADER_BIN_VIRTUAL) ) { // rename for hoisting in the list\n        M5_FS.rename( DOWNLOADER_BIN_VIRTUAL, DOWNLOADER_BIN );\n      } else { // create a dummy file to enable the feature\n        fs::File dummyDownloader = M5_FS.open( DOWNLOADER_BIN, FILE_WRITE);\n        dummyDownloader.print(\"Fake Binary\");\n        dummyDownloader.close();\n      }\n    } else { // cleanup old legacy file if necessary\n      if( M5_FS.exists(DOWNLOADER_BIN_VIRTUAL) ) {\n        M5_FS.remove( DOWNLOADER_BIN_VIRTUAL );\n      }\n    }\n  #endif\n\n}\n\n\nvoid doFSInventory()\n{\n  tft.setTextColor( TFT_WHITE );\n  tft.setTextSize( 1 );\n  tft.clear();\n  progressBar( SDU_DISPLAY_OBJ_PTR, 110, 112, 100, 20, 20);\n  appsCount = 0;\n  listDir(M5_FS, \"/\", 0, false); // count valid files first so a progress meter can be displayed\n  appsCountProgress = appsCount;\n  appsCount = 0;\n  tft.drawJpg(sdUpdaterIcon32x40_jpg, sdUpdaterIcon32x40_jpg_len, (tft.width()-32)/2, 40);\n  tft.setTextDatum(MC_DATUM);\n  tft.setFont( &Font2 );\n  tft.drawString(\"Scanning SD Card\", 160, 95 );\n  listDir(M5_FS, \"/\", 0, true); // now retrieve files meta\n  tft.setTextDatum(TL_DATUM);\n  aSortFiles(); // bubble sort alphabetically\n\n  if( factory_partition ) {\n    // TODO: insert partitions from NVS\n  }\n\n  buildM5Menu();\n  drawM5Menu( true ); // render the menu\n  lastcheck = millis(); // reset the timer\n  lastpush = millis(); // reset the timer\n  heapState();\n}\n\n\nvoid HIDMenuObserve() {\n\n  HIDSignal hidState = getControls();\n\n  if( hidState!=HID_INERT && brightness != MAX_BRIGHTNESS ) {\n    // some activity occured, restore brightness\n    Serial.println(\".. !!! Waking up !!\");\n    brightness = MAX_BRIGHTNESS;\n    tft.setBrightness( brightness );\n  }\n  switch( hidState ) {\n    #if defined _CHIMERA_CORE_ && defined USE_SCREENSHOTS\n      case HID_SCREENSHOT:\n        M5.ScreenShot->snap( \"screenshot\" );\n      break;\n    #endif\n    case HID_DOWN:\n      if( !inInfoMenu ) {\n        menuDown();\n      } else {\n        drawM5Menu( inInfoMenu );\n      }\n    break;\n    case HID_UP:\n      if( inInfoMenu ) {\n        drawM5Menu( inInfoMenu );\n      } else {\n        menuUp();\n      }\n    break;\n    case HID_SELECT:\n      if( !inInfoMenu ) {\n        menuInfo();\n      } else {\n        launchApp( fileInfo[MenuID] );\n      }\n    break;\n    case HID_PAGE_DOWN:\n      #if defined USE_DOWNLOADER\n      if( inInfoMenu ) {\n        // update\n        if( fileInfo[MenuID].fileName == String( DOWNLOADER_BIN ) ) {\n          downloaderMenu();\n        } else {\n          updateApp( fileInfo[MenuID] );\n        }\n      } else {\n        pageDown();\n      }\n      #else\n        pageDown();\n      #endif\n    break;\n    case HID_PAGE_UP:\n      if( inInfoMenu ) {\n        // ignore\n      } else {\n        pageUp();\n      }\n    break;\n    default:\n    case HID_INERT:\n      if( inInfoMenu ) { // !! scrolling text also prevents sleep mode !!\n        renderScroll( fileInfo[MenuID].jsonMeta.credits );\n      }\n    break;\n  }\n  //M5.update();\n}\n\n\nvoid sleepTimer() {\n  if( lastpush + MS_BEFORE_SLEEP < millis() ) { // go to sleep if nothing happens for a while\n    if( brightness > 1 ) { // slowly dim the screen first\n      brightness--;\n      if( brightness %10 == 0 ) {\n        Serial.print(\"(\\\".¬.\\\") \");\n      }\n      if( brightness %30 == 0 ) {\n        Serial.print(\" Yawn... \");\n      }\n      if( brightness %7 == 0 ) {\n        Serial.println(\" .zzZzzz. \");\n      }\n      tft.setBrightness( brightness );\n      lastpush = millis() - (MS_BEFORE_SLEEP - brightness*10); // exponential dimming effect\n      return;\n    }\n    gotoSleep();\n  }\n}\n\n//#pragma GCC pop_options\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/partition_manager.h",
    "content": "/*\n *\n * M5Stack SD Menu\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2019 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n */\n\n#pragma once\n\n#include \"esp_partition.h\"\n\n#if !defined(ARDUINO_M5STACK_ATOM_AND_TFCARD)\n#include \"core.h\"\n#endif\n\nconst esp_partition_t *factory_partition = nullptr;\n\n// // from https://github.com/lovyan03/M5Stack_LovyanLauncher\n// bool comparePartition(const esp_partition_t* src1, const esp_partition_t* src2, size_t length)\n// {\n//   size_t lengthLeft = length;\n//   const size_t bufSize = SPI_FLASH_SEC_SIZE;\n//   std::unique_ptr<uint8_t[]> buf1(new uint8_t[bufSize]);\n//   std::unique_ptr<uint8_t[]> buf2(new uint8_t[bufSize]);\n//   uint32_t offset = 0;\n//   size_t i;\n//   while( lengthLeft > 0) {\n//     size_t readBytes = (lengthLeft < bufSize) ? lengthLeft : bufSize;\n//     if (!ESP.flashRead(src1->address + offset, reinterpret_cast<uint32_t*>(buf1.get()), (readBytes + 3) & ~3)\n//      || !ESP.flashRead(src2->address + offset, reinterpret_cast<uint32_t*>(buf2.get()), (readBytes + 3) & ~3)) {\n//         return false;\n//     }\n//     for (i = 0; i < readBytes; ++i) if (buf1[i] != buf2[i]) return false;\n//     lengthLeft -= readBytes;\n//     offset += readBytes;\n//   }\n//   return true;\n// }\n\n// from https://github.com/lovyan03/M5Stack_LovyanLauncher\n// bool copyPartition(File* fs, const esp_partition_t* dst, const esp_partition_t* src, size_t length)\n// {\n//   tft.fillRect( 110, 112, 100, 20, 0);\n//   size_t lengthLeft = length;\n//   const size_t bufSize = SPI_FLASH_SEC_SIZE;\n//   std::unique_ptr<uint8_t[]> buf(new uint8_t[bufSize]);\n//   uint32_t offset = 0;\n//   uint32_t progress = 0, progressOld = 0;\n//   while( lengthLeft > 0) {\n//     size_t readBytes = (lengthLeft < bufSize) ? lengthLeft : bufSize;\n//     if (!ESP.flashRead(src->address + offset, reinterpret_cast<uint32_t*>(buf.get()), (readBytes + 3) & ~3)\n//      || !ESP.flashEraseSector((dst->address + offset) / bufSize)\n//      || !ESP.flashWrite(dst->address + offset, reinterpret_cast<uint32_t*>(buf.get()), (readBytes + 3) & ~3)) {\n//         return false;\n//     }\n//     if (fs) fs->write(buf.get(), (readBytes + 3) & ~3);\n//     lengthLeft -= readBytes;\n//     offset += readBytes;\n//     progress = 100 * offset / length;\n//     if (progressOld != progress) {\n//       progressOld = progress;\n//       progressBar( SDU_DISPLAY_OBJ_PTR, 110, 112, 100, 20, progress);\n//     }\n//   }\n//   return true;\n// }\n\n\n// void copyPartition( const char* binfilename = PROGMEM {MENU_BIN} )\n// {\n//   const esp_partition_t *running = esp_ota_get_running_partition();\n//   const esp_partition_t *nextupdate = esp_ota_get_next_update_partition(NULL);\n//   //const char* menubinfilename PROGMEM {MENU_BIN} ;\n//   size_t sksize = ESP.getSketchSize();\n//   bool flgSD = M5_FS.begin();\n//   File dst;\n//   tft.setFont( &Font2 );\n//   if (flgSD) {\n//     dst = (M5_FS.open(binfilename, FILE_WRITE ));\n//     tft.drawString(\"Overwriting \" MENU_BIN, 160, 38 );\n//   }\n//   if (Flash::copyPartition( flgSD ? &dst : NULL, nextupdate, running, sksize)) {\n//     tft.drawString(\"Done\", 160, 52 );\n//   }\n//   if (flgSD) dst.close();\n// }\n\n\nvoid lsPart()\n{\n  esp_partition_iterator_t pi = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);\n  log_w(\"Partition  Type   Subtype    Address   PartSize   ImgSize    Info    Digest\");\n  log_w(\"---------+------+---------+----------+----------+---------+--------+--------\");\n  while(pi != NULL) {\n    const esp_partition_t* part = esp_partition_get(pi);\n    esp_image_metadata_t meta = esp_image_metadata_t();\n    Flash::digest_t digest;\n    bool isFactory = part->type==ESP_PARTITION_TYPE_APP && part->subtype==ESP_PARTITION_SUBTYPE_APP_FACTORY;\n    bool isOta = part->type==ESP_PARTITION_TYPE_APP && (part->subtype>=ESP_PARTITION_SUBTYPE_APP_OTA_MIN && part->subtype<ESP_PARTITION_SUBTYPE_APP_OTA_MAX);\n    //bool isOta = (part->label[3]=='1' || part->label[3] == '0');\n    String OTAName = \"OTA\";\n    if( isOta || isFactory ) {\n      meta  = Flash::getSketchMeta( part );\n      OTAName += String( part->subtype-ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n    }/* else if( isFactory ) {\n      if( esp_partition_get_sha256(part, meta.image_digest) != ESP_OK ) {\n        log_e(\"Bad sha\");\n        memset( meta.image_digest, 0, sizeof( meta.image_digest ) );\n      }\n    }*/\n    log_w(\"%-8s   0x%02x      0x%02x   0x%06x   %8d  %8s %8s %8s\",\n      String( part->label ),\n      part->type,\n      part->subtype,\n      part->address,\n      part->size,\n      meta.image_len>0 ? String(meta.image_len) : \"n/a\",\n      isOta ? OTAName.c_str() : isFactory ? \"Factory\" : \"n/a\",\n      (isOta || isFactory) /*&& meta.image_len>0*/ ? digest.toString(meta.image_digest) : \"\"\n    );\n    pi = esp_partition_next( pi );\n  }\n  esp_partition_iterator_release(pi);\n}\n\n\n\n\n// from https://github.com/lovyan03/M5Stack_LovyanLauncher\nvoid checkMenuStickyPartition( const char* menubinfilename = PROGMEM {MENU_BIN} )\n{\n  const esp_partition_t* running     = esp_ota_get_running_partition();\n  const esp_partition_t* nextupdate  = esp_ota_get_next_update_partition(NULL);\n  //const esp_partition_t* factorypart = SDUpdater::getFactoryPartition();\n\n  if (!running) {\n    log_e( \"Can't fetch running partition info !!\" );\n    return;\n  }\n  if (!nextupdate) {\n    log_e( \"Can't fetch nextupdate partition info !!\" );\n    return;\n  }\n\n  lsPart();\n\n  size_t sksize = ESP.getSketchSize();\n  tft.setTextDatum(MC_DATUM);\n  tft.setCursor(0,0);\n\n  // if a factory partition exists, propagate there\n  if( Flash::getFactoryPartition() ) {\n    Flash::loadFactory(); // should trigger a restart\n    log_e(\"Switching to factory app failed :-(\");\n  }\n\n  // no factory partition found (or propagation failed), try OTA0/OTA1 and propagate to SD\n  tft.setFont( &Font2 );\n\n  if (!nextupdate) {\n    // tft.setTextFont(4);\n    tft.print(\"! WARNING !\\r\\nNo OTA partition.\\r\\nCan't use SD-Updater.\");\n    delay(3000);\n  } else if( running->subtype==ESP_PARTITION_SUBTYPE_APP_OTA_MIN && nextupdate->subtype==ESP_PARTITION_SUBTYPE_APP_OTA_MIN+1) { // OTA0 was flashed and OTA1 is the next in range\n    tft.drawString(\"Checking 'app0' partition\", 160, 10 );\n    size_t sksize = ESP.getSketchSize();\n    if (!Flash::comparePartition(running, nextupdate, sksize)) {\n      // TODO: handle SPIFFS\n      bool flgSD = M5_FS.begin( /*TFCARD_CS_PIN, SPI, 40000000*/ );\n      tft.drawString(\"Synchronizing to 'app1' partition\", 160, 24 );\n      File dst;\n      if (flgSD) {\n        dst = (M5_FS.open(menubinfilename, FILE_WRITE ));\n        tft.drawString(\"Overwriting \" MENU_BIN, 160, 38 );\n      }\n      if (Flash::copyPartition( flgSD ? &dst : NULL, nextupdate, running, sksize)) {\n        tft.drawString(\"Done\", 160, 52 );\n      }\n\n      if (flgSD) dst.close();\n    } else {\n      log_d(\"Running partition and nextupdate partition match, no propagation needed\");\n    }\n\n    SDUpdater::updateNVS();\n    tft.drawString(\"Hot-loading 'app1' partition\", 160, 66 );\n\n    if (Update.canRollBack()) {\n      Update.rollBack();\n      ESP.restart();\n    } else {\n      tft.print(\"! WARNING !\\r\\nUpdate.rollBack() failed.\");\n      log_e(\"Failed to rollback after copy\");\n    }\n  }\n  tft.setTextDatum(TL_DATUM);\n}\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/partitions_16MB_4_or_6_apps.csv",
    "content": "## 4 Apps + Factory\n## Name,   Type, SubType,    Offset,     Size\n#nvs,      data, nvs,        0x9000,   0x5000\n#otadata,  data, ota,        0xe000,   0x2000\n#ota_0,    0,    ota_0,     0x10000, 0x300000\n#ota_1,    0,    ota_1,    0x310000, 0x300000\n#ota_2,    0,    ota_2,    0x610000, 0x300000\n#ota_3,    0,    ota_3,    0x910000, 0x300000\n#firmware, app,  factory,  0xC10000, 0x0F0000\n#spiffs,   data, spiffs,   0xD00000, 0x2F0000\n#coredump, data, coredump, 0xFF0000,  0x10000\n\n\n\n# 6 Apps + Factory\n# Name,   Type, SubType,    Offset,     Size\nnvs,      data, nvs,        0x9000,   0x5000\notadata,  data, ota,        0xe000,   0x2000\nota_0,    0,    ota_0,     0x10000, 0x200000\nota_1,    0,    ota_1,    0x210000, 0x200000\nota_2,    0,    ota_2,    0x410000, 0x200000\nota_3,    0,    ota_3,    0x610000, 0x200000\nota_4,    0,    ota_4,    0x810000, 0x200000\nota_5,    0,    ota_5,    0xA10000, 0x200000\nfirmware, app,  factory,  0xC10000, 0x0F0000\nspiffs,   data, spiffs,   0xD00000, 0x2F0000\ncoredump, data, coredump, 0xFF0000,  0x10000\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/partitions_16MB_8apps.csv",
    "content": "# 4 Apps + Factory\n# Name,   Type, SubType,    Offset,     Size\nnvs,      data, nvs,        0x9000,   0x5000\notadata,  data, ota,        0xe000,   0x2000\nota_0,    0,    ota_0,     0x10000, 0x300000\nota_1,    0,    ota_1,    0x310000, 0x300000\nota_2,    0,    ota_2,    0x610000, 0x300000\nota_3,    0,    ota_3,    0x910000, 0x300000\nfirmware, app,  factory,  0xC10000, 0x0F0000\nspiffs,   data, spiffs,   0xD00000, 0x2F0000\ncoredump, data, coredump, 0xFF0000,  0x10000\n\n# 6 Apps + Factory\n# Name,   Type, SubType,    Offset,     Size\nnvs,      data, nvs,        0x9000,   0x5000\notadata,  data, ota,        0xe000,   0x2000\nota_0,    0,    ota_0,     0x10000, 0x200000\nota_1,    0,    ota_1,    0x210000, 0x200000\nota_2,    0,    ota_2,    0x410000, 0x200000\nota_3,    0,    ota_3,    0x610000, 0x200000\nota_4,    0,    ota_4,    0x810000, 0x200000\nota_5,    0,    ota_5,    0xA10000, 0x200000\nfirmware, app,  factory,  0xC10000, 0x0F0000\nspiffs,   data, spiffs,   0xD00000, 0x2F0000\ncoredump, data, coredump, 0xFF0000,  0x10000\n\n# Based on default_16MB.csv:\n## Name,   Type, SubType, Offset,  Size, Flags\n#nvs,      data, nvs,     0x9000,  0x5000,\n#otadata,  data, ota,     0xe000,  0x2000,\n#app0,     app,  ota_0,   0x10000, 0x640000,\n#app1,     app,  ota_1,   0x650000,0x640000,\n#spiffs,   data, spiffs,  0xc90000,0x360000,\n#coredump, data, coredump,0xFF0000,0x10000,\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/platformio.ini",
    "content": "[platformio]\nsrc_dir = main\n;default_envs = m5stack-fire\ndefault_envs = m5stack-core-esp32\n;default_envs = m5stack-core-esp32\n;default_envs = odroid_esp32\n;default_envs = m5stack-atom-lite\n\n[env]\nplatform          = espressif32\nplatform_packages = framework-arduinoespressif32\n;platform          = https://github.com/platformio/platform-espressif32.git\n;platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.7\nframework         = arduino\nupload_speed      = 1500000\nmonitor_speed     = 115200\nbuild_flags =\n  -DCORE_DEBUG_LEVEL=4\nlib_extra_dirs    = ../../../M5Stack-SD-Updater\nlib_deps =\n  FS\n  SPI\n  Wire\n  LovyanGFX\n  ESP32-Chimera-Core\n;  M5Stack-SD-Updater\n  WiFi\n  HTTPClient\n  WiFiClientSecure\n  Preferences\n  Update\n  bblanchon/ArduinoJson\n\n\n\n[env:m5stack-fire]\nboard = m5stack-fire\nboard_build.partitions = default_16MB.csv\nlib_deps =\n  ${env.lib_deps}\n  fastled/FastLED@3.4.0\n\n[env:m5stack-core-esp32]\nboard = m5stack-core-esp32\ndebug_build_flags = -Os\nboard_build.partitions = min_spiffs.csv\nlib_deps =\n  ${env.lib_deps}\n\n[env:m5stack-core2]\nboard = m5stack-core2\nboard_build.partitions = default_16MB.csv\nlib_deps =\n  ${env.lib_deps}\n\n[env:odroid_esp32]\nboard = odroid_esp32\nboard_build.partitions = min_spiffs.csv\nlib_deps =\n  ${env.lib_deps}\n\n[env:m5stack-atom-lite]\nboard = m5stack-atom\nboard_build.partitions = min_spiffs.csv\nlib_deps =\n  bblanchon/ArduinoJson\n  ESP32-Chimera-Core\n  ;https://github.com/riraosan/M5Stack-SD-Updater.git#develop\n  ;M5Stack-SD-Updater\n  lennarthennigs/Button2@2.2.2\n\nlib_ignore =\n  #LovyanGFX\n  #ESP32-Chimera-Core\n\n\nbuild_flags =\n  ${env.build_flags}\n  -D ARDUINO_M5STACK_ATOM_AND_TFCARD\n  -D _MOSI=19\n  -D _MISO=33\n  -D _CLK=23\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/registry.default.h",
    "content": "// left in a separate file for easy editing / overwriting\n\n//see #define DEFAULT_REGISTRY_BOARD in menu.h\n\n#define DEFAULT_REGISTRY_NAME \"SDUpdater\"\n#define DEFAULT_REGISTRY_DESC \"Tobozo's \" PLATFORM_NAME \" Application registry @ phpsecu.re\"\n#define DEFAULT_REGISTRY_URL \"https://phpsecu.re/\" DEFAULT_REGISTRY_BOARD \"/registry/phpsecu.re.json\" // should exist as \"/.registry/default.json\" on SD Card\n#define DEFAULT_REGISTRY_CHANNEL \"unstable\" // \"master\" or \"unstable\"\n\n#define DEFAULT_MASTER_DESC \"Master channel at phpsecu.re/\" DEFAULT_REGISTRY_BOARD \" registry\"\n#define DEFAULT_MASTER_URL \"https://phpsecu.re/\" DEFAULT_REGISTRY_BOARD \"/sd-updater/\"\n#define DEFAULT_MASTER_API_HOST \"phpsecu.re\"\n#define DEFAULT_MASTER_API_PATH \"/\" DEFAULT_REGISTRY_BOARD\n#define DEFAULT_MASTER_API_CERT_PATH \"/cert/\"\n#define DEFAULT_MASTER_UPDATER_PATH \"/sd-updater\"\n#define DEFAULT_MASTER_CATALOG_ENDPOINT \"/catalog.json\"\n\n#define DEFAULT_UNSTABLE_DESC \"Unstable channel at phpsecu.re/\" DEFAULT_REGISTRY_BOARD \" registry\"\n#define DEFAULT_UNSTABLE_URL \"https://phpsecu.re/\" DEFAULT_REGISTRY_BOARD \"/sd-updater/unstable/\"\n#define DEFAULT_UNSTABLE_API_HOST \"phpsecu.re\"\n#define DEFAULT_UNSTABLE_API_PATH \"/\" DEFAULT_REGISTRY_BOARD\n#define DEFAULT_UNSTABLE_API_CERT_PATH \"/cert/\"\n#define DEFAULT_UNSTABLE_UPDATER_PATH \"/sd-updater/unstable\"\n#define DEFAULT_UNSTABLE_CATALOG_ENDPOINT \"/catalog.json\"\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/registry.h",
    "content": "/*\n *\n * M5Stack SD Menu\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2019 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n */\n\n// load the registry information this launcher is attached to\n#include \"registry.default.h\"\n\ntypedef struct {\n  String name;\n  String description;\n  String url;\n  String api_host;\n  String api_path;\n  String api_cert_path;\n  String updater_path;\n  String catalog_endpoint;\n\n  String api_cert_provider_url_http;\n  String api_cert_provider_url_https;\n  String api_url_https;\n  String api_url_http;\n\n  void init() {\n    api_cert_provider_url_http  = \"http://\" + api_host + api_path + api_cert_path;\n    api_cert_provider_url_https = \"https://\" + api_host + api_path + api_cert_path;\n    api_url_https               = \"https://\" + api_host + api_path + updater_path;\n    api_url_http                = \"https://\" + api_host + api_path + updater_path;\n  }\n  void print() {\n    log_i(\"\\n\\tname: %s\\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\\n\\n\",\n      name.c_str(),\n      description.c_str(),\n      url.c_str(),\n      api_host.c_str(),\n      api_path.c_str(),\n      api_cert_path.c_str(),\n      updater_path.c_str(),\n      catalog_endpoint.c_str(),\n      api_cert_provider_url_https.c_str(),\n      api_url_https.c_str(),\n      api_url_http.c_str()\n    );\n  }\n} AppRegistryItem;\n\ntypedef struct {\n  String name;\n  String description;\n  String url;\n  String pref_default_channel; // local option for SDUpdater use only\n  AppRegistryItem masterChannel;\n  AppRegistryItem unstableChannel;\n  AppRegistryItem defaultChannel;\n  void init() {\n    masterChannel.init();\n    unstableChannel.init();\n    if( pref_default_channel == \"master\" ) {\n      defaultChannel = masterChannel;\n    } else {\n      defaultChannel = unstableChannel;\n    }\n    print();\n  }\n  void print() {\n    log_i(\"%s\", \"Registry infos:\");\n    log_i(\"\\n\\tname: %s\\n\\tdescription: %s\\n\\turl: %s\\n\\tpref_default_channel: %s\\n\\n\",\n      name.c_str(),\n      description.c_str(),\n      url.c_str(),\n      pref_default_channel.c_str()\n    );\n    log_i(\"%s\", \"Master channel infos:\");\n    masterChannel.print();\n    log_i(\"%s\", \"Unstable channel infos:\");\n    unstableChannel.print();\n    log_i(\"%s\", \"Default channel infos:\");\n    defaultChannel.print();\n  }\n} AppRegistry;\n\n\nAppRegistryItem defaultMasterChannel = {\n  \"master\", // name\n  DEFAULT_MASTER_DESC,\n  DEFAULT_MASTER_URL,\n  DEFAULT_MASTER_API_HOST,\n  DEFAULT_MASTER_API_PATH,\n  DEFAULT_MASTER_API_CERT_PATH,\n  DEFAULT_MASTER_UPDATER_PATH,\n  DEFAULT_MASTER_CATALOG_ENDPOINT\n};\n\nAppRegistryItem defaultUnstableChannel = {\n  \"unstable\", // name\n  DEFAULT_UNSTABLE_DESC,\n  DEFAULT_UNSTABLE_URL,\n  DEFAULT_UNSTABLE_API_HOST,\n  DEFAULT_UNSTABLE_API_PATH,\n  DEFAULT_UNSTABLE_API_CERT_PATH,\n  DEFAULT_UNSTABLE_UPDATER_PATH,\n  DEFAULT_UNSTABLE_CATALOG_ENDPOINT\n};\n\nAppRegistry defaultAppRegistry = {\n  DEFAULT_REGISTRY_NAME,\n  DEFAULT_REGISTRY_DESC,\n  DEFAULT_REGISTRY_URL, // should exist as \"default.json\" on SD Card\n  DEFAULT_REGISTRY_CHANNEL,\n  defaultMasterChannel,\n  defaultUnstableChannel\n};\n"
  },
  {
    "path": "examples/M5Stack-SD-Menu/wifi_manager.h",
    "content": "#include <ESPmDNS.h>\n#include <WiFiClient.h>\n#include \"WebServer.h\"\n\n\nconst IPAddress apIP(192, 168, 4, 1);\nconst char* apSSID = \"M5STACK_SETUP\";\nboolean settingMode;\nString ssidList;\nString wifi_ssid;\nString wifi_password;\n\n// DNSServer dnsServer;\nWebServer webServer(80);\nPreferences preferences;\n\nString makePage(String title, String contents);\nString urlDecode(String input);\nvoid setupMode();\nvoid startWebServer();\nboolean checkConnection();\nboolean restoreConfig();\n\n\n\nboolean restoreConfig() {\n  wifi_ssid = preferences.getString(\"WIFI_SSID\");\n  wifi_password = preferences.getString(\"WIFI_PASSWD\");\n  if( wifi_ssid!=\"\" && wifi_password!=\"\" ) {\n    Serial.print(\"WIFI-SSID: \");\n    tft.print(\"WIFI-SSID: \");\n    Serial.println(wifi_ssid);\n    tft.println(wifi_ssid);\n    Serial.print(\"WIFI-PASSWD: \");\n    tft.print(\"WIFI-PASSWD: \");\n    Serial.println(wifi_password);\n    tft.println(wifi_password);\n    WiFi.begin(wifi_ssid.c_str(), wifi_password.c_str());\n    return true;\n  } else {\n    tft.print(\"No config to restore\");\n    WiFi.begin();\n    return false;\n  }\n}\n\nboolean checkConnection() {\n  int count = 0;\n  Serial.print(\"Waiting for Wi-Fi connection\");\n  tft.print(\"Waiting for Wi-Fi connection\");\n  while ( count < 30 ) {\n    if (WiFi.status() == WL_CONNECTED) {\n      Serial.println();\n      tft.println();\n      Serial.println(\"Connected!\");\n      tft.println(\"Connected!\");\n      return (true);\n    }\n    delay(500);\n    Serial.print(\".\");\n    tft.print(\".\");\n    count++;\n  }\n  Serial.println(\"Timed out.\");\n  tft.println(\"Timed out.\");\n  return false;\n}\n\nvoid startWebServer() {\n  if (settingMode) {\n    Serial.print(\"Starting Web Server at \");\n    tft.print(\"Starting Web Server at \");\n    Serial.println(WiFi.softAPIP());\n    tft.println(WiFi.softAPIP());\n    webServer.on(\"/settings\", []() {\n      String s = \"<h1>Wi-Fi Settings</h1><p>Please enter your password by selecting the SSID.</p>\";\n      s += \"<form method=\\\"get\\\" action=\\\"setap\\\"><label>SSID: </label><select name=\\\"ssid\\\">\";\n      s += ssidList;\n      s += \"</select><br>Password: <input name=\\\"pass\\\" length=64 type=\\\"password\\\"><input type=\\\"submit\\\"></form>\";\n      webServer.send(200, \"text/html\", makePage(\"Wi-Fi Settings\", s));\n    });\n    webServer.on(\"/setap\", []() {\n      String ssid = urlDecode(webServer.arg(\"ssid\"));\n      Serial.print(\"SSID: \");\n      tft.print(\"SSID: \");\n      Serial.println(ssid);\n      tft.println(ssid);\n      String pass = urlDecode(webServer.arg(\"pass\"));\n      Serial.print(\"Password: \");\n      tft.print(\"Password: \");\n      Serial.println(pass);\n      tft.println(pass);\n      Serial.println(\"Writing SSID to EEPROM...\");\n      tft.println(\"Writing SSID to EEPROM...\");\n\n      // Store wifi config\n      Serial.println(\"Writing Password to nvr...\");\n      tft.println(\"Writing Password to nvr...\");\n      preferences.putString(\"WIFI_SSID\", ssid);\n      preferences.putString(\"WIFI_PASSWD\", pass);\n\n      Serial.println(\"Write nvr done!\");\n      tft.println(\"Write nvr done!\");\n      String s = \"<h1>Setup complete.</h1><p>device will be connected to \\\"\";\n      s += ssid;\n      s += \"\\\" after the restart.\";\n      webServer.send(200, \"text/html\", makePage(\"Wi-Fi Settings\", s));\n      delay(3000);\n      ESP.restart();\n    });\n    webServer.onNotFound([]() {\n      String s = \"<h1>AP mode</h1><p><a href=\\\"/settings\\\">Wi-Fi Settings</a></p>\";\n      webServer.send(200, \"text/html\", makePage(\"AP mode\", s));\n    });\n  }\n  else {\n    Serial.print(\"Starting Web Server at \");\n    tft.print(\"Starting Web Server at \");\n    Serial.println(WiFi.localIP());\n    tft.println(WiFi.localIP());\n    webServer.on(\"/\", []() {\n      String s = \"<h1>STA mode</h1><p><a href=\\\"/reset\\\">Reset Wi-Fi Settings</a></p>\";\n      webServer.send(200, \"text/html\", makePage(\"STA mode\", s));\n    });\n    webServer.on(\"/reset\", []() {\n      // reset the wifi config\n      preferences.remove(\"WIFI_SSID\");\n      preferences.remove(\"WIFI_PASSWD\");\n      String s = \"<h1>Wi-Fi settings was reset.</h1><p>Please reset device.</p>\";\n      webServer.send(200, \"text/html\", makePage(\"Reset Wi-Fi Settings\", s));\n      delay(3000);\n      ESP.restart();\n    });\n  }\n  webServer.begin();\n}\n\nvoid setupMode() {\n  WiFi.mode(WIFI_MODE_STA);\n  WiFi.disconnect();\n  delay(100);\n  int n = WiFi.scanNetworks();\n  delay(100);\n  Serial.println(\"\");\n  tft.println(\"\");\n  for (int i = 0; i < n; ++i) {\n    ssidList += \"<option value=\\\"\";\n    ssidList += WiFi.SSID(i);\n    ssidList += \"\\\">\";\n    ssidList += WiFi.SSID(i);\n    ssidList += \"</option>\";\n  }\n  delay(100);\n  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));\n  WiFi.softAP(apSSID);\n  WiFi.mode(WIFI_MODE_AP);\n  // WiFi.softAPConfig(IPAddress local_ip, IPAddress gateway, IPAddress subnet);\n  // WiFi.softAP(const char* ssid, const char* passphrase = NULL, int channel = 1, int ssid_hidden = 0);\n  // dnsServer.start(53, \"*\", apIP);\n  startWebServer();\n  Serial.print(\"Starting Access Point at \\\"\");\n  tft.print(\"Starting Access Point at \\\"\");\n  Serial.print(apSSID);\n  tft.print(apSSID);\n  Serial.println(\"\\\"\");\n  tft.println(\"\\\"\");\n}\n\nString makePage(String title, String contents) {\n  String s = \"<!DOCTYPE html><html><head>\";\n  s += \"<meta name=\\\"viewport\\\" content=\\\"width=device-width,user-scalable=0\\\">\";\n  s += \"<title>\";\n  s += title;\n  s += \"</title></head><body>\";\n  s += contents;\n  s += \"</body></html>\";\n  return s;\n}\n\nString urlDecode(String input) {\n  String s = input;\n  s.replace(\"%20\", \" \");\n  s.replace(\"+\", \" \");\n  s.replace(\"%21\", \"!\");\n  s.replace(\"%22\", \"\\\"\");\n  s.replace(\"%23\", \"#\");\n  s.replace(\"%24\", \"$\");\n  s.replace(\"%25\", \"%\");\n  s.replace(\"%26\", \"&\");\n  s.replace(\"%27\", \"\\'\");\n  s.replace(\"%28\", \"(\");\n  s.replace(\"%29\", \")\");\n  s.replace(\"%30\", \"*\");\n  s.replace(\"%31\", \"+\");\n  s.replace(\"%2C\", \",\");\n  s.replace(\"%2E\", \".\");\n  s.replace(\"%2F\", \"/\");\n  s.replace(\"%2C\", \",\");\n  s.replace(\"%3A\", \":\");\n  s.replace(\"%3A\", \";\");\n  s.replace(\"%3C\", \"<\");\n  s.replace(\"%3D\", \"=\");\n  s.replace(\"%3E\", \">\");\n  s.replace(\"%3F\", \"?\");\n  s.replace(\"%40\", \"@\");\n  s.replace(\"%5B\", \"[\");\n  s.replace(\"%5C\", \"\\\\\");\n  s.replace(\"%5D\", \"]\");\n  s.replace(\"%5E\", \"^\");\n  s.replace(\"%5F\", \"-\");\n  s.replace(\"%60\", \"`\");\n  return s;\n}\n\n\n\nvoid wifiManagerSetup() {\n  preferences.begin(\"wifi-config\");\n  tft.fillScreen( TFT_BLACK );\n\n  delay(10);\n  if (restoreConfig()) {\n    if (checkConnection()) {\n      settingMode = false;\n      startWebServer();\n      return;\n    }\n  }\n  settingMode = true;\n  setupMode();\n}\n\n\nvoid wifiManagerLoop() {\n  do {\n    webServer.handleClient();\n  } while( settingMode == true );\n}\n\n"
  },
  {
    "path": "examples/M5Stack-SDLoader-Snippet/M5Stack-SDLoader-Snippet.ino",
    "content": "/*\n *\n * M5Stack SD Loader Snippet\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2018 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n *\n * To be used with M5Stack SD Menu https://github.com/tobozo/M5Stack-SD-Updater\n * This sketch is useless without a precompiled \"menu.bin\" saved on the SD Card.\n * You may compile menu.bin from the M5Stack-SD-Menu sketch.\n *\n * Just use this sketch as your boilerplate and code your app over it, then\n * compile it and put the binary on the sdcard. See M5Stack-SD-Menu for more\n * info on the acceptable file formats.\n *\n * When this app is in memory, booting the M5Stack with the Button A pushed will\n * flash back the menu.bin into memory.\n *\n */\n/*\n#if defined( ARDUINO_M5Stack_Core_ESP32 ) || defined( ARDUINO_M5STACK_FIRE ) // M5Stack Classic/Fire\n  #include <M5Stack.h>\n  // #include <ESP32-Chimera-Core.h>\n#elif defined( ARDUINO_M5STACK_Core2 ) // M5Stack Core2\n  #include <M5Core2.h>\n#elif defined( ARDUINO_M5Stick_C ) // M5StickC\n  #include <M5StickC.h>\n#else\n  #include <ESP32-Chimera-Core.h> // any other ESP32 device with SD\n#endif\n*/\n#include <ESP32-targz.h> // optional: https://github.com/tobozo/ESP32-targz\n#include <ESP32-Chimera-Core.h>\n//#include <M5Stack.h>\n// #define SDU_HEADLESS\n\n#define SDU_APP_NAME \"M5Stack SDLoader Snippet\"\n#define SDU_APP_PATH \"/MY_SKETCH.bin\"\n\n#include <M5StackUpdater.h>\n\nvoid setup()\n{\n  M5.begin();\n\n  M5.Lcd.fillRect( 10, 10, 100, 100, TFT_BLUE );\n  delay(1000);\n\n  Serial.println(\"Welcome to the SD-Updater minimal example!\");\n  Serial.println(\"Now checking if a button was pushed during boot ...\");\n\n  SDUCfg.setLabelMenu(\"<< Menu\");        // BtnA label: load menu.bin\n  SDUCfg.setLabelSkip(\"Launch\");         // BtnB label: skip the lobby countdown and run the app\n  SDUCfg.setLabelSave(\"Save\");           // BtnC label: save the sketch to the SD\n  SDUCfg.setAppName( SDU_APP_NAME );     // Lobby screen label: application name\n  SDUCfg.setBinFileName( SDU_APP_PATH ); // If file path to bin is set for this app, it will be checked at boot and created if not exist\n\n  // checkSDUpdater( SD );\n  checkSDUpdater(\n    SD,           // filesystem (default=SD)\n    MENU_BIN,     // path to binary (default=/menu.bin, empty string=rollback only)\n    5000,         // wait delay, (default=0, will be forced to 2000 upon ESP.restart() )\n    TFCARD_CS_PIN // (usually default=4 but your mileage may vary)\n  );\n  Serial.println(\"Nope, will run the sketch normally\");\n  M5.Lcd.print(\"M5Stack SD Loader test\");\n}\n\n\nvoid loop()\n{\n  M5.update();\n  // provide means to copy the sketch to filesystem\n  if( M5.BtnB.pressedFor( 1000 ) ) {\n    Serial.println(\"Will copy this sketch to filesystem\");\n    if( saveSketchToFS( SD, SDU_APP_PATH, TFCARD_CS_PIN ) ) {\n      Serial.println(\"Copy successful !\");\n    } else {\n      Serial.println(\"Copy failed !\");\n    }\n  }\n\n}\n"
  },
  {
    "path": "examples/M5StickC-SPIFFS-Loader-Snippet/M5StickC-SPIFFS-Loader-Snippet.ino",
    "content": "/*\n *\n * M5StickC SPIFFS Loader Snippet\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2019 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n *\n * To be used with M5Stack SD Menu https://github.com/tobozo/M5Stack-SD-Updater\n * This sketch is useless without a precompiled \"menu.bin\" saved on the SD Card.\n * You may compile menu.bin from the M5Stack-SD-Menu sketch.\n *\n * Just use this sketch as your boilerplate and code your app over it, then\n * compile it and put the binary on the sdcard. See M5Stack-SD-Menu for more\n * info on the acceptable file formats.\n *\n * When this app is in memory, booting the M5StickC with the Button A pushed will\n * flash back the menu.bin into memory.\n *\n */\n#include <ESP32-targz.h> // optional: https://github.com/tobozo/ESP32-targz\n#include <M5StickC.h>\n#include <M5StackUpdater.h>\n\n//#define APP2\n\n#if defined APP1\n  #pragma message \"Compiling app1.bin\"\n\n  void setup() {\n    Serial.begin(115200);\n    Serial.println(\"Welcome to the SPIFFS-Update example!\");\n    Serial.print(\"M5StickC initializing...\");\n    M5.begin();\n    M5.update();\n    M5.Lcd.setRotation(3);\n    M5.Lcd.println(\"**** APP1 ****\");\n    M5.Lcd.println(\"BtnA: rollback\");\n    M5.Lcd.println(\"BtnB: app2\");\n  }\n\n  void loop()\n  {\n    M5.update();\n    if( M5.BtnA.wasPressed() ) {\n      updateRollBack(\"Hot-Loading\");\n    }\n    if( M5.BtnB.wasPressed() ) {\n      updateFromFS( SPIFFS, \"/app2.bin\" );\n    }\n  }\n\n\n#elif defined APP2\n\n  #pragma message \"Compiling app2.bin\"\n\n  void setup() {\n    Serial.begin(115200);\n    Serial.println(\"Welcome to the SPIFFS-Update example!\");\n    Serial.print(\"M5StickC initializing...\");\n    M5.begin();\n    M5.update();\n    M5.Lcd.setRotation(3);\n    M5.Lcd.println(\"**** APP2 ****\");\n    M5.Lcd.println(\"BtnA: rollback\");\n    M5.Lcd.println(\"BtnB: app1\");\n  }\n\n  void loop()\n  {\n    M5.update();\n    if( M5.BtnA.wasPressed() ) {\n      updateRollBack(\"Hot-Loading\");\n    }\n    if( M5.BtnB.wasPressed() ) {\n      updateFromFS( SPIFFS, \"/app1.bin\" );\n    }\n  }\n\n#else\n\n  #pragma message \"Compiling example\"\n\n  void setup() {\n    Serial.begin(115200);\n    Serial.println(\"Welcome to the SPIFFS-Update example!\");\n    Serial.print(\"M5StickC initializing...\");\n    M5.begin();\n    M5.update();\n    M5.Lcd.setRotation(3);\n    M5.Lcd.setTextSize(2);\n    M5.Lcd.println(\"-> LAUNCHER <\");\n\n    M5.Lcd.println(\"BtnA: app1\");\n    M5.Lcd.println(\"BtnB: app2\");\n  }\n\n  void loop()\n  {\n    M5.update();\n    if( M5.BtnA.wasPressed() ) {\n      updateFromFS( SPIFFS, \"/app1.bin\" );\n    }\n    if( M5.BtnB.wasPressed() ) {\n      updateFromFS( SPIFFS, \"/app2.bin\" );\n    }\n  }\n\n#endif\n"
  },
  {
    "path": "examples/M5Unified/M5Unified.ino",
    "content": "\n#include <SD.h>\n#include <M5Unified.h>\n//#define TFCARD_CS_PIN 4\n#include <ESP32-targz.h> // optional: https://github.com/tobozo/ESP32-targz\n#include <M5StackUpdater.h>\n\nvoid setup(void)\n{\n  M5.begin();\n  Serial.begin(115200);\n\n  SDUCfg.setLabelMenu(\"< Menu\");               // BtnA label: load menu.bin\n  SDUCfg.setLabelSkip(\"Launch\");               // BtnB label: skip the lobby countdown and run the app\n  SDUCfg.setLabelSave(\"Save\");                 // BtnC label: save the sketch to the SD\n  SDUCfg.setAppName(\"M5Unified test\");         // lobby screen label: application name\n  SDUCfg.setBinFileName(\"/M5UnifiedTest.bin\"); // if file path to bin is set for this app, it will be checked at boot and created if not exist\n\n  checkSDUpdater(\n    SD,           // filesystem (default=SD)\n    MENU_BIN,     // path to binary (default=/menu.bin, empty string=rollback only)\n    5000,        // wait delay, (default=0, will be forced to 2000 upon ESP.restart() )\n    TFCARD_CS_PIN // usually default=4 but your mileage may vary\n  );\n\n  M5.Display.print(\"M5Unified test\");\n}\n\nvoid loop(void)\n{\n  // do your stuff\n}\n\n"
  },
  {
    "path": "examples/MultiFS/MultiFS.ino",
    "content": "\n/*\n#include <SPIFFS.h>\n#include <LittleFS.h>\n#include <SD.h>\n#include <SD_MMC.h>\n#include <FFat.h>\n#include <SdFat.h>\n#include <ESP32-targz.h>*/\n\n#define SDU_NO_AUTODETECT                // Disable SDUpdater autodetect: this prevents <SD.h> to be auto-selected, however it also disables board detection\n\n\n#define SDU_USE_SDFATFS                      // Tell M5StackUpdater to load <SdFat.h> and wrap SdFat32 into fs::FS::SdFat32FSImpl\n#define SDU_USE_SD\n#define SDU_USE_SD_MMC\n#define SDU_USE_SPIFFS\n#define SDU_USE_FFAT\n#define SDU_USE_LITTLEFS\n#define SDU_ENABLE_GZ\n\n#include <M5StackUpdater.h>\n\nSdFs sd;\nauto SdFatSPIConfig = SdSpiConfig( TFCARD_CS_PIN, SHARED_SPI, SD_SCK_MHZ(25) );\n\nvoid setup()\n{\n  Serial.begin(115200);\n\n  SDUpdater sdUpdater;\n\n  bool has_littlefs = ConfigManager::hasFS( &sdUpdater, LittleFS );\n  bool has_spiffs   = ConfigManager::hasFS( &sdUpdater, SPIFFS );\n  bool has_sd       = ConfigManager::hasFS( &sdUpdater, SD );\n  bool has_sd_mmc   = ConfigManager::hasFS( &sdUpdater, SD_MMC );\n  bool has_ffat     = ConfigManager::hasFS( &sdUpdater, FFat );\n  bool has_sdfat    = ConfigManager::hasFS( &sdUpdater, *ConfigManager::SDU_SdFatFsPtr );\n\n\n  Serial.printf(\"Has littlefs:\\t%s\\nHas spiffs:\\t%s\\nHas sd:  \\t%s\\nHas sd_mmc:\\t%s\\nHas ffat:\\t%s\\nHas sdfat:\\t%s\\n\",\n    has_littlefs?\"true\":\"false\",\n    has_spiffs  ?\"true\":\"false\",\n    has_sd      ?\"true\":\"false\",\n    has_sd_mmc  ?\"true\":\"false\",\n    has_ffat    ?\"true\":\"false\",\n    has_sdfat   ?\"true\":\"false\"\n  );\n\n\n}\n\nvoid loop()\n{\n\n}\n"
  },
  {
    "path": "examples/SdFatUpdater/SdFatUpdater.ino",
    "content": "#define TFCARD_CS_PIN 4 // customize this\n\n\n#include <M5Unified.h> // Note: don't mix M5Unified with LovyanGFX\n//#include <M5Core2.h>\n//#include <M5Stack.h>\n//#define LGFX_AUTODETECT\n//#include <LovyanGFX.h>\n\n\n// M5StackUpdater library configuration\n\n#define SDU_NO_PRAGMAS                   // don't spawn pragma messages during compilation\n#define SDU_ENABLE_GZ                    // auto-load ESP32-targz (implicit if ESP32-targz.h is included before M5StackUpdater.h)\n//#include <ESP32-targz.h>                 // optional gzipped firmware support, overriden by SDU_ENABLE_GZ -> https://github.com/tobozo/ESP32-targz\n#define SDU_NO_AUTODETECT                // Disable SDUpdater autodetect: this prevents <SD.h> to be auto-selected, however it also disables board detection\n#define USE_SDFATFS                      // Tell M5StackUpdater to load <SdFat.h> and wrap SdFat32 into fs::FS::SdFat32FSImpl\n#define SDU_APP_NAME \"SdFatUpdater test\"\n#define SDU_APP_PATH \"/SdFatUpdater.bin\"\n\n#if __has_include(<LovyanGFX.h>)\n  LGFX lcd;\n  #define HAS_M5_API                       // Use M5 API (M5.BtnA, BtnB, BtnC...) for triggers\n  #define SDU_USE_DISPLAY                  // Enable display (progress bar, lobby, b\n  #define HAS_LGFX                         // Use LGFX Family display driver with zo\n  #define SDU_Sprite LGFX_Sprite           // Inherit Sprite type from LGFX\n  #define SDU_DISPLAY_TYPE LGFX*           // inherit display type from LGFX\n  #define SDU_DISPLAY_OBJ_PTR &lcd         // alias display pointer from lcd object\n#elif __has_include(<M5Unified.h>)\n  #define HAS_M5_API                       // Use M5 API (M5.BtnA, BtnB, BtnC...) for triggers\n  #define SDU_USE_DISPLAY                  // Enable display (progress bar, lobby, buttons)\n  #define HAS_LGFX                         // Use LGFX Family display driver with zoom, rotate\n  #define SDU_Sprite LGFX_Sprite           // Inherit Sprite type from M5GFX\n  #define SDU_DISPLAY_TYPE M5GFX*          // inherit display type from M5GFX\n  #define SDU_DISPLAY_OBJ_PTR &M5.Display  // alias display pointer from M5Unified\n  #if defined ARDUINO_M5STACK_Core2\n    #define SDU_TouchButton LGFX_Button    // inherit Buttons types from M5Unified\n    #define SDU_HAS_TOUCH\n  #endif\n#elif __has_include(<M5Core2.h>) || __has_include(<M5Stack.h>)\n  #define HAS_M5_API                       // Use M5 API (M5.BtnA, BtnB, BtnC...) for triggers\n  #define SDU_USE_DISPLAY                  // Enable display (progress bar, lobby, buttons)\n  #define SDU_Sprite TFT_eSprite           // Inherit TFT_eSprite type from M5 Core\n  #define SDU_DISPLAY_TYPE M5Display*      // inherit TFT_eSpi type from M5 Core\n  #define SDU_DISPLAY_OBJ_PTR &M5.Lcd      // alias display pointer from M5 Core\n  #if defined ARDUINO_M5STACK_Core2 && __has_include(<M5Core2.h>)\n    #define SDU_TouchButton TFT_eSPI_Button // inherit Buttons types from TFT_eSPI\n    #define SDU_HAS_TOUCH\n  #endif\n\n#else\n\n  #error \"This example only supports the following cores: LovyanGFX.h, M5Unified.h, M5Stack.h or M5Core2.h\"\n\n#endif\n\n//#include <SdFat.h> // not necessary when `USE_SDFATFS` is defined\n#include <M5StackUpdater.h>\n\nSdFs sd;\nauto SdFatSPIConfig = SdSpiConfig( TFCARD_CS_PIN, SHARED_SPI, SD_SCK_MHZ(25) );\n\n\nvoid setup()\n{\n  M5.begin();\n\n  SDUCfg.use_rollback = false;                 // Disable rollbadk (loading the menu may be slower, but no false positives during tests)\n  SDUCfg.setLabelMenu(\"< Menu\");               // BtnA label: load menu.bin\n  SDUCfg.setLabelSkip(\"Launch\");               // BtnB label: skip the lobby countdown and run the app\n  SDUCfg.setLabelSave(\"Save\");                 // BtnC label: save the sketch to the SD\n  SDUCfg.setAppName( SDU_APP_NAME );      // lobby screen label: application name\n  SDUCfg.setBinFileName( SDU_APP_PATH );  // if file path to bin is set for this app, it will be checked at boot and created if not exist\n\n  checkSDUpdater(\n    sd,             // filesystem (must be type SdFat32)\n    MENU_BIN,       // path to binary (default=/menu.bin, empty string=rollback only)\n    5000,           // wait delay, (default=0, will be forced to 2000 upon ESP.restart() )\n    &SdFatSPIConfig // usually default=4 but your mileage may vary\n  );\n}\n\nvoid loop()\n{\n  // do your stuff\n}\n"
  },
  {
    "path": "examples/Test/build_test/dev_lib_deps.ini",
    "content": "[lib_sdupdater]\nlib_deps          =\n  M5Stack-SD-Updater\n  ESP32-targz\n\n[lib_lgfx]\nlib_deps          =\n  SD\n  git+https://github.com/lovyan03/LovyanGFX#develop\n  ${lib_sdupdater.lib_deps}\nbuild_flags =\n  ${env.build_flags}\n  -D LGFX_AUTODETECT\n  -D LGFX_USE_V1\nlib_ldf_mode      = deep\n\n[lib_chimeracore]\nlib_deps          =\n  git+https://github.com/lovyan03/LovyanGFX#develop\n  git+https://github.com/tobozo/ESP32-Chimera-Core#1.5.0\n  ${lib_sdupdater.lib_deps}\n\n[lib_m5unified]\nlib_deps          =\n  SD\n  git+https://github.com/M5Stack/M5GFX#develop\n  git+https://github.com/M5Stack/M5Unified#develop\n  ${lib_sdupdater.lib_deps}\n\n[lib_m5core2]\nlib_deps          =\n  M5Core2\n  ${lib_sdupdater.lib_deps}\n\n[lib_m5stickc]\nlib_deps          =\n  M5StickC\n  ${lib_sdupdater.lib_deps}\n\n[lib_sdfatupdater]\nlib_deps =\n  SdFat\n  M5Unified\n  ${lib_sdupdater.lib_deps}\n"
  },
  {
    "path": "examples/Test/build_test/main/main.cpp",
    "content": "\n\n\n//#include <stddef.h>\n\n#if defined TEST_LGFX\n  #include \"../../../LGFX-SDLoader-Snippet/LGFX-SDLoader-Snippet.ino\"\n#elif defined TEST_M5Core2\n  #include \"../../../M5Core2-SDLoader-Snippet/M5Core2-SDLoader-Snippet.ino\"\n#elif defined TEST_M5Stack || defined TEST_S3Box\n  #include \"../../../M5Stack-SDLoader-Snippet/M5Stack-SDLoader-Snippet.ino\"\n#elif defined TEST_M5StickC\n  #include \"../../../M5StickC-SPIFFS-Loader-Snippet/M5StickC-SPIFFS-Loader-Snippet.ino\"\n#elif defined TEST_M5Unified || defined TestM5CoreS3 || defined ARDUINO_M5STACK_ATOM_AND_TFCARD\n  #include \"../../../M5Unified/M5Unified.ino\"\n#elif defined TEST_SdFat\n    #include \"../../../SdFatUpdater/SdFatUpdater.ino\"\n#else\n  #error \"No device to test\"\n#endif\n\n\n"
  },
  {
    "path": "examples/Test/build_test/platformio.ini",
    "content": "[platformio]\ndefault_envs      = m5stack-core-esp32\nsrc_dir           = main\nextra_configs     = dev_lib_deps.ini\n\n[env]\nframework         = arduino\nboard             = m5stack-core-esp32\nbuild_type        = debug\nlib_ldf_mode      = deep\n\n[platform_default]\nplatform          = espressif32\nplatform_packages = framework-arduinoespressif32\n\n[platform_tasmota]\nplatform          = https://github.com/tasmota/platform-espressif32\n\n\n[lib_sdupdater]\nlib_deps          =\n  M5Stack-SD-Updater\n  ESP32-targz\n\n[lib_lgfx]\nlib_deps          =\n  SPI\n  SD\n  ${lib_sdupdater.lib_deps}\n  LovyanGFX\nbuild_flags =\n  ${env.build_flags}\n  -D LGFX_AUTODETECT\n  -D LGFX_USE_V1\nlib_ldf_mode      = deep\n\n\n[lib_chimeracore]\nlib_deps          =\n  ${lib_sdupdater.lib_deps}\n  ESP32-Chimera-Core\n\n[lib_m5unified]\nlib_deps          =\n  SD\n  ${lib_sdupdater.lib_deps}\n  M5Unified\n\n[lib_m5core2]\nlib_deps          =\n  ${lib_sdupdater.lib_deps}\n  M5Core2\n\n[lib_m5stickc]\nlib_deps          =\n  ${lib_sdupdater.lib_deps}\n  M5StickC\n\n[lib_m5gfx]\nlib_deps =\n  SPI\n  SD\n  ${lib_sdupdater.lib_deps}\n  M5GFX\n  Button2\n\n[lib_sdfatupdater]\nlib_deps =\n  SdFat\n  M5Unified\n  ${lib_sdupdater.lib_deps}\n\n[env:m5stack-core-esp32]\nextends           = lib_chimeracore, platform_default\nboard             = m5stack-core-esp32\nbuild_flags       = -DTEST_M5Stack\n\n[env:m5stack-core2]\nextends           = lib_m5core2, platform_default\nboard             = m5stack-core2\nbuild_flags       = -DTEST_M5Core2\n\n[env:m5stick-c]\nextends           = lib_m5stickc, platform_default\nboard             = m5stick-c\nbuild_flags       = -DTEST_M5StickC\n\n[env:m5unified]\nextends           = lib_m5unified, platform_default\nbuild_flags       = -DTEST_M5Unified\n\n[env:lgfx]\nextends           = lib_lgfx, platform_default\nbuild_flags       = -DTEST_LGFX\n\n[env:s3box]\nextends           = lib_chimeracore, platform_default\nboard             = esp32dev\nboard_build.mcu   = esp32s3\n; prevent \"tools/sdk/esp32s3/bin/bootloader_dio_40m.elf' not found\" error\nboard_build.f_flash = 80000000L\nbuild_flags       = -DTEST_S3Box\n\n[env:m5stack-atom]\nextends = lib_m5gfx, lib_m5unified, platform_default\nboard = m5stack-atom\nboard_build.partitions = min_spiffs.csv\nbuild_flags =\n  ${env.build_flags}\n  -D ARDUINO_M5STACK_ATOM_AND_TFCARD\n  -D _MOSI=19\n  -D _MISO=33\n  -D _CLK=23\n\n[env:sdfat-test]\nextends           = lib_sdfatupdater, platform_default\nboard             = esp32dev\nbuild_flags       = -DTEST_SdFat\nlib_deps =\n  SdFat\n  M5Unified\n  M5Stack-SD-Updater\n  ESP32-targz@^1.1.8\n\n[env:m5stack-cores3]\nextends = lib_m5unified, platform_default\n# platform = espressif32 @ 6.2.0\nboard = esp32-s3-devkitc-1\nboard_upload.flash_size = 16MB\nboard_upload.maximum_size = 16777216\nboard_build.partitions = default_16MB.csv\nboard_build.arduino.memory_type = qio_qspi\nbuild_flags =\n    ${env.build_flags}\n    -DARDUINO_M5STACK_CORES3\n    -DBOARD_HAS_PSRAM\n    -DARDUINO_UDB_MODE=1\n    -DTestM5CoreS3\n"
  },
  {
    "path": "library.json",
    "content": "{\n  \"name\": \"M5Stack-SD-Updater\",\n  \"description\": \"Make your apps loadable from the SD card\",\n  \"keywords\": [\"SD-Updater\", \"M5Stack\", \"M5Core2\", \"Odroid-Go\", \"ttgo-ts\", \"d-duino-32-xs\", \"esp32-wrover-kit\", \"SDUpdater\"],\n  \"authors\": {\n    \"name\": \"tobozo\",\n    \"url\": \"https://github.com/tobozo/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/tobozo/M5Stack-SD-Updater.git\"\n  },\n  \"dependencies\": [\n    {\n      \"name\": \"ESP32-targz\",\n      \"version\": \">=1.1.7\"\n    }\n  ],\n  \"version\": \"1.2.8\",\n  \"framework\": \"arduino\",\n  \"headers\": \"M5StackUpdater.h\",\n  \"platforms\": \"espressif32\"\n}\n"
  },
  {
    "path": "library.properties",
    "content": "name=M5Stack-SD-Updater\nversion=1.2.8\nauthor=tobozo <tobozo@noreply.github.com>\nmaintainer=tobozo <tobozo@noreply.github.com>\nsentence=SD Card Loader for M5 Stack\nparagraph=Package your apps on an SD card and load them from a menu app, button or MQTT message.\ncategory=Uncategorized\nurl=https://github.com/tobozo/M5Stack-SD-Updater/\narchitectures=esp32\ndepends=ESP32-targz,ArduinoJson\n"
  },
  {
    "path": "src/ConfigManager/ConfigManager.cpp",
    "content": "#include \"ConfigManager.hpp\"\n\nnamespace SDUpdaterNS\n{\n\n  bool DigitalPinButton_t::changed()\n  {\n    static int lastbtnstate = digitalRead( pin );\n    if( digitalRead( pin ) != lastbtnstate ) {\n      lastbtnstate = 1-lastbtnstate;\n      log_d(\"btnstate: %d\", lastbtnstate );\n      return true;\n    }\n    return false;\n  }\n\n\n  namespace ConfigManager\n  {\n\n    config_sdu_t::config_sdu_t() { }\n\n    void config_sdu_t::buttonsPoll(){ if( buttonsUpdate ) buttonsUpdate(); }\n\n    void config_sdu_t::setDisplay( void* ptr )                    { display=ptr; };\n    void config_sdu_t::setCSPin( const int param )                { TFCardCsPin = param; }\n    void config_sdu_t::setWaitDelay( unsigned long delay )        { waitdelay = delay; }\n    void config_sdu_t::setFS( fs::FS *param )                     { fs = param; }\n    void config_sdu_t::setButtonsTheme( SDU_UI::Theme_t *_theme ) { theme = _theme; }\n    void config_sdu_t::setProgressCb( onProgressCb cb )           { onProgress = cb; }\n    void config_sdu_t::setMessageCb( onMessageCb cb )             { onMessage = cb; }\n    void config_sdu_t::setErrorCb( onErrorCb cb )                 { onError = cb; }\n    void config_sdu_t::setBeforeCb( onBeforeCb cb )               { onBefore = cb; }\n    void config_sdu_t::setAfterCb( onAfterCb cb )                 { onAfter = cb; }\n    void config_sdu_t::setSplashPageCb( onSplashPageCb cb )       { onSplashPage = cb; }\n    void config_sdu_t::setButtonDrawCb( onButtonDrawCb cb )       { onButtonDraw = cb; }\n    void config_sdu_t::setWaitForActionCb( onWaitForActionCb cb ) { onWaitForAction = cb; }\n\n    void config_sdu_t::setLabelMenu( const char* label )          { labelMenu = label; }\n    void config_sdu_t::setLabelSkip( const char* label )          { labelSkip = label; }\n    void config_sdu_t::setLabelRollback( const char* label )      { labelRollback = label; }\n    void config_sdu_t::setLabelSave( const char* label )          { labelSave = label; }\n    void config_sdu_t::setAppName( const char* name )             { appName = name; }\n    void config_sdu_t::setAuthorName( const char* name )          { authorName = name; }\n    void config_sdu_t::setBinFileName( const char* name )         { if( name && strstr(name, \".gz\")!=NULL ) binFileName = name; }\n    void config_sdu_t::useRolllback( bool use )                   { use_rollback = use; }\n\n    void config_sdu_t::setBtnPoller( BtnPollCb cb )            { log_v(\"Assigning Btn Poller\"); buttonsUpdate = cb; }\n    void config_sdu_t::setBtnA( BtnXPressCb Btn )              { log_v(\"Assigning BtnA\"); setBtns(SDU_BTNA_MENU, Btn ); }\n    void config_sdu_t::setBtnB( BtnXPressCb Btn )              { log_v(\"Assigning BtnB\"); setBtns(SDU_BTNB_SKIP, Btn ); }\n    void config_sdu_t::setBtnC( BtnXPressCb Btn )              { log_v(\"Assigning BtnC\"); setBtns(SDU_BTNC_SAVE, Btn ); }\n    void config_sdu_t::setBtns( SDUBtnActions BtnVal, BtnXPressCb cb )\n    {\n      int _id = -1;\n      switch( BtnVal ) {\n        case SDU_BTNA_MENU: _id = 0; break;\n        case SDU_BTNB_SKIP: _id = 1; break;\n        case SDU_BTNC_SAVE: _id = 2; break;\n        default: log_e(\"Invalid button val: %d\", BtnVal ); return; break;\n      }\n      Buttons[_id].cb  = cb;\n      Buttons[_id].val = BtnVal;\n    }\n    //config_sdu_t SDUCfg;\n    config_sdu_t SDUCfg;\n\n  };\n\n};\n"
  },
  {
    "path": "src/ConfigManager/ConfigManager.hpp",
    "content": "/*\n *\n * M5Stack SD Updater\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2018 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n */\n#pragma once\n\n#include \"./misc/config.h\"\n#include \"./misc/types.h\"\n#include <FS.h>\n\n\nnamespace SDUpdaterNS\n{\n\n  class SDUpdater;\n\n  namespace ConfigManager\n  {\n\n    #if defined ARDUINO_ESP32_S3_BOX\n      static DigitalPinButton_t S3MuteButton(GPIO_NUM_1);\n      bool S3MuteButtonChanged();\n    #endif\n\n    typedef bool (*fsCheckerCb)( SDUpdater* sdu, fs::FS &fs, bool report_errors );\n\n    extern void setup();\n    extern bool hasFS( SDUpdater *sdu, fs::FS &fs, bool report_errors );\n    extern UpdateInterfaceNS::UpdateManagerInterface_t *GetUpdateInterface();\n\n    using namespace UpdateInterfaceNS;\n\n    // SDUpdater config callbacks and params\n    struct config_sdu_t\n    {\n      config_sdu_t();\n      fs::FS *fs = nullptr;\n      //FS_Config_t *fsConfig = nullptr;\n      bool mounted = false;\n      bool fs_begun = false;\n      void *display = nullptr; // dereferenced display object\n      void* getCompilationTimeDisplay();\n      void* getRunTimeDisplay();\n      void useBuiltinPushButton();\n      void useBuiltinTouchButton();\n      void useBuiltinSerial();\n      SDU_UI::Theme_t *theme = nullptr; // buttons theme\n      TriggerSource::triggerMap_t *triggers = nullptr;\n      int TFCardCsPin = -1;\n      unsigned long waitdelay = 5000;\n      //bool load_defaults = true;\n      bool use_rollback = true;\n      bool rollBackToFactory = false;\n      const char* labelMenu     = LAUNCHER_LABEL;\n      const char* labelSkip     = SKIP_LABEL;\n      const char* labelRollback = ROLLBACK_LABEL;\n      const char* labelSave     = SAVE_LABEL;\n      const char* binFileName   = SDU_APP_PATH;\n      const char* appName       = SDU_APP_NAME;\n      const char* authorName    = SDU_APP_AUTHOR;\n\n      BtnXAction Buttons[3]     =\n      {\n        { nullptr, SDU_BTNA_MENU, true },\n        { nullptr, SDU_BTNB_SKIP, true },\n        { nullptr, SDU_BTNC_SAVE, true }\n      };\n\n      BtnPollCb         buttonsUpdate    = nullptr;\n      onProgressCb      onProgress       = nullptr;\n      onMessageCb       onMessage        = nullptr;\n      onErrorCb         onError          = nullptr;\n      onBeforeCb        onBefore         = nullptr;\n      onAfterCb         onAfter          = nullptr;\n      onSplashPageCb    onSplashPage     = nullptr;\n      onButtonDrawCb    onButtonDraw     = nullptr;\n      onWaitForActionCb onWaitForAction  = nullptr;\n      fsCheckerCb       fsChecker        = nullptr;\n\n      void setDefaults();\n      void setDisplay( void* ptr=nullptr );\n\n      void buttonsPoll();\n\n      void setCSPin( const int param );\n      void setWaitDelay( unsigned long waitdelay );\n      void setFS( fs::FS *param );\n      void setButtonsTheme( SDU_UI::Theme_t *_theme );\n      void setProgressCb( onProgressCb cb );\n      void setMessageCb( onMessageCb cb );\n      void setErrorCb( onErrorCb cb );\n      void setBeforeCb( onBeforeCb cb );\n      void setAfterCb( onAfterCb cb );\n      void setSplashPageCb( onSplashPageCb cb );\n      void setButtonDrawCb( onButtonDrawCb cb );\n      void setWaitForActionCb( onWaitForActionCb cb );\n\n      void setLabelMenu( const char* label );\n      void setLabelSkip( const char* label );\n      void setLabelRollback( const char* label );\n      void setLabelSave( const char* label );\n      void setAppName( const char* name );\n      void setAuthorName( const char* name );\n      void setBinFileName( const char* name );\n      void useRolllback( bool use );\n\n      void setBtnPoller( BtnPollCb cb );\n      void setBtnA( BtnXPressCb Btn );\n      void setBtnB( BtnXPressCb Btn );\n      void setBtnC( BtnXPressCb Btn );\n      void setBtns( SDUBtnActions BtnVal, BtnXPressCb cb );\n\n      // some methods were renamed\n      [[deprecated(\"use setBtnPoller()\")]] void setSDUBtnPoller( BtnPollCb cb ) { setBtnPoller(cb); }\n      [[deprecated(\"use setBtnA()\")]]      void setSDUBtnA( BtnXPressCb Btn ) { setBtnA( Btn ); }\n      [[deprecated(\"use setBtnB()\")]]      void setSDUBtnB( BtnXPressCb Btn ) { setBtnB( Btn ); }\n      [[deprecated(\"use setBtnC()\")]]      void setSDUBtnC( BtnXPressCb Btn ) { setBtnC( Btn ); }\n\n    };\n\n    // override this from sketch\n    [[maybe_unused]] static onConfigLoad SDUCfgLoader = nullptr;\n\n    extern config_sdu_t SDUCfg;\n  };\n\n  using ConfigManager::SDUCfg;\n\n}; // end namespace\n\n\n\n"
  },
  {
    "path": "src/FS/ffat.hpp",
    "content": "#pragma once\n\n#if defined SDU_HAS_FFAT\n\n  #include \"../misc/config.h\"\n  #include \"../misc/types.h\"\n  #include <FFat.h>\n\n  namespace SDUpdaterNS\n  {\n\n    namespace ConfigManager\n    {\n      struct FFat_FS_Config_t\n      {\n        bool formatOnFail{false};\n        const char * basePath{\"/ffat\"};\n        uint8_t maxOpenFiles{10};\n        const char * partitionLabel{FFAT_PARTITION_LABEL};\n      };\n\n      static FFat_FS_Config_t *FFat_ConfigPtr = nullptr;\n      static fs::FS *SDU_FFat_Ptr = &FFat;\n    };\n\n    inline ConfigManager::FFat_FS_Config_t* SDU_FFat_CONFIG_GET()\n    {\n      if( ConfigManager::FFat_ConfigPtr ) return ConfigManager::FFat_ConfigPtr;\n      static ConfigManager::FFat_FS_Config_t FFat_Config = ConfigManager::FFat_FS_Config_t();\n      ConfigManager::FFat_ConfigPtr = &FFat_Config;\n      return ConfigManager::FFat_ConfigPtr;\n    }\n\n    #define SDU_CONFIG_FFat *SDU_FFat_CONFIG_GET()\n\n    inline ConfigManager::FS_Config_t* SDU_FFAT_GET()\n    {\n      static ConfigManager::FS_Config_t FFat_FS_Config = {\"ffat\", ConfigManager::SDU_FFat_Ptr, ConfigManager::FFat_ConfigPtr};\n      return &FFat_FS_Config;\n    }\n\n\n    inline bool SDU_FFat_Begin(ConfigManager::FFat_FS_Config_t cfg=ConfigManager::FFat_FS_Config_t())\n    {\n      ConfigManager::FFat_ConfigPtr = &cfg;\n      return FFat.begin( cfg.formatOnFail, cfg.basePath, cfg.maxOpenFiles, cfg.partitionLabel );\n    }\n\n\n  };\n\n#endif\n"
  },
  {
    "path": "src/FS/littlefs.hpp",
    "content": "#pragma once\n\n#if defined SDU_HAS_LITTLEFS\n\n  #include \"../misc/config.h\"\n  #include \"../misc/types.h\"\n  #include <LittleFS.h>\n\n  namespace SDUpdaterNS\n  {\n\n    namespace ConfigManager\n    {\n      struct LittleFS_FS_Config_t\n      {\n        bool formatOnFail{false};\n        const char * basePath{\"/littlefs\"};\n        uint8_t maxOpenFiles{10};\n        const char * partitionLabel{\"spiffs\"};\n      };\n      static LittleFS_FS_Config_t *LittleFS_ConfigPtr = nullptr;\n      static fs::FS *SDU_LittleFS_Ptr = &LittleFS;\n    };\n\n\n    inline ConfigManager::LittleFS_FS_Config_t* SDU_LittleFS_CONFIG_GET()\n    {\n      if( ConfigManager::LittleFS_ConfigPtr ) return ConfigManager::LittleFS_ConfigPtr;\n      static ConfigManager::LittleFS_FS_Config_t LittleFS_Config = ConfigManager::LittleFS_FS_Config_t();\n      ConfigManager::LittleFS_ConfigPtr = &LittleFS_Config;\n      return ConfigManager::LittleFS_ConfigPtr;\n    }\n\n    #define SDU_CONFIG_LittleFS *SDU_LittleFS_CONFIG_GET()\n\n\n    inline ConfigManager::FS_Config_t* SDU_LITTLEFS_GET()\n    {\n      static ConfigManager::FS_Config_t LittleFS_FS_Config = {\"littlefs\", ConfigManager::SDU_LittleFS_Ptr, ConfigManager::LittleFS_ConfigPtr};\n      return &LittleFS_FS_Config;\n    }\n\n\n    inline bool SDU_LittleFS_Begin(ConfigManager::LittleFS_FS_Config_t cfg=ConfigManager::LittleFS_FS_Config_t())\n    {\n      ConfigManager::LittleFS_ConfigPtr = &cfg;\n      return LittleFS.begin( cfg.formatOnFail, cfg.basePath, cfg.maxOpenFiles, cfg.partitionLabel );\n    }\n\n\n  };\n\n#endif\n"
  },
  {
    "path": "src/FS/sd.hpp",
    "content": "#pragma once\n\n#if defined SDU_HAS_SD\n\n  #include \"../misc/config.h\"\n  #include \"../misc/types.h\"\n  #include <SD.h>\n\n  namespace SDUpdaterNS\n  {\n\n    namespace ConfigManager\n    {\n      struct SD_FS_Config_t\n      {\n        uint8_t csPin{TFCARD_CS_PIN};\n        SPIClass *bus{&SPI};\n        uint32_t freq{4000000};\n      };\n      static SD_FS_Config_t *SD_ConfigPtr = nullptr;\n      static fs::FS *SDU_SD_Ptr = &SD;\n    };\n\n\n    inline ConfigManager::SD_FS_Config_t* SDU_SD_CONFIG_GET()\n    {\n      if( ConfigManager::SD_ConfigPtr ) return ConfigManager::SD_ConfigPtr;\n      static ConfigManager::SD_FS_Config_t SD_Config = ConfigManager::SD_FS_Config_t();\n      log_d(\"CREATED DEFAULT SD config csPin:%d, bus:%s, frq:%d\", SD_Config.csPin, SD_Config.bus?\"true\":\"false\", SD_Config.bus?SD_Config.freq:0);\n      ConfigManager::SD_ConfigPtr = &SD_Config;\n      return ConfigManager::SD_ConfigPtr;\n    }\n\n    #define SDU_CONFIG_SD *SDU_SD_CONFIG_GET()\n\n\n    inline ConfigManager::FS_Config_t* SDU_SD_GET()\n    {\n      static ConfigManager::FS_Config_t SD_FS_Config = {\"sd\", ConfigManager::SDU_SD_Ptr, ConfigManager::SD_ConfigPtr};\n      return &SD_FS_Config;\n    }\n\n\n    inline bool SDU_SDBegin( uint8_t csPin )\n    {\n      return SD.begin( csPin );\n    }\n\n    inline bool SDU_SDBegin( ConfigManager::SD_FS_Config_t cfg=ConfigManager::SD_FS_Config_t() )\n    {\n      ConfigManager::SD_ConfigPtr = &cfg;\n      log_d(\"SD will begin CS_PIN=%d, bus:%s, frq=%d\", cfg.csPin, cfg.bus?\"true\":\"false\", cfg.bus?cfg.freq:0);\n      return cfg.freq==0 || cfg.freq > 80000000\n        ? SD.begin()\n        : cfg.bus\n          ? SD.begin(cfg.csPin, *cfg.bus, cfg.freq)\n          : SD.begin(cfg.csPin);\n    }\n\n\n  };\n\n#endif\n\n\n"
  },
  {
    "path": "src/FS/sd_mmc.hpp",
    "content": "#pragma once\n\n#if defined SDU_HAS_SD_MMC\n\n  #include \"../misc/config.h\"\n  #include \"../misc/types.h\"\n  #include <SD_MMC.h>\n\n  namespace SDUpdaterNS\n  {\n\n    namespace ConfigManager\n    {\n      struct SD_MMC_Bus_Config_t\n      {\n        int freq{BOARD_MAX_SDMMC_FREQ};\n        int clk{-1};\n        int cmd{-1};\n        int d0{-1};\n        int d1{-1};\n        int d2{-1};\n        int d3{-1};\n      };\n      struct SD_MMC_FS_Config_t\n      {\n        const char * mountpoint{\"/sdcard\"};\n        bool mode1bit{false};\n        bool format_if_mount_failed{false};\n        SD_MMC_Bus_Config_t busCfg{SD_MMC_Bus_Config_t()};\n        uint8_t maxOpenFiles{5};\n      };\n      static SD_MMC_FS_Config_t *SD_MMC_ConfigPtr = nullptr;\n      static fs::FS *SDU_SD_MMC_Ptr = &SD_MMC;\n    };\n\n\n    inline ConfigManager::SD_MMC_FS_Config_t* SDU_SD_MMC_CONFIG_GET()\n    {\n      if( ConfigManager::SD_MMC_ConfigPtr ) return ConfigManager::SD_MMC_ConfigPtr;\n      static ConfigManager::SD_MMC_FS_Config_t SD_MMC_Config = ConfigManager::SD_MMC_FS_Config_t();\n      ConfigManager::SD_MMC_ConfigPtr = &SD_MMC_Config;\n      return ConfigManager::SD_MMC_ConfigPtr;\n    }\n\n    #define SDU_CONFIG_SD_MMC *SDU_SD_MMC_CONFIG_GET()\n\n\n    inline ConfigManager::FS_Config_t* SDU_SD_MMC_GET()\n    {\n      static ConfigManager::FS_Config_t SD_MMC_FS_Config = {\"sdmmc\", ConfigManager::SDU_SD_MMC_Ptr, ConfigManager::SD_MMC_ConfigPtr};\n      return &SD_MMC_FS_Config;\n    }\n\n\n    inline bool SDU_SD_MMC_Begin( ConfigManager::SD_MMC_FS_Config_t cfg=ConfigManager::SD_MMC_FS_Config_t() )\n    {\n      auto busCfg = cfg.busCfg;\n      if( busCfg.clk!=-1 && busCfg.cmd!=-1 && busCfg.d0!=-1 ) {\n        if( busCfg.d1!=-1 && busCfg.d2!=-1 && busCfg.d3!=-1 ) {\n          SD_MMC.setPins( busCfg.clk, busCfg.cmd, busCfg.d0, busCfg.d1, busCfg.d2, busCfg.d3 );\n        } else {\n          SD_MMC.setPins( busCfg.clk, busCfg.cmd, busCfg.d0 );\n        }\n      }\n      ConfigManager::SD_MMC_ConfigPtr = &cfg;\n      return SD_MMC.begin(cfg.mountpoint, cfg.mode1bit, cfg.format_if_mount_failed, cfg.busCfg.freq, cfg.maxOpenFiles);\n    }\n\n  };\n\n#endif\n"
  },
  {
    "path": "src/FS/sdfat.hpp",
    "content": "#pragma once\n\n// fs::FS layer for SdFat\n// Inspired by @ockernuts https://github.com/ockernuts\n// See https://github.com/greiman/SdFat/issues/148#issuecomment-1464448806\n\n#if defined SDU_HAS_SDFS\n\n  #if !defined SDFAT_FILE_TYPE\n    #define SDFAT_FILE_TYPE 3 // tell SdFat.h to support all filesystem types (fat16/fat32/ExFat)\n  #endif\n  #if SDFAT_FILE_TYPE!=3\n    #error \"SD Updater only supports SdFs\"\n  #endif\n\n  #include \"../misc/config.h\"\n  #include \"../misc/types.h\"\n\n  #undef __has_include // tell SdFat to define 'File' object needed to convert access mode to flag\n  #include <FS.h>\n  #include <FSImpl.h>\n  #include <SdFat.h>\n  #define __has_include(STR)  __has_include__(STR) // kudos to @GOB52 for this trick\n\n  // cfr https://en.cppreference.com/w/c/io/fopen + guesses\n  inline oflag_t _convert_access_mode_to_flag(const char* mode, const bool create = false)\n  {\n    int mode_chars = strlen(mode);\n    if (mode_chars==0) return O_RDONLY;\n    if (mode_chars==1) {\n      if (mode[0]=='r') return O_RDONLY;\n      if (mode[0]=='w') return O_WRONLY | create ? O_CREAT : 0;\n      if (mode[0]=='a') return O_APPEND | create ? O_CREAT : 0;\n    }\n    if (mode_chars==2) {\n      if (mode[1] ==  '+') {\n        if (mode[0] == 'r') return O_RDWR;\n        if (mode[0] == 'w') return O_RDWR | O_CREAT;\n        if (mode[0] == 'a') return O_RDWR | O_APPEND | O_CREAT;\n      }\n    }\n    return O_RDONLY;\n  }\n\n\n  class SdFsFileImpl : public fs::FileImpl\n  {\n    private:\n      mutable FsFile _file;\n    public:\n      SdFsFileImpl(FsFile file) : _file(file) {}\n      virtual ~SdFsFileImpl() { }\n\n      virtual size_t write(const uint8_t *buf, size_t size) { return _file.write(buf, size); }\n      virtual size_t read(uint8_t* buf, size_t size) { return _file.read(buf, size); }\n      virtual void flush() { return _file.flush(); }\n      virtual size_t position() const { return _file.curPosition(); }\n      virtual size_t size() const { return _file.size(); }\n      virtual void close() { _file.close(); }\n      virtual operator bool() { return _file.operator bool(); }\n      virtual boolean isDirectory(void) { return _file.isDirectory(); }\n      virtual fs::FileImplPtr openNextFile(const char* mode) { return  std::make_shared<SdFsFileImpl>(_file.openNextFile(_convert_access_mode_to_flag(mode))); }\n      virtual boolean seekDir(long position) { return _file.seek(position); }\n      virtual bool seek(uint32_t pos, fs::SeekMode mode)\n      {\n        if (mode == fs::SeekMode::SeekSet) {\n          return _file.seek(pos);\n        } else if (mode == fs::SeekMode::SeekCur) {\n          return _file.seek(position()+ pos);\n        } else if (mode == fs::SeekMode::SeekEnd) {\n          return _file.seek(size()-pos);\n        }\n        return false;\n      }\n      virtual const char* name() const\n      {\n        // static, so if one asks the name of another file the same buffer will be used.\n        // so we assume here the name ptr is not kept. (anyhow how would it be dereferenced and then cleaned...)\n        static char _name[256];\n        _file.getName(_name, sizeof(_name));\n        return _name;\n      }\n\n      virtual String getNextFileName(void) { /* not implemented and not needed */ return String(\"Unimplemented\"); }\n      virtual String getNextFileName(bool*) { /* not implemented and not needed */ return String(\"Unimplemented\"); }\n      virtual time_t getLastWrite() { /* not implemented and not needed */  return 0; }\n      virtual const char* path() const { /* not implemented and not needed */ return nullptr; }\n      virtual bool setBufferSize(size_t size) { /* not implemented and not needed */ return false; }\n      virtual void rewindDirectory(void) { /* not implemented and not needed */  }\n  };\n\n\n  class SdFsFSImpl : public fs::FSImpl\n  {\n    SdFs& sd;\n    public:\n      SdFsFSImpl(SdFs& sd) : sd(sd) { }\n      virtual ~SdFsFSImpl() {}\n      virtual fs::FileImplPtr open(const char* path, const char* mode, const bool create)\n      {\n          return std::make_shared<SdFsFileImpl>(sd.open(path, _convert_access_mode_to_flag(mode, create)));\n      }\n      virtual bool exists(const char* path) { return sd.exists(path); }\n      virtual bool rename(const char* pathFrom, const char* pathTo) { return sd.rename(pathFrom, pathTo); }\n      virtual bool remove(const char* path) { return sd.remove(path); }\n      virtual bool mkdir(const char *path) { return sd.mkdir(path); }\n      virtual bool rmdir(const char *path) { return sd.rmdir(path); }\n  };\n\n\n\n  namespace SDUpdaterNS\n  {\n\n    namespace ConfigManager\n    {\n      static void *SDU_SdFatPtr = nullptr;\n      static fs::FS *SDU_SdFatFsPtr = nullptr;\n      static SdSpiConfig *SDU_SdSpiConfigPtr = nullptr;\n    };\n\n    inline fs::FS* getSdFsFs( SdFs &sd )\n    {\n      static fs::FS _fs = fs::FS(fs::FSImplPtr(new SdFsFSImpl(sd)));\n      return &_fs;\n    }\n\n\n    inline ConfigManager::FS_Config_t* SDU_SDFAT_GET()\n    {\n      static ConfigManager::FS_Config_t SDFAT_FS_Config = {\"sdfat\", ConfigManager::SDU_SdFatPtr, ConfigManager::SDU_SdSpiConfigPtr};\n      return &SDFAT_FS_Config;\n    }\n\n    //#define SDU_CONFIG_SDFAT ConfigManager::SDU_SdSpiConfigPtr\n\n\n    inline bool SDU_SDFat_Begin( SdSpiConfig *SdFatCfg )\n    {\n      using namespace ConfigManager;\n      if( !SDU_SdFatPtr ) {\n        log_e(\"SDFat is not set\");\n        return false;\n      }\n\n      bool ret = false;\n      int errcode = 0, errdata = 0;\n      auto _fat = (SdFs*)SDU_SdFatPtr;\n      ret = _fat->begin( *SdFatCfg );\n      errcode = _fat->card()->errorCode();\n      errdata = int(_fat->card()->errorData());\n      (void)errcode;\n      (void)errdata;\n      if (!ret) {\n        log_e( \"SDFat init failed with error code: 0x%x, Error Data:0x%x\", errcode, errdata );\n        return false;\n      }\n      return ret;\n    }\n\n\n  };\n\n\n#endif\n\n"
  },
  {
    "path": "src/FS/spiffs.hpp",
    "content": "#pragma once\n\n#if defined SDU_HAS_SPIFFS\n\n  #include \"../misc/config.h\"\n  #include \"../misc/types.h\"\n  #include <SPIFFS.h>\n\n  namespace SDUpdaterNS\n  {\n\n    namespace ConfigManager\n    {\n      struct SPIFFS_FS_Config_t\n      {\n        bool formatOnFail{false};\n        const char * basePath{\"/spiffs\"};\n        uint8_t maxOpenFiles{10};\n        const char * partitionLabel{NULL};\n      };\n\n      static SPIFFS_FS_Config_t *SPIFFS_ConfigPtr = nullptr;\n      static fs::FS *SDU_SPIFFS_Ptr = &SPIFFS;\n\n    };\n\n    inline ConfigManager::SPIFFS_FS_Config_t* SDU_SPIFFS_CONFIG_GET()\n    {\n      if( ConfigManager::SPIFFS_ConfigPtr ) return ConfigManager::SPIFFS_ConfigPtr;\n      static ConfigManager::SPIFFS_FS_Config_t SPIFFS_Config = ConfigManager::SPIFFS_FS_Config_t();\n      ConfigManager::SPIFFS_ConfigPtr = &SPIFFS_Config;\n      return ConfigManager::SPIFFS_ConfigPtr;\n    }\n\n    #define SDU_CONFIG_SPIFFS *SDU_SPIFFS_CONFIG_GET()\n\n\n    inline ConfigManager::FS_Config_t* SDU_SPIFFS_GET()\n    {\n      static ConfigManager::FS_Config_t SPIFFS_FS_Config = {\"spiffs\", ConfigManager::SDU_SPIFFS_Ptr, ConfigManager::SPIFFS_ConfigPtr};\n      return &SPIFFS_FS_Config;\n    }\n\n\n    inline bool SDU_SPIFFS_Begin(ConfigManager::SPIFFS_FS_Config_t cfg=ConfigManager::SPIFFS_FS_Config_t())\n    {\n      ConfigManager::SPIFFS_ConfigPtr = &cfg;\n      return SPIFFS.begin( cfg.formatOnFail, cfg.basePath, cfg.maxOpenFiles, cfg.partitionLabel );\n    }\n\n  };\n\n#endif\n\n\n\n"
  },
  {
    "path": "src/M5StackUpdater.h",
    "content": "/*\\\n *\n * M5Stack SD Updater\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2018 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n\\*/\n#pragma once\n\n#include <Update.h>\n\n#ifdef __cplusplus\n\n  #include \"M5StackUpdater.hpp\"\n\n#else\n\n  #error M5Stack-SD-Updater requires a C++ compiler, please change file extension to .cc or .cpp\n\n#endif\n"
  },
  {
    "path": "src/M5StackUpdater.hpp",
    "content": "#pragma once\n#define __M5STACKUPDATER_H\n/*\n *\n * M5Stack SD Updater\n * Project Page: https://github.com/tobozo/M5Stack-SD-Updater\n *\n * Copyright 2018 tobozo http://github.com/tobozo\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (\"M5Stack SD Updater\"), to deal in the Software without\n * restriction, including without limitation the rights to use,\n * copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following\n * conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n *\n *\n * (Note to self: remember it is shared by both contexts before\n * messing with it!)\n *\n * This code is used by the menu but must also be included in\n * any app that will be compiled and copied the sd card.\n *\n *\n * In your sketch, find the line where the core library is included:\n *\n *  // #include <M5Stack.h>\n *  // #include <M5Core2.h>\n *  // #include <ESP32-Chimera-Core.h>\n *  // #include <M5StickC.h>\n *  // #include <M5Unified.h>\n *  // #include <LovyanGFX.h>\n *\n * And add this:\n *\n *  #include <M5StackUpdater.h>\n *\n *\n * In your setup() function, find the following statements:\n *\n *   M5.begin();\n *\n * And add this:\n *\n *   checkSDUpdater( SD );\n *\n * Then do whatever you need to do (button init, timers)\n * in the setup and the loop. Your app will be ready\n * to run normally except at boot if the Button A is\n * pressed, it will load the \"menu.bin\" from the sd card.\n *\n * Touch UI has no buttons, this raises the problem of\n * detecting a 'pushed' state when the touch is off.\n * As a compensation, an UI will be visible for 2 seconds\n * after every ESP.restart(), and this visibility can\n * be forced in the setup :\n *\n *   checkSDUpdater( SD, MENU_BIN, 2000 );\n *\n * Headless setups can overload SDUCfg.onWaitForAction\n * See SDUCfg.setWaitForActionCb() in M5StackUpdaterConfig.h\n * to assign a your own button/sensor/whatever detection routine\n * or even issue the \"update\" command via serial\n *\n *   if(digitalRead(BUTTON_A_PIN) == 0) {\n *     Serial.println(\"Will Load menu binary\");\n *     updateFromFS(SD);\n *     ESP.restart();\n *   }\n *\n *\n */\n\n#include \"gitTagVersion.h\"\n#include \"./misc/assets.h\"\n#include \"./misc/config.h\"\n#include \"./misc/types.h\"\n#include <FS.h>\n#include <Update.h>\n\n\n#define resetReason (int)rtc_get_reset_reason(0)\n\n// use `#define SDU_NO_PRAGMAS` to disable duplicate pragma messages\n#if !defined SDU_NO_PRAGMAS && CORE_DEBUG_LEVEL>=ARDUHAL_LOG_LEVEL_ERROR\n  #define SDU_STRINGIFY(a) #a\n  #define SDU_PRAGMA_MESSAGE(msg) \\\n    _Pragma( SDU_STRINGIFY( message msg ) )\n#else\n  #define SDU_PRAGMA_MESSAGE(msg)\n#endif\n\n\n// inherit filesystem includes from sketch\n\n#if defined _SD_H_ || defined SDU_USE_SD\n  #define SDU_HAS_SD\n  #include \"./FS/sd.hpp\"\n  #if !defined SDU_BEGIN_SD\n    #define SDU_BEGIN_SD SDU_SDBegin\n  #endif\n#endif\n\n#if defined _SDMMC_H_ || defined SDU_USE_SD_MMC\n  #define SDU_HAS_SD_MMC\n  #include \"./FS/sd_mmc.hpp\"\n  #if !defined SDU_BEGIN_SD_MMC\n    #define SDU_BEGIN_SD_MMC SDU_SD_MMC_Begin\n  #endif\n#endif\n\n#if defined _SPIFFS_H_ || defined SDU_USE_SPIFFS\n  #define SDU_HAS_SPIFFS\n  #include \"./FS/spiffs.hpp\"\n  #if !defined SDU_BEGIN_SPIFFS\n    #define SDU_BEGIN_SPIFFS SDU_SPIFFS_Begin\n  #endif\n#endif\n\n#if defined _FFAT_H_ || defined SDU_USE_FFAT\n  #define SDU_HAS_FFAT\n  #include \"./FS/ffat.hpp\"\n  #if !defined SDU_BEGIN_FFat\n    #define SDU_BEGIN_FFat SDU_FFat_Begin\n  #endif\n#endif\n\n#if defined _LiffleFS_H_ || defined SDU_USE_LITTLEFS\n  #define SDU_HAS_LITTLEFS\n  #include \"./FS/littlefs.hpp\"\n  #if !defined SDU_BEGIN_LittleFS\n    #define SDU_BEGIN_LittleFS SDU_LittleFS_Begin\n  #endif\n#endif\n\n#if defined _LIFFLEFS_H_ || __has_include(<LITTLEFS.h>)\n  // LittleFS is now part of esp32 package, the older, external version isn't supported\n  #warning \"Older version of <LITTLEFS.h> is unsupported and will be ignored\"\n  #warning \"Use builtin version with #include <LittleFS.h> instead, if using platformio add LittleFS(esp32)@^2.0.0 to lib_deps\"\n#endif\n\n// Note: SdFat can't be detected using __has_include(<SdFat.h>) without creating problems downstream in the code.\n//       Until this is solved, enabling SdFat is done by adding `#defined SDU_USE_SDFATFS` to the sketch before including the library.\n#if defined SDU_USE_SDFATFS || defined USE_SDFATFS\n  #define SDU_HAS_SDFS\n  SDU_PRAGMA_MESSAGE(\"SDUpdater will use SdFat\")\n  #include \"./FS/sdfat.hpp\"\n  #if !defined SDU_BEGIN_SDFat\n    #define SDU_BEGIN_SDFat SDU_SDFat_Begin\n  #endif\n#endif\n\n\n#if !defined SDU_HAS_SD && !defined SDU_HAS_SD_MMC && !defined SDU_HAS_SPIFFS && !defined SDU_HAS_LITTLEFS && !defined SDU_HAS_SDFS && !defined SDU_HAS_FFAT\n  SDU_PRAGMA_MESSAGE(\"SDUpdater didn't detect any  preselected filesystem, will use SD as default\")\n  #define SDU_HAS_SD\n  #include \"./FS/sd.hpp\"\n  #if !defined SDU_BEGIN_SD\n    #define SDU_BEGIN_SD SDU_SDBegin\n  #endif\n#endif\n\n#if defined SDU_ENABLE_GZ || defined _ESP_TGZ_H || __has_include(<ESP32-targz.h>)\n  #define SDU_HAS_TARGZ\n  SDU_PRAGMA_MESSAGE(\"gzip and tar support detected!\")\n  #include <ESP32-targz.h>\n#endif\n\n\n#if !defined SDU_NO_AUTODETECT // lobby/buttons\n\n  #if !defined SDU_HEADLESS\n    #define SDU_USE_DISPLAY // any detected display is default enabled unless SDU_HEADLESS mode is selected\n  #endif\n\n  #define HAS_M5_API // This is M5Stack Updater, assume it has the M5.xxx() API, will be undef'd otherwise\n\n  #if defined _CHIMERA_CORE_\n    SDU_PRAGMA_MESSAGE(\"Chimera Core detected\")\n    #define SDU_Sprite LGFX_Sprite\n    #define SDU_DISPLAY_TYPE M5Display*\n    #define SDU_DISPLAY_OBJ_PTR &M5.Lcd\n    #define SDU_TouchButton LGFX_Button\n    #define HAS_LGFX\n    // ESP32-Chimera-Core creates the HAS_TOUCH macro when the selected display supports it\n    #if !defined SDU_HAS_TOUCH && defined HAS_TOUCH\n      #define SDU_HAS_TOUCH\n    #endif\n  #elif defined _M5STACK_H_\n    SDU_PRAGMA_MESSAGE(\"M5Stack.h detected\")\n    #define SDU_Sprite TFT_eSprite\n    #define SDU_DISPLAY_TYPE M5Display*\n    #define SDU_DISPLAY_OBJ_PTR &M5.Lcd\n  #elif defined _M5Core2_H_\n    SDU_PRAGMA_MESSAGE(\"M5Core2.h detected\")\n    #define SDU_Sprite TFT_eSprite\n    #define SDU_DISPLAY_TYPE M5Display*\n    #define SDU_DISPLAY_OBJ_PTR &M5.Lcd\n    #define SDU_TouchButton TFT_eSPI_Button\n    #define SDU_HAS_TOUCH // M5Core2 has implicitely enabled touch interface\n  #elif defined _M5CORES3_H_\n    SDU_PRAGMA_MESSAGE(\"M5Core3.h detected\")\n    #define SDU_Sprite TFT_eSprite\n    #define SDU_DISPLAY_TYPE M5Display*\n    #define SDU_DISPLAY_OBJ_PTR &M5.Lcd\n    #define SDU_TouchButton TFT_eSPI_Button\n    #define SDU_HAS_TOUCH // M5Core3 has implicitely enabled touch interface\n  #elif defined _M5STICKC_H_\n    SDU_PRAGMA_MESSAGE(\"M5StickC.h detected\")\n    #define SDU_Sprite TFT_eSprite\n    #define SDU_DISPLAY_TYPE M5Display*\n    #define SDU_DISPLAY_OBJ_PTR &M5.Lcd\n  #elif defined __M5UNIFIED_HPP__\n    SDU_PRAGMA_MESSAGE(\"M5Unified.h detected\")\n    #define SDU_Sprite LGFX_Sprite\n    #define SDU_DISPLAY_TYPE M5GFX*\n    #define SDU_DISPLAY_OBJ_PTR &M5.Display\n    #define SDU_TouchButton LGFX_Button\n    #define HAS_LGFX\n    #if !defined SDU_HAS_TOUCH && (defined ARDUINO_M5STACK_Core2 || defined ARDUINO_M5STACK_CORE2 || defined ARDUINO_M5STACK_CORES3 )\n      #define SDU_HAS_TOUCH\n    #endif\n  #else\n    #if defined SDU_USE_DISPLAY\n      SDU_PRAGMA_MESSAGE(message \"No display driver detected\")\n      #undef SDU_USE_DISPLAY\n    #endif\n    #define SDU_DISPLAY_OBJ_PTR nullptr\n    //#define SDU_DISPLAY_TYPE void*\n    //#define SDU_Sprite void*\n    #undef HAS_M5_API\n  #endif\n#endif\n\n\n#if defined HAS_M5_API // SDUpdater can use M5.update(), and M5.Btnx API\n  #define DEFAULT_BTN_POLLER M5.update()\n  #define DEFAULT_BTNA_CHECKER M5.BtnA.isPressed()\n  #define DEFAULT_BTNB_CHECKER M5.BtnB.isPressed()\n  #if defined _M5STICKC_H_ // M5StickC has no BtnC\n    #define DEFAULT_BTNC_CHECKER false\n  #else\n    #define DEFAULT_BTNC_CHECKER M5.BtnC.isPressed()\n  #endif\n#else\n  // SDUpdater will use Serial as trigger source\n  #if !defined SDU_NO_AUTODETECT && !defined SDU_HEADLESS\n    SDU_PRAGMA_MESSAGE(message \"No M5 API found\")\n  #endif\n  #define DEFAULT_BTN_POLLER nullptr\n  #define DEFAULT_BTNA_CHECKER false\n  #define DEFAULT_BTNB_CHECKER false\n  #define DEFAULT_BTNC_CHECKER false\n#endif\n\n// dispatch predefined triggers\n#if !defined SDU_TRIGGER_SOURCE_DEFAULT\n  #if defined SDU_HAS_TOUCH\n    SDU_PRAGMA_MESSAGE(\"Trigger source: Touch Button\")\n    #define SDU_TRIGGER_SOURCE_DEFAULT TriggerSource::SDU_TRIGGER_TOUCHBUTTON\n  #elif defined HAS_M5_API\n    SDU_PRAGMA_MESSAGE(\"Trigger source: Push Button\")\n    #define SDU_TRIGGER_SOURCE_DEFAULT TriggerSource::SDU_TRIGGER_PUSHBUTTON\n  #else\n    SDU_PRAGMA_MESSAGE(\"Trigger source: Serial\")\n    #define SDU_TRIGGER_SOURCE_DEFAULT TriggerSource::SDU_TRIGGER_SERIAL\n  #endif\n#endif\n\n// now that all the contextual flags are created, load the SDUpdater stack\n#include \"./ConfigManager/ConfigManager.hpp\"\n//#include \"./NVS/NVSUtils.hpp\"\n#include \"./SDUpdater/Update_Interface.hpp\"\n#include \"./SDUpdater/SDUpdater_Class.hpp\"\n#include \"./UI/common.hpp\"\n\n#if defined SDU_USE_DISPLAY // load the lobby and button decorations if applicable\n  SDU_PRAGMA_MESSAGE(\"Attached UI\")\n  #define SDU_GFX ((SDU_DISPLAY_TYPE)(SDUCfg.display)) // type-casted display macro for UI.hpp\n  #include \"./UI/UI.hpp\"\n  #if defined SDU_HAS_TOUCH // load touch helpers if applicable\n    SDU_PRAGMA_MESSAGE(\"Attached Touch support\")\n    #include \"./UI/Touch.hpp\"\n  #endif\n#else // bind null display to SDUCfg.display\n  #define SDU_GFX ((void*)(SDUCfg.display)) // macro for UI.hpp\n#endif\n\n\nnamespace SDUpdaterNS\n{\n  namespace ConfigManager\n  {\n\n    inline void* config_sdu_t::getCompilationTimeDisplay()\n    {\n      #if defined SDU_DISPLAY_OBJ_PTR\n        return (void*)SDU_DISPLAY_OBJ_PTR;\n      #else\n        return nullptr;\n      #endif\n    }\n\n\n    inline void* config_sdu_t::getRunTimeDisplay()\n    {\n      return (void*)SDU_GFX;\n    }\n\n\n    inline void config_sdu_t::useBuiltinTouchButton()\n    {\n      using namespace TriggerSource;\n      using namespace SDU_UI;\n      SDUCfg.triggers = new triggerMap_t( SDU_TRIGGER_TOUCHBUTTON, labelMenu, labelSkip, labelRollback, triggerInitTouch, triggerActionTouch, triggerFinalizeTouch );\n    }\n\n    inline void config_sdu_t::useBuiltinPushButton()\n    {\n      using namespace TriggerSource;\n      using namespace SDU_UI;\n      SDUCfg.triggers = new triggerMap_t( SDU_TRIGGER_PUSHBUTTON, labelMenu, labelSkip, labelRollback, triggerInitButton, triggerActionButton, triggerFinalizeButton );\n    }\n\n    inline void config_sdu_t::useBuiltinSerial()\n    {\n      using namespace TriggerSource;\n      using namespace SDU_UI;\n      SDUCfg.triggers = new TriggerSource::triggerMap_t( SDU_TRIGGER_SERIAL, labelMenu, labelSkip, labelRollback, triggerInitSerial, triggerActionSerial, triggerFinalizeSerial );\n    }\n\n\n\n    inline void config_sdu_t::setDefaults()\n    {\n      using namespace TriggerSource;\n      using namespace SDU_UI;\n      fsChecker=hasFS;\n\n      TriggerSources_t triggerSource = SDU_TRIGGER_SOURCE_DEFAULT; // default\n\n      if(!buttonsUpdate) setBtnPoller( FN_LAMBDA_VOID(DEFAULT_BTN_POLLER) );\n      if( !Buttons[0].cb ) setBtnA( FN_LAMBDA_BOOL(DEFAULT_BTNA_CHECKER) );\n      if( !Buttons[1].cb ) setBtnB( FN_LAMBDA_BOOL(DEFAULT_BTNB_CHECKER) );\n      if( !Buttons[2].cb ) setBtnC( FN_LAMBDA_BOOL(DEFAULT_BTNC_CHECKER) );\n\n      if( !labelMenu      && LAUNCHER_LABEL ) labelMenu     = LAUNCHER_LABEL;\n      if( !labelSkip      && SKIP_LABEL     ) labelSkip     = SKIP_LABEL;\n      if( !labelRollback  && ROLLBACK_LABEL ) labelRollback = ROLLBACK_LABEL;\n      if( !labelSave      && SAVE_LABEL     ) labelSave     = SAVE_LABEL;\n      if( !binFileName    && SDU_APP_PATH   ) binFileName   = SDU_APP_PATH;\n      if( !appName        && SDU_APP_NAME   ) appName       = SDU_APP_NAME;\n      if( !authorName     && SDU_APP_AUTHOR ) authorName    = SDU_APP_AUTHOR;\n\n      // detect display\n      if( display ) {\n        log_d(\"Found display driver set by user\");\n      } else if( getCompilationTimeDisplay() ) {\n        log_d(\"Found display driver set by macro\");\n        setDisplay( getCompilationTimeDisplay() );\n      } else if( getRunTimeDisplay() ) {\n        log_d(\"Found display driver set by config\");\n        setDisplay( getRunTimeDisplay() );\n      } else {\n        log_w(\"No display driver found :-(\" );\n      }\n\n      // attach default callbacks\n      if( display ) {\n\n        #if defined SDU_USE_DISPLAY\n          if( !onProgress   )  { setProgressCb(   SDMenuProgressUI );  log_v(\"Attached onProgress\");   }\n          if( !onMessage    )  { setMessageCb(    DisplayUpdateUI );   log_v(\"Attached onMessage\");    }\n          if( !onError      )  { setErrorCb(      DisplayErrorUI );    log_v(\"Attached onError\");      }\n          if( !onBefore     )  { setBeforeCb(     freezeTextStyle );   log_v(\"Attached onBefore\");     }\n          if( !onAfter      )  { setAfterCb(      thawTextStyle );     log_v(\"Attached onAfter\");      }\n          if( !onSplashPage )  { setSplashPageCb( drawSDUSplashPage ); log_v(\"Attached onSplashPage\"); }\n          if( !onButtonDraw )  { setButtonDrawCb( drawSDUPushButton ); log_v(\"Attached onButtonDraw\"); }\n        #endif\n\n        #if defined ARDUINO_ESP32_S3_BOX\n          //setSDUBtnA( ConfigManager::MuteChanged );   log_v(\"Attached Mute Read\");\n          //setSDUBtnA( ConfigManager::S3MuteButtonChanged );    log_v(\"Attached Mute Read\");\n          // setBtnB( nullptr );       log_d(\"Detached BtnB\");\n          // setBtnC( nullptr );       log_d(\"Detached BtnC\");\n          // setLabelSkip( nullptr );     log_d(\"Disabled Skip\");\n          // setLabelRollback( nullptr ); log_d(\"Disabled Rollback\");\n          // setLabelSave( nullptr );     log_d(\"Disabled Save\");\n        #endif\n        #if defined _M5STICKC_H_\n          setBtnC( nullptr );       log_d(\"Detached BtnC\");\n        #endif\n\n      } else {\n\n        if( !onProgress ) { setProgressCb( SDMenuProgressHeadless ); log_v(\"Attached onProgress\"); }\n        if( !onMessage  ) { setMessageCb( DisplayUpdateHeadless );   log_v(\"Attached onMessage\"); }\n        triggerSource = SDU_TRIGGER_SERIAL; // no display detected, fallback to serial\n\n      }\n\n      if( !onWaitForAction) { setWaitForActionCb( actionTriggered ); log_v(\"Attached onWaitForAction(any)\"); }\n\n      if( !triggers ) {\n        switch( triggerSource ) {\n          case SDU_TRIGGER_PUSHBUTTON:  useBuiltinPushButton();  log_d(\"Attaching trigger source: Push Button\");break;\n          case SDU_TRIGGER_TOUCHBUTTON: useBuiltinTouchButton(); log_d(\"Attaching trigger source: Touch Button\");break;\n          default:\n          case SDU_TRIGGER_SERIAL:      useBuiltinSerial();      log_d(\"Attaching trigger source: Serial\"); break;\n        }\n      }\n    }\n\n\n\n    // logic block generator for hasFS() filesystem detection/init\n    #define SDU_MOUNT_ANY_FS_IF( cond, begin_cb, name )      \\\n      if( cond ) {                                          \\\n        if( !begin_cb ){                                    \\\n          msg[0] = name \" MOUNT FAILED\";                    \\\n          if( report_errors ) sdu->_error( msg, 2 );        \\\n          return false;                                     \\\n        } else { log_d(\"%s Successfully mounted\", name); }  \\\n        mounted = true;                                     \\\n      }                                                     \\\n\n    // function call generator to SDU_MOUNT_ANY_FS_IF( condition, begin-callback, filesystem-name )\n    #define SDU_MOUNT_FS_IF( fsobj ) SDU_MOUNT_ANY_FS_IF( &fs == &fsobj, SDU_BEGIN_##fsobj (SDU_CONFIG_##fsobj ), #fsobj );\n\n    inline bool hasFS( SDUpdater* sdu, fs::FS &fs, bool report_errors=true )\n    {\n      assert(sdu);\n      bool mounted = sdu->cfg->mounted; // inherit config mount state as default (can be triggered by rollback)\n      const char* msg[] = {nullptr, \"ABORTING\"};\n      #if defined SDU_HAS_SPIFFS // _SPIFFS_H_\n        SDU_MOUNT_FS_IF( SPIFFS );\n      #endif\n      #if defined SDU_HAS_LITTLEFS // _LITTLEFS_H_\n        SDU_MOUNT_FS_IF( LittleFS );\n      #endif\n      #if defined SDU_HAS_FFAT //_FFAT_H_\n        SDU_MOUNT_FS_IF( FFat );\n      #endif\n      #if defined SDU_HAS_SD // _SD_H_\n        SDU_SD_CONFIG_GET()->csPin = sdu->cfg->TFCardCsPin;\n        SDU_MOUNT_FS_IF( SD );\n      #endif\n      #if defined SDU_HAS_SD_MMC // _SDMMC_H_\n        //SDU_SD_MMC_CONFIG_GET()->busCfg.freq = 40000000;\n        SDU_MOUNT_FS_IF( SD_MMC );\n      #endif\n      #if defined SDU_HAS_SDFS\n        SDU_MOUNT_ANY_FS_IF( &fs==ConfigManager::SDU_SdFatFsPtr, SDU_BEGIN_SDFat(ConfigManager::SDU_SdSpiConfigPtr), \"SDFat\" );\n      #endif\n      return mounted;\n    }\n\n  };\n\n\n  inline void updateFromFS( const String& fileName )\n  {\n    if( !SDUCfg.fs ) {\n      log_e(\"NO FILESYSTEM\");\n      return;\n    }\n    SDUpdater sdUpdater( &SDUCfg );\n    sdUpdater.updateFromFS( fileName );\n    ESP.restart();\n  }\n\n\n  // provide an imperative function to avoid breaking button-based (older) versions of the M5Stack SD Updater\n  inline void updateFromFS( fs::FS &fs, const String& fileName, const int TfCardCsPin )\n  {\n    SDUCfg.setFS( &fs );\n    SDUCfg.setCSPin( TfCardCsPin );\n    SDUpdater sdUpdater( &SDUCfg );\n    sdUpdater.updateFromFS( fs, fileName );\n    ESP.restart();\n  }\n\n\n  // copy compiled sketch from flash partition to filesystem binary file\n  inline bool saveSketchToFS(fs::FS &fs, const char* binfilename, const int TfCardCsPin )\n  {\n    SDUCfg.setFS( &fs );\n    SDUCfg.setCSPin( TfCardCsPin );\n    SDUpdater sdUpdater( &SDUCfg );\n    return sdUpdater.saveSketchToFS( fs, binfilename );\n  }\n\n\n  // provide a rollback function for custom usages\n  inline void updateRollBack( String message )\n  {\n    bool wasmounted = SDUCfg.mounted;\n    SDUCfg.mounted = true;\n    SDUpdater sdUpdater( &SDUCfg );\n    sdUpdater.doRollBack( message );\n    SDUCfg.mounted = wasmounted;\n  }\n\n\n  // provide a conditional function to cover more devices, including headless and touch\n  inline void checkSDUpdater( fs::FS *fsPtr, String fileName, unsigned long waitdelay, const int TfCardCsPin_ )\n  {\n    if( waitdelay == 0 ) {\n      // check for reset reset reason\n      switch( resetReason ) {\n        //case 1 : log_d(\"POWERON_RESET\");break;                  // 1, Vbat power on reset\n        //case 3 : log_d(\"SW_RESET\");break;                       // 3, Software reset digital core\n        //case 4 : log_d(\"OWDT_RESET\");break;                     // 4, Legacy watch dog reset digital core\n        //case 5 : log_d(\"DEEPSLEEP_RESET\");break;                // 5, Deep Sleep reset digital core\n        //case 6 : log_d(\"SDIO_RESET\");break;                     // 6, Reset by SLC module, reset digital core\n        //case 7 : log_d(\"TG0WDT_SYS_RESET\");break;               // 7, Timer Group0 Watch dog reset digital core\n        //case 8 : log_d(\"TG1WDT_SYS_RESET\");break;               // 8, Timer Group1 Watch dog reset digital core\n        //case 9 : log_d(\"RTCWDT_SYS_RESET\");break;               // 9, RTC Watch dog Reset digital core\n        //case 10 : log_d(\"INTRUSION_RESET\");break;               // 10, Instrusion tested to reset CPU\n        //case 11 : log_d(\"TGWDT_CPU_RESET\");break;               // 11, Time Group reset CPU\n        case 12 : log_d(\"SW_CPU_RESET\"); waitdelay=2000; break;   // 12, Software reset CPU\n        //case 13 : log_d(\"RTCWDT_CPU_RESET\");break;              // 13, RTC Watch dog Reset CPU\n        //case 14 : log_d(\"EXT_CPU_RESET\");break;                 // 14, for APP CPU, reseted by PRO CPU\n        //case 15 : log_d(\"RTCWDT_BROWN_OUT_RESET\");break;        // 15, Reset when the vdd voltage is not stable\n        case 16 : log_d(\"RTCWDT_RTC_RESET\"); waitdelay=500; break;// 16, RTC Watch dog reset digital core and rtc module\n        // case 21:  log_d(\"USB_UART_CHIP_RESET\"); waitdelay=2000; break;// Various reset reasons for ESP32-S3\n        // case 22:  log_d(\"USB_JTAG_CHIP_RESET\"); waitdelay=2000; break;// Various reset reasons for ESP32-S3\n        // case 24:  log_d(\"JTAG_RESET\"); waitdelay=2000; break;         // Various reset reasons for ESP32-S3\n\n        default : log_d(\"NO_MEAN\"); waitdelay=100;\n      }\n    }\n\n    log_n(\"Booting with reset reason: %d\", resetReason );\n\n    SDUCfg.setCSPin( TfCardCsPin_ );\n    SDUCfg.setFS( fsPtr );\n    SDUCfg.setWaitDelay( waitdelay );\n\n    // if( !fsPtr ) SDUCfg.Buttons[2].enabled = false; // disable \"Save SD/Rollback\" button\n\n    SDUpdater sdUpdater( &SDUCfg );\n\n    if( SDUCfg.display != nullptr ) {\n      sdUpdater.checkUpdaterUI( fileName );\n    } else {\n      if( SDUCfg.waitdelay <=100 ) SDUCfg.waitdelay = 2000;\n      sdUpdater.checkUpdaterHeadless( fileName );\n    }\n  }\n\n\n\n  inline void checkFWUpdater( unsigned long waitdelay=5000 )\n  {\n    return checkSDUpdater( SDUCfg.fs, \"\", waitdelay, SDUCfg.TFCardCsPin );\n  }\n\n\n\n  // provide a conditional function to cover more devices, including headless and touch\n  inline void checkSDUpdater( fs::FS &fs, String fileName, unsigned long waitdelay, const int TfCardCsPin_ )\n  {\n    return checkSDUpdater( &fs, fileName, waitdelay, TfCardCsPin_ );\n  }\n\n\n  // inline void checkSDUpdater( ConfigManager::FS_Config_t &cfg, String fileName, unsigned long waitdelay )\n  // {\n  //   SDUCfg.fsConfig = &cfg;\n  //   SDUpdater sdUpdater( &SDUCfg );\n  //\n  //   if( SDUCfg.display != nullptr ) {\n  //     sdUpdater.checkSDUpdaterUI( fileName, waitdelay );\n  //   } else {\n  //     if( waitdelay <=100 ) waitdelay = 2000;\n  //     sdUpdater.checkSDUpdaterHeadless( fileName, waitdelay );\n  //   }\n  // }\n\n\n\n  #if defined SDU_HAS_SDFS\n\n    inline void checkSDUpdater( SdFs &sd, String fileName=MENU_BIN, unsigned long waitdelay=0, SdSpiConfig *SdFatCfg=nullptr )\n    {\n      if( !SdFatCfg ) {\n        // load default config\n        auto cfg = SdSpiConfig( SDUCfg.TFCardCsPin, SHARED_SPI, SD_SCK_MHZ(25) );\n        SdFatCfg = (SdSpiConfig*)malloc( sizeof(SdSpiConfig) + 1 );\n        void *DstPtr = SdFatCfg;\n        void *SrcPtr = &cfg;\n        memcpy( DstPtr, SrcPtr, sizeof(SdSpiConfig) );\n      }\n      ConfigManager::SDU_SdSpiConfigPtr = SdFatCfg;\n      ConfigManager::SDU_SdFatPtr = &sd;\n      ConfigManager::SDU_SdFatFsPtr = getSdFsFs(sd);\n      checkSDUpdater( ConfigManager::SDU_SdFatFsPtr, fileName, waitdelay, SdFatCfg->csPin );\n    }\n\n  #endif\n\n\n\n};\n\n\nusing namespace SDUpdaterNS;\n"
  },
  {
    "path": "src/PartitionManager/NVS/NVSUtils.cpp",
    "content": "/*\\\n *\n * NVS Dumper\n *\n * Copyleft tobozo 2020\n *\n * Inspired from the following implementations:\n *\n *   - https://github.com/Edzelf/ESP32-Show_nvs_keys/\n *   - https://gist.github.com/themadsens/026d38f432727567c3c456fb4396621b\n *\n\\*/\n\n#include \"NVSUtils.hpp\"\n\n\nnamespace SDUpdaterNS\n{\n\n  namespace NVS\n  {\n\n    nvs_handle_t handle;\n    std::vector<PartitionDesc_t> Partitions; // filled by NVS\n\n\n    PartitionDesc_t* findPartition( uint8_t ota_num )\n    {\n      log_v(\"Find by ota number %d\", ota_num);\n      if( Partitions.size()==0 ) {\n        if( !getPartitions() ) return nullptr;\n      }\n      for( int i=0; i<Partitions.size(); i++ ) {\n        if( Partitions[i].ota_num == ota_num ) {\n          log_v(\"OTA %d found\", ota_num );\n          return &Partitions[i];\n        }\n      }\n      log_w(\"OTA %d not found\", ota_num );\n      return nullptr;\n    }\n\n\n    PartitionDesc_t* findPartition( const char* name )\n    {\n      assert(name);\n      if( Partitions.size()==0 ) {\n        if( !getPartitions() ) return nullptr;\n      }\n      log_v(\"Find by name %s\", name);\n      for( int i=0; i<Partitions.size(); i++ ) {\n        if( strcmp( Partitions[i].name, name ) == 0 ) {\n          log_v(\"OTA name '%s' found\", name );\n          return &Partitions[i];\n        }\n      }\n      return nullptr;\n    }\n\n\n    PartitionDesc_t* findPartition( Flash::Partition_t* flash_partition )\n    {\n      assert( flash_partition );\n      auto ota_num = flash_partition->part.subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN;\n      auto nvs_part = findPartition( ota_num );\n\n      if( nvs_part ) {\n        if( Flash::metadataHasDigest( &flash_partition->meta ) ) {\n          return nvs_part;\n        }\n      }\n      return nullptr;\n    }\n\n\n    int Erase()\n    {\n      esp_err_t result = nvs_flash_erase();\n      if( result != ESP_OK ) return -1;\n      return 0;\n    }\n\n\n\n\n    bool getPartitions()\n    {\n      if( Partitions.size()>0 )\n        Partitions.clear();\n      size_t blob_size;\n      bool ret = false;\n      blob_partition_t *bPart = nullptr;\n      auto err = nvs_open(PARTITION_NS, NVS_READONLY, &handle);\n      if( err != ESP_OK ) {\n        log_i(\"NVS Namespace not created yet\");\n        return false;\n      }\n\n      err = nvs_get_blob(handle, PARTITION_KEY, NULL, &blob_size);\n      if( err != ESP_OK ) {\n        log_i(\"NVS key not created yet\");\n        goto _nvs_close;\n      }\n\n      bPart = new blob_partition_t( blob_size );\n      if (!bPart->blob ) {\n        log_e(\"Could not alloc %d bytes\", blob_size );\n        goto _nvs_close;\n      }\n\n      err = nvs_get_blob(handle, PARTITION_KEY, bPart->blob, &blob_size);\n      if( err != ESP_OK ) {\n        log_e(\"Could not read blob\");\n        goto _nvs_close;\n      }\n\n      ret = parsePartitions( bPart->blob, blob_size );\n\n      _nvs_close:\n      nvs_close( handle );\n      if( bPart!=nullptr ) delete bPart;\n      return ret;\n    }\n\n\n    bool parsePartitions( const char* blob, size_t size )\n    {\n      size_t idx = 0;\n      size_t found = 0;\n      do {\n        PartitionDesc_t* sdu_part = (PartitionDesc_t*)&blob[idx];\n        auto sdu_part_ref = PartitionDesc_t();\n        memcpy( &sdu_part_ref, sdu_part, sizeof(PartitionDesc_t) );\n        Partitions.push_back( sdu_part_ref );\n        found++;\n        idx += sizeof(PartitionDesc_t);\n      } while( idx < size );\n\n      log_d(\"Found %d items\", found );\n\n      return found>0;\n    }\n\n\n\n    bool savePartitions()\n    {\n      bool ret = true;\n      if( Partitions.size() > 0 ) {\n        log_d(\"Saving partitions\");\n        size_t blob_size = (sizeof(PartitionDesc_t)*Partitions.size());\n        blob_partition_t *bPart = new blob_partition_t(blob_size);\n\n        if( !bPart->blob) {\n          log_e(\"Can't allocate %d bytes for blob\", blob_size );\n          return false;\n        }\n        size_t idx = 0;\n        for( int i=0; i<Partitions.size(); i++ ) {\n          idx = i*sizeof(PartitionDesc_t);\n          auto part = &Partitions[i];\n          memcpy( &bPart->blob[idx], part, sizeof(PartitionDesc_t) );\n        }\n\n        auto err = nvs_open(PARTITION_NS, NVS_READWRITE, &handle);\n        if( err != ESP_OK ) {\n          log_e(\"Cannote create NVS Namespace %s\", PARTITION_NS);\n          return false;\n        }\n\n        err = nvs_set_blob(handle, PARTITION_KEY, bPart->blob, blob_size);\n        if( err != ESP_OK ) {\n          log_e(\"Failed to save blob\");\n          ret = false;\n        } else {\n          log_v(\"Blob save success (%d bytes)\", blob_size);\n          nvs_commit( handle );\n        }\n\n        nvs_close( handle );\n        delete bPart;\n      }\n      return ret;\n    }\n\n\n    void deletePartitions()\n    {\n      auto err = nvs_open(PARTITION_NS, NVS_READWRITE, &handle);\n      if( err != ESP_OK ) {\n        log_e(\"Cannot open NVS Namespace %s for writing\", PARTITION_NS);\n        return;\n      }\n      err = nvs_erase_key(handle, PARTITION_KEY);\n      if( err != ESP_OK ) {\n        log_e(\"Failed to erase blob\");\n      } else {\n        log_v(\"Blob erase success (%d bytes)\");\n        nvs_commit( handle );\n      }\n      nvs_close( handle );\n    }\n\n\n    // Rollback helper: save menu.bin meta info in NVS\n    bool saveMenuPrefs()\n    {\n      const esp_partition_t* update_partition = esp_ota_get_next_update_partition( NULL );\n      if (!update_partition) {\n        log_e( \"Partition scheme does not support OTA\" );\n        return false;\n      }\n      esp_image_metadata_t nusketchMeta = Flash::getSketchMeta( update_partition );\n      uint32_t nuSize = nusketchMeta.image_len;\n\n      auto err = nvs_open(MENU_PREF_NS, NVS_READWRITE, &handle);\n      if( err != ESP_OK ) {\n        log_i(\"NVS Namespace %s not created yet\", MENU_PREF_NS );\n        return false;\n      }\n\n      bool ret = true;\n\n      err = nvs_set_blob(handle, DIGEST_KEY, nusketchMeta.image_digest, 32);\n      if( err != ESP_OK ) {\n        log_e(\"NVS failed to save %s::%s\", MENU_PREF_NS, DIGEST_KEY);\n        ret = false;\n      }\n\n      err = nvs_set_i32(handle, MENUSIZE_KEY, nuSize);\n      if( err != ESP_OK ) {\n        log_e(\"NVS failed to save %s::%s\", MENU_PREF_NS, MENUSIZE_KEY);\n        ret = false;\n      }\n\n      if( ret) nvs_commit( handle );\n\n      nvs_close( handle );\n      return ret;\n    }\n\n\n\n    bool getMenuPrefs( uint32_t *menuSize, uint8_t *image_digest )\n    {\n      if( nvs_open(MENU_PREF_NS, NVS_READONLY, &handle) != ESP_OK ) {\n        log_i(\"NVS Namespace %s not created yet\", MENU_PREF_NS );\n        return false;\n      }\n\n      bool ret = false;\n      size_t blob_size;\n\n      if( nvs_get_blob(handle, DIGEST_KEY, NULL, &blob_size) != ESP_OK ) {\n        log_i(\"NVS key %s::%s not created yet\", MENU_PREF_NS, DIGEST_KEY);\n        goto _nvs_close;\n      }\n\n      if( blob_size != 32 ) {\n        log_e(\"NVS key %s::%s has invalid size( expect=32, got=%d)\", MENU_PREF_NS, DIGEST_KEY, blob_size);\n        goto _nvs_close;\n      }\n\n      if( nvs_get_blob(handle, DIGEST_KEY, image_digest, &blob_size) != ESP_OK ) {\n        log_i(\"NVS key %s::%s not created yet\", MENU_PREF_NS, DIGEST_KEY);\n        goto _nvs_close;\n      }\n\n      if( nvs_get_i32(handle, MENUSIZE_KEY, (int32_t*)menuSize) != ESP_OK ) {\n        log_i(\"NVS key %s::%s not created yet\", MENU_PREF_NS, MENUSIZE_KEY);\n      }\n\n      ret = true;\n\n      _nvs_close:\n      nvs_close( handle );\n      return ret;\n    }\n\n\n\n\n  }; // end namespace NVS\n\n}; // end namespace SDUpdaterNS\n"
  },
  {
    "path": "src/PartitionManager/NVS/NVSUtils.hpp",
    "content": "#pragma once\n#define SDU_NVSUTILS_HPP\n\n//#include <Arduino.h>\n//#include <Preferences.h>\n#include <cstring>\n#include <ctype.h>\n#include <stdio.h>\n#include <vector>\n#include <esp32-hal-log.h>\n#include <esp_partition.h>\n#include <nvs_flash.h>\n#include <Stream.h>\n#include <StreamString.h>\n\n#include \"../Partitions/PartitionUtils.hpp\"\n\n\nnamespace SDUpdaterNS\n{\n\n  namespace NVS\n  {\n\n    // NVS namespace/key for virtual partitions array (may hold fw-menu partition)\n    constexpr const char* PARTITION_NS  = \"sdu\";\n    constexpr const char* PARTITION_KEY = \"partitions\";\n    // NVS namespace/keys for sd-menu partition (blob digest + size)\n    constexpr const char* MENU_PREF_NS  = \"sd-menu\";\n    constexpr const char* DIGEST_KEY    = \"digest\";\n    constexpr const char* MENUSIZE_KEY  = \"menusize\";\n\n\n    // NVS representation of flash partition\n    struct __attribute__((__packed__)) PartitionDesc_t\n    {\n      uint8_t ota_num{0};    // OTA partition number\n      size_t  bin_size{0};   // firmware size\n      uint8_t digest[32]{0}; // firmware digest\n      char    name[40]{0};   // firmware name\n      //char    desc[40]{0};   // firmware desc\n    };\n\n    struct blob_partition_t\n    {\n      char* blob{nullptr};\n      bool needs_free{false};\n      blob_partition_t() : blob(nullptr), needs_free(false) { }\n      blob_partition_t( size_t blob_size ) : blob(nullptr), needs_free(false)\n      {\n        this->blob = (char*)calloc(blob_size+1, sizeof(char));\n        if (!this->blob ) {\n          log_e(\"Could not alloc %d bytes\", blob_size );\n        } else {\n          needs_free = true;\n        }\n      }\n      ~blob_partition_t()\n      {\n        if( needs_free )\n          free(this->blob);\n      }\n    };\n\n    extern nvs_handle_t handle;\n    extern std::vector<PartitionDesc_t> Partitions; // filled by NVS\n\n    PartitionDesc_t* findPartition( uint8_t ota_num );\n    PartitionDesc_t* findPartition( const char* name );\n    PartitionDesc_t* findPartition( Flash::Partition_t* flash_partition );\n\n    int  erase();\n    bool getPartitions();\n    void deletePartitions();\n    bool savePartitions();\n    bool parsePartitions( const char* blob, size_t size );\n\n    // save menu.bin meta info in NVS\n    bool saveMenuPrefs();\n    bool getMenuPrefs( uint32_t *menuSize, uint8_t *image_digest );\n\n  };\n\n}; // end namespace SDUpdaterNS\n\n"
  },
  {
    "path": "src/PartitionManager/PartitionManager.cpp",
    "content": "\n//#include \"./PartitionManager.hpp\"\n#include \"../SDUpdater/SDUpdater_Class.hpp\"\n\n\nnamespace SDUpdaterNS\n{\n  namespace PartitionManager\n  {\n\n\n    void createPartitions()\n    {\n      NVS::Partitions.clear();\n      Flash::digest_t digests = Flash::digest_t();\n      for( int i=0; i<Flash::Partitions.size(); i++ ) {\n        auto part = &Flash::Partitions[i].part;\n        auto meta = &Flash::Partitions[i].meta;\n        if( Flash::partitionIsFactory( part ) ) continue;\n        if( !Flash::partitionIsApp( part ) ) continue;\n        NVS::PartitionDesc_t nvs_part;\n        nvs_part.ota_num  = part->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN;\n        snprintf( nvs_part.name, 39, \"OTA %d\",  nvs_part.ota_num );\n        nvs_part.bin_size = 0;\n        if( Flash::metadataHasDigest( meta ) ) {\n          nvs_part.bin_size = meta->image_len;\n          memcpy( nvs_part.digest, meta->image_digest, 32 );\n          log_d(\"Added flash digest to NVS::Partitions[%d]: %s\", NVS::Partitions.size(), digests.toString( nvs_part.digest ) );\n        }\n        NVS::Partitions.push_back( nvs_part );\n      }\n      NVS::savePartitions();\n      //debugPartitions();\n      log_i(\"Partition scheme has %d app slot(s)\", NVS::Partitions.size() );\n    }\n\n\n\n    // update NVS blob with flash partition infos\n    void updatePartitions()\n    {\n      log_v(\"Comparing Flash and NVS partitions\");\n      bool needs_saving = false;\n      Flash::digest_t digests = Flash::digest_t();\n      for( int i=0; i<Flash::Partitions.size(); i++ ) {\n        auto sdu_flash_part = &Flash::Partitions[i];\n        auto flash_part = &sdu_flash_part->part;\n        auto meta = &sdu_flash_part->meta;\n        if( !Flash::metadataHasDigest( meta ) ) continue;       // ignore empty partitions\n        if( Flash::partitionIsFactory( flash_part ) ) continue; // ignore factory partition\n        if( !Flash::partitionIsApp( flash_part ) ) continue;    // ignore non-app partitions\n        auto sdu_nvs_part = NVS::findPartition(sdu_flash_part);\n\n        if( sdu_nvs_part ) { // flash partition is documented in NVS, compare digests\n          if( !digests.match( sdu_nvs_part->digest, sdu_flash_part->meta.image_digest ) ) { // image digests differ\n            memcpy( sdu_nvs_part->digest, sdu_flash_part->meta.image_digest, 32 ); // update NVS digest\n            needs_saving = true;\n          }\n        } else {\n          log_e(\"No matching partition for ota #%d\", flash_part->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n        }\n      }\n\n      if( needs_saving ) {\n        NVS::savePartitions();\n        //debugPartitions();\n      }\n    }\n\n\n    // Called after factory firmware was flashed and copied to factory partition,\n    // will erase the originating OTA partition to make it available for next flashing.\n    void processPartitions()\n    {\n      Flash::digest_t digests = Flash::digest_t();\n      for( int i=0; i<NVS::Partitions.size(); i++ ) {\n        auto nvs_part = &NVS::Partitions[i];\n\n        bool is_factory_dupe = Flash::FactoryPartition!=nullptr\n                        && !digests.isEmpty(Flash::FactoryPartition->meta.image_digest)\n                        && digests.match(nvs_part->digest, Flash::FactoryPartition->meta.image_digest);\n\n        if( is_factory_dupe > 0 ) {\n          // erase ota version of factory partition now that it's been duplicated\n          if( Flash::erase( nvs_part->ota_num ) ) {\n            nvs_part->bin_size = 0;\n            memset( nvs_part->digest, 0, 32 );\n            if( NVS::savePartitions() ) {\n              //debugPartitions();\n              // TODO: implement partitions reload instead of restart\n              ESP.restart();\n            }\n          }\n        }\n      }\n    }\n\n\n    // copy firmware from SD/SPIFFS://path to OTA flash partition\n    //bool Flash( fs::FS &fs, const char* path, const esp_partition_t *dstpart )\n    bool flash( const esp_partition_t *dstpart, fs::FS *dstfs, const char* srcpath )\n    {\n      if( !srcpath ) return false;\n      if( !dstpart ) return false;\n      if( !dstfs   ) return false;\n\n      // - check if file exists on SD\n      if( !dstfs->exists( srcpath ) ) return false;\n      auto file = dstfs->open( srcpath );\n      if( !file || file.size()==0 ) return false;\n      auto fsize = file.size();\n      SDUpdater::_message(\"Copying FS to Flash\");\n      // - flash binary\n      bool ret = Flash::copyPartition(dstpart, &file, fsize );\n      file.close();\n      if( ret ) {\n        auto nvs_part = NVS::findPartition( dstpart->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n        if( !nvs_part ) {\n          log_e(\"FATAL: can't update nvs with new partition info\");\n          SDUpdater::_error(\"NVS persistence fail\");\n          return false;\n        }\n        snprintf(nvs_part->name, 39, srcpath );\n        nvs_part->bin_size = fsize;\n        if( esp_partition_get_sha256(dstpart, nvs_part->digest) != ESP_OK ) {\n          log_e(\"WARN: partition has no sha\");\n          //return false;\n        }\n        ret = NVS::savePartitions();\n        //debugPartitions();\n      }\n      return ret;\n    }\n\n\n    // Copy firmware from filesystem to ota slot in transaction style.\n    // Implements a picker for source filesystem and source filename.\n    bool flash( uint8_t ota_num, sdu_fs_picker_t fsPicker, sdu_file_picker_t filePicker )\n    {\n      auto dest_part = Flash::findPartition( ota_num );\n      //if( !dest_part.part ) return false; // invalid ota number\n      sdu_fs_copy_t fsCopy;\n      fsCopy.dstPart = &dest_part.part;\n      fsCopy.srcFs = fsPicker(); // select a source filesystem\n      if( !fsCopy.srcFs ) return false; // action cancelled\n      fsCopy.name = filePicker( fsCopy.srcFs );\n      if( !fsCopy.name ) return false; // action cancelled\n      return fsCopy.commit();\n    }\n\n\n    bool backupFlash( fs::FS* dstFs, const char* dstName )\n    {\n      SDUpdater::_message(\"Backing up full Flash\");\n      bool ret = Flash::dumpFW( dstFs, \"/full_dump.fw\" );\n      if( ! ret ) SDUpdater::_error(\"Backup failed\");\n      return ret;\n    }\n\n\n\n    bool backup( uint8_t ota_num, sdu_fs_picker_t fsPicker )\n    {\n      auto nvs_part = NVS::findPartition( ota_num );\n      if( !nvs_part ) return false;\n      return backup( nvs_part, fsPicker );\n    }\n\n\n    bool backup( NVS::PartitionDesc_t *src_nvs_part, sdu_fs_picker_t fsPicker )\n    {\n      if( !src_nvs_part ) return false;\n      if( src_nvs_part->bin_size == 0 ) return false;\n      String name = String(src_nvs_part->name);\n      auto flash_part = Flash::getPartition( src_nvs_part->ota_num );\n      if( !flash_part ) return false;\n      if( !name.startsWith(\"/\") ) name = \"/\" + name;\n      if( !name.endsWith(\".bin\") ) name = name + \".bin\";\n      auto fs = fsPicker();\n      if( !fs ) return false;\n      auto destfile = fs->open( name, \"w\" );\n      if(!destfile) return false;\n      bool ret = Flash::copyPartition(&destfile, flash_part, flash_part->size);\n      destfile.close();\n      return ret;\n    }\n\n\n    bool verify( uint8_t ota_num )\n    {\n      auto part = Flash::getPartition( ota_num );\n      if( !part ) return false;\n      auto meta = Flash::getSketchMeta( part );\n      Flash::digest_t digests = Flash::digest_t();\n      if( digests.isEmpty( meta.image_digest ) ) return false;\n\n      auto nvs_part = NVS::findPartition( ota_num );\n      if( !nvs_part ) return false;\n\n      uint8_t sha256[32];\n\n      auto err = bootloader_common_get_sha256_of_partition( part->address, part->size, part->type, sha256 );\n\n      if( err != ESP_OK ) return false;\n\n      return digests.match(sha256, nvs_part->digest) && digests.match(meta.image_digest, nvs_part->digest);\n    }\n\n\n    // void debugPartitions()\n    // {\n    //   log_d(\"OTA, size, digest, name, desc\");\n    //   Flash::digest_t digests;\n    //   for( int i=0;i<NVS::Partitions.size();i++ ) {\n    //     log_d(\"%d %s %s %s %d bytes\",\n    //       NVS::Partitions[i].ota_num, // OTA partition number\n    //       digests.toString( NVS::Partitions[i].digest ), // firmware digest\n    //       NVS::Partitions[i].name, // firmware name\n    //       NVS::Partitions[i].desc,  // firmware desc\n    //       NVS::Partitions[i].bin_size // firmware size\n    //     );\n    //   }\n    //\n    //   log_d(\"Partition  Type   Subtype    Address   PartSize   ImgSize    Info    Digest\");\n    //   log_d(\"---------+------+---------+----------+----------+---------+--------+--------\");\n    //   for( int i=0; i<Flash::Partitions.size(); i++ ) {\n    //     //printFlashPartition( &Flash::Partitions[i] );\n    //     Flash::digest_t digests = Flash::digest_t();\n    //\n    //     auto sdu_partition = &Flash::Partitions[i];\n    //     auto part = sdu_partition->part;\n    //     auto meta = sdu_partition->meta;\n    //\n    //     String AppName = \"n/a\";\n    //\n    //     if( Flash::partitionIsApp( &part ) ) {\n    //       if( Flash::partitionIsFactory( &part ) ) {\n    //         AppName = \"Factory\";\n    //       } else {\n    //         AppName = \"OTA\" + String( part.subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n    //       }\n    //     }\n    //\n    //     log_d(\"%-8s   0x%02x      0x%02x   0x%06x   %8d  %8s %8s %8s\",\n    //       String( part.label ).c_str(),\n    //       part.type,\n    //       part.subtype,\n    //       part.address,\n    //       part.size,\n    //       meta.image_len>0 ? String(meta.image_len).c_str() : \"n/a\",\n    //       AppName.c_str(),\n    //       Flash::partitionIsApp(&part)&&Flash::metadataHasDigest(&meta) ? digests.toString(meta.image_digest) : \"n/a\"\n    //     );\n    //   }\n    // }\n\n\n    bool erase( uint8_t ota_num )\n    {\n      NVS::PartitionDesc_t* nvs_part = NVS::findPartition(ota_num);\n      if( !nvs_part ) {\n        log_e(\"Cannot erase partition,  NVS::findPartition(%d) found nothing\", ota_num);\n        return false;\n      }\n      auto flash_part = Flash::getPartition( ota_num );\n      if( !flash_part ) {\n        log_e(\"Cannot erase partition,  Flash::getPartition(%d) found nothing\", ota_num);\n        return false;\n      }\n      if( Flash::erase(ota_num) ) {\n\n        nvs_part->bin_size = 0;\n        nvs_part->name[0] = 0;\n        //nvs_part->desc[0] = 0;\n        for( int i=0;i<32;i++ ) nvs_part->digest[i] = 0;\n\n        Flash::scan();\n        //debugPartitions();\n\n        if( NVS::savePartitions() ) {\n          log_d(\"TODO: implement partitions reload instead of restart\");\n          //debugPartitions();\n          ESP.restart(); // force partition reload\n        }\n      } else {\n        log_e(\"Could erase partition,  Flash::erase(%d) failed\", ota_num);\n      }\n      return false;\n    }\n\n\n    bool canMigrateToFactory()\n    {\n      if( !Flash::hasFactory() ) return false;\n      if( Flash::isRunningFactory() ) return false;\n      return true;\n    }\n\n\n    bool flashFactory()\n    {\n      if( !canMigrateToFactory() ) return false; // need a factory partition scheme\n      SDUpdater::_message( String(\"Migrating to factory\") );\n      auto ret = Flash::saveSketchToFactory();\n      if( !ret ) {\n        SDUpdater::_error( String(\"Migration failed :(\") );\n      }\n      return ret;\n    }\n\n\n    bool migrateSketch( const char* binFileName )\n    {\n      assert(binFileName);\n      log_v(\"Checking %s for migration\", binFileName);\n      Flash::digest_t digests = Flash::digest_t();\n\n      if( !canMigrateToFactory() ) {\n        return false; // need a factory partition scheme\n      }\n\n      if( !NVS::getPartitions() ) {\n        return false; // need a NVS partition management already set\n      }\n\n      esp_image_metadata_t running_meta;\n      esp_image_metadata_t dst_meta;\n\n      NVS::PartitionDesc_t* NVSPart = nullptr;\n      Flash::Partition_t *FlashPart = nullptr;\n\n      uint8_t ota_num = 0;\n      size_t sksize = 0;\n\n      String error = \"\";\n      String msg = \"\";\n\n      Flash::running_partition = esp_ota_get_running_partition();\n      if( !Flash::running_partition ) {\n        log_e(\"Flash inconsistency: running partition not found\" );\n        return false; // uh-oh\n      }\n\n      if( Flash::running_partition->subtype != ESP_PARTITION_SUBTYPE_APP_OTA_MIN ) {\n\n      }\n\n\n      running_meta = Flash::getSketchMeta( Flash::running_partition );\n\n      NVSPart = NVS::findPartition( binFileName );\n\n      if( !NVSPart /*|| (NVSPart && digests.isEmpty( NVSPart->digest))*/ ) {\n        // No NVS partition found named [binFilename], but NVS may be wrong so try to also find by digest\n        FlashPart = Flash::findDupePartition( &running_meta, Flash::running_partition->type, Flash::running_partition->subtype );\n        if( FlashPart ) {\n          //log_d(\"Duplicate partition found: NVS has no entry named %s but partition meta %d exists\", binFileName, FlashPart->part.subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n          error = \"Error: NVS dupe found, please erase NVS\";\n          goto _error_nvs;\n        }\n        log_d(\"NVS Partition named %s not found\", binFileName );\n        // new slot, get next flashable partition\n        Flash::nextupd_partition = Flash::getNextAvailPartition( Flash::running_partition->type, Flash::running_partition->subtype );\n        if( !Flash::nextupd_partition ) {\n          error = \"Migration canceled: partitions full\";//migration to new slot is not possible\n          goto _error_nvs;\n        }\n        // store NVS app number\n        ota_num = Flash::nextupd_partition->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN;\n        sksize = ESP.getSketchSize();\n\n        msg = String(\"Migrating to new slot #\") + String(ota_num);\n        SDUpdater::_message( msg );\n\n        // copy to next partition\n        if( !Flash::copyPartition( Flash::nextupd_partition, Flash::running_partition, sksize) ) {\n          error = \"Migration failed\";\n          goto _error_nvs;\n        }\n\n        NVSPart = NVS::findPartition(ota_num);\n        if( !NVSPart ) {\n          error = \"Error: please erase NVS\";\n          goto _error_nvs;\n        }\n\n        goto _update_nvs;\n      }\n\n      // NVSPart exists with similar binFile name, figure out if overwrite is needed\n\n      ota_num = NVSPart->ota_num;\n      // get the destination slot according to NVS\n      Flash::nextupd_partition = Flash::getPartition( ota_num );\n      if( !Flash::nextupd_partition ) {\n        // log_e(\"NVS inconsistency: destination slot #%d not found\", ota_num );\n        error = \"Error: please erase Flash\";\n        goto _error_nvs;\n      }\n      dst_meta = Flash::getSketchMeta( Flash::nextupd_partition );\n\n      // health check: compare NVSPart with the flash partition it represents\n      if( ota_num != Flash::nextupd_partition->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN ) {\n        //log_e(\"NVS inconsistency: slot mismatch (%d vs %d)\", ota_num, Flash::nextupd_partition->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n        error = \"Error: please erase Flash\";\n        goto _error_nvs;\n      }\n      // health check: compare NVSPart with the flash partition it represents\n      if( ! digests.match( NVSPart->digest, dst_meta.image_digest ) ) {\n        log_w(\"NVS inconsistency: digest mismatch, will be overwritten\");\n      } else {\n        // log_d(\"NVS data is consistent\");\n      }\n      // functional check: verify that source and destination differ\n      if( ota_num == Flash::running_partition->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN ) {\n        // check for duplicate meta\n        FlashPart = Flash::findDupePartition( &running_meta, Flash::running_partition->type, Flash::running_partition->subtype );\n        if( FlashPart ) {\n          // erase duplicate (will trigger a restart on success)\n          msg = String(\"Erasing old partition\");\n          SDUpdater::_message( msg );\n          if( !PartitionManager::erase( FlashPart->part.subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN ) ) {\n            error = \"Erasing failed!\";\n            goto _error_nvs;\n          }\n\n        }\n        log_i(\"this sketch is already running from the right partition (%d) according to NVS\", ota_num);\n        return false;\n      }\n\n      log_w(\"Current sketch is not running from its assigned partition (NVS want=%d, has=%d)\", ota_num, Flash::running_partition->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n\n      // overwrite if digests differ\n      if( ! digests.match( NVSPart->digest, running_meta.image_digest ) ) {\n        msg  = \"Overwriting slot (in=\";\n        msg += digests.toString( NVSPart->digest );\n        msg += \", out=\";\n        msg += digests.toString( running_meta.image_digest );\n        msg += \")\";\n        SDUpdater::_message( String(\"Overwriting slot\") );\n        sksize = ESP.getSketchSize();\n        if( !Flash::copyPartition( Flash::nextupd_partition, Flash::running_partition, sksize) ) {\n          error = \"Overwriting failed!\";\n          goto _error_nvs;\n        }\n        goto _update_nvs;\n      } else {\n        log_d(\"names and digests match, no overwrite, just switch\");\n        goto _boot_partition;\n      }\n\n      _error_nvs:\n        SDUpdater::_error( error );\n        return false;\n\n      _update_nvs:\n\n        NVSPart->bin_size = running_meta.image_len;\n        memcpy( NVSPart->digest, running_meta.image_digest, 32 );\n        snprintf( NVSPart->name, 39, \"%s\", binFileName );\n        log_d(\"Updated NVSPart->name %s=%s\", NVSPart->name, binFileName);\n        NVS::savePartitions();\n        //debugPartitions();\n\n      _boot_partition:\n\n        Flash::bootPartition( ota_num );\n\n      return false;\n    }\n\n\n  };\n\n};\n"
  },
  {
    "path": "src/PartitionManager/PartitionManager.hpp",
    "content": "#pragma once\n\n\n#include \"./NVS/NVSUtils.hpp\"\n#include \"./Partitions/PartitionUtils.hpp\"\n#include <FS.h>\n\n\nnamespace SDUpdaterNS\n{\n  namespace PartitionManager\n  {\n\n    typedef void(*partitionFoundCb_t)( const esp_partition_t *part );\n    typedef fs::FS*(*sdu_fs_picker_t)();\n    typedef const char*(*sdu_file_picker_t)(fs::FS*);\n\n    void createPartitions();\n    void updatePartitions();\n    void processPartitions();\n    void debugPartitions();\n\n    bool flashFactory();\n    bool migrateSketch( const char* binFileName );\n    bool canMigrateToFactory();\n\n    bool verify( uint8_t ota_num );\n    bool erase( uint8_t ota_num );\n\n    bool flash( const esp_partition_t *dstpart, fs::FS *dstfs, const char* srcpath );\n    bool flash( uint8_t ota_num, sdu_fs_picker_t fsPicker, sdu_file_picker_t filePicker );\n\n    bool backup( uint8_t ota_num, sdu_fs_picker_t fsPicker );\n    bool backup( NVS::PartitionDesc_t *src_nvs_part, sdu_fs_picker_t fsPicker );\n\n    bool backupFlash( fs::FS* dstFs, const char* dstName );\n\n    struct sdu_fs_copy_t\n    {\n      fs::FS* srcFs{nullptr};\n      const char* name{nullptr};\n      const esp_partition_t *dstPart{nullptr};\n      bool commit()\n      {\n        return flash( dstPart, srcFs, name );\n      }\n    };\n\n  };\n\n};\n"
  },
  {
    "path": "src/PartitionManager/Partitions/PartitionUtils.cpp",
    "content": "#include \"./PartitionUtils.hpp\"\n#include \"../NVS/NVSUtils.hpp\"\n#include \"../../ConfigManager/ConfigManager.hpp\"\n\n\n#if !defined SPI_FLASH_SEC_SIZE\n  #define SPI_FLASH_SEC_SIZE 4096\n#endif\n\nnamespace SDUpdaterNS\n{\n\n  namespace Flash\n  {\n\n    std::vector<Partition_t> Partitions; // all partitions (flash)\n    Partition_t* FactoryPartition = nullptr;\n    const esp_partition_t* running_partition = nullptr;\n    const esp_partition_t* factory_partition = nullptr;\n    const esp_partition_t* nextupd_partition = nullptr;\n\n\n    const char* digest_t::toString( const uint8_t dig[32] )\n    {\n      str = \"\";\n      char hex[3] = {0};\n      for(int i=0;i<32;i++) {\n        snprintf( hex, 3, \"%02x\", dig[i] );\n        str += String(hex);\n      }\n      return str.c_str();\n    }\n\n\n    bool digest_t::match( const uint8_t d1[32], const uint8_t d2[32] )\n    {\n      for(int i=0; i<32; i++) {\n        if (d1[i] != d2[i]) return false;\n      }\n      return true;\n    }\n\n\n    bool digest_t::isEmpty( const uint8_t d1[32] )\n    {\n      int sum = -1;\n      for( int i=0; i<32; i++ ) {\n        sum += d1[i];\n      }\n      return sum<=0;\n    }\n\n\n    bool comparePartition(const esp_partition_t* src1, const esp_partition_t* src2, size_t length)\n    {\n      size_t lengthLeft = length;\n      const size_t bufSize = SPI_FLASH_SEC_SIZE;\n      std::unique_ptr<uint8_t[]> buf1(new uint8_t[bufSize]);\n      std::unique_ptr<uint8_t[]> buf2(new uint8_t[bufSize]);\n      uint32_t offset = 0;\n      size_t i;\n      while( lengthLeft > 0) {\n        size_t readBytes = (lengthLeft < bufSize) ? lengthLeft : bufSize;\n        if (!ESP.flashRead(src1->address + offset, reinterpret_cast<uint32_t*>(buf1.get()), (readBytes + 3) & ~3)\n        || !ESP.flashRead(src2->address + offset, reinterpret_cast<uint32_t*>(buf2.get()), (readBytes + 3) & ~3)) {\n            return false;\n        }\n        for (i = 0; i < readBytes; ++i) if (buf1[i] != buf2[i]) return false;\n        lengthLeft -= readBytes;\n        offset += readBytes;\n      }\n      return true;\n    }\n\n\n    bool comparePartition(const esp_partition_t* src1, fs::File* src2, size_t length)\n    {\n      size_t lengthLeft = length;\n      const size_t bufSize = SPI_FLASH_SEC_SIZE;\n      std::unique_ptr<uint8_t[]> buf1(new uint8_t[bufSize]);\n      std::unique_ptr<uint8_t[]> buf2(new uint8_t[bufSize]);\n      uint32_t offset = 0;\n      uint32_t progress = 0, progressOld = 1;\n      size_t i;\n      while( lengthLeft > 0) {\n        size_t readBytes = (lengthLeft < bufSize) ? lengthLeft : bufSize;\n        if (!ESP.flashRead(src1->address + offset, reinterpret_cast<uint32_t*>(buf1.get()), (readBytes + 3) & ~3)\n        || !src2->read(                           reinterpret_cast<uint8_t*>(buf2.get()), (readBytes + 3) & ~3)\n        ) {\n          return false;\n        }\n        for (i = 0; i < readBytes; ++i) if (buf1[i] != buf2[i]) return false;\n        lengthLeft -= readBytes;\n        offset += readBytes;\n        if( SDUCfg.onProgress ) {\n          progress = 100 * offset / length;\n          if (progressOld != progress) {\n            progressOld = progress;\n            SDUCfg.onProgress( (uint8_t)progress, 100 );\n          }\n        }\n      }\n      return true;\n    }\n\n\n    bool copyPartition( fs::FS* fs, const char* binfilename )\n    {\n      bool ret = false;\n      running_partition = esp_ota_get_running_partition();\n      nextupd_partition = esp_ota_get_next_update_partition(NULL);\n      size_t sksize = ESP.getSketchSize();\n      bool flgSD = fs?true:false;\n      File dst;\n      if (flgSD) {\n        dst = (fs->open(binfilename, FILE_WRITE ));\n      }\n      ret = copyPartition( flgSD ? &dst : NULL, nextupd_partition, running_partition, sksize);\n      if (flgSD) dst.close();\n      return ret;\n    }\n\n\n    bool copyPartition(fs::File* dst, const esp_partition_t* src, size_t length)\n    {\n      size_t lengthLeft = length;\n      const size_t bufSize = SPI_FLASH_SEC_SIZE;\n      std::unique_ptr<uint8_t[]> buf(new uint8_t[bufSize]);\n      uint32_t offset = 0;\n      uint32_t progress = 0, progressOld = 1;\n      while( lengthLeft > 0) {\n        size_t readBytes = (lengthLeft < bufSize) ? lengthLeft : bufSize;\n        if (!ESP.flashRead(src->address + offset, reinterpret_cast<uint32_t*>(buf.get()), (readBytes + 3) & ~3) ) {\n          return false;\n        }\n        if (dst) dst->write(buf.get(), (readBytes + 3) & ~3);\n        lengthLeft -= readBytes;\n        offset += readBytes;\n        if( SDUCfg.onProgress ) {\n          progress = 100 * offset / length;\n          if (progressOld != progress) {\n            progressOld = progress;\n            SDUCfg.onProgress( (uint8_t)progress, 100 );\n            vTaskDelay(10);\n          }\n        }\n      }\n      return true;\n    }\n\n\n    bool copyPartition(fs::File* dstFile, const esp_partition_t* dst, const esp_partition_t* src, size_t length)\n    {\n      if( dst->size < length ) {\n        log_e(\"data won't fit in destination partition (available: %d, needed: %d)\", dst->size, length );\n        return false;\n      }\n      size_t lengthLeft = length;\n      const size_t bufSize = SPI_FLASH_SEC_SIZE;\n      std::unique_ptr<uint8_t[]> buf(new uint8_t[bufSize]);\n      uint32_t offset = 0;\n      uint32_t progress = 0, progressOld = 0;\n      while( lengthLeft > 0) {\n        size_t readBytes = (lengthLeft < bufSize) ? lengthLeft : bufSize;\n        if (!ESP.flashRead(src->address + offset, reinterpret_cast<uint32_t*>(buf.get()), (readBytes + 3) & ~3)\n        || !ESP.flashEraseSector((dst->address + offset) / bufSize)\n        || !ESP.flashWrite(dst->address + offset, reinterpret_cast<uint32_t*>(buf.get()), (readBytes + 3) & ~3)) {\n          return false;\n        }\n        if (dstFile) dstFile->write(buf.get(), (readBytes + 3) & ~3);\n        lengthLeft -= readBytes;\n        offset += readBytes;\n        if( SDUCfg.onProgress ) {\n          progress = 100 * offset / length;\n          if (progressOld != progress) {\n            progressOld = progress;\n            SDUCfg.onProgress( (uint8_t)progress, 100 );\n          }\n        }\n      }\n      return true;\n    }\n\n\n    bool copyPartition(const esp_partition_t* dst, const esp_partition_t* src, size_t length)\n    {\n      if( dst->size < length ) {\n        log_e(\"data won't fit in destination partition (available: %d, needed: %d)\", dst->size, length );\n        return false;\n      }\n      size_t lengthLeft = length;\n      const size_t bufSize = SPI_FLASH_SEC_SIZE;\n      std::unique_ptr<uint8_t[]> buf(new uint8_t[bufSize]);\n      uint32_t offset = 0;\n      uint32_t progress = 0, progressOld = 0;\n      while( lengthLeft > 0) {\n        size_t readBytes = (lengthLeft < bufSize) ? lengthLeft : bufSize;\n        if (!ESP.flashRead(src->address + offset, reinterpret_cast<uint32_t*>(buf.get()), (readBytes + 3) & ~3)\n        || !ESP.flashEraseSector((dst->address + offset) / bufSize)\n        || !ESP.flashWrite(dst->address + offset, reinterpret_cast<uint32_t*>(buf.get()), (readBytes + 3) & ~3)) {\n          return false;\n        }\n        lengthLeft -= readBytes;\n        offset += readBytes;\n        if( SDUCfg.onProgress ) {\n          progress = 100 * offset / length;\n          if (progressOld != progress) {\n            progressOld = progress;\n            SDUCfg.onProgress( (uint8_t)progress, 100 );\n          }\n        }\n      }\n      return true;\n    }\n\n\n    bool copyPartition(const esp_partition_t* dst, Stream* src, size_t length)\n    {\n      if( dst->size < length ) {\n        log_e(\"data won't fit in destination partition (available: %d, needed: %d)\", dst->size, length );\n        return false;\n      }\n      size_t lengthLeft = length;\n      const size_t bufSize = SPI_FLASH_SEC_SIZE;\n      std::unique_ptr<uint8_t[]> buf(new uint8_t[bufSize]);\n      uint32_t offset = 0;\n      uint32_t progress = 0, progressOld = 0;\n      while( lengthLeft > 0) {\n        size_t readBytes = (lengthLeft < bufSize) ? lengthLeft : bufSize;\n        if (!src->readBytes( reinterpret_cast<char*>(buf.get()), (readBytes + 3) & ~3)\n        || !ESP.flashEraseSector((dst->address + offset) / bufSize)\n        || !ESP.flashWrite(dst->address + offset, reinterpret_cast<uint32_t*>(buf.get()), (readBytes + 3) & ~3)) {\n            return false;\n        }\n        lengthLeft -= readBytes;\n        offset += readBytes;\n        if( SDUCfg.onProgress ) {\n          progress = 100 * offset / length;\n          if (progressOld != progress) {\n            progressOld = progress;\n            SDUCfg.onProgress( (uint8_t)progress, 100 );\n          }\n        }\n      }\n      return true;\n    }\n\n\n    bool copyPartition(const esp_partition_t* dst, fs::FS *fs, const char* srcFilename)\n    {\n      if( !dst || !fs || !srcFilename ) return false;\n      auto file = fs->open( srcFilename );\n      if( !file ) return false;\n      bool ret = copyPartition( dst, &file, file.size() );\n      if( ret ) {\n        // TODO: update NVS\n      }\n      file.close();\n      return ret;\n    }\n\n\n\n    bool dumpFW( fs::FS *fs, const char* fw_name )\n    {\n      const uint32_t fw_size = ESP.getFlashChipSize();//0x1000000; // 0x1000000 = 16MB\n      //ESP.getFlashChipSize();\n\n\n      uint8_t buffer[SPI_FLASH_SEC_SIZE];\n\n      size_t size_left = fw_size;\n      uint32_t page_addr = 0;\n      uint32_t total_written = 0;\n\n      auto file = fs->open( fw_name, \"w\" );\n      if( !file ) return false;\n\n      log_w(\"Start dumping (estimated 0x%06x bytes to read)\", fw_size );\n\n      uint32_t progress = 0, progressOld = 0;\n\n      while( size_left ) {\n        #if ESP_IDF_VERSION_MAJOR < 5\n          esp_err_t ret = spi_flash_read(page_addr, buffer, SPI_FLASH_SEC_SIZE);\n        #else\n          esp_err_t ret = esp_flash_read(NULL, buffer, page_addr, SPI_FLASH_SEC_SIZE);\n        #endif\n        if( ret ) return false;\n        total_written += file.write( buffer, SPI_FLASH_SEC_SIZE );\n        size_left -= SPI_FLASH_SEC_SIZE;\n        page_addr += SPI_FLASH_SEC_SIZE;\n        if( SDUCfg.onProgress ) {\n          progress = 100 * page_addr / fw_size;\n          if (progressOld != progress) {\n            progressOld = progress;\n            SDUCfg.onProgress( (uint8_t)progress, 100 );\n          }\n        }\n      }\n\n      log_w(\"Dumping finished, wrote 0x%06x bytes\", total_written);\n\n      file.close();\n      return fw_size == total_written;\n    }\n\n\n    //***********************************************************************************************\n    //                                B A C K T O F A C T O R Y                                     *\n    //***********************************************************************************************\n    // https://www.esp32.com/posting.php?mode=quote&f=2&p=19066&sid=5ba5f33d5fe650eb8a7c9f86eb5b61b8\n    // Return to factory version.                                                                   *\n    // This will set the otadata to boot from the factory image, ignoring previous OTA updates.     *\n    //***********************************************************************************************\n    void loadFactory()\n    {\n      factory_partition = getFactoryPartition();\n\n      if( !factory_partition ) {\n        log_e( \"Failed to find factory partition\" );\n        return;\n      }\n\n      auto err = esp_ota_set_boot_partition ( factory_partition ) ; // Set partition for boot\n\n      if ( err != ESP_OK ) {                         // Check error\n        log_e( \"Failed to set boot partition\" ) ;\n      } else {\n        log_i(\"Will reboot to factory partition\");\n        esp_restart() ;                              // Restart ESP\n      }\n    }\n\n\n    esp_image_metadata_t getSketchMeta( const esp_partition_t* src )\n    {\n      esp_image_metadata_t data;\n      if ( !src ) {\n        log_e(\"No source partition provided\");\n        return data;\n      }\n      const esp_partition_pos_t src_pos  = {\n        .offset = src->address,\n        .size = src->size,\n      };\n      data.start_addr = src_pos.offset;\n\n      esp_app_desc_t app_desc;\n      if( esp_ota_get_partition_description(src, &app_desc) != ESP_OK ) {\n        // nothing flashed here\n        memset( data.image_digest, 0, sizeof(data.image_digest) );\n        data.image_len = 0;\n        return data;\n      }\n\n      if( partitionIsOTA(src) ) { // only verify OTA partitions\n        esp_err_t ret = esp_image_verify( ESP_IMAGE_VERIFY, &src_pos, &data );\n        if( ret != ESP_OK ) {\n          log_e(\"Failed to verify image %s at addr %x\", String( src->label ), src->address );\n        } else {\n          log_v(\"Successfully verified image %s at addr %x\", String( src->label[3] ), src->address );\n        }\n      } else if( partitionIsFactory(src)  ) { // compute the digest\n        if( esp_partition_get_sha256(src, data.image_digest) != ESP_OK ) {\n          memset( data.image_digest, 0, sizeof(data.image_digest) );\n          data.image_len = 0;\n        }\n      }\n      return data;\n    }\n\n\n    bool hasFactory()\n    {\n      factory_partition = getFactoryPartition();\n      if(!factory_partition) return false;\n      return true;\n    }\n\n\n    bool hasFactoryApp()\n    {\n      if( !hasFactory() ) return false;\n      auto meta = getSketchMeta( factory_partition );\n      digest_t digests = digest_t();\n      if( digests.isEmpty( meta.image_digest ) ) return false;\n      return true;\n    }\n\n\n    bool isRunningFactory()\n    {\n      running_partition = esp_ota_get_running_partition();\n      if( !hasFactory() ) return false;\n      return factory_partition==running_partition;\n    }\n\n\n\n    bool saveSketchToPartition( const esp_partition_t* dst_partition )\n    {\n      if (!running_partition) {\n        log_e( \"Can't fetch running partition info !!\" );\n        return false;\n      }\n\n      size_t sksize = ESP.getSketchSize();\n\n      if (!comparePartition(running_partition, dst_partition, sksize)) {\n        if( copyPartition( dst_partition, running_partition, sksize) ) {\n          log_d(\"Sketch successfully propagated to destination partition\");\n          return true;\n        } else {\n          log_e(\"Sketch propagation to destination partition failed\");\n          return false;\n        }\n      } else {\n        log_i(\"Current sketch and destination partition already match\");\n        return true;\n      }\n      return false;\n    }\n\n\n\n    bool saveSketchToFactory()\n    {\n      if( isRunningFactory() ) {\n        log_d(\"Sketch is running from factory partition, no need to propagate\");\n        return false;\n      }\n\n      if( !hasFactory() ) {\n        log_w( \"This flash has no factory partition\" );\n        return false;\n      }\n\n      if (!running_partition) {\n        log_e( \"Can't fetch running partition info !!\" );\n        return false;\n      }\n\n      size_t sksize = ESP.getSketchSize();\n\n      if (!comparePartition(running_partition, factory_partition, sksize)) {\n        if( copyPartition( factory_partition, running_partition, sksize) ) {\n          log_d(\"Sketch successfully propagated to factory partition\");\n          return true;\n        } else {\n          log_e(\"Sketch propagation to factory partition failed\");\n          return false;\n        }\n      } else {\n        log_i(\"Current sketch and factory partition already match\");\n        return true;\n      }\n      return false;\n    }\n\n\n    const esp_partition_t* getFactoryPartition()\n    {\n      auto factorypi = esp_partition_find( ESP_PARTITION_TYPE_APP,  ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL );\n      if( factorypi != NULL ) {\n        return esp_partition_get(factorypi);\n      }\n      return NULL;\n    }\n\n\n    const esp_partition_t* getPartition( uint8_t ota_num )\n    {\n      esp_partition_subtype_t subtype = (esp_partition_subtype_t)(ESP_PARTITION_SUBTYPE_APP_OTA_MIN+ota_num);\n      esp_partition_iterator_t pi = esp_partition_find(ESP_PARTITION_TYPE_APP, subtype, NULL);\n      if( pi != NULL ) {\n        const esp_partition_t* part = esp_partition_get(pi);\n        esp_partition_iterator_release(pi);\n        return part;\n      }\n      return nullptr;\n    }\n\n\n    Partition_t findPartition( uint8_t ota_num )\n    {\n      auto part = getPartition( ota_num );\n      auto meta = esp_image_metadata_t();\n      if( part ) {\n        meta = getSketchMeta(part);\n      }\n      return {*part, meta};\n    }\n\n\n    Partition_t* findDupePartition( esp_image_metadata_t *meta, esp_partition_type_t type, esp_partition_subtype_t filter_subtype )\n    {\n      if( Partitions.size()==0 ) {\n        scan();\n      }\n      digest_t digests = digest_t();\n      for( int i=0; i<Partitions.size(); i++ ) {\n        if( Partitions[i].part.type==type\n         && Partitions[i].part.subtype!=filter_subtype\n         && digests.match( meta->image_digest, Partitions[i].meta.image_digest ) ) {\n          return &Partitions[i];\n        }\n      }\n      return nullptr;\n    }\n\n\n    bool bootPartition( uint8_t ota_num )\n    {\n      const esp_partition_t* part = getPartition( ota_num );\n      if ( !part ) {\n        log_e( \"Failed to find partition OTA%d\", ota_num ) ;\n        return false;\n      }\n      esp_err_t err = esp_ota_set_boot_partition ( part );\n      if ( err != ESP_OK ) {\n        log_e( \"Failed to set OTA%d as boot partition\", ota_num );\n        return false;\n      }\n      log_i(\"Will reboot to partition OTA%d\", ota_num);\n      esp_restart() ;\n      return true;\n    }\n\n\n    bool partitionIsApp( const esp_partition_t *part )\n    {\n      return part->type==ESP_PARTITION_TYPE_APP;\n    }\n\n\n    bool partitionIsFactory( const esp_partition_t *part )\n    {\n      return partitionIsApp( part ) && part->subtype==ESP_PARTITION_SUBTYPE_APP_FACTORY;\n    }\n\n\n    bool partitionIsOTA( const esp_partition_t *part )\n    {\n      return partitionIsApp( part ) && (part->subtype>=ESP_PARTITION_SUBTYPE_APP_OTA_MIN && part->subtype<ESP_PARTITION_SUBTYPE_APP_OTA_MAX);\n    }\n\n\n    bool metadataHasDigest( const esp_image_metadata_t *meta )\n    {\n      digest_t digests = digest_t();\n      return meta && meta->image_digest ? !digests.isEmpty( meta->image_digest ) : false;\n    }\n\n\n    bool isEmpty( Partition_t* sdu_partition )\n    {\n      return partitionIsApp( &sdu_partition->part ) && !partitionIsFactory( &sdu_partition->part ) && !metadataHasDigest( &sdu_partition->meta );\n    }\n\n\n    const esp_partition_t* getNextAvailPartition(esp_partition_type_t type, esp_partition_subtype_t filter_subtype)\n    {\n      if( Partitions.size()==0 ) {\n        scan();\n      }\n      for( int i=0; i<Partitions.size(); i++ ) {\n        if( Partitions[i].part.type !=type ) continue;\n        if( Partitions[i].part.subtype ==filter_subtype ) continue;\n        if( isEmpty( &Partitions[i] ) ) {\n          return &Partitions[i].part;\n        }\n      }\n      return nullptr;\n    }\n\n\n    void memoize( const esp_partition_t *part )\n    {\n      esp_image_metadata_t meta = esp_image_metadata_t();\n\n      if( partitionIsApp(part) ) {\n        meta  = getSketchMeta( part );\n      }\n      Partitions.push_back({*part,meta});\n      if( partitionIsFactory( part ) ) {\n        log_v(\"Found factory partition\");\n        FactoryPartition = &Partitions[Partitions.size()-1];\n      }\n    }\n\n\n    void scan()\n    {\n      if( Partitions.size()>0 ) {\n        Partitions.clear(); // reset last scan results\n      }\n      esp_partition_iterator_t pi = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);\n      while(pi != NULL) {\n        const esp_partition_t* part = esp_partition_get(pi);\n        memoize( part );\n        pi = esp_partition_next( pi );\n      }\n      esp_partition_iterator_release(pi);\n    }\n\n\n    bool erase( const esp_partition_t *part )\n    {\n      assert(part);\n      log_d(\"Erasing ota partition %#x\", part->subtype );\n      if( part && ESP.partitionEraseRange(part, 0, part->size ) ) {\n        return true;\n      }\n      log_e(\"FATAL: can't erase partition\");\n      return false;\n    }\n\n\n    bool erase( uint8_t ota_num )\n    {\n      auto part = getPartition( ota_num );\n      return erase( part );\n    }\n\n  };\n};\n"
  },
  {
    "path": "src/PartitionManager/Partitions/PartitionUtils.hpp",
    "content": "#pragma once\n\n#include <memory>\n#include <vector>\n#include <esp_partition.h>\n#include <esp_flash.h>\nextern \"C\" {\n  #include \"esp_ota_ops.h\"\n  #include \"esp_image_format.h\"\n  #include \"bootloader_common.h\"\n}\n#include <Stream.h>\n#include <FS.h>\n#include <Stream.h>\n\n\nnamespace SDUpdaterNS\n{\n\n  namespace Flash\n  {\n\n    struct digest_t\n    {\n      String str{\"0000000000000000000000000000000000000000000000000000000000000000\"};\n      const char* toString( const uint8_t dig[32] );\n      bool match( const uint8_t d1[32], const uint8_t d2[32] );\n      bool isEmpty( const uint8_t d1[32] );\n    };\n\n    struct Partition_t\n    {\n      const esp_partition_t part;\n      const esp_image_metadata_t meta;\n    };\n\n    extern Partition_t* FactoryPartition;\n    extern std::vector<Partition_t> Partitions; // all partitions (flash)\n    extern const esp_partition_t* running_partition;\n    extern const esp_partition_t* nextupd_partition;\n    extern const esp_partition_t* factory_partition;\n\n    void loadFactory();\n    void memoize( const esp_partition_t *part );\n    void scan();\n\n    bool bootPartition( uint8_t ota_num );\n    bool erase( uint8_t ota_num );\n    bool erase( const esp_partition_t *part );\n\n    bool copyPartition(fs::FS *fs, const char* binfilename); // copy from OTA0 to OTA1 and filesystem\n    bool copyPartition(fs::File* dstFile, const esp_partition_t* src, size_t length); // copy from given partition to filesystem\n    bool copyPartition(fs::File* dstFile, const esp_partition_t* dst, const esp_partition_t* src, size_t length); // copy from given partition to other partition and filesystem\n    bool copyPartition(const esp_partition_t* dst, fs::FS *fs, const char* srcFilename); // copy from fs to given partition\n    bool copyPartition(const esp_partition_t* dst, Stream* src, size_t length); // copy from stream to given partition\n    bool copyPartition(const esp_partition_t* dst, const esp_partition_t* src, size_t length); // copy from one partition to another\n\n    bool comparePartition(const esp_partition_t* src1, const esp_partition_t* src2, size_t length);\n    bool comparePartition(const esp_partition_t* src1, fs::File* src2, size_t length);\n\n    bool hasFactory();\n    bool hasFactoryApp();\n    bool isRunningFactory(); // checks if running partition is the factory partition\n    bool saveSketchToFactory();\n    bool saveSketchToPartition( const esp_partition_t* dst_partition );\n    bool dumpFW( fs::FS *fs, const char* fw_name );\n    bool partitionIsApp( const esp_partition_t *part );\n    bool partitionIsOTA( const esp_partition_t *part );\n    bool partitionIsFactory( const esp_partition_t *part );\n    bool metadataHasDigest( const esp_image_metadata_t *meta );\n    bool isEmpty( Flash::Partition_t* sdu_partition );\n\n    const esp_partition_t* getPartition( uint8_t ota_num );\n    const esp_partition_t* getFactoryPartition();\n    const esp_partition_t* getNextAvailPartition( esp_partition_type_t type=ESP_PARTITION_TYPE_APP, esp_partition_subtype_t filter_subtype=ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n\n    Partition_t findPartition( uint8_t ota_num );\n    Partition_t *findDupePartition( esp_image_metadata_t *meta, esp_partition_type_t type=ESP_PARTITION_TYPE_APP, esp_partition_subtype_t filter_subtype=ESP_PARTITION_SUBTYPE_APP_OTA_MIN );\n\n    esp_image_metadata_t getSketchMeta( const esp_partition_t* source_partition );\n\n  };\n};\n"
  },
  {
    "path": "src/SDUpdater/SDUpdater_Class.cpp",
    "content": "#include \"./SDUpdater_Class.hpp\"\n\nnamespace SDUpdaterNS\n{\n\n\n\n  void SDUpdater::_error( const char **errMsgs, uint8_t msgCount, unsigned long waitdelay )\n  {\n    for( int i=0; i<msgCount; i++ ) {\n      _error( String(errMsgs[i]), i<msgCount-1?0:waitdelay );\n    }\n  }\n\n\n  void SDUpdater::_error( const String& errMsg, unsigned long waitdelay )\n  {\n    SDU_SERIAL.print(\"[ERROR] \");\n    SDU_SERIAL.println( errMsg );\n    if( SDUCfg.onError ) SDUCfg.onError( errMsg, waitdelay );\n  }\n\n\n  void SDUpdater::_message( const String& msg )\n  {\n    SDU_SERIAL.println( msg );\n    if( SDUCfg.onMessage ) SDUCfg.onMessage( msg );\n  }\n\n\n  bool SDUpdater::saveSketchToFS( SDUpdater* sdu, fs::FS &fs, const char* binfilename, bool skipIfExists )\n  {\n    assert(sdu);\n    // no rollback possible, start filesystem\n    if( !_fsBegin( sdu, fs ) ) {\n      const char *msg[] = {\"No Filesystem mounted.\",\"Can't check firmware.\"};\n      _error( msg, 2 );\n      return false;\n    }\n\n    if( skipIfExists ) {\n      if( fs.exists( binfilename ) ) {\n        log_d(\"File %s exists, skipping overwrite\", binfilename );\n        //_message( String(\"\\nChecked \") + String(binfilename) );\n        return false;\n      }\n    }\n    if( SDUCfg.onBefore) SDUCfg.onBefore();\n    if( SDUCfg.onProgress ) SDUCfg.onProgress( 0, 100 );\n    const esp_partition_t *running = esp_ota_get_running_partition();\n    size_t sksize = ESP.getSketchSize();\n    bool ret = false;\n    fs::File dst = fs.open(binfilename, FILE_WRITE );\n    if( SDUCfg.onProgress ) SDUCfg.onProgress( 25, 100 );\n    _message( String(\"Overwriting \") + String(binfilename) );\n\n    if (Flash::copyPartition( &dst, running, sksize)) {\n      if( SDUCfg.onProgress ) SDUCfg.onProgress( 75, 100 );\n      _message( String(\"Done \") + String(binfilename) );\n      vTaskDelay(1000);\n      ret = true;\n    } else {\n      _error( \"Copy failed\" );\n    }\n    if( SDUCfg.onProgress ) SDUCfg.onProgress( 100, 100 );\n    dst.close();\n    if( SDUCfg.onAfter) SDUCfg.onAfter();\n\n    return ret;\n  }\n\n\n  // rollback helper, save menu.bin meta info in NVS\n  void SDUpdater::updateNVS()\n  {\n    SDU_SERIAL.printf( \"Updating NVS preferences with current partition's size/digest\" );\n    NVS::saveMenuPrefs();\n  }\n\n\n  // perform the actual update from a given stream\n  void SDUpdater::performUpdate( Stream &updateSource, size_t updateSize, String fileName )\n  {\n    assert(UpdateIface);\n    UpdateIface->setBinName( fileName, &updateSource );\n\n    _message( \"LOADING \" + fileName );\n    log_d( \"Binary size: %d bytes\", updateSize );\n    if( cfg->onProgress ) UpdateIface->onProgress( cfg->onProgress );\n    if (UpdateIface->begin( updateSize )) {\n      size_t written = UpdateIface->writeStream( updateSource, updateSize );\n      if ( written == updateSize ) {\n        SDU_SERIAL.println( \"Written : \" + String(written) + \" successfully\" );\n      } else {\n        SDU_SERIAL.println( \"Written only : \" + String(written) + \"/\" + String(updateSize) + \". Retry?\" );\n      }\n      if ( UpdateIface->end() ) {\n        SDU_SERIAL.println( \"OTA done!\" );\n        if ( UpdateIface->isFinished() ) {\n          if( strcmp( MenuBin, fileName.c_str() ) == 0 ) {\n            // maintain NVS signature\n            SDUpdater::updateNVS();\n          }\n          SDU_SERIAL.println( \"Update successfully completed. Rebooting.\" );\n        } else {\n          SDU_SERIAL.println( \"Update not finished? Something went wrong!\" );\n        }\n      } else {\n        SDU_SERIAL.println( \"Update failed. Error #: \" + String( UpdateIface->getError() ) );\n      }\n    } else {\n      SDU_SERIAL.println( \"Not enough space to begin OTA\" );\n    }\n  }\n\n\n  // forced rollback (doesn't check NVS digest)\n  void SDUpdater::doRollBack( const String& message )\n  {\n    assert(UpdateIface);\n    SDU_SERIAL.println( SDU_ROLLBACK_MSG );\n    log_d(\"Wil check for rollback capability\");\n    if( !cfg->onMessage)   { log_d(\"No message reporting\"); }\n    //if( !cfg->onError )    log_d(\"No error reporting\");\n    if( !cfg->onProgress ) { log_d(\"No progress reporting\"); }\n\n    if( UpdateIface->canRollBack() ) {\n      _message( message );\n      for( uint8_t i=1; i<50; i++ ) {\n        if( cfg->onProgress ) cfg->onProgress( i, 100 );\n        vTaskDelay(10);\n      }\n      UpdateIface->rollBack();\n      for( uint8_t i=50; i<=100; i++ ) {\n        if( cfg->onProgress ) cfg->onProgress( i, 100 );\n        vTaskDelay(10);\n      }\n      _message( \"Rollback done, restarting\" );\n      ESP.restart();\n    } else {\n      const char *msg[] = {\"Cannot rollback\", \"The other OTA\", \"partition doesn't\", \"seem to be\", \"populated or valid\"};\n      _error( msg, 5 );\n    }\n  }\n\n\n  void SDUpdater::tryRollback( String fileName )\n  {\n    if( cfg->rollBackToFactory ) return; // SDU settings: factory supersedes rollback\n    // if NVS has info about MENU_BIN flash size and digest, try rollback()\n    uint32_t menuSize;// = preferences.getInt( \"menusize\", 0 );\n    uint8_t image_digest[32];\n    NVS::getMenuPrefs( &menuSize, image_digest );\n\n    SDU_SERIAL.println( \"Trying rollback\" );\n\n    if( menuSize == 0 ) {\n      log_d( \"Failed to get expected menu size from NVS ram, can't check if rollback is worth a try...\" );\n      return;\n    }\n\n    const esp_partition_t* update_partition = esp_ota_get_next_update_partition( NULL );\n    if (!update_partition) {\n      log_d( \"Cancelling rollback as update partition is invalid\" );\n      return;\n    }\n    esp_image_metadata_t sketchMeta = Flash::getSketchMeta( update_partition );\n    uint32_t nuSize = sketchMeta.image_len;\n\n    if( nuSize != menuSize ) {\n      log_d( \"Cancelling rollback as flash sizes differ, update / current : %d / %d\",  nuSize, menuSize );\n      return;\n    }\n\n    SDU_SERIAL.println( \"Sizes match! Checking digest...\" );\n    bool match = true;\n    for( uint8_t i=0; i<32; i++ ) {\n      if( image_digest[i]!=sketchMeta.image_digest[i] ) {\n        SDU_SERIAL.println( \"NO match for NVS digest :-(\" );\n        match = false;\n        break;\n      }\n    }\n    if( match ) {\n      doRollBack( \"HOT-LOADING \" + fileName );\n    }\n  }\n\n\n  // do perform update\n  void SDUpdater::updateFromStream( Stream &stream, size_t updateSize, const String& fileName )\n  {\n    if ( updateSize > 0 ) {\n      SDU_SERIAL.println( \"Try to start update\" );\n      disableCore0WDT(); // disable WDT it as suggested by twitter.com/@lovyan03\n      performUpdate( stream, updateSize, fileName );\n      enableCore0WDT();\n    } else {\n      _error( \"Stream is empty\" );\n    }\n  }\n\n\n  void SDUpdater::updateFromFS( fs::FS &fs, const String& fileName )\n  {\n    cfg->setFS( &fs );\n    updateFromFS( fileName );\n  }\n\n\n  void SDUpdater::checkUpdaterHeadless( fs::FS &fs, String fileName )\n  {\n    cfg->setFS( &fs );\n    checkUpdaterHeadless( fileName );\n  }\n\n\n  void SDUpdater::checkUpdaterUI( fs::FS &fs, String fileName )\n  {\n    cfg->setFS( &fs );\n    checkUpdaterUI( fileName );\n  }\n\n\n  void SDUpdater::updateFromFS( const String& fileName )\n  {\n    SDU_SERIAL.printf( \"[\" SD_PLATFORM_NAME \"-SD-Updater] SD Updater version: %s\\n\", (char*)M5_SD_UPDATER_VERSION );\n    #ifdef M5_LIB_VERSION\n      SDU_SERIAL.printf( \"[\" SD_PLATFORM_NAME \"-SD-Updater] M5Stack Core version: %s\\n\", (char*)M5_LIB_VERSION );\n    #endif\n    SDU_SERIAL.printf( \"[\" SD_PLATFORM_NAME \"-SD-Updater] Application was Compiled on %s %s\\n\", __DATE__, __TIME__ );\n    SDU_SERIAL.printf( \"[\" SD_PLATFORM_NAME \"-SD-Updater] Will attempt to load binary %s \\n\", fileName.c_str() );\n\n    // try from flash, same as rollback but found from NVS partition table\n    if( fileName!=\"\" && Flash::hasFactoryApp() ) {\n      auto part = NVS::findPartition( fileName.c_str() );\n      if( part ) {\n        Flash::bootPartition( part->ota_num );\n      }\n    }\n    // try rollback\n    if( strcmp( MenuBin, fileName.c_str() ) == 0 ) {\n      if( cfg->use_rollback ) {\n        tryRollback( fileName );\n        log_e(\"Rollback failed, will try from filesystem\");\n      } else {\n        log_d(\"Skipping rollback per config\");\n      }\n    }\n    // no bootPartition()/rollback() possible, can't go any further without filesystem\n    if( cfg->fs == nullptr ) {\n      const char *msg[] = {\"No valid filesystem\", \"selected!\"};\n      _error( msg, 2 );\n      return;\n    }\n    // start filesystem\n    if( !_fsBegin(this) ) {\n      const char* msg[] = {\"No filesystem mounted.\", \"Can't load firmware.\"};\n      _error( msg, 2 );\n      return;\n    }\n\n    File updateBin = cfg->fs->open( fileName );\n    if ( updateBin ) {\n      updateFromStream( updateBin, updateBin.size(), fileName );\n      updateBin.close();\n    } else {\n      const char* msg[] = {\"Could not reach\", fileName.c_str(), \"Can't load firmware.\"};\n      _error( msg, 3 );\n    }\n  }\n\n\n  void SDUpdater::checkUpdaterHeadless( String fileName )\n  {\n    if( SDUCfg.waitdelay == 0 ) {\n      SDUCfg.waitdelay = 100; // at least give some time for the serial buffer to fill\n    }\n    SDU_SERIAL.printf(\"SDUpdater: you have %d milliseconds to send 'update', 'rollback', 'skip' or 'save' command\\n\", (int)SDUCfg.waitdelay);\n\n    if( !checkUpdaterCommon( fileName ) ) return;\n\n    SDU_SERIAL.println(\"Delay expired, no SD-Update will occur\");\n  }\n\n\n  void SDUpdater::checkUpdaterUI( String fileName )\n  {\n    bool draw = SDUHasTouch || (SDUCfg.waitdelay>100 && !SDUHasTouch); // note: draw forced if waitdelay>100\n\n    if( SDUCfg.waitdelay <= 100 ) {\n      draw = false;    // 100ms delay will just look like a blink, cancel UI draw\n      SDUCfg.waitdelay = 100; // button press/touch on boot still needs \"blind\" detection, round up to 100ms for debounce\n    }\n\n    if( draw ) { // bring up the UI\n      if( cfg->onBefore) cfg->onBefore();\n      if( cfg->onSplashPage) cfg->onSplashPage( BTN_HINT_MSG );\n    }\n\n    checkUpdaterCommon( fileName );\n\n    if( draw ) {\n      // reset text styles to avoid messing with the overlayed application\n      if( cfg->onAfter ) cfg->onAfter();\n    }\n  }\n\n\n  // common logic to checkUpdaterUI() and checkUpdaterHeadless()\n  bool SDUpdater::checkUpdaterCommon( String fileName )\n  {\n    bool hasFileName = (fileName!=\"\");\n\n    // if using factory: disable \"Save FW\" action button when running partition isn't the first partition\n    SDUCfg.Buttons[2].enabled = cfg->rollBackToFactory\n      ? esp_ota_get_running_partition()->subtype==ESP_PARTITION_SUBTYPE_APP_OTA_MIN\n      : SDUCfg.Buttons[2].enabled\n    ;\n\n    if( cfg->onWaitForAction ) {\n      int action = cfg->onWaitForAction( !hasFileName ? (char*)cfg->labelRollback : (char*)cfg->labelMenu,  (char*)cfg->labelSkip, (char*)cfg->labelSave, SDUCfg.waitdelay );\n\n      if ( action == ConfigManager::SDU_BTNA_MENU ) { // all BtnA successful actions trigger a restart\n        if( hasFileName ) {\n          if( cfg->fs == nullptr ) {\n            const char* msg[] = {\"No valid filesystem\", \"selected!\", \"Cannot load\", fileName.c_str()};\n            _error( msg, 4 );\n            return false;\n          }\n          SDU_SERIAL.printf( SDU_LOAD_TPL, fileName.c_str() );\n          updateFromFS( fileName );\n          ESP.restart();\n        } else {\n          if( cfg->rollBackToFactory ) Flash::loadFactory();\n          doRollBack( SDU_ROLLBACK_MSG );\n        }\n        return false; // rollback failed\n      } else if( cfg->binFileName != nullptr ) {\n        if( cfg->fs != nullptr ) {\n          log_v(\"Checking if %s needs saving\", cfg->binFileName );\n          saveSketchToFS( *cfg->fs,  cfg->binFileName, action != ConfigManager::SDU_BTNC_SAVE );\n        } else if( cfg->rollBackToFactory ) {\n          return PartitionManager::migrateSketch( cfg->binFileName );\n        } else if( action == ConfigManager::SDU_BTNC_SAVE ) {\n          const char* msg[] = {\"No valid filesystem\", \"selected!\", \"Cannot save\", fileName.c_str()};\n          _error( msg, 4 );\n          return false;\n        }\n      }\n      return true;\n    } else {\n      _error( \"Missing onWaitForAction!\" );\n      return false;\n    }\n  }\n\n\n}; // end namespace\n"
  },
  {
    "path": "src/SDUpdater/SDUpdater_Class.hpp",
    "content": "#pragma once\n\n#include \"../gitTagVersion.h\"\n#include <esp_partition.h> // required by getSketchMeta(), compareFsPartition() and copyFsPartition() methods\nextern \"C\" {\n  #include \"esp_ota_ops.h\"\n  #include \"esp_image_format.h\"\n}\n// required to guess the reset reason\n#if defined ESP_IDF_VERSION_MAJOR && ESP_IDF_VERSION_MAJOR >= 4\n  #if defined CONFIG_IDF_TARGET_ESP32\n    #include <esp32/rom/rtc.h>\n  #elif defined CONFIG_IDF_TARGET_ESP32S2\n    #include <esp32s2/rom/rtc.h>\n  #elif defined CONFIG_IDF_TARGET_ESP32C3\n    #include <esp32c3/rom/rtc.h>\n  #elif defined CONFIG_IDF_TARGET_ESP32S3\n    #include <rom/rtc.h>\n  #else\n    #warning \"Target CONFIG_IDF_TARGET is unknown\"\n    #include <rom/rtc.h>\n  #endif\n#else\n  #include <rom/rtc.h>\n#endif\n\n#include <FS.h>\n// #include <Update.h>\n// required to store the MENU_BIN hash\n// #include <Preferences.h>\n\n#include \"../ConfigManager/ConfigManager.hpp\"\n#include \"../PartitionManager/PartitionManager.hpp\"\n\n\nnamespace SDUpdaterNS\n{\n\n  extern void updateFromFS( const String& fileName );\n  // provide an imperative function to avoid breaking button-based (older) versions of the M5Stack SD Updater\n  extern void updateFromFS( fs::FS &fs, const String& fileName = MENU_BIN, const int TfCardCsPin = TFCARD_CS_PIN );\n  // copy compiled sketch from flash partition to filesystem binary file\n  extern bool saveSketchToFS(fs::FS &fs, const char* binfilename = PROGMEM {MENU_BIN}, const int TfCardCsPin = TFCARD_CS_PIN );\n  // provide a rollback function for custom usages\n  extern void updateRollBack( String message );\n  // provide a conditional function to cover more devices, including headless and touch\n  extern void checkSDUpdater( fs::FS &fs, String fileName = MENU_BIN, unsigned long waitdelay = 0, const int TfCardCsPin_ = TFCARD_CS_PIN );\n\n  using ConfigManager::config_sdu_t;\n  using UpdateInterfaceNS::UpdateManagerInterface_t;\n\n  #if !defined SDU_SERIAL\n    #define SDU_SERIAL Serial\n  #endif\n\n  class SDUpdater\n  {\n    public:\n\n      SDUpdater( config_sdu_t* _cfg );\n      SDUpdater( const int TFCardCsPin_ = TFCARD_CS_PIN);\n\n      // check methods\n      void checkUpdaterHeadless( String fileName );\n      void checkUpdaterHeadless( fs::FS &fs, String fileName );\n      void checkUpdaterUI( String fileName );\n      void checkUpdaterUI( fs::FS &fs, String fileName );\n      // update methods\n      void updateFromFS( const String& fileName );\n      void updateFromFS( fs::FS &fs, const String& fileName = MENU_BIN );\n      void updateFromStream( Stream &stream, size_t updateSize, const String& fileName );\n      void doRollBack( const String& message = \"\" );\n\n      static bool saveSketchToFS( SDUpdater* sdu, fs::FS &fs, const char* binfilename={MENU_BIN}, bool skipIfExists=false );\n      inline bool saveSketchToFS( fs::FS &fs, const char* binfilename={MENU_BIN}, bool skipIfExists=false ) { return saveSketchToFS(this, fs, binfilename, skipIfExists ); }\n\n      static bool saveSketchToFactory();\n      //static bool migrateSketch();\n      static void updateNVS();\n\n      // fs::File->name() changed behaviour after esp32 sdk 2.x.x\n      inline static const char* fs_file_path( fs::File *file )\n      {\n        #if defined ESP_IDF_VERSION_MAJOR && ESP_IDF_VERSION_MAJOR >= 4\n          return file->path();\n        #else\n          return file->name();\n        #endif\n      }\n\n      static void _error( const String& errMsg, unsigned long waitdelay = 2000 );\n      static void _error( const char **errMsgs, uint8_t msgCount=1, unsigned long waitdelay=2000 );\n      static void _message( const String& label );\n      config_sdu_t* cfg;\n\n    private:\n\n      UpdateManagerInterface_t *UpdateIface = nullptr;\n      const char* MenuBin = MENU_BIN;\n      bool checkUpdaterCommon( String fileName );\n      void performUpdate( Stream &updateSource, size_t updateSize, String fileName );\n      void tryRollback( String fileName );\n\n      #if defined _M5Core2_H_ || defined _M5CORES3_H_\n        // Implicitely assume touch button support for TFT_eSpi based cores as per M5.begin() default behaviour\n        const bool SDUHasTouch = true;\n      #else\n        const bool SDUHasTouch = false;\n      #endif\n      static bool _fsBegin( SDUpdater* sdu, bool report_errors = true );\n      static bool _fsBegin( SDUpdater* sdu, fs::FS &fs, bool report_errors = true );\n\n  };\n\n\n\n  inline SDUpdater::SDUpdater( config_sdu_t* _cfg ) : cfg(_cfg)\n  {\n    if( !UpdateIface ) {\n      UpdateIface = ConfigManager::GetUpdateInterface();\n    }\n    if( ConfigManager::SDUCfgLoader ) {\n      log_v(\"Config manager loader called\");\n      ConfigManager::SDUCfgLoader();\n    } else {\n      cfg->setDefaults();\n    }\n    cfg->fs_begun = _fsBegin( this, false );\n  };\n\n\n  // legacy constructor\n  inline SDUpdater::SDUpdater( const int TFCardCsPin_ )\n  {\n    if( !UpdateIface ) {\n      UpdateIface = ConfigManager::GetUpdateInterface();\n    }\n    //log_d(\"SDUpdater base mode on CS pin(%d)\", TFCardCsPin_ );\n    SDUCfg.setCSPin( TFCardCsPin_ );\n    cfg = &SDUCfg;\n    if( ConfigManager::SDUCfgLoader ) {\n      log_v(\"Config manager loader called\");\n      ConfigManager::SDUCfgLoader();\n    } else {\n      cfg->setDefaults();\n    }\n    cfg->fs_begun = _fsBegin( this, false );\n  };\n\n\n  inline bool SDUpdater::_fsBegin( SDUpdater* sdu, bool report_errors )\n  {\n    if( SDUCfg.fs != nullptr ) return _fsBegin( sdu, *SDUCfg.fs, report_errors );\n    if( !SDUCfg.mounted && report_errors ) _error( \"No filesystem selected\" ); // Note: rollback does not need filesystem\n    return false;\n  }\n\n\n  inline bool SDUpdater::_fsBegin( SDUpdater* sdu, fs::FS &fs, bool report_errors )\n  {\n    if( SDUCfg.fs_begun ) return true;\n    if( SDUCfg.fsChecker ) return SDUCfg.fsChecker( sdu, *SDUCfg.fs, report_errors );\n    return false;\n  }\n\n\n  inline bool SDUpdater::saveSketchToFactory()\n  {\n    if( !SDUCfg.triggers ) {\n      SDUCfg.setDefaults();\n    }\n    return PartitionManager::flashFactory();\n  }\n\n\n  // inline bool SDUpdater::migrateSketch()\n  // {\n  //   if( !SDUCfg.binFileName ) return false; // need this in NVS\n  //   if( !SDUCfg.triggers ) {\n  //     SDUCfg.setDefaults();\n  //   }\n  //   return PartitionManager::migrateSketch( SDUCfg.binFileName );\n  // }\n\n\n\n}; // end namespace\n"
  },
  {
    "path": "src/SDUpdater/Update_Interface.hpp",
    "content": "#pragma once\n\n\n#if defined SDU_HAS_TARGZ // binary may or may not be gzipped\n\n  // some macros with if(gz) logic\n  #define GzUpdate GzUpdateClass::getInstance()\n  #define GzUpdateEnd() (mode_z ? GzUpdate.endgz() : GzUpdate.end())\n  #define GzAbort() if (mode_z) GzUpdate.abortgz(); else GzUpdate.abort()\n  #define GzWriteStream(updateSource,updateSize) (mode_z ? GzUpdate.writeGzStream(updateSource,updateSize) : GzUpdate.writeStream(updateSource))\n  #define GzCanBegin( usize ) (mode_z ? GzUpdate.begingz(UPDATE_SIZE_UNKNOWN) : GzUpdate.begin(usize))\n  #define GzEnd() (mode_z ? GzUpdate.endgz() : GzUpdate.end() )\n\n  namespace SDUpdaterNS\n  {\n    namespace ConfigManager\n    {\n      using namespace UpdateInterfaceNS;\n      inline UpdateManagerInterface_t *GetUpdateInterface()\n      {\n        static UpdateManagerInterface_t Iface =\n        {\n          .begin       = [](size_t s)->bool{ return GzCanBegin(s); },\n          .writeStream = [](Stream &data,size_t size)->size_t{ return GzWriteStream(data, size); },\n          .abort       = [](){ GzAbort(); },\n          .end         = []()->bool{ return GzEnd(); },\n          .isFinished  = []()->bool{ return GzUpdate.isFinished(); },\n          .canRollBack = []()->bool{ return GzUpdate.canRollBack(); },\n          .rollBack    = []()->bool{ return GzUpdate.rollBack(); },\n          .onProgress  = [](UpdateClass::THandlerFunction_Progress fn){ GzUpdate.onProgress(fn); },\n          .getError    = []()->uint8_t{ return GzUpdate.getError(); },\n          .setBinName  = []( String& fileName, Stream* stream ) { mode_z=fileName.endsWith(\".gz\")?(stream->peek()==0x1f):false; log_d(\"Compression %s\", mode_z?\"enabled\":\"disabled\"); }\n        };\n        return &Iface;\n      }\n\n    };\n  };\n\n#else // no gzip support, only native firmwares\n\n  namespace SDUpdaterNS\n  {\n    namespace ConfigManager\n    {\n      using namespace UpdateInterfaceNS;\n      inline UpdateManagerInterface_t *GetUpdateInterface()\n      {\n        static UpdateManagerInterface_t Iface =\n        {\n          .begin       = [](size_t s)->bool{ return Update.begin(s); },\n          .writeStream = [](Stream &data,size_t size)->size_t{ return Update.writeStream(data); },\n          .abort       = [](){ Update.abort(); },\n          .end         = []()->bool{ return Update.end(); },\n          .isFinished  = []()->bool{ return Update.isFinished(); },\n          .canRollBack = []()->bool{ return Update.canRollBack(); },\n          .rollBack    = []()->bool{ return Update.rollBack(); },\n          .onProgress  = [](UpdateClass::THandlerFunction_Progress fn){ Update.onProgress(fn); },\n          .getError    = []()->uint8_t{ return Update.getError(); },\n          .setBinName  = [](String&fileName, Stream* stream) { if(fileName.endsWith(\".gz\")) log_e(\"Gz file detected but gz support is disabled!\"); }\n        };\n        return &Iface;\n      }\n\n    };\n  }\n\n#endif\n"
  },
  {
    "path": "src/UI/Touch.hpp",
    "content": "#pragma once\n\n#include \"../misc/assets.h\"\n#include \"../misc/types.h\"\n#include \"../UI/common.hpp\"\n\nnamespace SDUpdaterNS\n{\n\n  namespace SDU_UI\n  {\n\n    struct TouchButtonWrapper\n    {\n      bool iconRendered = false;\n      void handlePressed( SDU_TouchButton *btn, bool pressed, uint16_t x, uint16_t y);\n      void handleJustPressed( SDU_TouchButton *btn, const char* label );\n      bool justReleased( SDU_TouchButton *btn, bool pressed, const char* label );\n      void pushIcon(const char* label);\n    };\n\n    inline void TouchButtonWrapper::handlePressed( SDU_TouchButton *btn, bool pressed, uint16_t x, uint16_t y)\n    {\n      if (pressed && btn->contains(x, y)) {\n        log_v(\"Press at [%d:%d]\", x, y );\n        btn->press(true); // tell the button it is pressed\n      } else {\n        if( pressed ) {\n          log_v(\"Outside Press at [%d:%d]\", x, y );\n        }\n        btn->press(false); // tell the button it is NOT pressed\n      }\n    }\n\n    inline void TouchButtonWrapper::handleJustPressed( SDU_TouchButton *btn, const char* label )\n    {\n      if ( btn->justPressed() ) {\n        btn->drawButton(true, label);\n        pushIcon( label );\n      }\n    }\n\n    inline bool TouchButtonWrapper::justReleased( SDU_TouchButton *btn, bool pressed, const char* label )\n    {\n      bool ret = false;\n      if ( btn->justReleased() && (!pressed)) {\n        // callable\n        ret = true;\n      } else if ( btn->justReleased() && (pressed)) {\n        // state change but not callable\n        ret = false;\n      } else {\n        // no change, no need to draw\n        return false;\n      }\n      btn->drawButton(false, label);\n      pushIcon( label );\n      return ret;\n    }\n\n    inline void TouchButtonWrapper::pushIcon(const char* label)\n    {\n      if( strcmp( label, SDUCfg.labelMenu ) == 0 || strcmp( label, SDUCfg.labelRollback ) == 0 )\n      {\n        TouchStyles bs;\n        auto IconSprite = SDU_Sprite( SDU_GFX );\n        IconSprite.createSprite(15,16);\n        if( SDUCfg.rollBackToFactory ) {\n          IconSprite.drawJpg(flashUpdaterIcon16x16_jpg, flashUpdaterIcon16x16_jpg_len, 0,0, 16, 16);\n        } else {\n          IconSprite.drawJpg(sdUpdaterIcon15x16_jpg, sdUpdaterIcon15x16_jpg_len, 0,0, 15, 16);\n        }\n        IconSprite.pushSprite( bs.icon_x, bs.icon_y, SDU_GFX->color565( 0x01, 0x00, 0x80 ) );\n        IconSprite.deleteSprite();\n      }\n    }\n\n\n    inline TouchStyles::~TouchStyles()\n    {\n      if( Load ) delete Load;\n      if( Skip ) delete Skip;\n      if( Save ) delete Save;\n    }\n\n    inline TouchStyles::TouchStyles()\n    {\n      padx    = 4;                                    // buttons padding X\n      pady    = 1;                                    // buttons padding Y\n      marginx = 2;                                    // buttons margin X\n      marginy = 2;                                    // buttons margin Y\n      x1      = marginx + SDU_GFX->width()/4;              // button 1 X position\n      x2      = marginx+SDU_GFX->width()-SDU_GFX->width()/4;    // button 2 X position\n      x3      = SDU_GFX->width()/2;                         // button 3 X position\n      y       = SDU_GFX->height()/2;                       // buttons Y position\n      w       = (SDU_GFX->width()/2)-(marginx*2);          // buttons width\n      h       = SDU_GFX->height()/5,                       // buttons height\n      y1      = marginx*3+SDU_GFX->height()-h;               // button3 y position\n      icon_x  = marginx+12;                           // icon (button 1) X position\n      icon_y  = y-8;                                  // icon (button 1) Y position\n      pgbar_x = SDU_GFX->width()/2+(marginx*2)+(padx*2)-1; // progressbar X position\n      pgbar_y = (y+h/2)+(marginy*2)-1;                // progressbar Y position\n      pgbar_w = w-(marginx*4)-(padx*4);               // progressbar width\n      btn_fsize = (SDU_GFX->width()>240?2:1);               // touch buttons font size\n      Load = new BtnStyle_t( (uint16_t)TFT_ORANGE,                 SDU_GFX->color565( 0xaa, 0x00, 0x00), SDU_GFX->color565( 0xdd, 0xdd, 0xdd), (uint16_t)TFT_BLACK );\n      Skip = new BtnStyle_t( SDU_GFX->color565( 0x11, 0x11, 0x11), SDU_GFX->color565( 0x33, 0x88, 0x33), SDU_GFX->color565( 0xee, 0xee, 0xee), (uint16_t)TFT_BLACK );\n      Save = new BtnStyle_t( (uint16_t)TFT_ORANGE,                 (uint16_t)TFT_BLACK,                  (uint16_t)TFT_WHITE,                  (uint16_t)TFT_BLACK );\n    }\n\n  }\n\n\n\n  namespace TriggerSource\n  {\n\n\n    struct touchTriggerElements_t\n    {\n      touchTriggerElements_t() { };\n      touchTriggerElements_t( SDU_TouchButton *_LoadBtn, SDU_TouchButton *_SkipBtn, SDU_TouchButton *_SaveBtn, SDU_UI::TouchButtonWrapper _tbWrapper, SDU_UI::TouchStyles _ts )\n      : LoadBtn(_LoadBtn), SkipBtn(_SkipBtn), SaveBtn(_SaveBtn), tbWrapper(_tbWrapper), ts(_ts) { }\n      SDU_TouchButton *LoadBtn{nullptr};\n      SDU_TouchButton *SkipBtn{nullptr};\n      SDU_TouchButton *SaveBtn{nullptr};\n      SDU_UI::TouchButtonWrapper tbWrapper;\n      SDU_UI::TouchStyles ts;\n      bool ispressed = false;\n      uint16_t t_x{0};\n      uint16_t t_y{0}; // To store the touch coordinates\n    };\n\n\n\n    static void triggerInitTouch(triggerMap_t* trigger)\n    {\n      touchTriggerElements_t *el = new touchTriggerElements_t();\n\n      trigger->sharedptr = (void*)el;\n\n      el->LoadBtn = new SDU_TouchButton();\n      el->SkipBtn = new SDU_TouchButton();\n      el->SaveBtn = new SDU_TouchButton();\n\n      auto LoadBtn = el->LoadBtn;\n      auto SkipBtn = el->SkipBtn;\n      auto SaveBtn = el->SaveBtn;\n      auto &tbWrapper = el->tbWrapper;\n      auto &ts        = el->ts;\n      //SDU_UI::TouchStyles ts;\n\n      #if !defined HAS_LGFX\n        if( SDUCfg.Buttons[0].enabled ) LoadBtn->setFont(nullptr);\n        if( SDUCfg.Buttons[1].enabled ) SkipBtn->setFont(nullptr);\n        if( SDUCfg.binFileName != nullptr && SDUCfg.Buttons[2].enabled ) {\n          SaveBtn->setFont(nullptr);\n        }\n      #endif\n\n      if( SDUCfg.Buttons[0].enabled ) LoadBtn->initButton(\n        SDU_GFX,\n        ts.x1, ts.y,  ts.w, ts.h,\n        ts.Load->BorderColor, ts.Load->FillColor, ts.Load->TextColor,\n        (char*)trigger->labelLoad, ts.btn_fsize\n      );\n      if( SDUCfg.Buttons[1].enabled ) SkipBtn->initButton(\n        SDU_GFX,\n        ts.x2, ts.y,  ts.w, ts.h,\n        ts.Skip->BorderColor, ts.Skip->FillColor, ts.Skip->TextColor,\n        (char*)trigger->labelSkip, ts.btn_fsize\n      );\n\n      if( SDUCfg.binFileName != nullptr && SDUCfg.Buttons[2].enabled ) {\n        SaveBtn->initButton(\n          SDU_GFX,\n          ts.x3, ts.y1,  ts.w, ts.h,\n          ts.Save->BorderColor, ts.Save->FillColor, ts.Save->TextColor,\n          (char*)trigger->labelSave, ts.btn_fsize\n        );\n        SaveBtn->setLabelDatum(ts.padx, ts.pady, MC_DATUM);\n        SaveBtn->drawButton();\n        SaveBtn->press(false);\n      }\n\n      if( SDUCfg.Buttons[0].enabled ) LoadBtn->setLabelDatum(ts.padx, ts.pady, MC_DATUM);\n      if( SDUCfg.Buttons[1].enabled ) SkipBtn->setLabelDatum(ts.padx, ts.pady, MC_DATUM);\n\n      if( SDUCfg.Buttons[0].enabled ) LoadBtn->drawButton();\n      if( SDUCfg.Buttons[1].enabled ) SkipBtn->drawButton();\n\n      if( SDUCfg.Buttons[0].enabled ) LoadBtn->press(false);\n      if( SDUCfg.Buttons[1].enabled ) SkipBtn->press(false);\n\n      //uint16_t t_x = 0, t_y = 0; // To store the touch coordinates\n      //bool ispressed = false;\n      //int retval = -1; // return status\n\n      SDU_GFX->drawFastHLine( ts.pgbar_x, ts.pgbar_y, ts.pgbar_w-1, TFT_WHITE );\n    }\n\n    static bool triggerActionTouch(triggerMap_t* trigger, uint32_t msec )\n    {\n      auto el = (touchTriggerElements_t*)trigger->sharedptr;\n      if(!el) {\n        log_e(\"No trigger elements for this action, aborting!\");\n        return true;\n      }\n\n      auto LoadBtn    = el->LoadBtn;\n      auto SkipBtn    = el->SkipBtn;\n      auto SaveBtn    = el->SaveBtn;\n      auto &tbWrapper = el->tbWrapper;\n      auto &ts        = el->ts;\n      auto &ispressed = el->ispressed;\n      auto &t_x       = el->t_x;\n      auto &t_y       = el->t_y;\n\n      if( tbWrapper.iconRendered == false ) {\n        tbWrapper.pushIcon( trigger->labelLoad );\n        tbWrapper.iconRendered = true;\n      }\n      if( SDUCfg.Buttons[0].enabled ) tbWrapper.handlePressed( LoadBtn, ispressed, t_x, t_y );\n      if( SDUCfg.Buttons[1].enabled ) tbWrapper.handlePressed( SkipBtn, ispressed, t_x, t_y );\n      if( SDUCfg.binFileName != nullptr && SDUCfg.Buttons[2].enabled ) {\n        tbWrapper.handlePressed( SaveBtn, ispressed, t_x, t_y );\n      }\n      if( SDUCfg.Buttons[0].enabled ) tbWrapper.handleJustPressed( LoadBtn, trigger->labelLoad );\n      if( SDUCfg.Buttons[1].enabled ) tbWrapper.handleJustPressed( SkipBtn, trigger->labelSkip );\n      if( SDUCfg.binFileName != nullptr && SDUCfg.Buttons[2].enabled ) {\n        tbWrapper.handleJustPressed( SaveBtn, trigger->labelSave );\n      }\n\n      if( SDUCfg.Buttons[0].enabled && tbWrapper.justReleased( LoadBtn, ispressed, trigger->labelLoad ) ) {\n        trigger->ret = 1;\n        log_d(\"LoadBTN Pressed\");\n        return true;\n      }\n      if( SDUCfg.Buttons[1].enabled ) if( tbWrapper.justReleased( SkipBtn, ispressed, trigger->labelSkip ) ) {\n        trigger->ret = 0;\n        log_d(\"SkipBTN Pressed\");\n        return true;\n      }\n      if( SDUCfg.binFileName != nullptr && SDUCfg.Buttons[2].enabled ) {\n        if( tbWrapper.justReleased( SaveBtn, ispressed, trigger->labelSave ) ) {\n          trigger->ret = 2;\n          log_d(\"SaveBtn Pressed\");\n          return true;\n        }\n      }\n\n      #if defined HAS_LGFX\n        lgfx::touch_point_t tp;\n        uint16_t number = SDU_GFX->getTouch(&tp, 1);\n        t_x = tp.x;\n        t_y = tp.y;\n        ispressed = number > 0;\n      #else // M5Core2.h / TFT_eSPI_Button syntax\n        ispressed = SDU_GFX->getTouch(&t_x, &t_y);\n      #endif\n\n      float barprogress = float(millis() - msec) / float(trigger->waitdelay);\n      int linewidth = float(ts.pgbar_w) * barprogress;\n      if( linewidth > 0 ) {\n        int linepos = ts.pgbar_w - ( linewidth +1 );\n        uint16_t grayscale = 255 - (192*barprogress);\n        SDU_GFX->drawFastHLine( ts.pgbar_x,         ts.pgbar_y, ts.pgbar_w-linewidth-1, SDU_GFX->color565( grayscale, grayscale, grayscale ) );\n        SDU_GFX->drawFastHLine( ts.pgbar_x+linepos, ts.pgbar_y, 1,                      TFT_BLACK );\n      }\n      return false;\n    }\n\n    static void triggerFinalizeTouch( triggerMap_t* trigger, int ret )\n    {\n\n      auto el = (touchTriggerElements_t*)trigger->sharedptr;\n      if(!el) {\n        log_e(\"No trigger elements to delete!\");\n        return;\n      }\n\n      auto LoadBtn    = el->LoadBtn;\n      auto SkipBtn    = el->SkipBtn;\n      auto SaveBtn    = el->SaveBtn;\n\n      #if ! defined HAS_LGFX // defined _M5Core2_H_ || defined _M5CORES3_H_\n        // clear TFT_eSpi button handlers\n        if( SDUCfg.Buttons[0].enabled ) LoadBtn->delHandlers();\n        if( SDUCfg.Buttons[1].enabled ) SkipBtn->delHandlers();\n        if( SDUCfg.binFileName != nullptr && SDUCfg.Buttons[2].enabled ) SaveBtn->delHandlers();\n      #endif\n      delete LoadBtn;\n      delete SkipBtn;\n      delete SaveBtn;\n      delete el;\n      trigger->sharedptr = nullptr;\n    }\n\n\n    // static int touchButton( char* labelLoad, char* labelSkip, char* labelSave, unsigned long waitdelay )\n    // {\n    //   /* auto &tft = M5.Lcd; */\n    //   if( waitdelay == 0 ) return -1;\n    //   // touch support + buttons\n    //   SDU_TouchButton *LoadBtn = new SDU_TouchButton();\n    //   SDU_TouchButton *SkipBtn = new SDU_TouchButton();\n    //   SDU_TouchButton *SaveBtn = new SDU_TouchButton();\n    //\n    //   SDU_UI::TouchButtonWrapper tbWrapper;\n    //   SDU_UI::TouchStyles ts;\n    //\n    //   #if !defined HAS_LGFX\n    //     LoadBtn->setFont(nullptr);\n    //     SkipBtn->setFont(nullptr);\n    //     if( SDUCfg.binFileName != nullptr ) {\n    //       SaveBtn->setFont(nullptr);\n    //     }\n    //   #endif\n    //\n    //   LoadBtn->initButton(\n    //     SDU_GFX,\n    //     ts.x1, ts.y,  ts.w, ts.h,\n    //     ts.Load->BorderColor, ts.Load->FillColor, ts.Load->TextColor,\n    //     labelLoad, ts.btn_fsize\n    //   );\n    //   SkipBtn->initButton(\n    //     SDU_GFX,\n    //     ts.x2, ts.y,  ts.w, ts.h,\n    //     ts.Skip->BorderColor, ts.Skip->FillColor, ts.Skip->TextColor,\n    //     labelSkip, ts.btn_fsize\n    //   );\n    //\n    //   if( SDUCfg.binFileName != nullptr ) {\n    //     SaveBtn->initButton(\n    //       SDU_GFX,\n    //       ts.x3, ts.y1,  ts.w, ts.h,\n    //       ts.Save->BorderColor, ts.Save->FillColor, ts.Save->TextColor,\n    //       labelSave, ts.btn_fsize\n    //     );\n    //     SaveBtn->setLabelDatum(ts.padx, ts.pady, MC_DATUM);\n    //     SaveBtn->drawButton();\n    //     SaveBtn->press(false);\n    //   }\n    //\n    //   LoadBtn->setLabelDatum(ts.padx, ts.pady, MC_DATUM);\n    //   SkipBtn->setLabelDatum(ts.padx, ts.pady, MC_DATUM);\n    //\n    //   LoadBtn->drawButton();\n    //   SkipBtn->drawButton();\n    //\n    //   LoadBtn->press(false);\n    //   SkipBtn->press(false);\n    //\n    //   uint16_t t_x = 0, t_y = 0; // To store the touch coordinates\n    //   bool ispressed = false;\n    //   int retval = -1; // return status\n    //\n    //   SDU_GFX->drawFastHLine( ts.pgbar_x, ts.pgbar_y, ts.pgbar_w-1, TFT_WHITE );\n    //\n    //   auto msectouch = millis();\n    //   do {\n    //\n    //     if( tbWrapper.iconRendered == false ) {\n    //       tbWrapper.pushIcon( labelLoad );\n    //       tbWrapper.iconRendered = true;\n    //     }\n    //     tbWrapper.handlePressed( LoadBtn, ispressed, t_x, t_y );\n    //     tbWrapper.handlePressed( SkipBtn, ispressed, t_x, t_y );\n    //     if( SDUCfg.binFileName != nullptr ) {\n    //       tbWrapper.handlePressed( SaveBtn, ispressed, t_x, t_y );\n    //     }\n    //     tbWrapper.handleJustPressed( LoadBtn, labelLoad );\n    //     tbWrapper.handleJustPressed( SkipBtn, labelSkip );\n    //     if( SDUCfg.binFileName != nullptr ) {\n    //       tbWrapper.handleJustPressed( SaveBtn, labelSave );\n    //     }\n    //\n    //     if( tbWrapper.justReleased( LoadBtn, ispressed, labelLoad ) ) {\n    //       retval = 1;\n    //       log_v(\"LoadBTN Pressed at [%d:%d]!\", t_x, t_y);\n    //       break;\n    //     }\n    //     if( tbWrapper.justReleased( SkipBtn, ispressed, labelSkip ) ) {\n    //       retval = 0;\n    //       log_v(\"SkipBTN Pressed at [%d:%d]!\", t_x, t_y);\n    //       break;\n    //     }\n    //     if( SDUCfg.binFileName != nullptr ) {\n    //       if( tbWrapper.justReleased( SaveBtn, ispressed, labelSave ) ) {\n    //         retval = 2;\n    //         log_v(\"SaveBtn Pressed at [%d:%d]!\", t_x, t_y);\n    //         break;\n    //       }\n    //     }\n    //\n    //     #if defined HAS_LGFX\n    //       lgfx::touch_point_t tp;\n    //       uint16_t number = SDU_GFX->getTouch(&tp, 1);\n    //       t_x = tp.x;\n    //       t_y = tp.y;\n    //       ispressed = number > 0;\n    //     #else // M5Core2.h / TFT_eSPI_Button syntax\n    //       ispressed = SDU_GFX->getTouch(&t_x, &t_y);\n    //     #endif\n    //\n    //     float barprogress = float(millis() - msectouch) / float(waitdelay);\n    //     int linewidth = float(ts.pgbar_w) * barprogress;\n    //     if( linewidth > 0 ) {\n    //       int linepos = ts.pgbar_w - ( linewidth +1 );\n    //       uint16_t grayscale = 255 - (192*barprogress);\n    //       SDU_GFX->drawFastHLine( ts.pgbar_x,         ts.pgbar_y, ts.pgbar_w-linewidth-1, SDU_GFX->color565( grayscale, grayscale, grayscale ) );\n    //       SDU_GFX->drawFastHLine( ts.pgbar_x+linepos, ts.pgbar_y, 1,                      TFT_BLACK );\n    //     }\n    //\n    //   } while (millis() - msectouch < waitdelay);\n    //\n    //   #if defined _M5Core2_H_\n    //     // clean handlers\n    //     LoadBtn->delHandlers();\n    //     SkipBtn->delHandlers();\n    //     SaveBtn->delHandlers();\n    //   #endif\n    //\n    //   delete LoadBtn;\n    //   delete SkipBtn;\n    //   delete SaveBtn;\n    //\n    //   return retval;\n    // }\n\n  }; // end TriggerSource namespace\n\n};\n\n"
  },
  {
    "path": "src/UI/UI.hpp",
    "content": "#pragma once\n\n#include \"../misc/assets.h\"\n#include \"../misc/types.h\"\n#include \"../UI/common.hpp\"\n\nnamespace SDUpdaterNS\n{\n\n\n  namespace SDU_UI\n  {\n\n    #ifdef ARDUINO_ODROID_ESP32 // Odroid-GO has 4 buttons under the TFT\n      static const uint16_t BUTTON_WIDTH = 60;\n      static const uint16_t BUTTON_HWIDTH = BUTTON_WIDTH/2; // button half width\n      static const uint16_t BUTTON_HEIGHT = 28;\n      static const int16_t SDUButtonsXOffset[4] = {\n        1, 72, 188, 260\n      };\n      static const int16_t SDUButtonsYOffset[4] = {\n        0, 0, 0, 0\n      };\n    #else // assuming landscape mode /w 320x240 display\n      static const uint16_t BUTTON_WIDTH = 68;\n      static const uint16_t BUTTON_HWIDTH = BUTTON_WIDTH/2; // button half width\n      static const uint16_t BUTTON_HEIGHT = 28;\n      static const int16_t SDUButtonsXOffset[3] = {\n        31, 125, 219\n      };\n      static const int16_t SDUButtonsYOffset[3] = {\n        0, 0, 0\n      };\n    #endif\n\n\n\n    #if defined HAS_LGFX\n      //#define fontInto_t fontInfo_t\n      struct fontInfo_t\n      {\n        const lgfx::IFont* font;\n        const float fontSize;\n      };\n      fontInfo_t Font0Size1 = {&Font0, 1};\n      fontInfo_t Font0Size2 = {&Font0, 2};\n      fontInfo_t Font2Size1 = {&Font2, 1};\n    #else\n      //#define fontInto_t basic_fontInfo_t\n      struct fontInfo_t\n      {\n        const uint8_t  fontNumber;\n        const uint8_t  fontSize;\n      };\n      fontInfo_t Font0Size1 = {0, 1};\n      fontInfo_t Font0Size2 = {0, 2};\n      fontInfo_t Font2Size1 = {2, 1};\n    #endif\n\n\n\n    // default theme\n    static const BtnStyle_t DefaultLoadBtn{0x73AE,0x630C,TFT_WHITE,TFT_BLACK};\n    static const BtnStyle_t DefaultSkipBtn{0x73AE,0x630C,TFT_WHITE,TFT_BLACK};\n    static const BtnStyle_t DefaultSaveBtn{0x73AE,0x630C,TFT_WHITE,TFT_BLACK};\n    static const uint16_t DefaultMsgFontColors[2] = {TFT_WHITE, TFT_BLACK};\n\n    static const BtnStyles_t DefaultBtnStyle(\n      DefaultLoadBtn, DefaultSkipBtn, DefaultSaveBtn,\n      BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HWIDTH,\n      &Font0Size1, &Font0Size2, DefaultMsgFontColors\n    );\n\n    static SplashPageElementStyle_t SplashTitleStyle      = { TFT_BLACK,     TFT_WHITE, &Font0Size2, MC_DATUM, TFT_LIGHTGREY, TFT_DARKGREY };\n    static SplashPageElementStyle_t SplashAppNameStyle    = { TFT_LIGHTGREY, TFT_BLACK, &Font0Size2, BC_DATUM, 0, 0 };\n    static SplashPageElementStyle_t SplashAuthorNameStyle = { TFT_LIGHTGREY, TFT_BLACK, &Font0Size2, BC_DATUM, 0, 0 };\n    static SplashPageElementStyle_t SplashAppPathStyle    = { TFT_DARKGREY,  TFT_BLACK, &Font0Size1, BC_DATUM, 0, 0 };\n\n    static ProgressBarStyle_t ProgressStyle = {\n      200,        // width\n      8,          // height\n      true,       // clip \"xx%\" text\n      TFT_LIGHTGREY,  // border color\n      TFT_DARKGREY,  // fill color\n      &Font0Size1,\n      //0,          // font number\n      TC_DATUM,   // text alignment\n      //1,          // text size\n      TFT_WHITE,  // text color\n      TFT_BLACK   // text bgcolor\n    };\n\n    static SDUTextStyle_t SDUTextStyle; // temporary style holder\n\n\n    // API Specifics\n    #if defined HAS_LGFX // using LovyanGFX or M5GFX\n\n      #define setFontInfo(x, i) {auto info=(fontInfo_t*)i;if(info->font){x->setFont((lgfx::IFont*)info->font);}x->setTextSize(info->fontSize);}\n      #define getTextFgColor(x) x->getTextStyle().fore_rgb888\n      #define getTextBgColor(x) x->getTextStyle().back_rgb888\n      #define getTextDatum(x)   x->getTextStyle().datum\n      #define getTextSize(x)    x->getTextStyle().size_x\n      #define resetFont(x)      x->setFont( nullptr )\n      static SDU_Sprite *animSpr = nullptr;\n\n      inline void loaderAnimator_t::init()\n      {\n        assert( SDU_GFX );\n        //display = SDU_GFX;\n        if( !animSpr ) {\n          //log_d(\"[%d] Init animation sprite\", ESP.getFreeHeap() );\n          animSpr = new SDU_Sprite( SDU_GFX );\n        }\n        animSpr->createSprite( 32, 32 );\n      }\n\n      inline void loaderAnimator_t::animate()\n      {\n        if( !animSpr ) return;\n        float angle = sin( float(millis())/500.0 )*180.0; // 1/2 round per second\n        animSpr->clear();\n        animSpr->pushImageRotateZoom(animSpr->width()/2, animSpr->height()/2, 7.5, 8, angle, 1, 1, 15, 16, sdUpdaterIcon15x16_raw);\n        animSpr->pushSprite( SDU_GFX->width()/2-animSpr->width()/2, SDU_GFX->height()*.75-animSpr->height() );\n      }\n\n      inline void loaderAnimator_t::deinit()\n      {\n        if( animSpr ) {\n          animSpr->deleteSprite(); // free framebuffer\n          delete(animSpr);         // delete object\n          animSpr = nullptr;       // reset pointer\n          //log_d(\"[%d] Deinit animation sprite\", ESP.getFreeHeap() );\n        }\n      }\n\n      static void fillStyledRect( SplashPageElementStyle_t *style, int32_t x, int32_t y, uint16_t width, uint16_t height )\n      {\n        if( style->colorStart == style->colorEnd ) {\n          SDU_GFX->fillRect( x, y, width, height, style->bgColor );\n        } else {\n          for( int i=y; i<y+height; i++ ) {\n            SDU_GFX->drawGradientHLine( x, i, width, style->colorStart, style->colorEnd );\n          }\n        }\n      }\n\n      static void adjustFontSize( uint8_t *lineHeightBig, uint8_t *lineHeightSmall )\n      {\n        auto fontSize = SDU_GFX->getTextSizeX();\n        SDU_GFX->setTextSize( fontSize*2.0 );\n        *lineHeightBig = SDU_GFX->fontHeight();\n        SDU_GFX->setTextSize( fontSize );\n        *lineHeightSmall = SDU_GFX->fontHeight();\n      }\n\n      static void fillProgressBox( int32_t posX, int32_t posY, uint16_t offset, ProgressBarStyle_t *pgstyle )\n      {\n        for (uint8_t h = 0; h<pgstyle->height; h++) {\n          SDU_GFX->drawGradientHLine( posX+1, posY+1 + h, offset, pgstyle->bgColor, pgstyle->textColor);\n        }\n        uint16_t barmod = pgstyle->width/12;\n        if( barmod < 10 ) barmod = 10;\n        for (uint16_t v = 1; v<offset; v++) {\n          if(v%barmod == 0) {\n            SDU_GFX->drawFastVLine( posX+1 + v, posY+1, pgstyle->height, pgstyle->bgColor);\n          }\n        }\n      }\n\n\n      static void resetScroll()\n      {\n        assert( SDU_GFX );\n        uint16_t rst = 0;\n        SDU_GFX->startWrite();\n        SDU_GFX->writecommand(0x33);  // VSCRDEF Vertical scroll definition\n        SDU_GFX->writedata(rst >> 8); // Top Fixed Area line count\n        SDU_GFX->writedata(rst);\n        SDU_GFX->writedata(rst >> 8); // Vertical Scrolling Area line count\n        SDU_GFX->writedata(rst);\n        SDU_GFX->writedata(rst >> 8); // Bottom Fixed Area line count\n        SDU_GFX->writedata(rst);\n        SDU_GFX->endWrite();\n\n        SDU_GFX->startWrite();\n        SDU_GFX->writecommand(0x37); // VSCRSADD Vertical scrolling pointer\n        SDU_GFX->writedata(rst>>8);\n        SDU_GFX->writedata(rst);\n        SDU_GFX->endWrite();\n      }\n\n\n\n    #else // using TFT_eSPI based core (M5Core2.h, M5Stack.h, M5StickC.h).\n      #define setFontInfo(x, i) {auto info=(fontInfo_t*)i;x->setTextFont(info->fontNumber);x->setTextSize(info->fontSize);}\n      #define getTextFgColor(x) x->textcolor\n      #define getTextBgColor(x) x->textbgcolor\n      #define getTextDatum(x)   x->textdatum\n      #define getTextSize(x)    x->textsize\n      #define resetFont(x)      (void)0\n      inline void loaderAnimator_t::init() {}\n      inline void loaderAnimator_t::animate() {}\n      inline void loaderAnimator_t::deinit() {}\n      static void resetScroll() { }\n      static void fillStyledRect( SplashPageElementStyle_t *style, int32_t x, int32_t y, uint16_t width, uint16_t height ) { SDU_GFX->fillRect( x, y, width, height, style->bgColor ); }\n      static void adjustFontSize( uint8_t *lineHeightBig, uint8_t *lineHeightSmall ) { *lineHeightBig = 14; *lineHeightSmall = 8; }\n      static void fillProgressBox( int32_t posX, int32_t posY, uint16_t offset, ProgressBarStyle_t *pgstyle )\n      {\n        SDU_GFX->fillRect( posX+1,        posY+1, offset,                  pgstyle->height, pgstyle->fillColor );\n        SDU_GFX->fillRect( posX+1+offset, posY+1, pgstyle->width - offset, pgstyle->height, pgstyle->bgColor );\n      }\n    #endif\n\n    static void freezeTextStyle() // onBeforeCb\n    {\n      resetScroll();\n      if( SDUTextStyle.frozen ) {\n        // log_v(\"can't freeze twice, thaw first !\");\n        return;\n      }\n\n      SDUTextStyle.textcolor   = getTextFgColor(SDU_GFX);\n      SDUTextStyle.textbgcolor = getTextBgColor(SDU_GFX);\n      SDUTextStyle.textdatum   = getTextDatum(SDU_GFX);\n      SDUTextStyle.textsize    = getTextSize(SDU_GFX);\n      SDUTextStyle.frozen = true;\n      //log_d(\"Froze textStyle, size: %d, datum: %d, color: 0x%08x, bgcolor: 0x%08x\", SDUTextStyle.textsize, SDUTextStyle.textdatum, SDUTextStyle.textcolor, SDUTextStyle.textbgcolor );\n    }\n\n    static void thawTextStyle()\n    {\n      SDU_GFX->setTextSize( SDUTextStyle.textsize);\n      SDU_GFX->setTextDatum( SDUTextStyle.textdatum );\n      SDU_GFX->setTextColor( SDUTextStyle.textcolor , SDUTextStyle.textbgcolor );\n      resetFont(SDU_GFX);\n      SDU_GFX->setCursor(0,0);\n      SDU_GFX->fillScreen(TFT_BLACK);\n      //log_d(\"Thawed textStyle, size: %d, datum: %d, color: 0x%08x, bgcolor: 0x%08x\", SDUTextStyle.textsize, SDUTextStyle.textdatum, SDUTextStyle.textcolor, SDUTextStyle.textbgcolor );\n      SDUTextStyle.frozen = false;\n    }\n\n    static void drawTextShadow( const char* text, int32_t x, int32_t y, uint16_t textcolor, uint16_t shadowcolor )\n    {\n      if( textcolor != shadowcolor ) {\n        SDU_GFX->setTextColor( shadowcolor );\n        SDU_GFX->drawString( text, x+1, y+1 );\n      }\n      SDU_GFX->setTextColor( textcolor );\n      SDU_GFX->drawString( text, x, y );\n    }\n\n    static void drawSDUSplashElement( const char* msg, int32_t x, int32_t y, SplashPageElementStyle_t *style )\n    {\n      setFontInfo( SDU_GFX, style->fontInfo );\n      SDU_GFX->setTextDatum( style->textDatum );\n      uint8_t lineHeight = SDU_GFX->fontHeight()*1.8;\n      fillStyledRect( style, 0, y, SDU_GFX->width(), lineHeight );\n      drawTextShadow( msg, x, y+lineHeight/2, style->textColor, TFT_DARKGREY );\n    }\n\n    static void drawSDUSplashPage( const char* msg )\n    {\n      int32_t centerX = SDU_GFX->width() >> 1;\n\n      uint8_t lineHeightBig   = 14;\n      uint8_t lineHeightSmall = 8;\n\n      adjustFontSize( &lineHeightBig, &lineHeightSmall );\n\n      uint8_t titleNamePosy   = 0;\n      uint8_t appNamePosy     = lineHeightBig*1.8+lineHeightSmall;\n      uint8_t authorNamePosY  = appNamePosy + lineHeightBig*1.8;\n      uint8_t binFileNamePosY = authorNamePosY+lineHeightSmall*1.8;\n\n      SDU_GFX->fillScreen(TFT_BLACK); // M5StickC does not have tft.clear()\n\n      drawSDUSplashElement( msg, centerX, titleNamePosy, &SplashTitleStyle );\n\n      if( SDUCfg.appName != nullptr ) {\n        drawSDUSplashElement( SDUCfg.appName, centerX, appNamePosy, &SplashAppNameStyle  );\n      }\n      if( SDUCfg.authorName != nullptr ) {\n        drawSDUSplashElement( String( \"By \" + String(SDUCfg.authorName) ).c_str(), centerX, authorNamePosY, &SplashAuthorNameStyle  );\n      }\n      if( SDUCfg.binFileName != nullptr ) {\n        drawSDUSplashElement( String(\"File name: \" + String(&SDUCfg.binFileName[1])).c_str(), centerX, binFileNamePosY, &SplashAppPathStyle );\n      }\n      //SDU_GFX->drawJpg( sdUpdaterIcon32x40_jpg, sdUpdaterIcon32x40_jpg_len, (centerX)-16, (SDU_GFX->height()/2)-20, 32, 40 );\n    }\n\n    static void drawSDUPushButton( const char* label, uint8_t position, uint16_t outlinecolor, uint16_t fillcolor, uint16_t textcolor, uint16_t shadowcolor )\n    {\n      Theme_t defaultTheme(&DefaultBtnStyle, &SplashTitleStyle, &SplashAppNameStyle, &SplashAuthorNameStyle, &SplashAppPathStyle, &ProgressStyle );\n      bool use_default_theme = false;\n      if( !SDUCfg.theme ) {\n        use_default_theme = true;\n        SDUCfg.theme = &defaultTheme;\n      }\n      const BtnStyles_t *bs = SDUCfg.theme->buttons; // ( SDUTheme.userBtnStyle == nullptr ) ? &DefaultBtnStyle : SDUTheme.userBtnStyle;\n      uint32_t bx = SDUButtonsXOffset[position];\n      uint32_t by = SDU_GFX->height() - bs->height - 2 - SDUButtonsYOffset[position];\n      SDU_GFX->fillRoundRect( bx, by, bs->width, bs->height, 3, fillcolor );\n      SDU_GFX->drawRoundRect( bx, by, bs->width, bs->height, 3, outlinecolor );\n      SDU_GFX->setTextDatum( MC_DATUM );\n      setFontInfo( SDU_GFX, bs->BtnFontInfo );\n      drawTextShadow( label, bx+bs->width/2, by+bs->height/2, textcolor, shadowcolor );\n      if( use_default_theme ) {\n        SDUCfg.theme = nullptr;\n      }\n    }\n\n    static void SDMenuProgressUI( int state, int size )\n    {\n      static int SD_UI_Progress;\n\n      int posX = (SDU_GFX->width() - (ProgressStyle.width+2)) >> 1;\n      int posY = (SDU_GFX->height()- (ProgressStyle.height+2)) >> 1;\n\n      if( state <=0 || size <=0 ) {\n        // clear progress bar\n        SDU_GFX->fillRect( posX, posY, ProgressStyle.width+2, ProgressStyle.height+2, ProgressStyle.bgColor );\n      } else {\n        // draw frame\n        SDU_GFX->drawRect( posX, posY, ProgressStyle.width+2, ProgressStyle.height+2, ProgressStyle.borderColor );\n      }\n\n      int offset = ( state * ProgressStyle.width ) / size;\n      int percent = ( state * 100 ) / size;\n      if( offset == SD_UI_Progress ) {\n        // don't render twice the same value\n        return;\n      }\n      SD_UI_Progress = offset;\n\n      if ( offset >= 0 && offset <= ProgressStyle.width ) {\n        fillProgressBox( posX, posY, offset, &ProgressStyle );\n      } else {\n        SDU_GFX->fillRect( posX+1,        posY+1, ProgressStyle.width,        ProgressStyle.height, ProgressStyle.bgColor );\n      }\n      if( ProgressStyle.clipText ) {\n        String percentStr = \" \" + String( percent ) + \"% \";\n        setFontInfo( SDU_GFX, ProgressStyle.fontInfo );\n        SDU_GFX->setTextDatum( ProgressStyle.textDatum );\n        SDU_GFX->setTextColor( ProgressStyle.textColor, ProgressStyle.bgColor );\n        SDU_GFX->drawString( percentStr, SDU_GFX->width() >> 1, posY+ProgressStyle.height+SDU_GFX->fontHeight() );\n      }\n    };\n\n    static void DisplayUpdateUI( const String& label )\n    {\n      if (SDU_GFX->width() < SDU_GFX->height()) SDU_GFX->setRotation(SDU_GFX->getRotation() ^ 1);\n\n      SDU_GFX->fillScreen( TFT_BLACK );\n      SDU_GFX->setTextColor( TFT_WHITE, TFT_BLACK );\n      SDU_GFX->setTextDatum( MC_DATUM );\n      setFontInfo( SDU_GFX, &Font0Size2 );\n      if( SDU_GFX->textWidth(label)>SDU_GFX->width() ) {\n        setFontInfo( SDU_GFX, &Font0Size1 );\n      }\n      int posY = ((SDU_GFX->height()- ProgressStyle.height+2) >> 1) - 20;\n      SDU_GFX->drawString( label, SDU_GFX->width()/2, posY );\n    }\n\n    static void DisplayErrorUI( const String& msg, unsigned long wait )\n    {\n      static uint8_t msgposy = 0;\n      setFontInfo( SDU_GFX, &Font0Size2 );\n      uint8_t headerHeight = SDU_GFX->fontHeight()*1.8; // = 28px\n      log_v(\"Header height: %d\", headerHeight );\n      if( msgposy == 0 ) {\n        SDU_GFX->fillRect( 0, headerHeight+1, SDU_GFX->width(), SDU_GFX->height()-headerHeight, TFT_BLACK );\n      }\n      SDU_GFX->setTextColor( TFT_RED, TFT_BLACK );\n      SDU_GFX->setTextDatum( MC_DATUM );\n      SDU_GFX->drawString( msg.c_str(), SDU_GFX->width()/2, msgposy+headerHeight*2 );\n      msgposy += headerHeight;\n      delay(wait);\n    }\n\n  };\n\n};\n\n\n"
  },
  {
    "path": "src/UI/common.hpp",
    "content": "#pragma once\n\n#include \"../misc/types.h\"\n\n#pragma GCC diagnostic ignored \"-Wunused-function\"\n#pragma GCC diagnostic ignored \"-Wunused-variable\"\n\n\nnamespace SDUpdaterNS\n{\n\n  namespace ConfigManager\n  {\n\n  }\n\n\n  namespace SDU_UI\n  {\n\n    static void SDMenuProgressUI( int state, int size );\n    static void DisplayUpdateUI( const String& label );\n    static void DisplayErrorUI( const String& msg, unsigned long wait );\n    static void freezeTextStyle();\n    static void thawTextStyle();\n    static void drawSDUSplashPage( const char* msg );\n    static void drawSDUPushButton( const char* label, uint8_t position, uint16_t outlinecolor, uint16_t fillcolor, uint16_t textcolor, uint16_t shadowcolor );\n    static void fillStyledRect( SplashPageElementStyle_t *style, int32_t x, int32_t y, uint16_t width, uint16_t height );\n    static void adjustFontSize( uint8_t *lineHeightBig, uint8_t *lineHeightSmall );\n    static void drawTextShadow( const char* text, int32_t x, int32_t y, uint16_t textcolor, uint16_t shadowcolor );\n    static void drawSDUSplashElement( const char* msg, int32_t x, int32_t y, SplashPageElementStyle_t *style );\n    //void DisplayUpdateHeadless( const String& label );\n    //void SDMenuProgressHeadless( int state, int size );\n\n\n    static void inline DisplayUpdateHeadless( const String& label )\n    {\n      // TODO: draw some fancy serial output\n    };\n\n    static void inline SDMenuProgressHeadless( int state, int size )\n    {\n      static int Headless_Progress;\n      int percent = ( state * 100 ) / size;\n      if( percent == Headless_Progress ) {\n        // don't render twice the same value\n        return;\n      }\n      //Serial.printf(\"percent = %d\\n\", percent); // this is spammy\n      Headless_Progress = percent;\n      if ( percent >= 0 && percent < 101 ) {\n        Serial.print( \".\" );\n      } else {\n        Serial.println();\n      }\n    };\n\n\n\n\n\n  }\n\n  namespace TriggerSource\n  {\n    // static int pushButton( char* labelLoad, char* labelSkip, char* labelSave, unsigned long waitdelay=5000  );\n    // //static int touchButton( char* labelLoad, char* labelSkip, char* labelSave, unsigned long waitdelay=5000 );\n    // static int serial( char* labelLoad,  char* labelSkip, char* labelSave, unsigned long waitdelay=5000  );\n\n    static void triggerInitSerial(triggerMap_t* trigger) { }\n    static bool triggerActionSerial(triggerMap_t* trigger, uint32_t msec )\n    {\n      if( Serial.available() ) {\n        String out = Serial.readStringUntil('\\n');\n        if(      out == \"update\" ) { trigger->ret = 1; return true; }\n        else if( out == \"rollback\") { trigger->ret = 0; return true; }\n        else if( out == \"skip\" ) { trigger->ret = -1; return true; }\n        else if( out == \"save\" ) { trigger->ret = 2; return true; }\n        else Serial.printf(\"Ignored command: %s\\n\", out.c_str() );\n      }\n      return false;\n    }\n    static void triggerFinalizeSerial( triggerMap_t* trigger, int ret ) {  }\n\n\n    static void triggerInitButton(triggerMap_t* trigger)\n    {\n      using namespace SDU_UI;\n      log_d(\"Init button\");\n      if( trigger->waitdelay > 100 ) {\n        log_d(\"Waitdelay: %d\", trigger->waitdelay );\n        if( SDUCfg.onBefore ) SDUCfg.onBefore();\n        if( SDUCfg.onSplashPage ) SDUCfg.onSplashPage( BTN_HINT_MSG );\n        if( SDUCfg.onButtonDraw ) {\n          log_d(\"Drawing buttons\");\n          const BtnStyles_t btns;\n          if( SDUCfg.Buttons[0].enabled) SDUCfg.onButtonDraw( trigger->labelLoad, 0, btns.Load.BorderColor, btns.Load.FillColor, btns.Load.TextColor, btns.Load.ShadowColor );\n          if( SDUCfg.Buttons[1].enabled) SDUCfg.onButtonDraw( trigger->labelSkip, 1, btns.Skip.BorderColor, btns.Skip.FillColor, btns.Skip.TextColor, btns.Skip.ShadowColor );\n          if( SDUCfg.binFileName != nullptr && SDUCfg.Buttons[2].enabled ) {\n            SDUCfg.onButtonDraw( trigger->labelSave, 2, btns.Save.BorderColor, btns.Save.FillColor, btns.Save.TextColor, btns.Save.ShadowColor );\n          }\n        } else {\n          log_d(\"No buttondraw!\");\n        }\n      } else {\n        log_d(\"No Waitdelay! (%d<100)\", trigger->waitdelay );\n      }\n      if( SDUCfg.onProgress ) SDUCfg.onProgress( 100, 100 );\n\n      #if defined SDU_Sprite\n        loaderAnimator_t *loadingAnimation = new loaderAnimator_t();\n        trigger->sharedptr = loadingAnimation;\n        if( loadingAnimation ) {\n          loadingAnimation->init();\n        }\n      #endif\n    }\n\n    static bool triggerActionButton(triggerMap_t* trigger, uint32_t msec )\n    {\n      using namespace SDU_UI;\n      static uint32_t progress = 0, progressOld=1;\n      SDUCfg.buttonsPoll();\n\n      for( int i=0; i<3; i++ ) {\n        if( SDUCfg.Buttons[i].changed() ) {\n          log_v(\"SDUCfg.Buttons[%d] was triggered\", i);\n          trigger->ret = SDUCfg.Buttons[i].val;\n          return true;\n        }\n      }\n      if( SDUCfg.onProgress   ) {\n        float barprogress = float(millis() - msec) / float(trigger->waitdelay);\n        progress = 100- (100 * barprogress);\n        if (progressOld != progress) {\n          progressOld = progress;\n          SDUCfg.onProgress( (uint8_t)progress, 100 );\n        }\n      }\n      #if defined SDU_Sprite\n        loaderAnimator_t *loadingAnimation = (loaderAnimator_t *)trigger->sharedptr;\n        if( loadingAnimation ) {\n          loadingAnimation->animate();\n        }\n      #endif\n      return false;\n    }\n\n\n\n\n    static void triggerFinalizeButton( triggerMap_t* trigger, int ret )\n    {\n      using namespace SDU_UI;\n      if( SDUCfg.onProgress ) SDUCfg.onProgress( 0, 100 );\n      #if defined SDU_Sprite\n        loaderAnimator_t *loadingAnimation = (loaderAnimator_t *)trigger->sharedptr;\n        if( loadingAnimation ) {\n          loadingAnimation->deinit();\n          delete loadingAnimation;\n        }\n        trigger->sharedptr = nullptr;\n      #endif\n      if( ret > -1 ) { // wait for button release\n        log_v(\"Waiting for Button #%d to be released\", ret );\n        while( SDUCfg.Buttons[ret].changed() ) {\n          //if( SDUCfg.buttonsUpdate ) SDUCfg.buttonsUpdate();\n          SDUCfg.buttonsPoll();\n          vTaskDelay(10);\n        }\n      }\n    }\n\n\n\n    //static int actionTriggered( triggerMap_t *trigger  )\n    static int actionTriggered( char* labelLoad,  char* labelSkip, char* labelSave, unsigned long waitdelay )\n    {\n      auto trigger = SDUCfg.triggers;\n\n      if( !trigger ) {\n        log_e(\"No triggers assigned, aborting\");\n        return -1;\n      }\n\n      trigger->waitdelay = waitdelay;\n\n      if( trigger->waitdelay == 0 ) {\n        log_i(\"waitdelay=0 -> skipping action trigger detection\");\n        return -1;\n      }\n\n      switch( trigger->source ) {\n        case SDU_TRIGGER_SERIAL:      log_d(\"Listening to trigger source: Serial, delay=%dms\", trigger->waitdelay); break;\n        case SDU_TRIGGER_PUSHBUTTON:  log_d(\"Listening to trigger source: Push Button, delay=%dms\", trigger->waitdelay); break;\n        case SDU_TRIGGER_TOUCHBUTTON: log_d(\"Listening to trigger source: Touch Button, delay=%dms\", trigger->waitdelay); break;\n      }\n      auto msec = millis();\n      int ret = -1;\n      if( trigger->init ) {\n        log_d(\"Trigger init\");\n        trigger->init( trigger );\n      }\n      do {\n        if( trigger->get( trigger, msec  ) ) {\n          ret = trigger->ret;\n          break;\n        }\n      } while (millis() - msec < trigger->waitdelay);\n      if( trigger->finalize ) {\n        log_d(\"Trigger finalize\");\n        trigger->finalize( trigger, ret );\n      }\n      return ret;\n    }\n\n\n\n    // headless method\n    static inline int serial( char* labelLoad,  char* labelSkip, char* labelSave, unsigned long waitdelay )\n    {\n      int64_t msec = millis();\n      do {\n        if( Serial.available() ) {\n          String out = Serial.readStringUntil('\\n');\n          if(      out == \"update\" ) return 1;\n          else if( out == \"rollback\") return 0;\n          else if( out == \"skip\" ) return -1;\n          else if( out == \"save\" ) return 2;\n          else Serial.printf(\"Ignored command: %s\\n\", out.c_str() );\n        }\n      } while( msec > int64_t( millis() ) - int64_t( waitdelay ) );\n      return -1;\n    }\n\n    static int pushButton( char* labelLoad, char* labelSkip, char* labelSave, unsigned long waitdelay )\n    {\n      using namespace SDU_UI;\n      auto msec = millis();\n      if( waitdelay > 100 ) {\n        if( SDUCfg.onBefore ) SDUCfg.onBefore();\n        if( SDUCfg.onSplashPage ) SDUCfg.onSplashPage( BTN_HINT_MSG );\n        if( SDUCfg.onButtonDraw ) {\n          const BtnStyles_t btns;\n          if( SDUCfg.Buttons[0].enabled) SDUCfg.onButtonDraw( labelLoad, 0, btns.Load.BorderColor, btns.Load.FillColor, btns.Load.TextColor, btns.Load.ShadowColor );\n          if( SDUCfg.Buttons[1].enabled) SDUCfg.onButtonDraw( labelSkip, 1, btns.Skip.BorderColor, btns.Skip.FillColor, btns.Skip.TextColor, btns.Skip.ShadowColor );\n          if( SDUCfg.binFileName != nullptr && SDUCfg.Buttons[2].enabled ) {\n            SDUCfg.onButtonDraw( labelSave, 2, btns.Save.BorderColor, btns.Save.FillColor, btns.Save.TextColor, btns.Save.ShadowColor );\n          }\n        }\n      }\n      uint32_t progress = 0, progressOld = 1;\n      if( SDUCfg.onProgress ) SDUCfg.onProgress( 100, 100 );\n\n      #if defined SDU_Sprite\n        loaderAnimator_t loadingAnimation;\n        loadingAnimation.init();\n      #endif\n\n      int ret = -1;\n\n      //if(!SDUCfg.buttonsUpdate) log_w(\"No button poller found in SDUCfg, does M5.update() run in another task ?\");\n\n      do {\n        //if( SDUCfg.buttonsUpdate ) SDUCfg.buttonsUpdate();\n        SDUCfg.buttonsPoll();\n\n        for( int i=0; i<3; i++ ) {\n          if( SDUCfg.Buttons[i].enabled && SDUCfg.Buttons[i].changed() ) {\n            log_v(\"SDUCfg.Buttons[%d] was triggered\", i);\n            ret = SDUCfg.Buttons[i].val; goto _endAssert;\n          }\n        }\n        if( SDUCfg.onProgress   ) {\n          float barprogress = float(millis() - msec) / float(waitdelay);\n          progress = 100- (100 * barprogress);\n          if (progressOld != progress) {\n            progressOld = progress;\n            SDUCfg.onProgress( (uint8_t)progress, 100 );\n          }\n        }\n        #if defined SDU_Sprite\n          loadingAnimation.animate();\n        #endif\n      } while (millis() - msec < waitdelay);\n\n      _endAssert:\n\n      if( SDUCfg.onProgress ) SDUCfg.onProgress( 0, 100 );\n      #if defined SDU_Sprite\n        loadingAnimation.deinit();\n      #endif\n      if( ret > -1 ) { // wait for button release\n        log_v(\"Waiting for Button #%d to be released\", ret );\n        while( SDUCfg.Buttons[ret].changed() ) {\n          //if( SDUCfg.buttonsUpdate ) SDUCfg.buttonsUpdate();\n          SDUCfg.buttonsPoll();\n          vTaskDelay(10);\n        }\n      }\n      return ret;\n    }\n\n\n    #if !defined SDU_HAS_TOUCH\n      //struct triggerMap_t { };\n      static void triggerInitTouch(triggerMap_t* trigger) { log_d(\"[NULLCB] Init\"); }\n      static bool triggerActionTouch(triggerMap_t* trigger, uint32_t msec ) { log_d(\"[NULLCB] trigger\"); return false; }\n      static void triggerFinalizeTouch( triggerMap_t* trigger, int ret ) { log_d(\"[NULLCB] Final\"); }\n    #endif\n\n  };\n\n\n  #if !defined SDU_USE_DISPLAY\n    static inline void SDMenuProgressUI( int state, int size ) { log_d(\"[NULLCB] Progress: %d/%d\", state, size); }\n    static inline void DisplayUpdateUI( const String& label ) { log_d(\"[NULLCB] Update: %s\", label.c_str()); }\n    static inline void DisplayErrorUI( const String& msg, unsigned long wait ) { log_d(\"[NULLCB] Error: %s\", msg.c_str()); delay(wait); }\n    static inline void freezeTextStyle() { log_d(\"[NULLCB] Freezing\"); }\n    static inline void thawTextStyle() { log_d(\"[NULLCB] Thawing\");}\n    static inline void drawSDUSplashPage( const char* msg ) { log_d(\"[NULLCB] Splash Page: %s\", msg ); }\n    static inline void drawSDUPushButton( const char* label, uint8_t position, uint16_t outlinecolor, uint16_t fillcolor, uint16_t textcolor, uint16_t shadowcolor )\n    {\n      log_d(\"[NULLCB] PushButton '%s' [X:%dpx] [Outline:0x%04x] [Fill:0x%04x] [Text:0x%04x] [Shadow:0x%04x]\", label, position, outlinecolor, fillcolor, textcolor, shadowcolor );\n    }\n  #endif\n\n\n\n};\n"
  },
  {
    "path": "src/gitTagVersion.h",
    "content": "#define SDU_VERSION_MAJOR 1\n#define SDU_VERSION_MINOR 2\n#define SDU_VERSION_PATCH 8\n#define _SDU_STR(x) #x\n#define SDU_STR(x) _SDU_STR(x)\n// Macro to convert library version number into an integer\n#define VERSION_VAL(major, minor, patch) ((major << 16) | (minor << 8) | (patch))\n// current library version as a string\n#define M5_SD_UPDATER_VERSION SDU_STR(SDU_VERSION_MAJOR) \".\" SDU_STR(SDU_VERSION_MINOR) \".\" SDU_STR(SDU_VERSION_PATCH)\n// current library version as an int, to be used in comparisons, such as M5_SD_UPDATER_VERSION_INT >= VERSION_VAL(2, 0, 0)\n#define M5_SD_UPDATER_VERSION_INT VERSION_VAL(SDU_VERSION_MAJOR, SDU_VERSION_MINOR, SDU_VERSION_PATCH)\n"
  },
  {
    "path": "src/misc/assets.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nconst uint16_t sdUpdaterIcon15x16_raw[] = {\n  0x0000, 0x1000, 0x3000, 0x107c, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x0000,\n  0x0f00, 0x1000, 0xffff, 0x3084, 0xffff, 0x0f7c, 0xffff, 0x2000, 0xffff, 0x0f7c, 0xffff, 0x2000, 0xffff, 0x1084, 0x0000, 0x1000,\n  0x3000, 0xffff, 0x2000, 0xffff, 0x2000, 0xffff, 0x2000, 0xffff, 0x2000, 0xffff, 0x2000, 0xffff, 0x2000, 0x0000, 0x0f00, 0x1000,\n  0xffff, 0x0000, 0xffff, 0x0000, 0xffff, 0x0000, 0xffff, 0x2000, 0xffff, 0x2000, 0xffff, 0x0000, 0x0000, 0x2c00, 0x4c00, 0xffff,\n  0x2000, 0xffff, 0x2000, 0xffff, 0x2000, 0xffff, 0x2000, 0xffff, 0x2000, 0xffff, 0x2000, 0x0000, 0x0c00, 0xba73, 0xffff, 0xffff,\n  0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x0000, 0x0f84, 0xffff, 0xffff, 0xffff, 0xffff,\n  0xffff, 0xfde7, 0xfeef, 0xfff7, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff, 0xfff7, 0xfff7,\n  0x9395, 0xe83a, 0x549d, 0xfff7, 0xffff, 0xffff, 0xffff, 0xffff, 0x0000, 0xbff7, 0xffff, 0xffff, 0xffff, 0xfccf, 0x472b, 0xe832,\n  0xfde7, 0xfff7, 0xffff, 0xfff7, 0xfff7, 0xffff, 0xffff, 0x0000, 0x2800, 0x4808, 0xfff7, 0xfff7, 0xd27d, 0x472b, 0xb38d, 0xfde7,\n  0xfef7, 0xfff7, 0xfeef, 0x94a5, 0xdef7, 0xffff, 0x0000, 0x6508, 0x1384, 0xfef7, 0x949d, 0x661b, 0xa623, 0x072b, 0xf28d, 0xfeef,\n  0xfeef, 0x5176, 0x861b, 0xd28d, 0xfddf, 0x0000, 0xd37b, 0xffff, 0xfeef, 0xfeef, 0x3076, 0x861b, 0xd285, 0xfcdf, 0xfeef, 0x949d,\n  0x651b, 0x861b, 0x072b, 0xf28d, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff, 0xfeef, 0x949d, 0xfeff, 0xffff, 0xfff7, 0xfff7, 0x3176,\n  0x8623, 0x939d, 0xfef7, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff, 0xfeef, 0xfff7, 0xffff, 0xffff, 0xfef7, 0xfef7, 0x651b, 0x861b,\n  0xfdef, 0xfef7, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x9395, 0x0943, 0x949d, 0xfeef, 0xffff,\n  0xffff, 0x0000, 0xef7b, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xfff7, 0xffff, 0xfde7, 0xfde7, 0xfdef, 0xfef7, 0xffff, 0xffff\n};\n\nconst unsigned char sdUpdaterIcon15x16_jpg[] = {\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x0f, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n  0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,\n  0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n  0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,\n  0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n  0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25,\n  0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n  0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56,\n  0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86,\n  0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n  0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n  0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n  0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n  0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00,\n  0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n  0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,\n  0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n  0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08,\n  0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n  0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18,\n  0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n  0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55,\n  0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n  0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84,\n  0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n  0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa,\n  0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n  0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n  0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00,\n  0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfb,\n  0xef, 0xf6, 0x39, 0xff, 0x00, 0x82, 0x79, 0xff, 0x00, 0xc1, 0x4a, 0x3f,\n  0x66, 0x2f, 0xf8, 0x25, 0x97, 0x81, 0x7e, 0x1f, 0x9f, 0xda, 0x13, 0xe1,\n  0x8f, 0xec, 0x4f, 0x03, 0xfe, 0xcc, 0xff, 0x00, 0x12, 0xa4, 0xf1, 0xce,\n  0x9d, 0xe2, 0xaf, 0x8a, 0x9a, 0xef, 0xc2, 0x9d, 0x4f, 0xe1, 0xe7, 0xc5,\n  0x9f, 0x89, 0xbe, 0x01, 0xff, 0x00, 0x82, 0xbc, 0x58, 0x58, 0x78, 0xf7,\n  0xc7, 0x1e, 0x38, 0xf0, 0x7f, 0x81, 0xb5, 0x27, 0xf0, 0xb7, 0x88, 0xf4,\n  0xaf, 0x1b, 0x7e, 0xd4, 0x1f, 0xf0, 0x4c, 0x8f, 0x10, 0x6a, 0x1e, 0x27,\n  0xb4, 0xf1, 0x64, 0x9e, 0x27, 0xf0, 0x94, 0x7f, 0xb2, 0xf7, 0x89, 0x2d,\n  0xad, 0x05, 0xa6, 0xb1, 0xf0, 0xeb, 0xc1, 0x5a, 0x17, 0xc4, 0x0f, 0xd7,\n  0x1f, 0x04, 0x7e, 0xc9, 0xff, 0x00, 0xb6, 0x97, 0x8b, 0x7e, 0x3d, 0x69,\n  0x1e, 0x3d, 0xf8, 0xb9, 0xf1, 0xfb, 0xe1, 0xaf, 0xc6, 0x4f, 0xd9, 0x67,\n  0xc4, 0xff, 0x00, 0x15, 0x75, 0x2f, 0x14, 0x78, 0x9f, 0xe1, 0x46, 0xa9,\n  0xe2, 0xdf, 0x13, 0xf8, 0xeb, 0xc2, 0x9e, 0x2c, 0xf8, 0x1a, 0x9f, 0x12,\n  0xff, 0x00, 0xe0, 0xb0, 0x3e, 0x3b, 0xf8, 0x63, 0xe1, 0xfb, 0x0f, 0x02,\n  0x6b, 0xbf, 0x0e, 0x9b, 0xe1, 0xdc, 0xfa, 0xb6, 0x99, 0xe1, 0xaf, 0xda,\n  0x97, 0xfe, 0x09, 0x93, 0x05, 0xce, 0xa3, 0x69, 0x79, 0x68, 0xda, 0x9c,\n  0xdf, 0xb2, 0x86, 0xab, 0x15, 0xc7, 0x8a, 0x35, 0xbb, 0x0f, 0x86, 0x1f,\n  0x0b, 0xef, 0x3e, 0x20, 0xe8, 0x4b, 0xff, 0x00, 0x04, 0xe3, 0xf1, 0x07,\n  0x8a, 0xbe, 0x1d, 0xff, 0x00, 0xc1, 0x3f, 0x74, 0x3d, 0x27, 0xf6, 0xd8,\n  0xf8, 0xcf, 0xf1, 0x5f, 0xc2, 0x7f, 0xb2, 0xd7, 0x8b, 0x74, 0x8f, 0x8b,\n  0x9a, 0xbf, 0xc4, 0x4f, 0x8a, 0xda, 0xe6, 0x81, 0xf1, 0xcf, 0xc5, 0x1f,\n  0x1d, 0xbc, 0x41, 0x25, 0xb7, 0x88, 0x7c, 0x53, 0xa3, 0xfc, 0x55, 0x7f,\n  0x1a, 0x78, 0xe6, 0xc7, 0xc6, 0x57, 0x17, 0x7e, 0x22, 0x4d, 0x73, 0xc4,\n  0x66, 0xcf, 0xc0, 0x93, 0xdf, 0xea, 0xde, 0x23, 0xf0, 0xc7, 0x82, 0xbe,\n  0x1d, 0x6a, 0x71, 0x59, 0x78, 0x32, 0xde, 0xda, 0xff, 0x00, 0x40, 0xd0,\n  0xf5, 0x09, 0xfb, 0x8f, 0x85, 0x1f, 0x11, 0xff, 0x00, 0x69, 0x4b, 0x8f,\n  0xda, 0xaf, 0xf6, 0x8e, 0xf8, 0xb9, 0xf1, 0x4b, 0xe2, 0x1e, 0x99, 0xf0,\n  0xf3, 0xf6, 0x0d, 0xf0, 0xb7, 0x87, 0xfc, 0x17, 0xe0, 0xef, 0x00, 0x37,\n  0xc5, 0xdf, 0x87, 0x77, 0x1f, 0x06, 0x9b, 0x55, 0xf1, 0x64, 0x57, 0xb7,\n  0xf6, 0xba, 0x9e, 0xab, 0xa0, 0xb7, 0x8f, 0x7c, 0x59, 0xa6, 0x78, 0xab,\n  0x46, 0xb4, 0xd2, 0x7c, 0x41, 0xab, 0x45, 0xe1, 0xed, 0x6b, 0xc7, 0x9e,\n  0x35, 0xf0, 0xd6, 0x8d, 0xa1, 0x7c, 0x54, 0xb9, 0xbd, 0xf0, 0xf5, 0xb7,\n  0xc3, 0x9d, 0x0e, 0x7d, 0x0f, 0x4b, 0xb1, 0xf1, 0x14, 0x9d, 0x9c, 0x51,\n  0xc4, 0x58, 0xbc, 0xf7, 0x8b, 0xb8, 0x83, 0x38, 0xc5, 0x51, 0x95, 0x79,\n  0xe7, 0xdc, 0x47, 0x99, 0xe6, 0x0f, 0x17, 0x86, 0xc3, 0xc7, 0x0d, 0x4b,\n  0x11, 0x53, 0x33, 0xc4, 0xe2, 0xb3, 0x2c, 0x4e, 0x31, 0x60, 0xe7, 0x8a,\n  0xc4, 0x54, 0xc0, 0x61, 0x55, 0x4a, 0xae, 0x11, 0xc3, 0xd5, 0xad, 0x5e,\n  0xa5, 0x17, 0x3a, 0x74, 0xbd, 0xb5, 0x66, 0xd4, 0xdf, 0xca, 0x2c, 0xe3,\n  0x1b, 0x82, 0xcc, 0x68, 0xe5, 0x95, 0xb2, 0x5c, 0x67, 0xd5, 0x2a, 0x62,\n  0x70, 0xd9, 0x76, 0x5f, 0x88, 0xa1, 0x3c, 0x36, 0x21, 0xd5, 0x84, 0x70,\n  0x12, 0xc4, 0xe2, 0x33, 0x0c, 0x4d, 0xab, 0x42, 0x78, 0x7c, 0x16, 0x1e,\n  0x70, 0x58, 0x39, 0x4e, 0x74, 0x54, 0xa5, 0x88, 0x95, 0x3e, 0x45, 0x27,\n  0x5a, 0x10, 0x5f, 0xff, 0xd9\n};\nconst unsigned int sdUpdaterIcon15x16_jpg_len = 1133;\n\nconst unsigned char sdUpdaterIcon32x40_jpg[] = {\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x01, 0x2c, 0x01, 0x2c, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x01, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x28, 0x00, 0x20, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x19, 0x00, 0x00, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x09, 0x0a, 0x00, 0x06, 0x07,\n  0xff, 0xc4, 0x00, 0x2b, 0x10, 0x00, 0x02, 0x02, 0x02, 0x02, 0x01, 0x04,\n  0x01, 0x02, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x05, 0x03,\n  0x06, 0x02, 0x07, 0x01, 0x08, 0x09, 0x00, 0x0a, 0x11, 0x13, 0x14, 0x15,\n  0x16, 0x12, 0x17, 0x18, 0x24, 0x31, 0x41, 0x71, 0x61, 0xff, 0xc4, 0x00,\n  0x19, 0x01, 0x00, 0x02, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x02, 0x03, 0x04, 0x05,\n  0xff, 0xc4, 0x00, 0x21, 0x11, 0x00, 0x02, 0x02, 0x02, 0x01, 0x05, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x01,\n  0x04, 0x05, 0x06, 0x00, 0x07, 0x11, 0x12, 0x13, 0x31, 0x14, 0x21, 0xff,\n  0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f,\n  0x00, 0x5b, 0xde, 0x08, 0xbb, 0x5d, 0xb1, 0xba, 0x01, 0xe2, 0x5b, 0xcc,\n  0x47, 0x75, 0x34, 0x65, 0x7f, 0x5e, 0x9f, 0xbb, 0xb5, 0x6d, 0xf7, 0xa6,\n  0x28, 0xab, 0x0c, 0xef, 0xf5, 0x99, 0x6c, 0x0a, 0xff, 0x00, 0x47, 0xb9,\n  0xdf, 0x5a, 0xd5, 0x9e, 0x2a, 0x3f, 0x05, 0xcc, 0x91, 0xb9, 0x94, 0x0c,\n  0xc1, 0xb0, 0x16, 0x6c, 0x01, 0x8c, 0xe8, 0x41, 0xb1, 0x6b, 0x08, 0x66,\n  0xcf, 0x11, 0x1c, 0x8d, 0x8c, 0x79, 0x38, 0x9b, 0x27, 0xb8, 0x53, 0xbf,\n  0x52, 0xed, 0x7b, 0x3e, 0xb2, 0x08, 0x6d, 0x16, 0xa5, 0x4f, 0xf5, 0x91,\n  0xbc, 0x7a, 0xea, 0x0b, 0x70, 0x35, 0xeb, 0x9c, 0xdf, 0xac, 0xa4, 0xd3,\n  0x3a, 0xe9, 0x1e, 0xc6, 0xaf, 0xb6, 0x08, 0x86, 0x17, 0x43, 0x95, 0xc9,\n  0x6c, 0x0a, 0xd2, 0x4f, 0xe4, 0x64, 0x79, 0xca, 0x4c, 0x54, 0x48, 0xb1,\n  0x42, 0x19, 0x29, 0x64, 0x8f, 0x99, 0x32, 0x93, 0xcd, 0x7d, 0xb4, 0x3e,\n  0x25, 0x98, 0xf6, 0x43, 0xc5, 0x8f, 0x73, 0xf5, 0xef, 0x73, 0xa8, 0x57,\n  0xda, 0x67, 0x5b, 0xbb, 0xc5, 0xb0, 0x34, 0x6d, 0x87, 0x5e, 0xce, 0xa1,\n  0xe7, 0x14, 0xdb, 0xbd, 0xd6, 0xb5, 0xa5, 0x19, 0xb0, 0xb2, 0x61, 0x6d,\n  0x4b, 0x94, 0xe0, 0x9c, 0x62, 0xca, 0xb1, 0xf6, 0xac, 0x14, 0x8c, 0xa9,\n  0x99, 0x80, 0x7d, 0x36, 0x80, 0x04, 0x6b, 0x2a, 0xec, 0x33, 0x59, 0x98,\n  0x8c, 0x4a, 0xa0, 0xa6, 0x5e, 0xde, 0xef, 0x1e, 0x73, 0x5a, 0xd9, 0x5f,\n  0xce, 0xcf, 0x77, 0x40, 0xd4, 0x9d, 0xc9, 0x77, 0xdf, 0xa6, 0xcf, 0x96,\n  0xd4, 0x82, 0x05, 0xf0, 0xdd, 0xef, 0x3a, 0xf6, 0x3d, 0x71, 0x62, 0x9b,\n  0x9c, 0x73, 0xad, 0xf1, 0xf4, 0xa4, 0xc2, 0xb3, 0x06, 0x39, 0x0a, 0x1e,\n  0x73, 0xff, 0x00, 0x18, 0x66, 0xf1, 0x99, 0x3c, 0x99, 0x9c, 0x3f, 0xdb,\n  0xf0, 0x7c, 0xfb, 0xc3, 0xe7, 0xde, 0x4b, 0xeb, 0xbf, 0x71, 0x27, 0x93,\n  0x5c, 0xba, 0x94, 0xb0, 0x11, 0x76, 0x3d, 0x01, 0x7e, 0xc3, 0x5f, 0xd5,\n  0x4e, 0xb2, 0x6f, 0x59, 0xf7, 0x40, 0xfa, 0xce, 0xbb, 0x26, 0xc0, 0x6f,\n  0x6c, 0xda, 0x1d, 0xb4, 0xcb, 0x54, 0xda, 0x56, 0x32, 0x54, 0x5e, 0x04,\n  0xeb, 0x5f, 0xdb, 0xd3, 0xd2, 0xa6, 0x8c, 0x31, 0x46, 0x5f, 0x42, 0x5c,\n  0xc0, 0x62, 0x47, 0xc0, 0xdc, 0x59, 0xe5, 0x3c, 0x92, 0x73, 0xcb, 0x3d,\n  0xeb, 0x47, 0x9d, 0x1e, 0xd6, 0x76, 0x4b, 0xbc, 0xba, 0x2b, 0xa6, 0xfb,\n  0x43, 0x5b, 0xf5, 0xd5, 0xb6, 0xa4, 0xdd, 0x9d, 0xde, 0xef, 0xaf, 0x53,\n  0xef, 0xb1, 0xe3, 0x47, 0xb3, 0x90, 0xd5, 0x8e, 0xae, 0xeb, 0xa5, 0x06,\n  0xb4, 0xea, 0x9f, 0x3e, 0x38, 0x35, 0xbb, 0x31, 0x43, 0x2b, 0xc7, 0xc4,\n  0x3b, 0x3c, 0x5b, 0x8e, 0x47, 0x23, 0x35, 0x2b, 0x30, 0xf9, 0x8e, 0x00,\n  0x53, 0x2c, 0xf8, 0xcf, 0x3c, 0xcf, 0x24, 0x3e, 0xdc, 0xdf, 0x15, 0xf7,\n  0xed, 0x6a, 0x00, 0x15, 0x37, 0xdb, 0x86, 0xdd, 0xaf, 0xda, 0xea, 0x2d,\n  0x7d, 0xa6, 0x55, 0xd8, 0x2a, 0xfb, 0xc5, 0x53, 0xb5, 0xcc, 0xa9, 0x1a,\n  0x8f, 0x6c, 0xcd, 0xb4, 0x6b, 0xd9, 0x2f, 0x7e, 0xa2, 0xbc, 0x48, 0x05,\n  0xb3, 0x1e, 0xf4, 0x2c, 0xc3, 0x36, 0x60, 0x34, 0xb9, 0xc5, 0x98, 0xc3,\n  0x4a, 0xb3, 0x11, 0xc4, 0x9e, 0x29, 0x65, 0xe0, 0x91, 0xd4, 0x5e, 0x0a,\n  0xfa, 0x31, 0xa4, 0xfb, 0x0b, 0xae, 0xbb, 0x37, 0x4a, 0x8f, 0x71, 0xff,\n  0x00, 0x33, 0x75, 0x76, 0xfa, 0xdf, 0x5d, 0x8e, 0xab, 0xe4, 0xe3, 0x63,\n  0xe2, 0xc6, 0xbd, 0x8e, 0xc7, 0xec, 0x72, 0x45, 0x68, 0x36, 0x3e, 0x4c,\n  0x13, 0xfe, 0x81, 0x07, 0xe5, 0xa2, 0xc9, 0x7a, 0x70, 0xf8, 0x44, 0xab,\n  0x82, 0xa1, 0xc9, 0x54, 0xdf, 0x74, 0x9c, 0x94, 0x4f, 0xdb, 0x96, 0x3e,\n  0xa2, 0x24, 0x26, 0x22, 0x60, 0x42, 0x60, 0x63, 0x04, 0x06, 0x33, 0x04,\n  0x24, 0x25, 0x11, 0x22, 0x42, 0x51, 0x33, 0x04, 0x25, 0x13, 0x13, 0x13,\n  0x13, 0x31, 0x31, 0x31, 0x31, 0x3d, 0xb9, 0x10, 0x30, 0x60, 0x03, 0x16,\n  0x62, 0xc5, 0xb0, 0x44, 0xc0, 0xc0, 0xa0, 0x80, 0xc0, 0xa2, 0x08, 0x4c,\n  0x08, 0x66, 0x44, 0x84, 0x86, 0x62, 0x44, 0xa2, 0x66, 0x26, 0x26, 0x26,\n  0x26, 0x62, 0x78, 0x48, 0xf4, 0x93, 0xc8, 0x1f, 0x5c, 0x3b, 0x67, 0xd1,\n  0xfd, 0x69, 0xdd, 0x3a, 0x64, 0x72, 0xe8, 0xbd, 0x03, 0x67, 0x1d, 0xe2,\n  0x84, 0xc2, 0x6d, 0x9c, 0xea, 0x54, 0x5c, 0x2a, 0x21, 0xd2, 0x6e, 0x4d,\n  0xf5, 0xb6, 0x00, 0xb0, 0xe5, 0x63, 0xa3, 0xaa, 0xaa, 0x17, 0xe4, 0xce,\n  0xbf, 0x90, 0xe9, 0x61, 0x11, 0xaf, 0xe3, 0xf0, 0x14, 0xa1, 0x41, 0x84,\n  0x70, 0x49, 0xcf, 0xe3, 0x46, 0x86, 0xf7, 0x6f, 0x74, 0x2a, 0x3d, 0xe0,\n  0xf2, 0xad, 0x5f, 0xd0, 0x1b, 0x27, 0x7b, 0x55, 0xaa, 0x7d, 0x02, 0xd3,\n  0xcd, 0x98, 0x1a, 0xd0, 0x4e, 0x36, 0x20, 0x15, 0xcd, 0x69, 0xb8, 0x0e,\n  0xa7, 0xd6, 0x64, 0x71, 0x3c, 0xf6, 0xab, 0x18, 0xcd, 0x80, 0x53, 0x68,\n  0x5f, 0x64, 0xb8, 0xfe, 0x3a, 0x25, 0xeb, 0x88, 0x60, 0x52, 0xde, 0x6b,\n  0x43, 0xcd, 0x8a, 0xf8, 0x31, 0x35, 0x81, 0xa4, 0x4d, 0x4d, 0x7a, 0xfb,\n  0xad, 0xba, 0x1a, 0x81, 0xd7, 0x9a, 0xf7, 0x58, 0xe9, 0xfa, 0xa2, 0x98,\n  0x87, 0x40, 0xa8, 0xa2, 0x47, 0x43, 0x0b, 0x54, 0x88, 0xa2, 0x0c, 0xaa,\n  0x58, 0x55, 0x49, 0x0f, 0x28, 0x8c, 0x56, 0x40, 0x33, 0xfd, 0xb9, 0x1b,\n  0x99, 0xf9, 0x90, 0x49, 0x6c, 0x98, 0x1b, 0x29, 0x0c, 0xd8, 0xb2, 0x24,\n  0x86, 0xa6, 0x99, 0x3b, 0x29, 0xe5, 0x2f, 0x34, 0x41, 0xa7, 0x3d, 0xbc,\n  0x9a, 0xd2, 0x89, 0xdc, 0xe7, 0xfb, 0x2a, 0xe5, 0x63, 0x0e, 0xe7, 0xd5,\n  0x7a, 0xe9, 0x41, 0xdb, 0x35, 0x8e, 0xad, 0x63, 0x91, 0x05, 0xd8, 0x5a,\n  0xbf, 0x2a, 0x79, 0xa7, 0xfd, 0xa1, 0xb0, 0x25, 0x9a, 0x0e, 0x04, 0x3e,\n  0xa1, 0x50, 0x9e, 0x2c, 0x64, 0x8a, 0x78, 0x88, 0x98, 0xbb, 0x88, 0xb2,\n  0xae, 0x09, 0xb4, 0x03, 0xc5, 0x13, 0x8c, 0x0f, 0x56, 0xf5, 0x27, 0x1f,\n  0xb8, 0xe5, 0x5b, 0xad, 0x50, 0xc0, 0x53, 0xa9, 0x77, 0x0e, 0xcc, 0xdd,\n  0x27, 0xe6, 0xd4, 0xeb, 0x2d, 0xaf, 0x0c, 0x55, 0x53, 0x37, 0x7e, 0x7c,\n  0x9c, 0x0f, 0x7f, 0x6e, 0x11, 0xe2, 0x3e, 0x56, 0x81, 0x42, 0x6d, 0x36,\n  0xa9, 0x69, 0x94, 0xb7, 0xdc, 0x03, 0xc4, 0xb7, 0x57, 0x71, 0x7b, 0xf6,\n  0x69, 0xfa, 0x8e, 0x33, 0x57, 0xa1, 0x47, 0x21, 0x81, 0x6e, 0xc3, 0x42,\n  0xc6, 0xc4, 0x9b, 0x17, 0x1d, 0x50, 0x5a, 0x8a, 0x46, 0x76, 0x3f, 0x2e,\n  0x62, 0x03, 0xbf, 0xbf, 0x5d, 0xb0, 0x23, 0xe5, 0x71, 0x68, 0x13, 0x7b,\n  0x1c, 0x95, 0x57, 0x94, 0x3b, 0xf4, 0x2c, 0x61, 0xc6, 0x76, 0x0f, 0xbb,\n  0x1d, 0x51, 0xe9, 0x8e, 0xb6, 0xd6, 0xd6, 0xad, 0x8f, 0x74, 0x45, 0x5a,\n  0xa3, 0xde, 0x8c, 0x42, 0x87, 0x5a, 0xae, 0xa5, 0x84, 0x23, 0x48, 0xd9,\n  0xa5, 0x37, 0x00, 0xb9, 0xc1, 0xdd, 0x7d, 0x32, 0x4c, 0xb0, 0x87, 0x9a,\n  0x55, 0x7d, 0x51, 0x62, 0xb3, 0x64, 0xd1, 0x7e, 0x19, 0x04, 0x22, 0xd9,\n  0x06, 0x88, 0x18, 0xcb, 0x38, 0xd5, 0xc0, 0x16, 0x5b, 0xa1, 0x7c, 0x92,\n  0xd2, 0x91, 0x45, 0x96, 0xb6, 0xd9, 0x7b, 0xda, 0xfb, 0xf5, 0xa1, 0x38,\n  0x48, 0xe9, 0x49, 0x70, 0x9c, 0xb1, 0xaa, 0xa6, 0x23, 0xc6, 0x58, 0x0c,\n  0x00, 0x34, 0x7c, 0xe4, 0x80, 0xa1, 0x0b, 0x1a, 0x58, 0xe7, 0x1e, 0x78,\n  0x73, 0xca, 0x39, 0x63, 0xcf, 0x1c, 0xf1, 0xcb, 0x9e, 0x39, 0xf5, 0x19,\n  0x3e, 0xe0, 0x6e, 0xb7, 0x90, 0x87, 0x7d, 0x69, 0x7b, 0x20, 0xbb, 0x6a,\n  0xf5, 0xb6, 0x76, 0x96, 0xf2, 0x3d, 0xe2, 0x0a, 0x6e, 0x9f, 0x91, 0x4a,\n  0x71, 0x91, 0xeb, 0x8a, 0x22, 0x32, 0x90, 0x29, 0xa7, 0x56, 0x28, 0x89,\n  0x92, 0x43, 0x1c, 0xb1, 0x0e, 0xc5, 0xcb, 0xa2, 0x40, 0x1f, 0x99, 0x62,\n  0xe4, 0x87, 0x4c, 0x45, 0x38, 0xc2, 0x24, 0x98, 0xdc, 0xa7, 0x93, 0xd5,\n  0x2b, 0xf8, 0xf8, 0xea, 0x0d, 0x97, 0xa5, 0x5a, 0x35, 0x5e, 0xa8, 0x69,\n  0xb9, 0x6e, 0x9b, 0x39, 0x14, 0x61, 0x2e, 0x6c, 0xb2, 0xb7, 0x6f, 0x09,\n  0x2f, 0xd5, 0xae, 0x1b, 0x9c, 0x1e, 0x05, 0x59, 0xeb, 0xf5, 0x56, 0xab,\n  0x70, 0xc4, 0xc9, 0xeb, 0x52, 0xb8, 0x94, 0x89, 0xc4, 0x5c, 0x6c, 0x93,\n  0xc2, 0x14, 0xdf, 0x74, 0xe1, 0x73, 0x17, 0x26, 0x93, 0x8e, 0x77, 0x6b,\n  0xbb, 0x6e, 0xc5, 0x93, 0xdd, 0xf6, 0x5d, 0x72, 0xd6, 0x09, 0x15, 0xf0,\n  0xf8, 0x25, 0x51, 0x50, 0x5c, 0xaf, 0x65, 0x04, 0xda, 0xcf, 0xb1, 0x5e,\n  0x5e, 0x89, 0xb4, 0x23, 0x60, 0xd6, 0xc0, 0xbe, 0x9e, 0xec, 0x0a, 0xf5,\n  0x7c, 0xdd, 0x8e, 0x81, 0x4a, 0xac, 0x43, 0x25, 0x8c, 0x6a, 0xb4, 0x6a,\n  0x7b, 0xce, 0xd7, 0x98, 0xea, 0x36, 0xdf, 0xa9, 0xdd, 0xd6, 0xab, 0x55,\n  0xc0, 0xeb, 0x69, 0xc6, 0x24, 0x2f, 0xd6, 0xb7, 0x58, 0xdd, 0x4e, 0xc5,\n  0xaa, 0xa5, 0x66, 0xb9, 0x5c, 0x11, 0xb2, 0xc5, 0x38, 0x32, 0x68, 0xee,\n  0xc5, 0x55, 0xa5, 0xe6, 0xfc, 0x54, 0x02, 0x13, 0x68, 0x5b, 0x2d, 0x6b,\n  0x92, 0x6a, 0x54, 0xac, 0xb5, 0xbb, 0x8d, 0x5e, 0xbf, 0x6a, 0xa7, 0x3f,\n  0x4f, 0x69, 0xaa, 0xd8, 0x14, 0x00, 0xda, 0xbd, 0x63, 0xaf, 0xb0, 0x11,\n  0xaa, 0x47, 0x6a, 0x0d, 0x1e, 0x39, 0x81, 0x64, 0xad, 0x90, 0x32, 0xce,\n  0x19, 0xa1, 0x17, 0x06, 0x58, 0x4b, 0x01, 0x03, 0xcb, 0x24, 0x52, 0x61,\n  0x97, 0x1c, 0xe3, 0x97, 0x3e, 0xba, 0x1e, 0x7e, 0x7e, 0x39, 0xf8, 0xff,\n  0x00, 0x3f, 0xeb, 0xe7, 0x8f, 0x9e, 0x3e, 0x7f, 0xf7, 0x8f, 0x9e, 0x3e,\n  0x7f, 0xe7, 0xcf, 0x1f, 0xf7, 0xd6, 0xf5, 0xbd, 0x33, 0xb8, 0xe3, 0xe0,\n  0x43, 0xac, 0xba, 0x2b, 0xac, 0x6b, 0xdb, 0x5c, 0x4e, 0xc9, 0x6e, 0x13,\n  0x26, 0xdf, 0xbd, 0xa2, 0x84, 0x5e, 0x04, 0xc3, 0x70, 0x5c, 0x03, 0x20,\n  0x25, 0xb5, 0x91, 0xa1, 0x98, 0xdf, 0xc0, 0x03, 0x58, 0x6b, 0x7e, 0x5b,\n  0xb8, 0xab, 0x6b, 0x65, 0xcb, 0x83, 0x37, 0x31, 0x86, 0xe1, 0x4f, 0x26,\n  0x37, 0xe6, 0x6c, 0xcd, 0x61, 0x33, 0x99, 0x4c, 0x64, 0x6c, 0x92, 0x97,\n  0xd6, 0xcb, 0x45, 0x6e, 0x93, 0x59, 0x7f, 0x6f, 0xb8, 0xbe, 0x51, 0x56,\n  0xaa, 0xd6, 0x94, 0x9c, 0xea, 0xc3, 0x63, 0x7e, 0xc0, 0x55, 0x49, 0x52,\n  0x28, 0x5e, 0x3c, 0x84, 0x9c, 0xc9, 0xa3, 0x23, 0x64, 0x84, 0x50, 0x82,\n  0x10, 0x78, 0xf3, 0x9a, 0x72, 0x27, 0x97, 0x08, 0xe3, 0xc3, 0x1e, 0x79,\n  0xcb, 0x2e, 0x3d, 0x6f, 0x5b, 0xd6, 0x2a, 0x38, 0xda, 0x38, 0xc5, 0x1a,\n  0x68, 0xd6, 0x55, 0x70, 0x6b, 0x49, 0xef, 0x20, 0x89, 0x96, 0xd9, 0xb0,\n  0xc8, 0x88, 0x65, 0x9b, 0x4f, 0x39, 0x27, 0xda, 0xb4, 0xdf, 0x18, 0xf6,\n  0xd9, 0xb0, 0xc6, 0xbd, 0xb3, 0x11, 0x2c, 0x61, 0x4c, 0x73, 0x9d, 0x8d,\n  0xc4, 0xe3, 0x70, 0xe9, 0x62, 0x31, 0xb4, 0xd3, 0x54, 0x1e, 0xe3, 0xb3,\n  0x64, 0x82, 0x24, 0x9f, 0x6e, 0xd3, 0x20, 0x61, 0xb6, 0xee, 0xd9, 0x64,\n  0x9d, 0x8b, 0xb7, 0x1d, 0xe2, 0x3e, 0xeb, 0x76, 0xda, 0xeb, 0x2e, 0x98,\n  0x89, 0x6b, 0x4e, 0x7f, 0xbc, 0xff, 0xd9\n};\nconst unsigned int sdUpdaterIcon32x40_jpg_len = 1855;\n\nconst unsigned char flashUpdaterIcon16x16_jpg[] = {\n  0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,\n  0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,\n  0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09,\n  0x09, 0x08, 0x0a, 0x0c, 0x14, 0x0d, 0x0c, 0x0b, 0x0b, 0x0c, 0x19, 0x12,\n  0x13, 0x0f, 0x14, 0x1d, 0x1a, 0x1f, 0x1e, 0x1d, 0x1a, 0x1c, 0x1c, 0x20,\n  0x24, 0x2e, 0x27, 0x20, 0x22, 0x2c, 0x23, 0x1c, 0x1c, 0x28, 0x37, 0x29,\n  0x2c, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1f, 0x27, 0x39, 0x3d, 0x38, 0x32,\n  0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x09, 0x09,\n  0x09, 0x0c, 0x0b, 0x0c, 0x18, 0x0d, 0x0d, 0x18, 0x32, 0x21, 0x1c, 0x21,\n  0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,\n  0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,\n  0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,\n  0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,\n  0x32, 0x32, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x03,\n  0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,\n  0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n  0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,\n  0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n  0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,\n  0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n  0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25,\n  0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n  0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56,\n  0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86,\n  0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n  0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n  0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n  0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n  0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00,\n  0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,\n  0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n  0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00,\n  0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n  0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08,\n  0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n  0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18,\n  0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n  0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55,\n  0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n  0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84,\n  0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n  0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa,\n  0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n  0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n  0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n  0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00,\n  0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf1,\n  0x3b, 0x4b, 0x48, 0x7e, 0xcd, 0x14, 0x93, 0xdb, 0x96, 0xdf, 0x96, 0x04,\n  0x92, 0xbb, 0x97, 0x70, 0x1c, 0x7f, 0xdf, 0x2e, 0x33, 0xef, 0xed, 0x57,\n  0xee, 0x7c, 0x3c, 0xf6, 0xfe, 0x19, 0x1a, 0xc4, 0x90, 0x24, 0x50, 0xcb,\n  0x27, 0x93, 0x01, 0x79, 0x08, 0x79, 0x98, 0x01, 0xb9, 0xd1, 0x7b, 0xaa,\n  0x95, 0x20, 0x9e, 0x80, 0xc8, 0x07, 0x38, 0xe2, 0xa5, 0xad, 0xf5, 0xb9,\n  0xd3, 0xed, 0xed, 0xe7, 0x75, 0x47, 0x89, 0xd8, 0x6f, 0xc3, 0xb1, 0xd8,\n  0x59, 0x70, 0x3a, 0xe3, 0x03, 0x2e, 0xc3, 0x00, 0x1c, 0xe7, 0x24, 0xe4,\n  0x62, 0x5b, 0xad, 0x5e, 0x39, 0x74, 0x61, 0x63, 0x1c, 0x88, 0x88, 0x07,\n  0x98, 0xc8, 0x88, 0x41, 0x96, 0x4e, 0x30, 0x5c, 0x9e, 0xa4, 0x06, 0x75,\n  0x1d, 0x80, 0x53, 0x81, 0x97, 0x24, 0xc4, 0xf9, 0xf4, 0xe4, 0xef, 0xaf,\n  0xa7, 0xf5, 0xfd, 0x75, 0x04, 0x7f, 0xff, 0xd9\n};\nconst unsigned int flashUpdaterIcon16x16_jpg_len = 752;\n"
  },
  {
    "path": "src/misc/config.h",
    "content": "#pragma once\n\n#define ROLLBACK_LABEL   \"Rollback\" // reload app from the \"other\" OTA partition\n#define LAUNCHER_LABEL   \"Launcher\" // load Launcher (typically menu.bin)\n#define SKIP_LABEL       \"Skip >>|\" // resume normal operations (=no action taken)\n#define SAVE_LABEL       \"Save\"     // copy sketch binary to FS\n#define BTN_HINT_MSG     \"SD-Updater Lobby\"\n#define SDU_LOAD_TPL     \"Will Load menu binary : %s\\n\"\n#define SDU_ROLLBACK_MSG \"Will Roll back\"\n\n#if !defined SDU_APP_PATH\n  #define SDU_APP_PATH nullptr\n#endif\n#if !defined SDU_APP_NAME\n  #define SDU_APP_NAME nullptr\n#endif\n#if !defined SDU_APP_AUTHOR\n  #define SDU_APP_AUTHOR nullptr\n#endif\n\n#ifndef MENU_BIN\n  #define MENU_BIN \"/menu.bin\"\n#endif\n\n\n// Fancy names for detected boards\n#if defined ARDUINO_M5Stick_C || defined ARDUINO_M5STICK_C\n  #define SD_PLATFORM_NAME \"M5StickC\"\n#elif defined ARDUINO_ODROID_ESP32\n  #define SD_PLATFORM_NAME \"Odroid-GO\"\n#elif defined ARDUINO_M5Stack_Core_ESP32 || defined ARDUINO_M5STACK_CORE_ESP32\n  #define SD_PLATFORM_NAME \"M5Stack\"\n#elif defined ARDUINO_M5STACK_FIRE\n  #define SD_PLATFORM_NAME \"M5Fire\"\n#elif defined ARDUINO_M5STACK_Core2 || defined ARDUINO_M5STACK_CORE2\n  #define SD_PLATFORM_NAME \"M5Core2\"\n#elif defined ARDUINO_M5STACK_CORES3\n  #define SD_PLATFORM_NAME \"M5CoreS3\"\n#elif defined ARDUINO_ESP32_WROVER_KIT\n  #define SD_PLATFORM_NAME \"Wrover-Kit\"\n#elif defined ARDUINO_TTGO_T1             // TTGO T1\n  #define SD_PLATFORM_NAME \"TTGO-T1\"\n#elif defined ARDUINO_LOLIN_D32_PRO       // LoLin D32 Pro\n  #define SD_PLATFORM_NAME \"LoLin D32 Pro\"\n#elif defined ARDUINO_T_Watch || defined ARDUINO_T_WATCH            // TWatch, all models\n  #define SD_PLATFORM_NAME \"TTGO TWatch\"\n#elif defined ARDUINO_M5STACK_ATOM_AND_TFCARD\n  #define SD_PLATFORM_NAME \"Atom\"\n#elif defined ARDUINO_ESP32_S3_BOX\n  #define SD_PLATFORM_NAME \"S3Box\"\n#else\n  #define SD_PLATFORM_NAME \"ESP32\"\n#endif\n\n#if !defined(TFCARD_CS_PIN) // override this from your sketch if the guess is wrong\n  #if defined ARDUINO_LOLIN_D32_PRO || defined ARDUINO_M5STACK_Core2|| defined ARDUINO_M5STACK_CORE2 || defined ARDUINO_M5Stack_Core_ESP32 || defined ARDUINO_M5STACK_CORE_ESP32 || defined ARDUINO_M5STACK_FIRE || defined ARDUINO_M5STACK_CORES3\n    #define TFCARD_CS_PIN  4\n  #elif defined( ARDUINO_ESP32_WROVER_KIT ) || defined( ARDUINO_ODROID_ESP32 )\n    #define TFCARD_CS_PIN 22\n  #elif defined ARDUINO_TWATCH_BASE || defined ARDUINO_TWATCH_2020_V1 || defined ARDUINO_TWATCH_2020_V2 || defined(ARDUINO_TTGO_T1)\n    #define TFCARD_CS_PIN 13\n  #else\n    #define TFCARD_CS_PIN SS\n  #endif\n#endif\n"
  },
  {
    "path": "src/misc/types.h",
    "content": "#pragma once\n\n#include <stdint.h>\n#include <functional>\n#include <vector>\n#include <Stream.h>\n#include <WString.h>\n\n#define FN_LAMBDA_VOID(x) []() { }\n#define FN_LAMBDA_BOOL(x) []() -> bool { return x; }\n#define FN_LAMBDA_FALSE   []() -> bool { return false; }\n\nnamespace SDUpdaterNS\n{\n\n  namespace UpdateInterfaceNS\n  {\n    [[maybe_unused]] static bool mode_z = false;\n    typedef std::function<void(size_t, size_t)> THandlerFunction_Progress;\n    struct UpdateManagerInterface_t\n    {\n      //typedef void    (*THandlerFunction_Progress)(size_t, size_t);\n      typedef bool    (*begin_t)(size_t);\n      typedef size_t  (*writeStream_t)(Stream &data,size_t size);\n      typedef void    (*abort_t)();\n      typedef bool    (*end_t)();\n      typedef bool    (*isFinished_t)();\n      typedef bool    (*canRollBack_t)();\n      typedef bool    (*rollBack_t)();\n      typedef void    (*onProgress_t)(THandlerFunction_Progress fn);\n      typedef uint8_t (*getError_t)();\n      typedef void    (*setBinName_t)(String& fileName, Stream* stream);\n      public:\n        begin_t begin;\n        writeStream_t writeStream;\n        abort_t abort;\n        end_t end;\n        isFinished_t isFinished;\n        canRollBack_t canRollBack;\n        rollBack_t rollBack;\n        onProgress_t onProgress;\n        getError_t getError;\n        setBinName_t setBinName;\n    };\n  };\n\n\n  namespace TriggerSource\n  {\n\n    enum TriggerSources_t\n    {\n      SDU_TRIGGER_SERIAL,     // headless\n      SDU_TRIGGER_PUSHBUTTON, // Push Button (GPIO or user provided)\n      SDU_TRIGGER_TOUCHBUTTON // Touche Button (using LGFX/eSPI touch driver)\n    };\n\n    struct triggerMap_t;\n\n    typedef void(*triggerInitCb)( triggerMap_t* trigger ); // start listening to trigger source (preinit)\n    typedef bool (*triggerActionCb)( triggerMap_t* trigger, uint32_t msec ); // listen to trigger source and return true if one was found\n    typedef void(*triggerDeinitCb)( triggerMap_t* trigger, int ret ); // stop listening to trigger source (deinit)\n\n    struct triggerMap_t\n    {\n      triggerMap_t() { };\n      triggerMap_t(TriggerSources_t _source, const char*_labelLoad,const char*_labelSkip,const char*_labelSave,triggerInitCb _init,triggerActionCb _get, triggerDeinitCb _final)\n      : source(_source), labelLoad(_labelLoad),labelSkip(_labelSkip),labelSave(_labelSave),init(_init),get(_get),finalize(_final) { };\n      TriggerSources_t source = SDU_TRIGGER_SERIAL;\n      const char* labelLoad = nullptr;\n      const char* labelSkip = nullptr;\n      const char* labelSave = nullptr;\n      unsigned long waitdelay = 5000;\n      int ret = -1;\n      void *sharedptr = nullptr;\n      triggerInitCb init;\n      triggerActionCb get;\n      triggerDeinitCb finalize;\n    };\n\n    //[[maybe_unused]] static int serial( char* labelLoad,  char* labelSkip, char* labelSave, unsigned long waitdelay=5000  );\n    //[[maybe_unused]] static int pushButton( char* labelLoad, char* labelSkip, char* labelSave, unsigned long waitdelay=5000  );\n    //[[maybe_unused]] static int touchButton( char* labelLoad, char* labelSkip, char* labelSave, unsigned long waitdelay=5000 );\n\n  }\n\n\n\n  namespace ConfigManager\n  {\n    // values to be returned by onWaitForActionCb\n    enum SDUBtnActions\n    {\n      SDU_BTNA_ROLLBACK =  0, // rollback = load the other OTA partition\n      SDU_BTNA_MENU     =  1, // menu = load /menu.bin from SD or rollback if partitions match\n      SDU_BTNB_SKIP     = -1, // skip = leave the lobby and start the current app\n      SDU_BTNC_SAVE     =  2, // save = copy the current firmware to the filesystem\n      SDU_BTN_NONE      = -2  // no activity\n    };\n\n\n    // callback signatures\n    typedef void (*onProgressCb)( int state, int size ); // progress bar when updating/saving\n    typedef void (*onMessageCb)( const String& label );  // misc info messages\n    typedef void (*onErrorCb)( const String& message, unsigned long delay ); // error messages\n    typedef void (*onBeforeCb)(); // called before using display\n    typedef void (*onAfterCb)();  // called after using display\n    typedef void (*onSplashPageCb)( const char* msg ); // lobby page\n    typedef void (*onButtonDrawCb)( const char* label, uint8_t position, uint16_t outlinecolor, uint16_t fillcolor, uint16_t textcolor, uint16_t shadowcolor );\n    typedef int  (*onWaitForActionCb)( char* labelLoad, char* labelSkip, char* labelSave, unsigned long waitdelay ); // action trigger\n    typedef void (*onConfigLoad)(); // external config loader, if set, will be called by SDUpdater constructor\n    typedef void (*BtnPollCb)(); // called to poll button state e.g. like M5.update()\n    typedef bool (*BtnXPressCb)(); // called when a button is pressed\n\n    struct BtnXAction\n    {\n      BtnXPressCb cb; // external callback, returns true when the button was pressed\n      bool changed() { return cb ? cb() : false; } // trigger checker\n      SDUBtnActions val; // button value to return when action is triggered\n      bool enabled;\n    };\n\n    // abstract filesystem config\n    struct FS_Config_t\n    {\n      const char* name;\n      void *fsPtr;\n      void *cfgPtr;\n    };\n\n\n  };\n\n  // directly callable (no poll, no pinMode) button\n  struct DigitalPinButton_t\n  {\n    DigitalPinButton_t( const int _pin ) : pin(_pin) { } // constructor\n    const int pin; // GPIO Pin\n    bool changed(); // trigger checker\n  };\n\n\n\n  namespace SDU_UI\n  {\n\n    struct SplashPageElementStyle_t\n    {\n      const uint16_t textColor;\n      const uint16_t bgColor;\n      const void* fontInfo; // holds font size + font face\n      const uint16_t textDatum;\n      const uint16_t colorStart; // gradient color start\n      const uint16_t colorEnd;   // gradient color end\n    };\n\n    struct ProgressBarStyle_t\n    {\n      const int      width;\n      const int      height;\n      const bool     clipText;\n      const uint16_t borderColor;\n      const uint16_t fillColor;\n\n      //const uint8_t  fontNumber;\n      const void* fontInfo; // holds font size + font face\n\n      const uint8_t  textDatum;\n\n      //const uint8_t  textSize;\n\n      const uint16_t textColor;\n      const uint16_t bgColor;\n    };\n\n    struct BtnStyle_t\n    {\n      BtnStyle_t( const uint16_t _BorderColor, const uint16_t _FillColor, const uint16_t _TextColor, const uint16_t _ShadowColor ) :\n        BorderColor(_BorderColor), FillColor(_FillColor), TextColor(_TextColor), ShadowColor(_ShadowColor) { }\n      const uint16_t BorderColor;\n      const uint16_t FillColor;\n      const uint16_t TextColor;\n      const uint16_t ShadowColor;\n    };\n\n    struct SDUTextStyle_t // somehow redundant with LGFX's textstyle_t\n    {\n      bool frozen           = false;\n      uint8_t textsize      = 0;\n      uint8_t textdatum     = 0;\n      uint32_t textcolor    = 0;\n      uint32_t textbgcolor  = 0;\n    };\n\n    struct BtnStyles_t\n    {\n      BtnStyles_t() { };\n      BtnStyles_t(\n        const BtnStyle_t _Load, const BtnStyle_t _Skip, const BtnStyle_t _Save,\n        const uint16_t _height, const uint16_t _width, const uint16_t _hwidth,\n        const void* _BtnfontInfo, const void* _MsgfontInfo,\n        const uint16_t _MsgFontColor[2]\n      ) :\n        Load(_Load), Skip(_Skip), Save(_Save),\n        height(_height), width(_width), hwidth(_hwidth),\n        BtnFontInfo(_BtnfontInfo), MsgFontInfo(_MsgfontInfo)\n      {\n        MsgFontColor[0] = _MsgFontColor[0];\n        MsgFontColor[1] = _MsgFontColor[1];\n      }\n      // 16bit colors:       Border  Fill    Text    Shadow\n      const BtnStyle_t Load{ 0x73AE, 0x630C, 0xFFFF, 0x0000 };\n      const BtnStyle_t Skip{ 0x73AE, 0x4208, 0xFFFF, 0x0000 };\n      const BtnStyle_t Save{ 0x73AE, 0x2104, 0xFFFF, 0x0000 };\n      const uint16_t height{28};\n      const uint16_t width{68};\n      const uint16_t hwidth{34};\n      const void* BtnFontInfo; // holds font size + font face\n      const void* MsgFontInfo; // holds font size + font face\n      uint16_t MsgFontColor[2]{0xFFFF, 0x0000}; // foreground, background\n    };\n\n\n    // animation played in the lobby while waiting for an action trigger\n    struct loaderAnimator_t\n    {\n      loaderAnimator_t() { };\n      void init();\n      void animate();\n      void deinit();\n    };\n\n\n    // Load/Skip/Save default Touch Button styles\n    struct TouchStyles\n    {\n      TouchStyles();\n      ~TouchStyles();\n      int padx     {0}, // buttons padding X\n          pady     {0}, // buttons padding Y\n          marginx  {0}, // buttons margin X\n          marginy  {0}, // buttons margin Y\n          x1       {0}, // button 1 X position\n          x2       {0}, // button 2 X position\n          x3       {0}, // button 3 X position\n          y        {0}, // buttons Y position\n          w        {0}, // buttons width\n          h        {0}, // buttons height\n          y1       {0}, // button3 y position\n          icon_x   {0}, // icon (button 1) X position\n          icon_y   {0}, // icon (button 1) Y position\n          pgbar_x  {0}, // progressbar X position\n          pgbar_y  {0}, // progressbar Y position\n          pgbar_w  {0}, // progressbar width\n          btn_fsize{0}  // touch buttons font size\n      ;\n      BtnStyle_t *Load{nullptr};\n      BtnStyle_t *Skip{nullptr};\n      BtnStyle_t *Save{nullptr};\n    };\n\n    // Theme holding all the Button styles\n    struct Theme_t\n    {\n      Theme_t() { };\n      Theme_t(\n        const BtnStyles_t*        _buttons,\n        SplashPageElementStyle_t* _SplashTitleStyle,\n        SplashPageElementStyle_t* _SplashAppNameStyle,\n        SplashPageElementStyle_t* _SplashAuthorNameStyle,\n        SplashPageElementStyle_t* _SplashAppPathStyle,\n        ProgressBarStyle_t      * _ProgressStyle\n      ) :\n        buttons              (_buttons              ),\n        SplashTitleStyle     (_SplashTitleStyle     ),\n        SplashAppNameStyle   (_SplashAppNameStyle   ),\n        SplashAuthorNameStyle(_SplashAuthorNameStyle),\n        SplashAppPathStyle   (_SplashAppPathStyle   ),\n        ProgressStyle        (_ProgressStyle        )\n      { };\n\n      const BtnStyles_t* buttons{nullptr};\n      SplashPageElementStyle_t* SplashTitleStyle     {nullptr};\n      SplashPageElementStyle_t* SplashAppNameStyle   {nullptr};\n      SplashPageElementStyle_t* SplashAuthorNameStyle{nullptr};\n      SplashPageElementStyle_t* SplashAppPathStyle   {nullptr};\n      ProgressBarStyle_t *ProgressStyle{nullptr};\n    };\n\n  }\n\n\n};\n"
  }
]