From 55aa5271c9f67de5d1bca20b81ae0186ee821b78 Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 20 Jan 2021 15:47:25 +0000 Subject: [PATCH 01/91] Revise appveyor CI tests (#2193) Further to #2192 this PR updates the appveyor CI scripts: * Add linux builds * Split scripts out into setup, build.setup and build.run stages as for travis * Downloads from espressif are slow, so cache affected files in SmingTools * Rename `appveyor` to `ci`, and use CI_BUILD_DIR variable to simplify future porting to other CI systems. * Build documentation in separate stage, add sming-docs.zip artifact [scan:coverity] --- .appveyor/build.cmd | 46 -------- .appveyor/install.cmd | 28 ----- .ci/build.cmd | 37 +++++++ .ci/build.sh | 34 ++++++ .ci/deploy.sh | 54 ++++++++++ .ci/install.cmd | 9 ++ .ci/install.sh | 13 +++ .ci/secrets.sh.enc | 7 ++ .../Arch/Esp32/Components/esp32/component.mk | 2 + Sming/Arch/Esp32/Tools/ci/build.run.cmd | 11 ++ Sming/Arch/Esp32/Tools/ci/build.run.sh | 4 + Sming/Arch/Esp32/Tools/ci/build.setup.cmd | 1 + Sming/Arch/Esp32/Tools/ci/build.setup.sh | 1 + Sming/Arch/Esp32/Tools/ci/install.cmd | 12 +++ Sming/Arch/Esp32/Tools/ci/install.sh | 14 +++ Sming/Arch/Esp8266/Tools/ci/build.run.cmd | 15 +++ Sming/Arch/Esp8266/Tools/ci/build.run.sh | 7 ++ Sming/Arch/Esp8266/Tools/ci/build.setup.cmd | 1 + Sming/Arch/Esp8266/Tools/ci/build.setup.sh | 6 ++ Sming/Arch/Esp8266/Tools/ci/install.cmd | 10 ++ Sming/Arch/Esp8266/Tools/ci/install.sh | 14 +++ Sming/Arch/Host/Tools/ci/build.run.cmd | 13 +++ Sming/Arch/Host/Tools/ci/build.run.sh | 19 ++++ Sming/Arch/Host/Tools/ci/build.setup.cmd | 10 ++ Sming/Arch/Host/Tools/ci/build.setup.sh | 23 ++++ Sming/Arch/Host/Tools/ci/coverity-scan.sh | 99 +++++++++++++++++ Sming/Arch/Host/Tools/ci/install.cmd | 8 ++ Sming/Arch/Host/Tools/ci/install.sh | 8 ++ Sming/Arch/Host/Tools/travis/install.sh | 7 +- appveyor.yml | 102 ++++++++++++++---- tests/HostTests/{ => app}/.cs | 0 tests/HostTests/include/.cs | 0 tests/HostTests/modules/.cs | 0 33 files changed, 516 insertions(+), 99 deletions(-) delete mode 100644 .appveyor/build.cmd delete mode 100644 .appveyor/install.cmd create mode 100755 .ci/build.cmd create mode 100755 .ci/build.sh create mode 100755 .ci/deploy.sh create mode 100755 .ci/install.cmd create mode 100755 .ci/install.sh create mode 100644 .ci/secrets.sh.enc create mode 100644 Sming/Arch/Esp32/Tools/ci/build.run.cmd create mode 100644 Sming/Arch/Esp32/Tools/ci/build.run.sh create mode 100644 Sming/Arch/Esp32/Tools/ci/build.setup.cmd create mode 100644 Sming/Arch/Esp32/Tools/ci/build.setup.sh create mode 100644 Sming/Arch/Esp32/Tools/ci/install.cmd create mode 100644 Sming/Arch/Esp32/Tools/ci/install.sh create mode 100644 Sming/Arch/Esp8266/Tools/ci/build.run.cmd create mode 100644 Sming/Arch/Esp8266/Tools/ci/build.run.sh create mode 100644 Sming/Arch/Esp8266/Tools/ci/build.setup.cmd create mode 100644 Sming/Arch/Esp8266/Tools/ci/build.setup.sh create mode 100644 Sming/Arch/Esp8266/Tools/ci/install.cmd create mode 100644 Sming/Arch/Esp8266/Tools/ci/install.sh create mode 100644 Sming/Arch/Host/Tools/ci/build.run.cmd create mode 100644 Sming/Arch/Host/Tools/ci/build.run.sh create mode 100644 Sming/Arch/Host/Tools/ci/build.setup.cmd create mode 100644 Sming/Arch/Host/Tools/ci/build.setup.sh create mode 100644 Sming/Arch/Host/Tools/ci/coverity-scan.sh create mode 100644 Sming/Arch/Host/Tools/ci/install.cmd create mode 100644 Sming/Arch/Host/Tools/ci/install.sh rename tests/HostTests/{ => app}/.cs (100%) create mode 100644 tests/HostTests/include/.cs create mode 100644 tests/HostTests/modules/.cs diff --git a/.appveyor/build.cmd b/.appveyor/build.cmd deleted file mode 100644 index c3beab0f5a..0000000000 --- a/.appveyor/build.cmd +++ /dev/null @@ -1,46 +0,0 @@ -REM Windows build script - -subst Z: %APPVEYOR_BUILD_FOLDER% -set SMING_HOME=Z:\Sming - -if "%build_compiler%" == "udk" set ESP_HOME=%UDK_ROOT% -if "%build_compiler%" == "eqt" set ESP_HOME=%EQT_ROOT% - -cd /d %SMING_HOME% -gcc -v - -set MAKE_PARALLEL=make -j2 - -REM Move samples and tests into directory outside of the Sming repo. -set SMING_PROJECTS_DIR=%APPVEYOR_BUILD_FOLDER%\.. -move ..\samples %SMING_PROJECTS_DIR% -move ..\tests %SMING_PROJECTS_DIR% - -REM This will build the Basic_Blink application and most of the framework Components -cd %SMING_PROJECTS_DIR%/samples/Basic_Blink -make help -make list-config -%MAKE_PARALLEL% || goto :error - -cd %SMING_HOME% - -if "%SMING_ARCH%" == "Host" ( - - REM Build a couple of basic applications - %MAKE_PARALLEL% Basic_Serial Basic_ProgMem STRICT=1 V=1 || goto :error - - REM Run basic tests - %MAKE_PARALLEL% tests || goto :error - -) else ( - - %MAKE_PARALLEL% Basic_Ssl || goto :error - %MAKE_PARALLEL% Basic_SmartConfig || goto :error - -) - -goto :EOF - -:error -echo Failed with error #%errorlevel%. -exit /b %errorlevel% diff --git a/.appveyor/install.cmd b/.appveyor/install.cmd deleted file mode 100644 index 8f06ce2d35..0000000000 --- a/.appveyor/install.cmd +++ /dev/null @@ -1,28 +0,0 @@ -REM Windows install script - -rmdir /s /q c:\MinGW -curl -Lo MinGW.7z %SMINGTOOLS%/MinGW-2020-10-19.7z -7z -oC:\ x MinGW.7z - -goto :%SMING_ARCH% - -:Esp8266 - - REM Old toolchain - set TOOLCHAIN=esp-udk-win32.7z - curl -LO %SMINGTOOLS%/%TOOLCHAIN% - 7z -o%UDK_ROOT% x %TOOLCHAIN% - - REM New toolchain - mkdir %EQT_ROOT% - set TOOLCHAIN=x86_64-w64-mingw32.xtensa-lx106-elf-e6a192b.201211.zip - curl -LO %SMINGTOOLS%/%TOOLCHAIN% - 7z -o%EQT_ROOT% x %TOOLCHAIN% - - goto :EOF - - -:Host - - goto :EOF - diff --git a/.ci/build.cmd b/.ci/build.cmd new file mode 100755 index 0000000000..9b53440f51 --- /dev/null +++ b/.ci/build.cmd @@ -0,0 +1,37 @@ +REM Windows build script + +REM Don't leak this +set SMING_SECRET= + +subst z: %CI_BUILD_DIR% +set SMING_HOME=z:\Sming + +cd /d %SMING_HOME% +call Arch\%SMING_ARCH%\Tools\ci\build.setup.cmd || goto :error + +env + +set MAKE_PARALLEL=make -j2 + +REM Move samples and tests into directory outside of the Sming repo. +set SMING_PROJECTS_DIR=%CI_BUILD_DIR%\projects +mkdir %SMING_PROJECTS_DIR% +move ..\samples %SMING_PROJECTS_DIR% +move ..\tests %SMING_PROJECTS_DIR% + +REM Full compile checks please +set STRICT=1 + +REM Diagnostic info +cd /d %SMING_PROJECTS_DIR%/samples/Basic_Blink +make help +make list-config + +cd /d %SMING_HOME% +call Arch\%SMING_ARCH%\Tools\ci\build.run.cmd || goto :error +goto :EOF + + +:error +echo Failed with error #%errorlevel%. +exit /b %errorlevel% diff --git a/.ci/build.sh b/.ci/build.sh new file mode 100755 index 0000000000..55ec52efc3 --- /dev/null +++ b/.ci/build.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -ex # exit with nonzero exit code if anything fails + +# Build times benefit from parallel building +export MAKE_PARALLEL="make -j3" + +cd $SMING_HOME +source Arch/$SMING_ARCH/Tools/ci/build.setup.sh + +# Don't leak this! +unset SMING_SECRET + +env + +# Move samples and tests into directory outside of the Sming repo. +export SMING_PROJECTS_DIR=$HOME/projects +mkdir -p $SMING_PROJECTS_DIR +cd $SMING_HOME/.. +mv samples $SMING_PROJECTS_DIR +mv tests $SMING_PROJECTS_DIR + +# Full compile checks please +export STRICT=1 + +# Diagnostic info +cd $SMING_PROJECTS_DIR/samples/Basic_Blink +make help +make list-config + +$MAKE_PARALLEL + +# Run ARCH build/tests +cd $SMING_HOME +source Arch/$SMING_ARCH/Tools/ci/build.run.sh diff --git a/.ci/deploy.sh b/.ci/deploy.sh new file mode 100755 index 0000000000..69c5e7c75f --- /dev/null +++ b/.ci/deploy.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -ex # exit with nonzero exit code if anything fails + +TAG=$1 +if [ -z $TAG ]; then + printf "Usage:\n\t$0 \n"; + exit 1; +fi + +# [ Create archive of all submodules used in this release (pulled in during documentation build stage) ] +cd $CI_BUILD_DIR +ALL_SUBMODULE_DIRS=$(find $SMING_HOME -name '.submodule' | xargs dirname | sed 's/^\(.*\)\/\(Sming\/.*\)$/\2/') +SUBMODULE_ARCHIVE=sming-submodules.tgz +tar czf $SUBMODULE_ARCHIVE $ALL_SUBMODULE_DIRS + +# [ Create new draft release for this tag] +set +x +source /tmp/secrets.sh +AUTH_HEADER="Authorization: token ${RELEASE_TOKEN}" +RESPONSE=$(curl -H "Content-Type:application/json" -H "$AUTH_HEADER" \ + -d "{\"tag_name\":\"$TAG\",\"target_commitish\": \"develop\",\"name\": \"$TAG\",\"body\":\"Coming soon\",\"draft\": true,\"prerelease\": true}" \ + https://api.github.com/repos/${CI_REPO_NAME}/releases) + +# Get release id +RELEASE_ID=$(echo "$RESPONSE" | jq -r .id) + +upload_asset() { + curl -H "$AUTH_HEADER" -H "Content-Type: $(file -b --mime-type $1)" --data-binary @$1 "https://uploads.github.com/repos/${CI_REPO_NAME}/releases/$RELEASE_ID/assets?name=$(basename $1)" +} + +upload_asset $SUBMODULE_ARCHIVE +upload_asset sming-docs.zip +set -x + +# [ Update the choco packages ] +cd /tmp +CHOCO_REPO="github.com/slaff/chocolatey-packages.git" +git clone -q https://${CHOCO_REPO} choco +cd choco +PACKAGES_TO_CHANGE="sming sming.source" + +for PACKAGE in $PACKAGES_TO_CHANGE; +do + xmlstarlet ed --inplace -N "ns=http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd" -u "/ns:package/ns:metadata/ns:version" -v "$TAG" packages/$PACKAGE/*.nuspec; +done + +git config user.email "appveyor@sminghub.local" +git config user.name "appveyor" +git commit -a -m "Updated chocolatey packages to latest stable $TAG version." || 1 + +# Push upstream +set +x +git push https://${CHOCO_TOKEN}@${CHOCO_REPO} master +set -x diff --git a/.ci/install.cmd b/.ci/install.cmd new file mode 100755 index 0000000000..9834f28957 --- /dev/null +++ b/.ci/install.cmd @@ -0,0 +1,9 @@ +REM Windows install script + +python -m pip install --upgrade pip + +rmdir /s /q c:\MinGW +curl -Lo MinGW.7z %SMINGTOOLS%/MinGW-2020-10-19.7z +7z -oC:\ x MinGW.7z + +call %SMING_HOME%\Arch\%SMING_ARCH%\Tools\ci\install.cmd diff --git a/.ci/install.sh b/.ci/install.sh new file mode 100755 index 0000000000..75b7fea9ca --- /dev/null +++ b/.ci/install.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -ex # exit with nonzero exit code if anything fails + +# Common install + +sudo apt-get update + +sudo update-alternatives --set gcc /usr/bin/gcc-9 +python -m pip install --upgrade pip + +sudo apt-get install -y gcc-9-multilib g++-9-multilib python3-setuptools + +source $SMING_HOME/Arch/$SMING_ARCH/Tools/ci/install.sh diff --git a/.ci/secrets.sh.enc b/.ci/secrets.sh.enc new file mode 100644 index 0000000000..a2e569f02f --- /dev/null +++ b/.ci/secrets.sh.enc @@ -0,0 +1,7 @@ +U2FsdGVkX1/VhqomygkMFsRejvogd3R4GpBC1qAiF+f+drEyY81gn12gf1UnGiaa +ZbLMqFqaBXMUmDxE5Ix0MR8uwSa5Bz1EGFBnhSY17iuMbQVfcAZOXDVpOJFB368G +sctrjME3JSWuvUw66PjOyCdfkKoAlCpQsD1sNGR70nXEqK06IF0JDKPvyTrhJxCC +RTx+Oc83fiLECmUcFityDAAwXkPAvv7A2iRBkoqCUjPmShhZz5niVUi9HuSZwOH6 +EPo6NE2P7B5Emvqh5Mx9qniIvN74KmUFb6JPuUT+tKqNWIVfbmSy/PHRfyGNGJ8v +EH5eWX9SfmGBZwFS7EtV3v81GEWUO3dz8PJ/unzwUgvYOiHYgBIvUPgL9viDhhxF +YzbTuxNjlY2UFHz172ZjxRJv5la7MW7NMI4GrSBHyr4= diff --git a/Sming/Arch/Esp32/Components/esp32/component.mk b/Sming/Arch/Esp32/Components/esp32/component.mk index 2bf48d5e6c..bb207b3ad9 100644 --- a/Sming/Arch/Esp32/Components/esp32/component.mk +++ b/Sming/Arch/Esp32/Components/esp32/component.mk @@ -252,7 +252,9 @@ SDK_PROJECT_PATH := $(COMPONENT_PATH)/project SDK_CONFIG_DEFAULTS := $(SDK_PROJECT_PATH)/sdkconfig.defaults SDKCONFIG_MAKEFILE ?= $(SDK_PROJECT_PATH)/sdkconfig +ifeq ($(MAKE_DOCS),) -include $(SDKCONFIG_MAKEFILE) +endif export SDKCONFIG_MAKEFILE # sub-makes (like bootloader) will reuse this path $(SDK_BUILD_BASE) $(SDK_COMPONENT_LIBDIR): diff --git a/Sming/Arch/Esp32/Tools/ci/build.run.cmd b/Sming/Arch/Esp32/Tools/ci/build.run.cmd new file mode 100644 index 0000000000..49677d701a --- /dev/null +++ b/Sming/Arch/Esp32/Tools/ci/build.run.cmd @@ -0,0 +1,11 @@ +REM Esp32 build.run.cmd + +%MAKE_PARALLEL% Basic_Blink Basic_WiFi HttpServer_ConfigNetwork DEBUG_VERBOSE_LEVEL=3 STRICT=1 || goto :error +%MAKE_PARALLEL% Basic_Ssl ENABLE_SSL=Bearssl DEBUG_VERBOSE_LEVEL=3 STRICT=1 || goto :error + +goto :EOF + + +:error +echo Failed with error #%errorlevel%. +exit /b %errorlevel% diff --git a/Sming/Arch/Esp32/Tools/ci/build.run.sh b/Sming/Arch/Esp32/Tools/ci/build.run.sh new file mode 100644 index 0000000000..01209dfe4c --- /dev/null +++ b/Sming/Arch/Esp32/Tools/ci/build.run.sh @@ -0,0 +1,4 @@ +# Esp32 build.run.sh + +$MAKE_PARALLEL Basic_Blink Basic_WiFi HttpServer_ConfigNetwork DEBUG_VERBOSE_LEVEL=3 STRICT=1 +$MAKE_PARALLEL Basic_Ssl ENABLE_SSL=Bearssl DEBUG_VERBOSE_LEVEL=3 STRICT=1 diff --git a/Sming/Arch/Esp32/Tools/ci/build.setup.cmd b/Sming/Arch/Esp32/Tools/ci/build.setup.cmd new file mode 100644 index 0000000000..ea6e6a24cf --- /dev/null +++ b/Sming/Arch/Esp32/Tools/ci/build.setup.cmd @@ -0,0 +1 @@ +REM Esp32 build.setup.cmd diff --git a/Sming/Arch/Esp32/Tools/ci/build.setup.sh b/Sming/Arch/Esp32/Tools/ci/build.setup.sh new file mode 100644 index 0000000000..4aaa83f008 --- /dev/null +++ b/Sming/Arch/Esp32/Tools/ci/build.setup.sh @@ -0,0 +1 @@ +# Esp32 build.setup.sh diff --git a/Sming/Arch/Esp32/Tools/ci/install.cmd b/Sming/Arch/Esp32/Tools/ci/install.cmd new file mode 100644 index 0000000000..087ec4fa00 --- /dev/null +++ b/Sming/Arch/Esp32/Tools/ci/install.cmd @@ -0,0 +1,12 @@ +REM Esp32 install.cmd + +git clone -b release/v4.1 https://github.com/espressif/esp-idf.git %IDF_PATH% + +REM Espressif downloads very slow, fetch from SmingTools +mkdir %IDF_TOOLS_PATH% +set ESPTOOLS=esp32-tools-windows-4.1.7z +curl -LO %SMINGTOOLS%/%ESPTOOLS% +7z -o%IDF_TOOLS_PATH%\dist x %ESPTOOLS% + +python %IDF_PATH%\tools\idf_tools.py install +python -m pip install -r %IDF_PATH%\requirements.txt diff --git a/Sming/Arch/Esp32/Tools/ci/install.sh b/Sming/Arch/Esp32/Tools/ci/install.sh new file mode 100644 index 0000000000..17a7577081 --- /dev/null +++ b/Sming/Arch/Esp32/Tools/ci/install.sh @@ -0,0 +1,14 @@ +# Esp32 install.sh + +sudo apt-get install -y git wget flex bison gperf ninja-build ccache libffi-dev libssl-dev dfu-util + +git clone -b release/v4.1 https://github.com/espressif/esp-idf.git $IDF_PATH + +# Espressif downloads very slow, fetch from SmingTools +mkdir -p $IDF_TOOLS_PATH +ESPTOOLS=esp32-tools-linux-4.1.zip +wget --no-verbose $SMINGTOOLS/$ESPTOOLS +unzip $ESPTOOLS -d $IDF_TOOLS_PATH/dist + +python $IDF_PATH/tools/idf_tools.py install +python -m pip install -r $IDF_PATH/requirements.txt diff --git a/Sming/Arch/Esp8266/Tools/ci/build.run.cmd b/Sming/Arch/Esp8266/Tools/ci/build.run.cmd new file mode 100644 index 0000000000..830f5d6c46 --- /dev/null +++ b/Sming/Arch/Esp8266/Tools/ci/build.run.cmd @@ -0,0 +1,15 @@ +REM Esp8266 build.run.cmd + +make -C "%SMING_PROJECTS_DIR%\samples\HttpServer_FirmwareUpload" python-requirements +%MAKE_PARALLEL% samples || goto :error + +make clean samples-clean +%MAKE_PARALLEL% Basic_Blink ENABLE_CUSTOM_HEAP=1 DEBUG_VERBOSE_LEVEL=3 || goto :error +%MAKE_PARALLEL% HttpServer_ConfigNetwork ENABLE_CUSTOM_LWIP=2 STRICT=1 || goto :error + +goto :EOF + + +:error +echo Failed with error #%errorlevel%. +exit /b %errorlevel% diff --git a/Sming/Arch/Esp8266/Tools/ci/build.run.sh b/Sming/Arch/Esp8266/Tools/ci/build.run.sh new file mode 100644 index 0000000000..678b068a60 --- /dev/null +++ b/Sming/Arch/Esp8266/Tools/ci/build.run.sh @@ -0,0 +1,7 @@ +# Esp8266 build.run.sh + +make -C "$SMING_PROJECTS_DIR/samples/HttpServer_FirmwareUpload" python-requirements +$MAKE_PARALLEL samples +make clean samples-clean +$MAKE_PARALLEL Basic_Blink ENABLE_CUSTOM_HEAP=1 DEBUG_VERBOSE_LEVEL=3 +$MAKE_PARALLEL HttpServer_ConfigNetwork ENABLE_CUSTOM_LWIP=2 STRICT=1 diff --git a/Sming/Arch/Esp8266/Tools/ci/build.setup.cmd b/Sming/Arch/Esp8266/Tools/ci/build.setup.cmd new file mode 100644 index 0000000000..f6f2238948 --- /dev/null +++ b/Sming/Arch/Esp8266/Tools/ci/build.setup.cmd @@ -0,0 +1 @@ +REM Esp8266 build.setup.cmd diff --git a/Sming/Arch/Esp8266/Tools/ci/build.setup.sh b/Sming/Arch/Esp8266/Tools/ci/build.setup.sh new file mode 100644 index 0000000000..941d5df444 --- /dev/null +++ b/Sming/Arch/Esp8266/Tools/ci/build.setup.sh @@ -0,0 +1,6 @@ +# Esp8266 build.setup.sh + +unset SPIFFY +unset ESPTOOL2 +unset SDK_BASE + diff --git a/Sming/Arch/Esp8266/Tools/ci/install.cmd b/Sming/Arch/Esp8266/Tools/ci/install.cmd new file mode 100644 index 0000000000..8aa477c552 --- /dev/null +++ b/Sming/Arch/Esp8266/Tools/ci/install.cmd @@ -0,0 +1,10 @@ +REM Esp8266 install.cmd + +call :install %UDK_ROOT% esp-udk-win32.7z +call :install %EQT_ROOT% x86_64-w64-mingw32.xtensa-lx106-elf-e6a192b.201211.zip +goto :EOF + +:install +mkdir %1 +curl -LO %SMINGTOOLS%/%2 +7z -o%1 x %2 diff --git a/Sming/Arch/Esp8266/Tools/ci/install.sh b/Sming/Arch/Esp8266/Tools/ci/install.sh new file mode 100644 index 0000000000..87a3918c0f --- /dev/null +++ b/Sming/Arch/Esp8266/Tools/ci/install.sh @@ -0,0 +1,14 @@ +# Esp8266 install.sh + +# Old toolchain +TOOLCHAIN=esp-open-sdk-linux-x86_64.tar.gz +wget --no-verbose $SMINGTOOLS/$TOOLCHAIN +tar -zxf $TOOLCHAIN +mkdir -p $UDK_ROOT +ln -s $(pwd)/esp-open-sdk/xtensa-lx106-elf $UDK_ROOT/. + +# New toolchain +TOOLCHAIN=x86_64-linux-gnu.xtensa-lx106-elf-e6a192b.201211.tar.gz +wget --no-verbose $SMINGTOOLS/$TOOLCHAIN +mkdir -p $EQT_ROOT +tar -zxf $TOOLCHAIN -C $EQT_ROOT --totals diff --git a/Sming/Arch/Host/Tools/ci/build.run.cmd b/Sming/Arch/Host/Tools/ci/build.run.cmd new file mode 100644 index 0000000000..735cc871f0 --- /dev/null +++ b/Sming/Arch/Host/Tools/ci/build.run.cmd @@ -0,0 +1,13 @@ +REM Host build.run.cmd + +REM Build a couple of basic applications +%MAKE_PARALLEL% Basic_Serial Basic_ProgMem STRICT=1 V=1 || goto :error + +REM Run basic tests +%MAKE_PARALLEL% tests || goto :error + +goto :EOF + +:error +echo Failed with error #%errorlevel%. +exit /b %errorlevel% diff --git a/Sming/Arch/Host/Tools/ci/build.run.sh b/Sming/Arch/Host/Tools/ci/build.run.sh new file mode 100644 index 0000000000..ada35bb8fb --- /dev/null +++ b/Sming/Arch/Host/Tools/ci/build.run.sh @@ -0,0 +1,19 @@ +# Host build.run.sh + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" + +if [[ $CHECK_SCA -eq 1 ]]; then + $DIR/coverity-scan.sh +else + $MAKE_PARALLEL Basic_Blink Basic_DateTime Basic_Delegates Basic_Interrupts Basic_ProgMem Basic_Serial Basic_Servo Basic_Ssl LiveDebug DEBUG_VERBOSE_LEVEL=3 +fi + +# Build and run tests +export SMING_TARGET_OPTIONS='--flashfile=$(FLASH_BIN) --flashsize=$(SPI_SIZE)' +$MAKE_PARALLEL tests diff --git a/Sming/Arch/Host/Tools/ci/build.setup.cmd b/Sming/Arch/Host/Tools/ci/build.setup.cmd new file mode 100644 index 0000000000..27b7b02d62 --- /dev/null +++ b/Sming/Arch/Host/Tools/ci/build.setup.cmd @@ -0,0 +1,10 @@ +REM Host build.setup.cmd + +REM Build documentation +make -C %SMING_HOME% docs || goto :error + +goto :EOF + +:error +echo Failed with error #%errorlevel%. +exit /b %errorlevel% diff --git a/Sming/Arch/Host/Tools/ci/build.setup.sh b/Sming/Arch/Host/Tools/ci/build.setup.sh new file mode 100644 index 0000000000..095cbd0cce --- /dev/null +++ b/Sming/Arch/Host/Tools/ci/build.setup.sh @@ -0,0 +1,23 @@ +# Host build.setup.sh + +# Check coding style +make cs +DIFFS=$(git diff) +if [ "$DIFFS" != "" ]; then + echo "!!! Coding Style issues Found!!!" + echo " Run: 'make cs' to fix them. " + echo "$DIFFS" + exit 1 +fi + +# Make deployment keys, etc. available +if [ -z "$CI_PULL_REQUEST" ]; then + set +x + openssl aes-256-cbc -d -a -iter 100 -in $CI_BUILD_DIR/.ci/secrets.sh.enc -out /tmp/secrets.sh -pass pass:$SMING_SECRET + source /tmp/secrets.sh + set -x +fi +unset SMING_SECRET + +# Build documentation +make -C $SMING_HOME docs diff --git a/Sming/Arch/Host/Tools/ci/coverity-scan.sh b/Sming/Arch/Host/Tools/ci/coverity-scan.sh new file mode 100644 index 0000000000..ae800b7472 --- /dev/null +++ b/Sming/Arch/Host/Tools/ci/coverity-scan.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +set -e + +COVERITY_SCAN_PROJECT_NAME=${CI_PROJECT_SLUG} +COVERITY_SCAN_NOTIFICATION_EMAIL="slaff@attachix.com" +COVERITY_SCAN_BUILD_COMMAND="$MAKE_PARALLEL Basic_Blink Basic_DateTime Basic_Delegates Basic_Interrupts Basic_ProgMem Basic_Serial Basic_Servo Basic_Ssl HttpServer_FirmwareUpload SMING_ARCH=Host DEBUG_VERBOSE_LEVEL=3" + +set +x +source /tmp/secrets.sh + +# Environment check +[ -z "$COVERITY_SCAN_PROJECT_NAME" ] && echo "ERROR: COVERITY_SCAN_PROJECT_NAME must be set" && exit 1 +[ -z "$COVERITY_SCAN_NOTIFICATION_EMAIL" ] && echo "ERROR: COVERITY_SCAN_NOTIFICATION_EMAIL must be set" && exit 1 +[ -z "$COVERITY_SCAN_BUILD_COMMAND" ] && echo "ERROR: COVERITY_SCAN_BUILD_COMMAND must be set" && exit 1 +[ -z "$COVERITY_SCAN_TOKEN" ] && echo "ERROR: COVERITY_SCAN_TOKEN must be set" && exit 1 + +PLATFORM=$(uname) +TOOL_ARCHIVE=/tmp/cov-analysis-${PLATFORM}.tgz +TOOL_URL=https://scan.coverity.com/download/${PLATFORM} +TOOL_BASE=/tmp/coverity-scan-analysis +UPLOAD_URL="https://scan.coverity.com/builds" +SCAN_URL="https://scan.coverity.com" + +# Do not run on pull requests +if [ -n "${CI_PULL_REQUEST}" ]; then + echo -e "\033[33;1mINFO: Skipping Coverity Analysis: branch is a pull request.\033[0m" + exit 0 +fi + +# Verify upload is permitted +permit=true +AUTH_RES=$(curl -s --form project="$COVERITY_SCAN_PROJECT_NAME" --form token="$COVERITY_SCAN_TOKEN" $SCAN_URL/api/upload_permitted) +if [ "$AUTH_RES" = "Access denied" ]; then + echo -e "\033[33;1mCoverity Scan API access denied. Check COVERITY_SCAN_PROJECT_NAME and COVERITY_SCAN_TOKEN.\033[0m" + exit 1 +else + AUTH=$(echo $AUTH_RES | ruby -e "require 'rubygems'; require 'json'; puts JSON[STDIN.read]['upload_permitted']") + if [ "$AUTH" = "true" ]; then + echo -e "\033[33;1mCoverity Scan analysis authorized per quota.\033[0m" + else + WHEN=$(echo $AUTH_RES | ruby -e "require 'rubygems'; require 'json'; puts JSON[STDIN.read]['next_upload_permitted_at']") + echo -e "\033[33;1mOops!Coverity Scan analysis engine NOT authorized until $WHEN.\033[0m" + permit=false + fi +fi + +if [ "$permit" = true ]; then +if [ ! -d $TOOL_BASE ]; then + # Download Coverity Scan Analysis Tool + if [ ! -e $TOOL_ARCHIVE ]; then + echo -e "\033[33;1mDownloading Coverity Scan Analysis Tool...\033[0m" + wget -nv -O $TOOL_ARCHIVE $TOOL_URL --post-data "project=$COVERITY_SCAN_PROJECT_NAME&token=$COVERITY_SCAN_TOKEN" + fi + + # Extract Coverity Scan Analysis Tool + echo -e "\033[33;1mExtracting Coverity Scan Analysis Tool...\033[0m" + mkdir -p $TOOL_BASE + CURRENT_DIR=$(pwd) + cd $TOOL_BASE + tar xzf $TOOL_ARCHIVE + cd $CURRENT_DIR +fi + +TOOL_DIR=$(find $TOOL_BASE -type d -name 'cov-analysis*') +export PATH=$TOOL_DIR/bin:$PATH + +# Build +echo -e "\033[33;1mRunning Coverity Scan Analysis Tool...\033[0m" +COV_BUILD_OPTIONS="" +#COV_BUILD_OPTIONS="--return-emit-failures 8 --parse-error-threshold 85" +RESULTS_DIR="cov-int" +eval "${COVERITY_SCAN_BUILD_COMMAND_PREPEND}" +COVERITY_UNSUPPORTED=1 cov-build --dir $RESULTS_DIR $COV_BUILD_OPTIONS $COVERITY_SCAN_BUILD_COMMAND +cov-import-scm --dir $RESULTS_DIR --scm git --log $RESULTS_DIR/scm_log.txt 2>&1 + +# Upload results +echo -e "\033[33;1mTarring Coverity Scan Analysis results...\033[0m" +RESULTS_ARCHIVE=analysis-results.tgz +tar czf $RESULTS_ARCHIVE $RESULTS_DIR +SHA=$(git rev-parse --short HEAD) + +echo -e "\033[33;1mUploading Coverity Scan Analysis results...\033[0m" +response=$(curl \ + --silent --write-out "\n%{http_code}\n" \ + --form project=$COVERITY_SCAN_PROJECT_NAME \ + --form token=$COVERITY_SCAN_TOKEN \ + --form email=$COVERITY_SCAN_NOTIFICATION_EMAIL \ + --form file=@$RESULTS_ARCHIVE \ + --form version=$SHA \ + --form description="CI build" \ + $UPLOAD_URL) +status_code=$(echo "$response" | sed -n '$p') +if [ "$status_code" -ge "400" ]; then + TEXT=$(echo "$response" | sed '$d') + echo -e "\033[33;1mCoverity Scan upload failed: $TEXT. Status code: $status_code.\033[0m" + exit 1 +fi +fi diff --git a/Sming/Arch/Host/Tools/ci/install.cmd b/Sming/Arch/Host/Tools/ci/install.cmd new file mode 100644 index 0000000000..3cee90ae9a --- /dev/null +++ b/Sming/Arch/Host/Tools/ci/install.cmd @@ -0,0 +1,8 @@ +REM Host install.cmd + +curl -LO https://doxygen.nl/files/doxygen-1.9.1-setup.exe +doxygen-1.9.1-setup /silent + +choco install -y graphviz + +python -m pip install -r %SMING_HOME%/../docs/requirements.txt diff --git a/Sming/Arch/Host/Tools/ci/install.sh b/Sming/Arch/Host/Tools/ci/install.sh new file mode 100644 index 0000000000..673729e2fc --- /dev/null +++ b/Sming/Arch/Host/Tools/ci/install.sh @@ -0,0 +1,8 @@ +# Host install.sh + +sudo apt-get install -y clang-format-6.0 \ + python3-sphinx python3-setuptools python3-cairocffi \ + doxygen graphviz-dev xmlstarlet jq +sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/bin/clang-format-6.0 100 + +python -m pip install -r $SMING_HOME/../docs/requirements.txt diff --git a/Sming/Arch/Host/Tools/travis/install.sh b/Sming/Arch/Host/Tools/travis/install.sh index 5db995b801..fd27cb5ba9 100755 --- a/Sming/Arch/Host/Tools/travis/install.sh +++ b/Sming/Arch/Host/Tools/travis/install.sh @@ -2,5 +2,8 @@ set -ex # exit with nonzero exit code if anything fails sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/bin/clang-format-6.0 100 -python3 -m pip install --upgrade pip -python3 -m pip install -r $TRAVIS_BUILD_DIR/docs/requirements.txt +python3 -m pip install -q --upgrade pip + +if [ "$BUILD_DOCS" == "1" ]; then + python3 -m pip install -q -r $TRAVIS_BUILD_DIR/docs/requirements.txt +fi diff --git a/appveyor.yml b/appveyor.yml index baf514cc46..26f19578bf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,30 +1,86 @@ -os: Windows Server 2012 R2 +image: +- Ubuntu2004 +- Visual Studio 2019 + +platform: x86 environment: SMINGTOOLS: https://github.com/SmingHub/SmingTools/releases/download/1.0 - PATH: C:\MinGW\msys\1.0\bin;C:\MinGW\bin;%PATH% - PYTHON: C:\Python38\python - UDK_ROOT: C:\Espressif - EQT_ROOT: C:\esp-quick-toolchain + SMING_SECRET: + secure: rCs19uNvDR07w1d2pfwJIzewiEQ2zpKXdqFp5BqHQAA= + matrix: - - build_platform: "x86" - build_compiler: "mingw" - SMING_ARCH: Host - - - build_platform: "x86" - build_compiler: "udk" - SMING_ARCH: Esp8266 - SDK_VERSION: 3.0.1 - - - build_platform: "x86" - build_compiler: "eqt" - SMING_ARCH: Esp8266 - SDK_VERSION: 3.0.1 - -# cache: -# - src/ # preserve "packages" directory in the root of build folder but will reset it if packages.config is modified + - SMING_ARCH: Host + + - SMING_ARCH: Esp8266 + BUILD_COMPILER: udk + + - SMING_ARCH: Esp8266 + BUILD_COMPILER: eqt + + - SMING_ARCH: Esp32 + + install: - - cmd: .appveyor/install.cmd + - ps: | + # Set up environment variables for all environments and build types + $env:CI_BUILD_DIR = $env:APPVEYOR_BUILD_FOLDER + $env:CI_PROJECT_SLUG = $env:APPVEYOR_PROJECT_SLUG + $env:CI_REPO_NAME = $env:APPVEYOR_REPO_NAME + if ($env:APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME -ne $env:APPVEYOR_REPO_NAME) { + $env:CI_PULL_REQUEST = "true" + } + # Esp8266 + $env:UDK_ROOT = Join-Path $env:CI_BUILD_DIR "opt/esp-alt-sdk" + $env:EQT_ROOT = Join-Path $env:CI_BUILD_DIR "opt/esp-quick-toolchain" + if ($env:BUILD_COMPILER -eq "udk") { + $env:ESP_HOME = $env:UDK_ROOT + } else { + $env:ESP_HOME = $env:EQT_ROOT + } + # Esp32 + $env:IDF_PATH = Join-Path $env:CI_BUILD_DIR "opt/esp-idf" + $env:IDF_TOOLS_PATH = Join-Path $env:CI_BUILD_DIR "opt/tools/esp32" + # General + $env:SMING_HOME = Join-Path $env:CI_BUILD_DIR "Sming" + + - cmd: | + set PATH=C:\Python39;C:\MinGW\msys\1.0\bin;C:\MinGW\bin;%PATH% + set PYTHON=C:\Python39\python + set ESP32_PYTHON_PATH=C:\Python39 + .ci\install.cmd + set PATH=C:\Python39\Scripts;%PATH%;C:\Program Files\doxygen\bin + + - sh: | + export PYTHON=$HOME/venv3.9/bin/python + export ESP32_PYTHON_PATH=$HOME/venv3.9/bin + source $HOME/venv3.9/bin/activate + .ci/install.sh + + +before_build: + - sh: | + # Check if we could run static code analysis + CHECK_SCA=0 + if [[ $APPVEYOR_REPO_TAG_NAME != "" || ( $APPVEYOR_REPO_COMMIT_MESSAGE == *"[scan:coverity]"* && $CI_PULL_REQUEST == "" ) ]]; then + CHECK_SCA=1 + fi build_script: - - cmd: .appveyor/build.cmd + - cmd: .ci\build.cmd + - sh: .ci/build.sh + +after_build: + - ps: | + if ($env:SMING_ARCH -eq "Host") { + $docFile = "sming-docs.zip" + Compress-Archive -Path $env:CI_BUILD_DIR/docs/build/html -DestinationPath $docFile + Push-AppveyorArtifact $docFile + } + + +deploy_script: + - sh: | + if [[ $APPVEYOR_REPO_TAG_NAME != "" && $APPVEYOR_BUILD_WORKER_IMAGE == "Ubuntu2004" && $SMING_ARCH == "Host" ]]; then + .ci/deploy.sh $APPVEYOR_REPO_TAG_NAME + fi diff --git a/tests/HostTests/.cs b/tests/HostTests/app/.cs similarity index 100% rename from tests/HostTests/.cs rename to tests/HostTests/app/.cs diff --git a/tests/HostTests/include/.cs b/tests/HostTests/include/.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/HostTests/modules/.cs b/tests/HostTests/modules/.cs new file mode 100644 index 0000000000..e69de29bb2 From 3df8e9486ae9c508a34d8b957cc741801c21f0b7 Mon Sep 17 00:00:00 2001 From: slaff Date: Thu, 21 Jan 2021 10:30:37 +0100 Subject: [PATCH 02/91] Trigger coverity scan on merge ... (#2196) * Trigger coverity scan on merge if the commit message contains [scan:coverity]. * Fix detection of pull request so coverity scan works Co-authored-by: mikee47 --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 26f19578bf..0f9fca8c40 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,7 +27,7 @@ install: $env:CI_BUILD_DIR = $env:APPVEYOR_BUILD_FOLDER $env:CI_PROJECT_SLUG = $env:APPVEYOR_PROJECT_SLUG $env:CI_REPO_NAME = $env:APPVEYOR_REPO_NAME - if ($env:APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME -ne $env:APPVEYOR_REPO_NAME) { + if ($env:APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME -ne "") { $env:CI_PULL_REQUEST = "true" } # Esp8266 @@ -62,7 +62,7 @@ before_build: - sh: | # Check if we could run static code analysis CHECK_SCA=0 - if [[ $APPVEYOR_REPO_TAG_NAME != "" || ( $APPVEYOR_REPO_COMMIT_MESSAGE == *"[scan:coverity]"* && $CI_PULL_REQUEST == "" ) ]]; then + if [[ $APPVEYOR_REPO_TAG_NAME != "" || ( $APPVEYOR_REPO_COMMIT_MESSAGE_EXTENDED == *"[scan:coverity]"* && $CI_PULL_REQUEST == "" ) ]]; then CHECK_SCA=1 fi From d4f99b5c836a0f9e78301d5fbfd53314ee68f089 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 21 Jan 2021 14:43:59 +0000 Subject: [PATCH 03/91] Check APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME for null or empty string; export CHECK_SCA (#2197) --- appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 0f9fca8c40..f0886bc55d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,7 +27,7 @@ install: $env:CI_BUILD_DIR = $env:APPVEYOR_BUILD_FOLDER $env:CI_PROJECT_SLUG = $env:APPVEYOR_PROJECT_SLUG $env:CI_REPO_NAME = $env:APPVEYOR_REPO_NAME - if ($env:APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME -ne "") { + if ($env:APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME) { $env:CI_PULL_REQUEST = "true" } # Esp8266 @@ -61,9 +61,9 @@ install: before_build: - sh: | # Check if we could run static code analysis - CHECK_SCA=0 + export CHECK_SCA=0 if [[ $APPVEYOR_REPO_TAG_NAME != "" || ( $APPVEYOR_REPO_COMMIT_MESSAGE_EXTENDED == *"[scan:coverity]"* && $CI_PULL_REQUEST == "" ) ]]; then - CHECK_SCA=1 + export CHECK_SCA=1 fi build_script: From 30d705b3b8ce5048793b404989fea92947e12c16 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 25 Jan 2021 08:30:37 +0000 Subject: [PATCH 04/91] Update UPnP and UPnP-Schema libraries (#2199) UPnP: * Make class information optional (Control classes obtained this from device description) UPnP-Schema * Remove 'device' directory, not required * Device/Service `class_` doesn't need to be a reference * Always invoke schema sub-make to ensure source code is up to date --- Sming/Libraries/UPnP | 2 +- Sming/Libraries/UPnP-Schema | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sming/Libraries/UPnP b/Sming/Libraries/UPnP index 066c84cf97..0dc7d07fe8 160000 --- a/Sming/Libraries/UPnP +++ b/Sming/Libraries/UPnP @@ -1 +1 @@ -Subproject commit 066c84cf971652a1d476468d9cd8796f337f54ab +Subproject commit 0dc7d07fe8436c96f8c0072acea4d4eca0074139 diff --git a/Sming/Libraries/UPnP-Schema b/Sming/Libraries/UPnP-Schema index 99a5fdbdee..1438502af5 160000 --- a/Sming/Libraries/UPnP-Schema +++ b/Sming/Libraries/UPnP-Schema @@ -1 +1 @@ -Subproject commit 99a5fdbdeed062680809329ffc8c533338d3ceb1 +Subproject commit 1438502af5a922bd04d81f158c6a820815fefe42 From 8066f473e0c1f136c7e931aa196ecbe103df4ceb Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 26 Jan 2021 08:23:17 +0000 Subject: [PATCH 05/91] Fix CI building before PR submitted (i.e. not a pull request) (#2198) [scan:coverity] --- Sming/Arch/Host/Tools/ci/build.setup.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sming/Arch/Host/Tools/ci/build.setup.sh b/Sming/Arch/Host/Tools/ci/build.setup.sh index 095cbd0cce..692cdae27a 100644 --- a/Sming/Arch/Host/Tools/ci/build.setup.sh +++ b/Sming/Arch/Host/Tools/ci/build.setup.sh @@ -11,13 +11,13 @@ if [ "$DIFFS" != "" ]; then fi # Make deployment keys, etc. available -if [ -z "$CI_PULL_REQUEST" ]; then - set +x +set +x +if [ -n "$SMING_SECRET" ]; then openssl aes-256-cbc -d -a -iter 100 -in $CI_BUILD_DIR/.ci/secrets.sh.enc -out /tmp/secrets.sh -pass pass:$SMING_SECRET source /tmp/secrets.sh - set -x + unset SMING_SECRET fi -unset SMING_SECRET +set -x # Build documentation make -C $SMING_HOME docs From c840b7cfa00ac7d1501e03f90b6aa41e2d7f3cb7 Mon Sep 17 00:00:00 2001 From: slaff Date: Wed, 27 Jan 2021 09:37:07 +0100 Subject: [PATCH 06/91] Make shell files exutable. (#2200) [scan:coverity] --- Sming/Arch/Esp32/Tools/ci/build.run.sh | 0 Sming/Arch/Esp32/Tools/ci/build.setup.sh | 0 Sming/Arch/Esp32/Tools/ci/install.sh | 0 Sming/Arch/Esp8266/Tools/ci/build.run.sh | 0 Sming/Arch/Esp8266/Tools/ci/build.setup.sh | 0 Sming/Arch/Esp8266/Tools/ci/install.sh | 0 Sming/Arch/Host/Tools/ci/build.run.sh | 0 Sming/Arch/Host/Tools/ci/build.setup.sh | 0 Sming/Arch/Host/Tools/ci/coverity-scan.sh | 0 Sming/Arch/Host/Tools/ci/install.sh | 0 Sming/Components/ssl/Tools/make_certs.sh | 0 11 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 Sming/Arch/Esp32/Tools/ci/build.run.sh mode change 100644 => 100755 Sming/Arch/Esp32/Tools/ci/build.setup.sh mode change 100644 => 100755 Sming/Arch/Esp32/Tools/ci/install.sh mode change 100644 => 100755 Sming/Arch/Esp8266/Tools/ci/build.run.sh mode change 100644 => 100755 Sming/Arch/Esp8266/Tools/ci/build.setup.sh mode change 100644 => 100755 Sming/Arch/Esp8266/Tools/ci/install.sh mode change 100644 => 100755 Sming/Arch/Host/Tools/ci/build.run.sh mode change 100644 => 100755 Sming/Arch/Host/Tools/ci/build.setup.sh mode change 100644 => 100755 Sming/Arch/Host/Tools/ci/coverity-scan.sh mode change 100644 => 100755 Sming/Arch/Host/Tools/ci/install.sh mode change 100644 => 100755 Sming/Components/ssl/Tools/make_certs.sh diff --git a/Sming/Arch/Esp32/Tools/ci/build.run.sh b/Sming/Arch/Esp32/Tools/ci/build.run.sh old mode 100644 new mode 100755 diff --git a/Sming/Arch/Esp32/Tools/ci/build.setup.sh b/Sming/Arch/Esp32/Tools/ci/build.setup.sh old mode 100644 new mode 100755 diff --git a/Sming/Arch/Esp32/Tools/ci/install.sh b/Sming/Arch/Esp32/Tools/ci/install.sh old mode 100644 new mode 100755 diff --git a/Sming/Arch/Esp8266/Tools/ci/build.run.sh b/Sming/Arch/Esp8266/Tools/ci/build.run.sh old mode 100644 new mode 100755 diff --git a/Sming/Arch/Esp8266/Tools/ci/build.setup.sh b/Sming/Arch/Esp8266/Tools/ci/build.setup.sh old mode 100644 new mode 100755 diff --git a/Sming/Arch/Esp8266/Tools/ci/install.sh b/Sming/Arch/Esp8266/Tools/ci/install.sh old mode 100644 new mode 100755 diff --git a/Sming/Arch/Host/Tools/ci/build.run.sh b/Sming/Arch/Host/Tools/ci/build.run.sh old mode 100644 new mode 100755 diff --git a/Sming/Arch/Host/Tools/ci/build.setup.sh b/Sming/Arch/Host/Tools/ci/build.setup.sh old mode 100644 new mode 100755 diff --git a/Sming/Arch/Host/Tools/ci/coverity-scan.sh b/Sming/Arch/Host/Tools/ci/coverity-scan.sh old mode 100644 new mode 100755 diff --git a/Sming/Arch/Host/Tools/ci/install.sh b/Sming/Arch/Host/Tools/ci/install.sh old mode 100644 new mode 100755 diff --git a/Sming/Components/ssl/Tools/make_certs.sh b/Sming/Components/ssl/Tools/make_certs.sh old mode 100644 new mode 100755 From 47d0f98cd99f1c6d4a4307948b21925279e01eaa Mon Sep 17 00:00:00 2001 From: slaff Date: Wed, 27 Jan 2021 09:57:35 +0100 Subject: [PATCH 07/91] Fix coverity scan project name. (#2201) [scan:coverity] --- Sming/Arch/Host/Tools/ci/coverity-scan.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Arch/Host/Tools/ci/coverity-scan.sh b/Sming/Arch/Host/Tools/ci/coverity-scan.sh index ae800b7472..f53b2afb6a 100755 --- a/Sming/Arch/Host/Tools/ci/coverity-scan.sh +++ b/Sming/Arch/Host/Tools/ci/coverity-scan.sh @@ -2,7 +2,7 @@ set -e -COVERITY_SCAN_PROJECT_NAME=${CI_PROJECT_SLUG} +COVERITY_SCAN_PROJECT_NAME=SmingHub/Sming COVERITY_SCAN_NOTIFICATION_EMAIL="slaff@attachix.com" COVERITY_SCAN_BUILD_COMMAND="$MAKE_PARALLEL Basic_Blink Basic_DateTime Basic_Delegates Basic_Interrupts Basic_ProgMem Basic_Serial Basic_Servo Basic_Ssl HttpServer_FirmwareUpload SMING_ARCH=Host DEBUG_VERBOSE_LEVEL=3" From bdf1d31db839385a8e1075f901b9cd95d6fb538d Mon Sep 17 00:00:00 2001 From: slaff Date: Wed, 27 Jan 2021 10:59:33 +0100 Subject: [PATCH 08/91] Make sure to install the python requirements for HttpServer_FirmwareUpload before running make. (#2202) [scan:coverity] --- Sming/Arch/Host/Tools/ci/coverity-scan.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sming/Arch/Host/Tools/ci/coverity-scan.sh b/Sming/Arch/Host/Tools/ci/coverity-scan.sh index f53b2afb6a..d914716e71 100755 --- a/Sming/Arch/Host/Tools/ci/coverity-scan.sh +++ b/Sming/Arch/Host/Tools/ci/coverity-scan.sh @@ -2,6 +2,8 @@ set -e +make -C $SMING_HOME/../samples/HttpServer_FirmwareUpload python-requirements + COVERITY_SCAN_PROJECT_NAME=SmingHub/Sming COVERITY_SCAN_NOTIFICATION_EMAIL="slaff@attachix.com" COVERITY_SCAN_BUILD_COMMAND="$MAKE_PARALLEL Basic_Blink Basic_DateTime Basic_Delegates Basic_Interrupts Basic_ProgMem Basic_Serial Basic_Servo Basic_Ssl HttpServer_FirmwareUpload SMING_ARCH=Host DEBUG_VERBOSE_LEVEL=3" From 1cc68c6dd1c1f467bbc7bad68a8812c340a07ec0 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 1 Feb 2021 09:06:35 +0000 Subject: [PATCH 09/91] Fix bug in `smg_uart_set_tx()` (#2210) Sets pin on wrong uart --- Sming/Arch/Esp8266/Components/driver/uart.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sming/Arch/Esp8266/Components/driver/uart.cpp b/Sming/Arch/Esp8266/Components/driver/uart.cpp index e6c50b94fc..22114d3818 100644 --- a/Sming/Arch/Esp8266/Components/driver/uart.cpp +++ b/Sming/Arch/Esp8266/Components/driver/uart.cpp @@ -910,9 +910,9 @@ void smg_uart_swap(smg_uart_t* uart, int tx_pin) bool smg_uart_set_tx(smg_uart_t* uart, int tx_pin) { if(uart != nullptr && uart->uart_nr == UART0 && smg_uart_tx_enabled(uart)) { - uart1_pin_restore(uart->tx_pin); + uart0_pin_restore(uart->tx_pin); uart->tx_pin = (tx_pin == 2) ? 2 : 1; - uart1_pin_select(uart->tx_pin); + uart0_pin_select(uart->tx_pin); return true; } From 93ab2e7d3b7dcc3e4e77441287dc9ed328d3afac Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 3 Feb 2021 08:57:44 +0000 Subject: [PATCH 10/91] MacOS build fixes (#2212) Also (probably) applies to FreeBSD. * Archive format seems to be different for MacOS so must use correct version of `ar` for linking. * `cp` command does not have `-u` option. * Fix OtaUpgrade scripted timestamp computation for MacOS/FreeBSD compatibility. FreeBSD `date` variant does not support nanoseconds. Use python scriptlet. --- Sming/Arch/Esp8266/Components/esp8266/component.mk | 2 +- Sming/Libraries/OtaUpgrade/component.mk | 2 +- Sming/project.mk | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sming/Arch/Esp8266/Components/esp8266/component.mk b/Sming/Arch/Esp8266/Components/esp8266/component.mk index dbbfcb4991..46c5d142c8 100644 --- a/Sming/Arch/Esp8266/Components/esp8266/component.mk +++ b/Sming/Arch/Esp8266/Components/esp8266/component.mk @@ -63,7 +63,7 @@ COMPONENT_TARGETS += $(LIBCRYPTO_ORIG) # Make backup then modify original $(COMPONENT_RULE)$(LIBCRYPTO_ORIG): $(LIBCRYPTO) cp $^ $@ - ar -d $^ aes-internal-dec.o + $(AR) -d $^ aes-internal-dec.o # Define linker symbols EXTRA_LDFLAGS += -Wl,--just-symbols=$(COMPONENT_PATH)/ld/crypto.sym diff --git a/Sming/Libraries/OtaUpgrade/component.mk b/Sming/Libraries/OtaUpgrade/component.mk index 572a4f1b10..d19bec3b07 100644 --- a/Sming/Libraries/OtaUpgrade/component.mk +++ b/Sming/Libraries/OtaUpgrade/component.mk @@ -96,7 +96,7 @@ OTA_DATE_REF := -2208988800000LL _ota-make-build-timestamp: | $(OTA_GENCODE_DIR) $(Q) echo '#include ' > $(OTA_BUILD_TIMESTAMP_SRC) $(Q) echo 'namespace OtaUpgrade {' >> $(OTA_BUILD_TIMESTAMP_SRC) - $(Q) echo 'extern const uint64_t BuildTimestamp PROGMEM = $(shell date +%s%3NLL) - $(OTA_DATE_REF);' >> $(OTA_BUILD_TIMESTAMP_SRC) + $(Q) echo 'extern const uint64_t BuildTimestamp PROGMEM = $(shell $(PYTHON) -c "import time; print('%uLL' % (1000 * time.time()))") - $(OTA_DATE_REF);' >> $(OTA_BUILD_TIMESTAMP_SRC) $(Q) echo '} // namespace OtaUpgrade' >> $(OTA_BUILD_TIMESTAMP_SRC) App-build: _ota-make-build-timestamp diff --git a/Sming/project.mk b/Sming/project.mk index b7ee31eed9..ec9440cbc1 100644 --- a/Sming/project.mk +++ b/Sming/project.mk @@ -336,7 +336,7 @@ COMPONENT_DIRS := $(foreach d,$(ALL_SEARCH_DIRS),$(wildcard $d/*)) %/component.mk: @if [ -f $(@D)/../.patches/$(notdir $(@D))/component.mk ]; then \ echo Patching $(abspath $(@D)/../.patches/$(notdir $(@D))/component.mk); \ - cp -u $(@D)/../.patches/$(notdir $(@D))/component.mk $@; \ + cp $(@D)/../.patches/$(notdir $(@D))/component.mk $@; \ fi SUBMODULES_FOUND = $(wildcard $(SUBMODULES:=/.submodule)) From fdb772a76316c2697d3e0aa795bd67efd206ecd1 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 8 Feb 2021 08:37:37 +0000 Subject: [PATCH 11/91] Partition tables (#2171) !!! WARNING: BREAKING CHANGE !!! SPI_SIZE and DISABLE_SPIFFS directives are removed! If you have an application that is using spiffs make sure to add the following line in your component.mk and remove the `DISABLE_SPIFFS` directive, if present. ``` HWCONFIG := spiffs ``` If your application is not using spiffs then just remove the `DISABLE_SPIFFS` directive. For more information take a look at the [migration document](https://sming.readthedocs.io/en/feature-partition-tables/upgrading/4.2-4.3.html). More information about the changes can be seen below: ========================================== Implement class-based API for Sming with support for all architectures. See #1676 for discussion. Summary of changes: * Create new `Storage` component and port low-level Esp32 partition support * Use `gen_esp32part.py` tool as basis for new `hwconfig.py` tool. Integrate into build system. Add new targets. * Implement a C++ API. `Partition::SubType::App` and `Partition::SubType::Data` are strong enums used to identify all the standard types, but the base API still uses type/subtype so custom types can be accommodated. * Revise esp8266 sector layout - see updated documentation. This places all the critical information at the start of flash, whereas previously it was at the end. This avoids issues where flash size is set incorrectly. * Custom `sysParam` data partition sub-type used for esp8266 system parameters and RF calibration data. * Add support for 'external' devices (e.g. SPIRAM, supplementary flash, EEPROM, etc.) using a custom `Storage` partition type. Applications register a `Storage::Device` object implementation with the Partition API to register these external devices with a C++ interface for them. * Disentangle SPIFFS from rBoot - OTA upgrading uses partition API. * Drive SPIFFS image creation from partition table entries - more flexible, multiple images. * Generate build variables directly from hardware config file (see `out/.../hwconfig.mk`). Preserve existing build variables where possible, document changes. * Document hardware configuration with schema and incorporate validation. This can assist with fixing malformed configurations and supplements checks included in the partition tool. * Add `Basic_Storage` sample to more thoroughly demonstrate usage * Update documentation * Test (HostTests) on all arches * Remove `DISABLE_SPIFFS`, `SPI_SIZE` and `SPIFF_SIZE` build variables from all samples, including those in submodules. * Include samples and tests within submodules to CI * Add check to ensure image size will fit within partition --- .ci/install.cmd | 2 +- .ci/install.sh | 2 +- .github/workflows/codeql-analysis.yml | 10 +- .../Components/esp32/sdk/partitions/base.csv | 5 - .../Esp32/Components/esp32/src/startup.cpp | 2 + Sming/Arch/Esp32/Components/esp_spiffs/.cs | 0 .../Esp32/Components/esp_spiffs/README.rst | 4 - .../Esp32/Components/esp_spiffs/component.mk | 1 - .../Components/esp_spiffs/spiffs_config.c | 37 -- .../Esp32/Components/sming-arch/component.mk | 1 - Sming/Arch/Esp32/app.mk | 49 -- Sming/Arch/Esp32/standard.hw | 35 ++ .../Esp8266/Components/esp8266/component.mk | 5 - .../Esp8266/Components/esp8266/startup.cpp | 70 +-- Sming/Arch/Esp8266/Tools/ci/build.run.sh | 6 +- Sming/Arch/Esp8266/app.mk | 17 - Sming/Arch/Esp8266/spiffs-two-roms.hw | 13 + Sming/Arch/Esp8266/standard.hw | 36 ++ Sming/Arch/Esp8266/two-rom-mode.hw | 16 + .../Arch/Host/Components/hostlib/startup.cpp | 3 + Sming/Arch/Host/Components/vflash/README.rst | 2 +- .../Arch/Host/Components/vflash/component.mk | 36 +- Sming/Arch/Host/Components/vflash/vflash.py | 123 ++++ Sming/Arch/Host/Tools/ci/coverity-scan.sh | 2 - Sming/Arch/Host/Tools/setup-network-linux.sh | 2 +- Sming/Arch/Host/app.mk | 3 - Sming/Arch/Host/standard.hw | 20 + Sming/Components/FlashString | 2 +- Sming/Components/Storage/README.rst | 335 +++++++++++ Sming/Components/Storage/Tools/README.rst | 6 + .../Storage/Tools/hwconfig/common.py | 96 ++++ .../Storage/Tools/hwconfig/config.py | 106 ++++ .../Storage/Tools/hwconfig/hwconfig.py | 111 ++++ .../Storage/Tools/hwconfig/partition.py | 541 ++++++++++++++++++ .../Storage/Tools/hwconfig/storage.py | 116 ++++ .../Components/{esptool => Storage}/blank.bin | 0 Sming/Components/Storage/component.mk | 186 ++++++ Sming/Components/Storage/notes.rst | 69 +++ Sming/Components/Storage/requirements.txt | 1 + Sming/Components/Storage/schema.json | 178 ++++++ Sming/Components/Storage/src/CustomDevice.cpp | 42 ++ Sming/Components/Storage/src/Device.cpp | 112 ++++ Sming/Components/Storage/src/Iterator.cpp | 68 +++ Sming/Components/Storage/src/ObjectList.cpp | 65 +++ Sming/Components/Storage/src/Partition.cpp | 251 ++++++++ .../Storage/src/PartitionStream.cpp | 61 ++ .../Components/Storage/src/PartitionTable.cpp | 37 ++ Sming/Components/Storage/src/ProgMem.cpp | 35 ++ Sming/Components/Storage/src/SpiFlash.cpp | 66 +++ Sming/Components/Storage/src/Storage.cpp | 75 +++ Sming/Components/Storage/src/SysMem.cpp | 25 + .../Components/Storage/src/include/Storage.h | 62 ++ .../src/include/Storage/CustomDevice.h | 35 ++ .../Storage/src/include/Storage/Device.h | 140 +++++ .../Storage/src/include/Storage/Iterator.h | 78 +++ .../Storage/src/include/Storage/Object.h | 144 +++++ .../Storage/src/include/Storage/ObjectList.h | 137 +++++ .../Storage/src/include/Storage/Partition.h | 376 ++++++++++++ .../src/include/Storage/PartitionStream.h | 61 ++ .../src/include/Storage/PartitionTable.h | 92 +++ .../Storage/src/include/Storage/ProgMem.h | 89 +++ .../Storage/src/include/Storage/SpiFlash.h | 38 ++ .../src/include/Storage/StreamDevice.h | 67 +++ .../Storage/src/include/Storage/SysMem.h | 88 +++ .../src/include/Storage/partition_info.h | 32 ++ Sming/Components/esptool/README.rst | 10 +- Sming/Components/esptool/component.mk | 60 +- Sming/Components/rboot/.patches/rboot.patch | 39 +- Sming/Components/rboot/README.rst | 98 +++- .../rboot/appcode/rboot-overrides.c | 43 -- Sming/Components/rboot/component.mk | 47 +- .../rboot/include/Network/RbootHttpUpdater.h | 2 +- .../{appcode => include}/rboot-integration.h | 0 Sming/Components/rboot/rboot | 2 +- .../rboot/{host => src/Arch/Host}/rboot.cpp | 6 +- .../Components/rboot/src/RbootHttpUpdater.cpp | 2 +- .../rboot/src/RbootOutputStream.cpp | 4 +- Sming/Components/spiffs/README.rst | 43 +- Sming/Components/spiffs/blankfs.bin | 1 - Sming/Components/spiffs/component.mk | 80 +-- Sming/Components/spiffs/spiffs_config.h | 4 +- Sming/Components/spiffs/spiffs_sming.cpp | 122 ++-- Sming/Components/spiffs/spiffs_sming.h | 45 +- Sming/Components/spiffs/spiffy/spiffy.c | 8 +- Sming/Core/Data/CString.h | 62 +- Sming/Core/SmingCore.h | 2 + .../Arduino_TensorFlowLite/README.rst | 8 - Sming/Libraries/ArduCAM/ArduCAMStream.cpp | 2 +- Sming/Libraries/Arduino_TensorFlowLite | 2 +- .../CS5460/samples/generic/component.mk | 1 - Sming/Libraries/DIAL | 2 +- Sming/Libraries/HueEmulator | 2 +- .../ModbusMaster/samples/generic/component.mk | 1 - .../OtaUpgrade/OtaUpgrade/BasicStream.cpp | 2 +- Sming/Libraries/RapidXML | 2 +- Sming/Libraries/RingTone | 2 +- Sming/Libraries/SmingTest | 2 +- Sming/Libraries/TFT_S1D13781 | 2 +- Sming/Libraries/UPnP | 2 +- .../modbusino/samples/generic/component.mk | 1 - Sming/Makefile | 45 +- Sming/build.mk | 8 +- Sming/building.rst | 9 +- Sming/component-wrapper.mk | 1 + Sming/component.mk | 23 +- Sming/project.mk | 3 + Sming/spiffs.hw | 17 + Sming/standard-4m.hw | 9 + Tools/requirements.txt | 2 + docs/Makefile | 4 +- .../arch/esp8266/getting-started/config.rst | 31 +- docs/source/information/flash.rst | 59 +- docs/source/information/rboot-ota.rst | 89 +-- .../source/troubleshooting/random-restart.rst | 29 +- docs/source/upgrading/4.2-4.3.rst | 98 ++++ docs/source/upgrading/index.rst | 1 + samples/Accelerometer_MMA7455/component.mk | 2 - samples/Arducam/component.mk | 2 +- samples/Basic_APA102/component.mk | 2 - samples/Basic_AWS/component.mk | 1 - samples/Basic_Audio/component.mk | 2 - samples/Basic_Blink/component.mk | 8 +- samples/Basic_Capsense/component.mk | 2 - samples/Basic_DateTime/component.mk | 2 - samples/Basic_Delegates/component.mk | 1 - samples/Basic_HwPWM/component.mk | 2 - samples/Basic_Interrupts/component.mk | 1 - samples/Basic_NFC/component.mk | 2 - samples/Basic_Neopixel/component.mk | 2 - samples/Basic_ProgMem/component.mk | 1 - samples/Basic_ScannerI2C/component.mk | 1 - samples/Basic_Serial/component.mk | 3 +- samples/Basic_Servo/component.mk | 2 - samples/Basic_SmartConfig/component.mk | 1 - samples/Basic_Ssl/component.mk | 5 - samples/Basic_Storage/.cproject | 153 +++++ samples/Basic_Storage/.project | 28 + samples/Basic_Storage/Makefile | 9 + samples/Basic_Storage/README.rst | 7 + samples/Basic_Storage/app/application.cpp | 99 ++++ samples/Basic_Storage/basic_storage.hw | 53 ++ samples/Basic_Storage/component.mk | 4 + .../files/spiffs0/file for spiffs0.txt | 7 + .../files/spiffs1/file for spiffs1.txt | 7 + .../files/spiffs2/file for spiffs2.txt | 7 + samples/Basic_Storage/user0.bin | 1 + samples/Basic_Tasks/component.mk | 1 - samples/Basic_Utility/component.mk | 3 - samples/Basic_WebSkeletonApp/README.rst | 2 +- .../Basic_WebSkeletonApp/app/application.cpp | 2 + .../Basic_WebSkeletonApp/app/webserver.cpp | 2 +- samples/Basic_WebSkeletonApp/component.mk | 10 +- samples/Basic_WebSkeletonApp_LTS/component.mk | 3 +- samples/Basic_WiFi/component.mk | 1 - samples/Basic_rBoot/README.rst | 18 +- samples/Basic_rBoot/app/application.cpp | 92 ++- samples/Basic_rBoot/basic_rboot.hw | 19 + samples/Basic_rBoot/component.mk | 32 +- samples/CanBus/component.mk | 2 - samples/CommandProcessing_Debug/component.mk | 2 +- samples/Compass_HMC5883L/component.mk | 2 - samples/DFPlayerMini/component.mk | 2 - samples/DS3232RTC_NTP_Setter/component.mk | 2 - samples/Display_TM1637/component.mk | 2 - samples/Distance_Vl53l0x/component.mk | 2 - samples/DnsCaptivePortal/component.mk | 1 - samples/Echo_Ssl/component.mk | 5 - samples/FtpServer_Files/component.mk | 2 + samples/Gesture_APDS-9960/component.mk | 2 - samples/HttpClient/component.mk | 3 +- samples/HttpClient_Instapush/component.mk | 3 +- samples/HttpClient_ThingSpeak/component.mk | 3 +- samples/HttpServer_AJAX/component.mk | 1 + samples/HttpServer_Bootstrap/component.mk | 2 + samples/HttpServer_ConfigNetwork/component.mk | 1 + samples/HttpServer_FirmwareUpload/README.rst | 10 +- .../HttpServer_FirmwareUpload/component.mk | 1 + samples/HttpServer_WebSockets/component.mk | 1 + samples/Humidity_AM2321/component.mk | 2 - samples/Humidity_DHT22/component.mk | 2 - samples/Humidity_SI7021/component.mk | 2 - samples/IR_lib/component.mk | 2 - samples/LED_WS2812/component.mk | 2 - samples/LED_YeelightBulb/component.mk | 1 - samples/Light_BH1750/component.mk | 2 - samples/LiquidCrystal_44780/component.mk | 2 - samples/LiveDebug/component.mk | 2 - samples/MeteoControl/component.mk | 1 + samples/MeteoControl_mqtt/component.mk | 2 - samples/MqttClient_Hello/component.mk | 5 +- samples/Nextion_Button/component.mk | 2 - samples/PortExpander_MCP23017/component.mk | 2 - samples/Pressure_BMP180/component.mk | 2 - samples/Radio_RCSwitch/component.mk | 2 - samples/Radio_nRF24L01/component.mk | 2 - samples/Radio_si4432/component.mk | 2 - samples/SDCard/component.mk | 1 - samples/ScreenLCD_5110/component.mk | 2 - samples/ScreenOLED_SSD1306/component.mk | 2 - samples/ScreenTFT_ILI9163C/component.mk | 2 - .../ScreenTFT_ILI9340-ILI9341/component.mk | 1 + samples/ScreenTFT_ST7735/component.mk | 1 + samples/SmtpClient/component.mk | 1 + samples/SystemClock_NTP/component.mk | 2 - samples/TcpClient_NarodMon/component.mk | 1 - .../Telnet_TCPServer_TCPClient/component.mk | 1 - samples/Temperature_DS1820/component.mk | 2 - samples/UdpServer_Echo/component.mk | 1 - samples/UdpServer_mDNS/component.mk | 3 +- samples/Ultrasonic_HCSR04/component.mk | 2 - samples/Websocket_Client/component.mk | 5 +- samples/Wifi_Sniffer/component.mk | 1 - tests/HostTests/component.mk | 3 +- tests/HostTests/host-tests.hw | 42 ++ tests/HostTests/include/modules.h | 1 + tests/HostTests/modules/ArduinoJson6.cpp | 26 +- tests/HostTests/modules/Spiffs.cpp | 8 +- tests/HostTests/modules/Storage.cpp | 110 ++++ tests/SharedComponent/Project/component.mk | 5 - 219 files changed, 5626 insertions(+), 949 deletions(-) delete mode 100644 Sming/Arch/Esp32/Components/esp32/sdk/partitions/base.csv delete mode 100644 Sming/Arch/Esp32/Components/esp_spiffs/.cs delete mode 100644 Sming/Arch/Esp32/Components/esp_spiffs/README.rst delete mode 100644 Sming/Arch/Esp32/Components/esp_spiffs/component.mk delete mode 100644 Sming/Arch/Esp32/Components/esp_spiffs/spiffs_config.c create mode 100644 Sming/Arch/Esp32/standard.hw create mode 100644 Sming/Arch/Esp8266/spiffs-two-roms.hw create mode 100644 Sming/Arch/Esp8266/standard.hw create mode 100644 Sming/Arch/Esp8266/two-rom-mode.hw create mode 100644 Sming/Arch/Host/Components/vflash/vflash.py create mode 100644 Sming/Arch/Host/standard.hw create mode 100644 Sming/Components/Storage/README.rst create mode 100644 Sming/Components/Storage/Tools/README.rst create mode 100644 Sming/Components/Storage/Tools/hwconfig/common.py create mode 100644 Sming/Components/Storage/Tools/hwconfig/config.py create mode 100644 Sming/Components/Storage/Tools/hwconfig/hwconfig.py create mode 100644 Sming/Components/Storage/Tools/hwconfig/partition.py create mode 100644 Sming/Components/Storage/Tools/hwconfig/storage.py rename Sming/Components/{esptool => Storage}/blank.bin (100%) create mode 100644 Sming/Components/Storage/component.mk create mode 100644 Sming/Components/Storage/notes.rst create mode 100644 Sming/Components/Storage/requirements.txt create mode 100644 Sming/Components/Storage/schema.json create mode 100644 Sming/Components/Storage/src/CustomDevice.cpp create mode 100644 Sming/Components/Storage/src/Device.cpp create mode 100644 Sming/Components/Storage/src/Iterator.cpp create mode 100644 Sming/Components/Storage/src/ObjectList.cpp create mode 100644 Sming/Components/Storage/src/Partition.cpp create mode 100644 Sming/Components/Storage/src/PartitionStream.cpp create mode 100644 Sming/Components/Storage/src/PartitionTable.cpp create mode 100644 Sming/Components/Storage/src/ProgMem.cpp create mode 100644 Sming/Components/Storage/src/SpiFlash.cpp create mode 100644 Sming/Components/Storage/src/Storage.cpp create mode 100644 Sming/Components/Storage/src/SysMem.cpp create mode 100644 Sming/Components/Storage/src/include/Storage.h create mode 100644 Sming/Components/Storage/src/include/Storage/CustomDevice.h create mode 100644 Sming/Components/Storage/src/include/Storage/Device.h create mode 100644 Sming/Components/Storage/src/include/Storage/Iterator.h create mode 100644 Sming/Components/Storage/src/include/Storage/Object.h create mode 100644 Sming/Components/Storage/src/include/Storage/ObjectList.h create mode 100644 Sming/Components/Storage/src/include/Storage/Partition.h create mode 100644 Sming/Components/Storage/src/include/Storage/PartitionStream.h create mode 100644 Sming/Components/Storage/src/include/Storage/PartitionTable.h create mode 100644 Sming/Components/Storage/src/include/Storage/ProgMem.h create mode 100644 Sming/Components/Storage/src/include/Storage/SpiFlash.h create mode 100644 Sming/Components/Storage/src/include/Storage/StreamDevice.h create mode 100644 Sming/Components/Storage/src/include/Storage/SysMem.h create mode 100644 Sming/Components/Storage/src/include/Storage/partition_info.h delete mode 100644 Sming/Components/rboot/appcode/rboot-overrides.c rename Sming/Components/rboot/{appcode => include}/rboot-integration.h (100%) rename Sming/Components/rboot/{host => src/Arch/Host}/rboot.cpp (89%) delete mode 100644 Sming/Components/spiffs/blankfs.bin delete mode 100644 Sming/Libraries/.patches/Arduino_TensorFlowLite/README.rst create mode 100644 Sming/spiffs.hw create mode 100644 Sming/standard-4m.hw create mode 100644 Tools/requirements.txt create mode 100644 docs/source/upgrading/4.2-4.3.rst delete mode 100644 samples/Basic_Interrupts/component.mk delete mode 100644 samples/Basic_ProgMem/component.mk create mode 100644 samples/Basic_Storage/.cproject create mode 100644 samples/Basic_Storage/.project create mode 100644 samples/Basic_Storage/Makefile create mode 100644 samples/Basic_Storage/README.rst create mode 100644 samples/Basic_Storage/app/application.cpp create mode 100644 samples/Basic_Storage/basic_storage.hw create mode 100644 samples/Basic_Storage/component.mk create mode 100644 samples/Basic_Storage/files/spiffs0/file for spiffs0.txt create mode 100644 samples/Basic_Storage/files/spiffs1/file for spiffs1.txt create mode 100644 samples/Basic_Storage/files/spiffs2/file for spiffs2.txt create mode 100644 samples/Basic_Storage/user0.bin delete mode 100644 samples/Basic_WiFi/component.mk create mode 100644 samples/Basic_rBoot/basic_rboot.hw delete mode 100644 samples/DnsCaptivePortal/component.mk create mode 100644 samples/FtpServer_Files/component.mk create mode 100644 samples/HttpServer_Bootstrap/component.mk create mode 100644 samples/HttpServer_WebSockets/component.mk delete mode 100644 samples/TcpClient_NarodMon/component.mk delete mode 100644 samples/Telnet_TCPServer_TCPClient/component.mk delete mode 100644 samples/UdpServer_Echo/component.mk delete mode 100644 samples/Wifi_Sniffer/component.mk create mode 100644 tests/HostTests/host-tests.hw create mode 100644 tests/HostTests/modules/Storage.cpp diff --git a/.ci/install.cmd b/.ci/install.cmd index 9834f28957..9dfc9811b3 100755 --- a/.ci/install.cmd +++ b/.ci/install.cmd @@ -1,6 +1,6 @@ REM Windows install script -python -m pip install --upgrade pip +python -m pip install --upgrade pip -r %SMING_HOME%\..\Tools\requirements.txt rmdir /s /q c:\MinGW curl -Lo MinGW.7z %SMINGTOOLS%/MinGW-2020-10-19.7z diff --git a/.ci/install.sh b/.ci/install.sh index 75b7fea9ca..21a90b2d2c 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -6,7 +6,7 @@ set -ex # exit with nonzero exit code if anything fails sudo apt-get update sudo update-alternatives --set gcc /usr/bin/gcc-9 -python -m pip install --upgrade pip +python -m pip install --upgrade pip -r $SMING_HOME/../Tools/requirements.txt sudo apt-get install -y gcc-9-multilib g++-9-multilib python3-setuptools diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 216059625b..634f26726d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,11 +36,6 @@ jobs: # a pull request then we can checkout the head. fetch-depth: 2 - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 @@ -68,13 +63,14 @@ jobs: run: | sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test sudo apt-get update -y - sudo apt-get install gcc-9-multilib g++-9-multilib + sudo apt-get install gcc-9-multilib g++-9-multilib python3-wheel python3-setuptools sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 60 --slave /usr/bin/g++ g++ /usr/bin/g++-9 + python3 -m pip install --upgrade pip wheel -r Tools/requirements.txt env cd Sming export SMING_HOME=$(pwd) cd ../samples/Basic_Blink - make SMING_ARCH=Host + make -j3 SMING_ARCH=Host - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 diff --git a/Sming/Arch/Esp32/Components/esp32/sdk/partitions/base.csv b/Sming/Arch/Esp32/Components/esp32/sdk/partitions/base.csv deleted file mode 100644 index 97c8e0148e..0000000000 --- a/Sming/Arch/Esp32/Components/esp32/sdk/partitions/base.csv +++ /dev/null @@ -1,5 +0,0 @@ -# Name, Type, SubType, Offset, Size, Flags -# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap -nvs, data, nvs, 0x9000, 0x6000, -phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 0x1F0000, diff --git a/Sming/Arch/Esp32/Components/esp32/src/startup.cpp b/Sming/Arch/Esp32/Components/esp32/src/startup.cpp index 602dd46c13..cdb1ff521b 100644 --- a/Sming/Arch/Esp32/Components/esp32/src/startup.cpp +++ b/Sming/Arch/Esp32/Components/esp32/src/startup.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #ifndef ESP32_STACK_SIZE #define ESP32_STACK_SIZE 16384U @@ -63,6 +64,7 @@ void main(void*) esp_init_flash(); esp_init_wifi(); ets_init_tasks(); + Storage::initialize(); System.initialize(); init(); diff --git a/Sming/Arch/Esp32/Components/esp_spiffs/.cs b/Sming/Arch/Esp32/Components/esp_spiffs/.cs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Sming/Arch/Esp32/Components/esp_spiffs/README.rst b/Sming/Arch/Esp32/Components/esp_spiffs/README.rst deleted file mode 100644 index 9869b71f45..0000000000 --- a/Sming/Arch/Esp32/Components/esp_spiffs/README.rst +++ /dev/null @@ -1,4 +0,0 @@ -Esp32 SPIFFS -============ - -SPIFFS implementation for ESP32 devices. diff --git a/Sming/Arch/Esp32/Components/esp_spiffs/component.mk b/Sming/Arch/Esp32/Components/esp_spiffs/component.mk deleted file mode 100644 index 12ce0a9792..0000000000 --- a/Sming/Arch/Esp32/Components/esp_spiffs/component.mk +++ /dev/null @@ -1 +0,0 @@ -COMPONENT_DEPENDS := spiffs diff --git a/Sming/Arch/Esp32/Components/esp_spiffs/spiffs_config.c b/Sming/Arch/Esp32/Components/esp_spiffs/spiffs_config.c deleted file mode 100644 index c8e5f095e9..0000000000 --- a/Sming/Arch/Esp32/Components/esp_spiffs/spiffs_config.c +++ /dev/null @@ -1,37 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/SmingHub/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * rboot-overrides.c - * - ****/ - -#include "spiffs_sming.h" -#include -#include - -/* - * rBoot uses different spiffs organization and we need to override this method - * during application compile time in order to make automatic - * mounting with `spiffs_mount()` work as expected. - */ -spiffs_config spiffs_get_storage_config() -{ - spiffs_config cfg = {0}; - - const esp_partition_t* partition = - esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, NULL); - - if(partition == NULL) { - debug_w("No SPIFFS partition registered"); - } else { - cfg.phys_addr = partition->address; - cfg.phys_size = partition->size; - debug_w("SPIFFS partition found at 0x%08x, size 0x%08x", cfg.phys_addr, cfg.phys_size); - // TODO: Check partition->flash_chip is valid? - } - - return cfg; -} diff --git a/Sming/Arch/Esp32/Components/sming-arch/component.mk b/Sming/Arch/Esp32/Components/sming-arch/component.mk index 70ecd54254..d97a67fcac 100644 --- a/Sming/Arch/Esp32/Components/sming-arch/component.mk +++ b/Sming/Arch/Esp32/Components/sming-arch/component.mk @@ -18,7 +18,6 @@ COMPONENT_DEPENDS := \ driver \ heap \ fatfs \ - esp_spiffs \ esp32 \ gdbstub \ esptool diff --git a/Sming/Arch/Esp32/app.mk b/Sming/Arch/Esp32/app.mk index a78c572e86..4c9fca76bd 100644 --- a/Sming/Arch/Esp32/app.mk +++ b/Sming/Arch/Esp32/app.mk @@ -42,52 +42,3 @@ $(TARGET_OUT): $(COMPONENTS_AR) $(TARGET_BIN): $(TARGET_OUT) $(Q) $(ESPTOOL_CMDLINE) elf2image --min-rev 0 --elf-sha256-offset 0xb0 $(flashimageoptions) -o $@ $< - -##@Flashing - -# Partitions -PARTITIONS_CSV ?= $(BUILD_BASE)/partitions.csv -PARTITIONS_BIN = $(FW_BASE)/partitions.bin - -CUSTOM_TARGETS += $(PARTITIONS_CSV) - -$(BUILD_BASE)/partitions.csv: | $(BUILD_BASE) - $(Q) cp $(SDK_PARTITION_PATH)/base.csv $@ - @echo "storage, data, spiffs, $(SPIFF_START_ADDR), $(SPIFF_SIZE)," >> $@ - -$(PARTITIONS_BIN): $(PARTITIONS_CSV) - $(Q) $(ESP32_PYTHON) $(SDK_COMPONENTS_PATH)/partition_table/gen_esp32part.py $< $@ - -.PHONY: partitions -partitions: $(PARTITIONS_BIN) ##Generate partitions table - - -FLASH_PARTITION_CHUNKS := 0x8000=$(PARTITIONS_BIN) - -# Application - -FLASH_APP_CHUNKS := 0x10000=$(TARGET_BIN) - -.PHONY: flashboot -flashboot: $(FLASH_BOOT_LOADER) ##Write just the Bootloader - $(call WriteFlash,$(FLASH_BOOT_CHUNKS)) - -.PHONY: flashconfig - -flashconfig: partitions kill_term ##Write partition config - $(call WriteFlash,$(FLASH_PARTITION_CHUNKS)) - -flashpartition: flashconfig - -.PHONY: flashapp -flashapp: all kill_term ##Write just the application image - $(call WriteFlash,$(FLASH_APP_CHUNKS)) - -.PHONY: flash -flash: all partitions kill_term ##Write the boot loader, application image, partition table and (if enabled) SPIFFS image - $(call WriteFlash,$(FLASH_BOOT_CHUNKS) $(FLASH_APP_CHUNKS) $(FLASH_PARTITION_CHUNKS) $(FLASH_SPIFFS_CHUNKS)) -ifeq ($(ENABLE_GDB), 1) - $(GDB_CMDLINE) -else - $(TERMINAL) -endif diff --git a/Sming/Arch/Esp32/standard.hw b/Sming/Arch/Esp32/standard.hw new file mode 100644 index 0000000000..1f70b3ca4d --- /dev/null +++ b/Sming/Arch/Esp32/standard.hw @@ -0,0 +1,35 @@ +{ + "name": "Standard config with single ROM", + "comment": "Should work with any Esp32 variant", + "arch": "Esp32", + "partition_table_offset": "0x8000", + "devices": { + "spiFlash": { + "type": "flash", + "size": "4M", + "mode": "dio", + "speed": 40 + } + }, + "partitions": { + "phy_init": { + "address": "0x00f000", + "size": "0x1000", + "type": "data", + "subtype": "phy" + }, + "nvs": { + "address": "0x009000", + "size": "0x6000", + "type": "data", + "subtype": "nvs" + }, + "factory": { + "address": "0x010000", + "size": "0x1f0000", + "type": "app", + "subtype": "factory", + "filename": "$(TARGET_BIN)" + } + } +} diff --git a/Sming/Arch/Esp8266/Components/esp8266/component.mk b/Sming/Arch/Esp8266/Components/esp8266/component.mk index 46c5d142c8..789d7aa24f 100644 --- a/Sming/Arch/Esp8266/Components/esp8266/component.mk +++ b/Sming/Arch/Esp8266/Components/esp8266/component.mk @@ -8,11 +8,6 @@ FLASH_INIT_DATA = $(SDK_BASE)/bin/esp_init_data_default.bin CUSTOM_TARGETS += $(FLASH_INIT_DATA) -FLASH_INIT_CHUNKS += \ - $(call FlashOffset,0x5000)=$(BLANK_BIN) \ - $(call FlashOffset,0x4000)=$(FLASH_INIT_DATA) \ - $(call FlashOffset,0x2000)=$(BLANK_BIN) - # => 'Internal' SDK - for SDK Version 3+ as submodule in Sming repository # SDK_BASE just needs to point into our repo as it's overridden with the correct submodule path # This provides backward-compatiblity, so $(SMING)/third-party/ESP8266_NONOS_SDK) still works diff --git a/Sming/Arch/Esp8266/Components/esp8266/startup.cpp b/Sming/Arch/Esp8266/Components/esp8266/startup.cpp index f2adb9e490..ef6d1d07d3 100644 --- a/Sming/Arch/Esp8266/Components/esp8266/startup.cpp +++ b/Sming/Arch/Esp8266/Components/esp8266/startup.cpp @@ -14,6 +14,7 @@ #include #include #include +#include extern void init(); @@ -54,46 +55,9 @@ extern "C" void WEAK_ATTR user_rf_pre_init(void) extern "C" uint32 ICACHE_FLASH_ATTR WEAK_ATTR user_rf_cal_sector_set(void) { - enum flash_size_map size_map = system_get_flash_size_map(); - uint32 rf_cal_sec = 0; - - switch (int(size_map)) { - case FLASH_SIZE_2M: - rf_cal_sec = 64 - 5; - break; - - case FLASH_SIZE_4M_MAP_256_256: - rf_cal_sec = 128 - 5; - break; - - case FLASH_SIZE_8M_MAP_512_512: - rf_cal_sec = 256 - 5; - break; - - case FLASH_SIZE_16M_MAP_512_512: - case FLASH_SIZE_16M_MAP_1024_1024: - rf_cal_sec = 512 - 5; - break; - - case FLASH_SIZE_32M_MAP_512_512: - case FLASH_SIZE_32M_MAP_1024_1024: - rf_cal_sec = 1024 - 5; - break; - - case 8: // FLASH_SIZE_64M_MAP_1024_1024 - rf_cal_sec = 2048 - 5; - break; - - case 9: // FLASH_SIZE_128M_MAP_1024_1024 - rf_cal_sec = 4096 - 5; - break; - - default: - rf_cal_sec = 0; - break; - } - - return rf_cal_sec; + // RF calibration stored in last sector of sysParam + auto sysParam = *Storage::findPartition(Storage::Partition::SubType::Data::sysParam); + return ((sysParam.address() + sysParam.size()) / SPI_FLASH_SEC_SIZE) - 1; } #ifdef SDK_INTERNAL @@ -104,25 +68,19 @@ extern "C" uint32 ICACHE_FLASH_ATTR WEAK_ATTR user_rf_cal_sector_set(void) extern "C" void ICACHE_FLASH_ATTR WEAK_ATTR user_pre_init(void) { - const uint32_t MAX_PROGRAM_SECTORS = 0x100000 / SPI_FLASH_SEC_SIZE; // 1MB addressable + Storage::initialize(); - // WARNING: Sming supports SDK 3.0 with rBoot enabled apps ONLY! - const partition_type_t SYSTEM_PARTITION_RBOOT_CONFIG = static_cast(SYSTEM_PARTITION_CUSTOMER_BEGIN + 0); - const partition_type_t SYSTEM_PARTITION_PROGRAM = static_cast(SYSTEM_PARTITION_CUSTOMER_BEGIN + 1); + auto sysParam = *Storage::findPartition(Storage::Partition::SubType::Data::sysParam); + auto phy = *Storage::findPartition(Storage::Partition::SubType::Data::phy); - // Partitions offsets and sizes must be in sector multiples, so work in sectors - #define PARTITION_ITEM(_type, _start, _length) \ - {_type, (_start) * SPI_FLASH_SEC_SIZE, (_length) * SPI_FLASH_SEC_SIZE} + // RF calibration stored in last sector of sysParam + auto sysParamSize = sysParam.size() - SPI_FLASH_SEC_SIZE; - // Partitions in position order - uint32_t rfCalSector = user_rf_cal_sector_set(); static const partition_item_t partitions[] = { - PARTITION_ITEM(SYSTEM_PARTITION_BOOTLOADER, 0, 1), - PARTITION_ITEM(SYSTEM_PARTITION_RBOOT_CONFIG, 1, 1), - PARTITION_ITEM(SYSTEM_PARTITION_PROGRAM, 2, std::min(MAX_PROGRAM_SECTORS, rfCalSector) - 2), - PARTITION_ITEM(SYSTEM_PARTITION_RF_CAL, rfCalSector, 1), - PARTITION_ITEM(SYSTEM_PARTITION_PHY_DATA, rfCalSector + 1, 1), - PARTITION_ITEM(SYSTEM_PARTITION_SYSTEM_PARAMETER, rfCalSector + 2, 3), + {SYSTEM_PARTITION_BOOTLOADER, 0, SPI_FLASH_SEC_SIZE}, + {SYSTEM_PARTITION_PHY_DATA, phy.address(), phy.size()}, + {SYSTEM_PARTITION_SYSTEM_PARAMETER, sysParam.address(), sysParamSize}, + {SYSTEM_PARTITION_RF_CAL, sysParam.address() + sysParamSize, SPI_FLASH_SEC_SIZE}, }; enum flash_size_map sizeMap = system_get_flash_size_map(); @@ -134,7 +92,7 @@ extern "C" void ICACHE_FLASH_ATTR WEAK_ATTR user_pre_init(void) os_printf("partition[%u]: %u, 0x%08x, 0x%08x\n", i, part.type, part.addr, part.size); } if(sizeMap < FLASH_SIZE_8M_MAP_512_512) { - os_printf("** Note: SDK 3.0.1 requires SPI_SIZE >= 1M\n"); + os_printf("** Note: SDK 3.0.1 requires spiFlash size >= 1M\n"); } while(1) { // Cannot proceed diff --git a/Sming/Arch/Esp8266/Tools/ci/build.run.sh b/Sming/Arch/Esp8266/Tools/ci/build.run.sh index 678b068a60..9ffe3edc5f 100755 --- a/Sming/Arch/Esp8266/Tools/ci/build.run.sh +++ b/Sming/Arch/Esp8266/Tools/ci/build.run.sh @@ -1,7 +1,11 @@ # Esp8266 build.run.sh -make -C "$SMING_PROJECTS_DIR/samples/HttpServer_FirmwareUpload" python-requirements $MAKE_PARALLEL samples make clean samples-clean $MAKE_PARALLEL Basic_Blink ENABLE_CUSTOM_HEAP=1 DEBUG_VERBOSE_LEVEL=3 $MAKE_PARALLEL HttpServer_ConfigNetwork ENABLE_CUSTOM_LWIP=2 STRICT=1 + +# Some samples (UPnP, for example) require more recent compiler +if [ "$BUILD_COMPILER" == "eqt" ]; then + $MAKE_PARALLEL component-samples +fi diff --git a/Sming/Arch/Esp8266/app.mk b/Sming/Arch/Esp8266/app.mk index 88f45e491f..2521c1df13 100644 --- a/Sming/Arch/Esp8266/app.mk +++ b/Sming/Arch/Esp8266/app.mk @@ -71,24 +71,7 @@ $(TARGET_OUT_1): $(COMPONENTS_AR) $(LIBMAIN_DST) ##@Flashing -.PHONY: flashboot -flashboot: $(RBOOT_BIN) ##Write just the rBoot boot sector - $(call WriteFlash,$(FLASH_RBOOT_BOOT_CHUNKS)) - .PHONY: flashconfig flashconfig: kill_term ##Erase the rBoot config sector $(info Erasing rBoot config sector) $(call WriteFlash,$(FLASH_RBOOT_ERASE_CONFIG_CHUNKS)) - -.PHONY: flashapp -flashapp: all kill_term ##Write just the application image - $(call WriteFlash,$(FLASH_RBOOT_APP_CHUNKS)) - -.PHONY: flash -flash: all kill_term ##Write the rBoot boot sector, application image and (if enabled) SPIFFS image - $(call WriteFlash,$(FLASH_RBOOT_BOOT_CHUNKS) $(FLASH_RBOOT_APP_CHUNKS) $(FLASH_SPIFFS_CHUNKS)) -ifeq ($(ENABLE_GDB), 1) - $(GDB_CMDLINE) -else - $(TERMINAL) -endif diff --git a/Sming/Arch/Esp8266/spiffs-two-roms.hw b/Sming/Arch/Esp8266/spiffs-two-roms.hw new file mode 100644 index 0000000000..9503891e2a --- /dev/null +++ b/Sming/Arch/Esp8266/spiffs-two-roms.hw @@ -0,0 +1,13 @@ +{ + "name": "Two ROM slots with single SPIFFS", + "base_config": "spiffs", + "partitions": { + "rom1": { + "address": "0x108000", + "size": "992K", + "type": "app", + "subtype": "ota_0", + "filename": "$(RBOOT_ROM_1_BIN)" + } + } +} diff --git a/Sming/Arch/Esp8266/standard.hw b/Sming/Arch/Esp8266/standard.hw new file mode 100644 index 0000000000..240e56f679 --- /dev/null +++ b/Sming/Arch/Esp8266/standard.hw @@ -0,0 +1,36 @@ +{ + "name": "Standard config with single ROM", + "comment": "Should work with any Esp8266 variant", + "arch": "Esp8266", + "partition_table_offset": "0x2000", + "devices": { + "spiFlash": { + "type": "flash", + "size": "1M", + "mode": "dio", + "speed": 40 + } + }, + "partitions": { + "phy_init": { + "address": "0x003000", + "size": "4K", + "type": "data", + "subtype": "phy", + "filename": "$(FLASH_INIT_DATA)" + }, + "sys_param": { + "address": "0x004000", + "size": "16K", + "type": "data", + "subtype": "sysparam" + }, + "rom0": { + "address": "0x008000", + "size": "992K", + "type": "app", + "subtype": "factory", + "filename": "$(RBOOT_ROM_0_BIN)" + } + } +} diff --git a/Sming/Arch/Esp8266/two-rom-mode.hw b/Sming/Arch/Esp8266/two-rom-mode.hw new file mode 100644 index 0000000000..9d6399179f --- /dev/null +++ b/Sming/Arch/Esp8266/two-rom-mode.hw @@ -0,0 +1,16 @@ +{ + "name": "Two ROM slots in 1MB of flash", + "base_config": "standard", + "partitions": { + "rom0": { + "size": "480K" + }, + "rom1": { + "address": "0x80000", + "size": "512K", + "type": "app", + "subtype": "ota_0", + "filename": "$(RBOOT_ROM_1_BIN)" + } + } +} diff --git a/Sming/Arch/Host/Components/hostlib/startup.cpp b/Sming/Arch/Host/Components/hostlib/startup.cpp index 330c6c44f7..e9f95c91b2 100644 --- a/Sming/Arch/Host/Components/hostlib/startup.cpp +++ b/Sming/Arch/Host/Components/hostlib/startup.cpp @@ -32,6 +32,7 @@ #include #include #include "include/hostlib/CommandLine.h" +#include #include #include @@ -213,6 +214,8 @@ int main(int argc, char* argv[]) if(config.initonly) { hostmsg("Initialise-only requested"); } else { + Storage::initialize(); + CThread::startup(); hw_timer_init(); diff --git a/Sming/Arch/Host/Components/vflash/README.rst b/Sming/Arch/Host/Components/vflash/README.rst index f94b6416f1..862312c3af 100644 --- a/Sming/Arch/Host/Components/vflash/README.rst +++ b/Sming/Arch/Host/Components/vflash/README.rst @@ -19,7 +19,7 @@ The following options are interpreted and used to provide command-line parameter This defaults to a combination of the above variables, but you can override if necessary. -The size of the flash memory is set via :envvar:`SPI_SIZE`. +The size of the flash memory is set via :ref:`hardware_config`. See :component-esp8266:`esptool` for details and other applicable variables. diff --git a/Sming/Arch/Host/Components/vflash/component.mk b/Sming/Arch/Host/Components/vflash/component.mk index 10dc7b47cc..98410de609 100644 --- a/Sming/Arch/Host/Components/vflash/component.mk +++ b/Sming/Arch/Host/Components/vflash/component.mk @@ -7,32 +7,38 @@ DD := dd CACHE_VARS += FLASH_BIN FLASH_BIN ?= $(FW_BASE)/flash.bin -CONFIG_VARS += SPI_SIZE -SPI_SIZE ?= 4M +DEBUG_VARS += SPI_SIZE +SPI_SIZE = $(STORAGE_DEVICE_spiFlash_SIZE) # Options to add when running emulator CACHE_VARS += HOST_FLASH_OPTIONS HOST_FLASH_OPTIONS ?= --flashfile=$(FLASH_BIN) --flashsize=$(SPI_SIZE) CLI_TARGET_OPTIONS += $(HOST_FLASH_OPTIONS) -# Write data to flash -# $1 -> Start offset -# $2 -> File containing data to write -define WriteFlashChunk - $(info WriteFlash $1 -> $2) - $(Q) if [ ! -f $(FLASH_BIN) ]; then \ - $(EraseFlash); \ - fi - $(Q) $(DD) if=$2 of=$(FLASH_BIN) obs=1 seek=$$(($1)) conv=notrunc -endef +# Virtual flasher tool +VFLASH := $(PYTHON) $(COMPONENT_PATH)/vflash.py $(FLASH_BIN) $(STORAGE_DEVICE_spiFlash_SIZE_BYTES) # Write one or more chunks to flash # $1 -> List of `Offset=File` chunks define WriteFlash - $(foreach c,$1,$(call WriteFlashChunk,$(word 1,$(subst =, ,$c)),$(word 2,$(subst =, ,$c)))) + $(if $1,$(Q) $(VFLASH) write-chunks $1) +endef + +# Read flash memory into file +# $1 -> `Offset,Size` chunk +# $2 -> Output filename +define ReadFlash + $(info ReadFlash $1,$2) + $(Q) $(VFLASH) read-chunks $1=$2 +endef + +# Erase a region of Flash +# $1 -> List of `Offset,Size` chunks +define EraseFlashRegion + $(Q) $(VFLASH) fill-regions $1 endef # Reset/create flash backing file define EraseFlash - $(DD) if=/dev/zero ibs=1 count=$(SPI_SIZE) | tr "\000" "\377" > $(FLASH_BIN) -endef + $(Q) $(VFLASH) erase +endef diff --git a/Sming/Arch/Host/Components/vflash/vflash.py b/Sming/Arch/Host/Components/vflash/vflash.py new file mode 100644 index 0000000000..cf91295cf5 --- /dev/null +++ b/Sming/Arch/Host/Components/vflash/vflash.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# +# Virtual flash tool to support operations on flash backing file for Sming Host. +# +# Copyright 2021 mikee47 +# +# This file is part of the Sming Framework Project +# +# This library is free software: you can redistribute it and/or modify it under the terms of the +# GNU General Public License as published by the Free Software Foundation, version 3 or later. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# + +import os, argparse + +# Interpret argument such as 0x1234 +def auto_int(x): + return int(x, 0) + +def fill_region(offset, length, value): + print(" fill_region(0x%08x, 0x%04x, 0x%02x)" % (offset, length, value)) + flashImage.seek(offset, os.SEEK_SET) + tmp = bytearray([value] * length) + flashImage.write(tmp) + +def erase(args): + fill_region(0, args.imagesize, 0xff) + +def erase_region(args): + fill_region(args.offset, args.size, 0xff) + +# Offset,size +def erase_regions(args): + for s in args.chunks: + offset, size = s.split(',') + fill_region(int(offset, 0), int(size, 0), 0xff) + +# Offset,size=value +def fill_regions(args): + for s in args.chunks: + offset, tmp = s.split(',') + x = tmp.split('=') + if len(x) == 1: + size = x[0] + value = '0xff' + else: + size, value = x + fill_region(int(offset, 0), int(size, 0), int(value,0)) + +def write_chunk(offset, filename): + print(" write_chunk(0x%08x, '%s')" % (offset, filename)) + flashImage.seek(offset, os.SEEK_SET) + data = open(filename, "rb").read() + len = flashImage.write(data) + print(" - wrote 0x%04x bytes" % len) + +# Offset=filename +def write_chunks(args): + for s in args.chunks: + offset, file = s.split('=') + write_chunk(int(offset, 0), file) + + +def read_chunk(offset, size, filename): + print(" read_chunk(0x%08x, 0x%04x, '%s')" % (offset, size, filename)) + flashImage.seek(offset, os.SEEK_SET) + data = flashImage.read(size) + open(filename, "wb").write(data) + +# Offset,size=filename +def read_chunks(args): + for s in args.chunks: + offset, x = s.split(',') + size, filename = x.split('=') + read_chunk(int(offset, 0), int(size, 0), filename) + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description='Virtual flash tool') + parser.add_argument('filename', help='Flash image filename') + parser.add_argument('imagesize', type=auto_int, help='Flash image size') + parser.set_defaults(func=None) + subparsers = parser.add_subparsers() + + fill_regions_parser = subparsers.add_parser('fill-regions', help='Fill region(s) of flash') + fill_regions_parser.add_argument('chunks', nargs='+', help='List of "offset,size[=value]" chunks, default value is 0xff') + fill_regions_parser.set_defaults(func=fill_regions) + + erase_parser = subparsers.add_parser('erase', help='Erase entire flash') + erase_parser.set_defaults(func=erase) + + write_chunks_parser = subparsers.add_parser('write-chunks', help='Write chunks to flash') + write_chunks_parser.add_argument('chunks', nargs='+', help='List of "offset=file" chunks') + write_chunks_parser.set_defaults(func=write_chunks) + + read_chunks_parser = subparsers.add_parser('read-chunks', help='Read chunks from flash') + read_chunks_parser.add_argument('chunks', nargs='+', help='List of "offset,size=file" chunks') + read_chunks_parser.set_defaults(func=read_chunks) + + args = parser.parse_args() + + flashImage = open(args.filename, "rb+" if os.path.isfile(args.filename) else "wb+") + + print("vflash('%s')" % args.filename) + + # Pad flash image file if it's not big enough + flashImage.seek(0, os.SEEK_END) + imgsize = flashImage.tell() + if imgsize < args.imagesize: + fill_region(imgsize, args.imagesize - imgsize, 0xff) + + # Invoke any user-provided functions (optional) + fn = args.func + if fn is None: + parser.print_usage() + else: + fn(args) + + flashImage.close() diff --git a/Sming/Arch/Host/Tools/ci/coverity-scan.sh b/Sming/Arch/Host/Tools/ci/coverity-scan.sh index d914716e71..f53b2afb6a 100755 --- a/Sming/Arch/Host/Tools/ci/coverity-scan.sh +++ b/Sming/Arch/Host/Tools/ci/coverity-scan.sh @@ -2,8 +2,6 @@ set -e -make -C $SMING_HOME/../samples/HttpServer_FirmwareUpload python-requirements - COVERITY_SCAN_PROJECT_NAME=SmingHub/Sming COVERITY_SCAN_NOTIFICATION_EMAIL="slaff@attachix.com" COVERITY_SCAN_BUILD_COMMAND="$MAKE_PARALLEL Basic_Blink Basic_DateTime Basic_Delegates Basic_Interrupts Basic_ProgMem Basic_Serial Basic_Servo Basic_Ssl HttpServer_FirmwareUpload SMING_ARCH=Host DEBUG_VERBOSE_LEVEL=3" diff --git a/Sming/Arch/Host/Tools/setup-network-linux.sh b/Sming/Arch/Host/Tools/setup-network-linux.sh index 1d2585241a..7f5b7c2df8 100755 --- a/Sming/Arch/Host/Tools/setup-network-linux.sh +++ b/Sming/Arch/Host/Tools/setup-network-linux.sh @@ -17,7 +17,7 @@ fi # Create a tap0 interface with IP network 192.168.13.1/24 sudo ip tuntap add dev tap0 mode tap user $(whoami) sudo ip a a dev tap0 192.168.13.1/24 -sudo ifconfig tap0 up +sudo ip link set tap0 up # The following lines are needed if you plan to access Internet sudo sysctl net.ipv4.ip_forward=1 diff --git a/Sming/Arch/Host/app.mk b/Sming/Arch/Host/app.mk index 8e26c5a03f..9e4f858af4 100644 --- a/Sming/Arch/Host/app.mk +++ b/Sming/Arch/Host/app.mk @@ -43,6 +43,3 @@ $(RUN_SCRIPT):: $(foreach id,$(ENABLE_HOST_UARTID),echo '$(call RunHostTerminal,$(id))' >> $@;) \ echo '$(TARGET_OUT_0) $(CLI_TARGET_OPTIONS) -- $(HOST_PARAMETERS)' >> $@; \ chmod a+x $@ - -.PHONY: flash -flash: all flashfs ##Write all images to (virtual) flash diff --git a/Sming/Arch/Host/standard.hw b/Sming/Arch/Host/standard.hw new file mode 100644 index 0000000000..7572825a4c --- /dev/null +++ b/Sming/Arch/Host/standard.hw @@ -0,0 +1,20 @@ +{ + "name": "Standard config with single ROM", + "arch": "Host", + "partition_table_offset": "0x2000", + "devices": { + "spiFlash": { + "type": "flash", + "size": "4M" + } + }, + "partitions": { + "rom0": { + "address": "0x008000", + "size": "992K", + "type": "app", + "subtype": "factory", + "filename": "$(BLANK_BIN)" + } + } +} diff --git a/Sming/Components/FlashString b/Sming/Components/FlashString index 4d0934f499..e0e95dbe53 160000 --- a/Sming/Components/FlashString +++ b/Sming/Components/FlashString @@ -1 +1 @@ -Subproject commit 4d0934f499d9d2a583d1de62c65ce5266f4ec82f +Subproject commit e0e95dbe5399c15a4637eee9dbf46a7cb212ef36 diff --git a/Sming/Components/Storage/README.rst b/Sming/Components/Storage/README.rst new file mode 100644 index 0000000000..e4afc51031 --- /dev/null +++ b/Sming/Components/Storage/README.rst @@ -0,0 +1,335 @@ +Storage Management +================== + +.. highlight:: bash + +This Component provides support for using storage devices in a structured way by partitioning +them into areas for specific uses. +Partitions may can contain information such as: + +- Application (firmware) images +- Filesystem(s) +- Configuration/calibration/parameter data +- Custom flash storage areas + +A single partition table is located on the main flash device, :cpp:var:`Storage::spiFlash`, +and defines all partitions with a unique name and associated +:cpp:enum:`Storage::Partition::Type` / :cpp:type:`Storage::Partition::SubType`. + + + +.. _hardware_config: + +Hardware configuration +---------------------- + +Each project has an associated ``Hardware configuration``, specified by the :envvar:`HWCONFIG` setting: +this is a JSON file with a ``.hw`` extension. + +The build system locates the file by searching, in order: + +- ``{PROJECT_DIR}`` the root project directory +- ``{SMING_HOME}/Arch/{SMING_ARCH}`` +- ``{SMING_HOME}`` + +Each architecture provides a ``standard`` configuration which defines such things as the +partition table location and standard system partitions. Other configurations inherit +from this by providing a ``base_config`` value. + +You can list the available configs like this:: + + make hwconfig-list + +This also shows the file path should you wish to view or edit it. + +To select and view the resulting configuration, do this:: + + make hwconfig HWCONFIG=spiffs + +or, to show the partition map:: + + make map HWCONFIG=spiffs + +.. note:: + + You can set :envvar:`HWCONFIG` in your project's ``component.mk`` file, however as with other + configuration variables it will be overridden by the cached value set on the command line. + + For example, if you want to change from ``standard`` to ``standard-4m`` for your project, + first add this line to your component.mk file:: + + HWCONFIG := standard-4m + + Then either run ``make HWCONFIG=standard-4m`` or ``make config-clean``. + + +Custom configurations +--------------------- + +To customise the hardware configuration for a project, for example 'my_project': + +1. Create a new configuration file in your project root, such as ``my_project.hw``: + + .. code-block:: json + + { + "name": "My project config", + "base_config": "spiffs" + } + + You can use any available configuration as the base_config. + + +2. If required, modify any inherited settings: + + .. code-block:: json + + { + "name": "My config", + "base_config": "standard", + "devices": { + "spiFlash": { + "speed": 80, + "mode": "qio", + "size": "2M" + } + }, + "partitions": { + "rom0": { + "address": "0x10000", + "size": "0x80000" + } + } + } + + This will adjust flash parameters (previously via SPI_SPEED, SPI_MODE and SPI_SIZE), + and the location/size of the primary application partition. + +3. Add any additional partitions: + + .. code-block:: json + + { + "name": "My config", + "base_config": "standard-4m", + "partitions": { + "rom0": { + "address": "0x10000", + "size": "0x80000" + }, + "spiffs1": { + "address": "0x00280000", + "size": "256K", + "type": "data", + "subtype": "spiffs", + "filename": "$(FW_BASE)/spiffs1_rom.bin", + "build": { + "target": "spiffsgen", + "files": "files/spiffs1" + } + } + } + } + + This adds a second SPIFFS partition, and instructs the build system to generate + an image file for it using the files in the project's ``files/spiffs1`` directory. + +4. Select the new configuration and re-build the project:: + + make HWCONFIG=my_project + + You should also add this to your project's ``component.mk`` file:: + + HWCONFIG := my_project + +5. Program your device:: + + make flash + + This will flash everything: bootloader, partition table and all defined partitions (those with a ``filename`` entry). + + +Partition maps +-------------- + +This is a concise view of your flash partitions. Display it like this:: + + make map + +For the :sample:`Basic_Storage` sample application, we get this: + +.. code-block:: text + + Basic_Storage: Invoking 'map' for Esp8266 (debug) architecture + Partition map: + Device Start End Size Type SubType Name Filename + ---------------- ---------- ---------- ---------- -------- -------- ---------------- ------------ + spiFlash 0x00000000 0x00001fff 8K Boot Sector + spiFlash 0x00002000 0x00002fff 4K Partition Table + spiFlash 0x00003000 0x00003fff 4K data phy phy_init $(FLASH_INIT_DATA) + spiFlash 0x00004000 0x00007fff 16K data sysparam sys_param + spiFlash 0x00008000 0x000fffff 992K app factory rom0 $(RBOOT_ROM_0_BIN) + spiFlash 0x00100000 0x001effff 960K (unused) + spiFlash 0x001f0000 0x001f3fff 16K user 0 user0 user0.bin + spiFlash 0x001f4000 0x001f7fff 16K user 1 user1 + spiFlash 0x001f8000 0x001fffff 32K (unused) + spiFlash 0x00200000 0x0027ffff 512K data spiffs spiffs0 $(SPIFF_BIN_OUT) + spiFlash 0x00280000 0x002bffff 256K data spiffs spiffs1 $(FW_BASE)/spiffs1_rom.bin + spiFlash 0x002c0000 0x002fffff 256K data spiffs spiffs2 $(FW_BASE)/spiffs2_rom.bin + spiFlash 0x00300000 0x003fffff 1M (unused) + +For comparison, here's the output for Esp32: + +.. code-block:: text + + Basic_Storage: Invoking 'map' for Esp32 (debug) architecture + Partition map: + Device Start End Size Type SubType Name Filename + ---------------- ---------- ---------- ---------- -------- -------- ---------------- ------------ + spiFlash 0x00000000 0x00007fff 32K Boot Sector + spiFlash 0x00008000 0x00008fff 4K Partition Table + spiFlash 0x00009000 0x0000efff 24K data nvs nvs + spiFlash 0x0000f000 0x0000ffff 4K data phy phy_init + spiFlash 0x00010000 0x001fffff 1984K app factory factory $(TARGET_BIN) + spiFlash 0x001f0000 0x001f3fff 16K user 0 user0 user0.bin + spiFlash 0x001f4000 0x001f7fff 16K user 1 user1 + spiFlash 0x001f8000 0x001fffff 32K (unused) + spiFlash 0x00200000 0x0027ffff 512K data spiffs spiffs0 $(SPIFF_BIN_OUT) + spiFlash 0x00280000 0x002bffff 256K data spiffs spiffs1 $(FW_BASE)/spiffs1_rom.bin + spiFlash 0x002c0000 0x002fffff 256K data spiffs spiffs2 $(FW_BASE)/spiffs2_rom.bin + spiFlash 0x00300000 0x003fffff 1M (unused) + + +To compare this with the partition map programmed into a device, do this:: + + make readmap map + + +JSON validation +--------------- + +When the binary partition table is built or updated, the configuration is first +validated against a schema :source:`Sming/Components/Storage/schema.json`. + +This complements the checks performed by the ``hwconfig`` tool. + +You can run the validation manually like this:: + + make hwconfig-validate + +See `JSON Schema `__ for details about JSON schemas. + + +Configuration +------------- + +.. envvar:: HWCONFIG + + default: standard + + Set this to the hardware configuration to use for your project. + + Default configurations: + + standard + Base profile with 1MB flash size which should work on all device variants. + Located in the ``Sming/Arch/{SMING_ARCH}`` directory. + + standard-4m + Overrides ``standard`` to set 4Mbyte flash size + + spiffs + Adds a single SPIFFS partition. See :component:`spiffs`. + + Other configurations may be available, depending on architecture. + You can see these by running ``make hwconfig-list``. + + For example, to select ``spiffs`` add the following line to your project:: + + HWCONFIG := spiffs + + You will also need to run ``make HWCONFIG=spiffs`` to change the cached value + (or ``make config-clean`` to reset everything). + + +Binary partition table +---------------------- + +Sming uses the same binary partition table structure as ESP-IDF, located immediately after the boot sector. +However, it is organised slighly differently to allow partitions to be registered for multiple storage devices. + +Entries are fixed 32-byte structures, :cpp:class:`Storage::esp_partition_info_t`, organised as follows: + +- The first entry is always a ``storage`` type defining the main :cpp:var:`spiFlash` device. +- This is followed by regular partition entries sorted in ascending address order. + There may be gaps between the partitions. +- The partition table md5sum entry is inserted as normal +- If any external devices are defined: + - A SMING_EXTENSION entry, which the esp32 bootloader interprets as the end of the partition table. + - The next entry is a ``storage`` type for the ``external`` device. + - This is followed by regular partition entries as before. + - A second md5sum entry is inserted for the entire partition table thus far +- The end of the partition table is identified by an empty sector (i.e. all bytes 0xFF). + + + +Partition API +------------- + +This is a C++ interface. Some examples:: + + Storage::Partition part = Storage::findPartition("spiffs0"); // Find by name + if(part) { + debugf("Partition '%s' found", part.name().c_str()); + } else { + debugf("Partition NOT found"); + } + + // Enumerate all partitions + for(auto it = Storage::findPartition(); it; ++it) { + auto part = *it; + debugf("Found '%s' at 0x%08x, size 0x%08x", part.name().c_str(), part.address(), part.size()); + } + + // Enumerate all SPIFFS partitions + for(auto it = Storage::findPartition(Partition::SubType::Data::spiffs; it; it++) { + debugf("Found '%s' at 0x%08x, size 0x%08x", it->name().c_str(), it->address(), it->size()); + } + + +A :cpp:class:`Storage::Partition` object is just a wrapper and can be freely copied around. +It defines methods which should be used to read/write/erase the partition contents. + +Each partition has an associated :cpp:class:`Storage::Device`. +This is usually :cpp:var:`Storage::spiFlash` for the main flash device. + +Other devices must be registed via :cpp:func:`Storage::PartitionTable::registerStorageDevice`. + +You can query partition entries from a Storage object directly, for example:: + + #include + + for(auto part: Storage::spiFlash->partitions()) { + debugf("Found '%s' at 0x%08x, size 0x%08x", part.name().c_str(), part.address(), part.size()); + } + + +External Storage +---------------- + +If your design has additional fixed storage devices, such as SPI RAM, flash or EEPROM, +you can take advantage of the partition API to manage them as follows: + +- Implement a class to manage the storage, inheriting from :cpp:class:`Storage::Device`. +- Create a custom hardware configuration for your project and add a ``devices`` entry + describing your storage device, plus partition entries: the ``device`` field identifies + which device these entries relate to. +- Create an instance of your custom device and make a call to :cpp:func:`Storage::registerDevice` + in your ``init()`` function (or elsewhere if more appropriate). + + +API +--- + +.. doxygennamespace:: Storage + :members: diff --git a/Sming/Components/Storage/Tools/README.rst b/Sming/Components/Storage/Tools/README.rst new file mode 100644 index 0000000000..04d56eacff --- /dev/null +++ b/Sming/Components/Storage/Tools/README.rst @@ -0,0 +1,6 @@ +Partition tools +=============== + +hwconfig.py + Supports parsing and creation of hardware configuration files which include partition information. + diff --git a/Sming/Components/Storage/Tools/hwconfig/common.py b/Sming/Components/Storage/Tools/hwconfig/common.py new file mode 100644 index 0000000000..8c28db32bb --- /dev/null +++ b/Sming/Components/Storage/Tools/hwconfig/common.py @@ -0,0 +1,96 @@ +# +# Common functions and definitions +# + +import sys, json, platform + +quiet = False + +def status(msg): + """ Print status message to stderr """ + if not quiet: + critical(msg) + + +def critical(msg): + """ Print critical message to stderr """ + sys.stderr.write(msg) + sys.stderr.write('\n') + + +def fixpath(path): + """ Paths in Windows can get a little weird """ + if len(path) > 2 and path[1] != ':' and platform.system() == 'Windows' and path[2] == '/': + return path[1] + ':' + path[2:] + return path + + +def parse_int(v, keywords=None): + """Generic parser for integer fields - int(x,0) with provision for + k/m/K/M suffixes and 'keyword' value lookup. + """ + if not isinstance(v, str): + return v + if keywords is None or len(keywords) == 0: + try: + for letter, multiplier in [("k", 1024), ("m", 1024 * 1024), ("g", 1024 * 1024 * 1024)]: + if v.lower().endswith(letter): + return parse_int(v[:-1], keywords) * multiplier + return int(v, 0) + except ValueError: + raise InputError("Invalid field value %s" % v) + try: + return keywords[v.lower()] + except KeyError: + raise InputError("Value '%s' is not valid. Known keywords: %s" % (v, ", ".join(keywords))) + + +def stringnum(s): + """Return number if s contains only digits, otherwise return the string + """ + return int(s) if s.isdigit() else s + + +def addr_format(a): + return "0x%08x" % a + + +def size_format(a): + if a != 0: + for (val, suffix) in [(0x40000000, "G"), (0x100000, "M"), (0x400, "K")]: + if a % val == 0: + return "%d%s" % (a // val, suffix) + return "0x%08x" % a + + +def quote(v): + return '"' + v + '"' + + +def contains_whitespace(s): + return ''.join(s.split()) != s + + +def to_json(obj): + return json.dumps(obj, indent=4) + + +def lookup_keyword(t, keywords): + for k, v in keywords.items(): + if t == v: + return k + return "%d" % t + + +class InputError(RuntimeError): + + def __init__(self, e): + super(InputError, self).__init__(e) + + +class ValidationError(InputError): + + def __init__(self, partition, message): + super(ValidationError, self).__init__( + "Partition %s invalid: %s" % (partition.name, message)) + diff --git a/Sming/Components/Storage/Tools/hwconfig/config.py b/Sming/Components/Storage/Tools/hwconfig/config.py new file mode 100644 index 0000000000..b4972375f6 --- /dev/null +++ b/Sming/Components/Storage/Tools/hwconfig/config.py @@ -0,0 +1,106 @@ +# +# Configuration object +# + +import os, partition, storage +from common import * +from builtins import classmethod + +def findConfig(name): + dirs = os.environ['HWCONFIG_DIRS'].split(' ') + for d in dirs: + path = os.path.join(fixpath(d), name + '.hw') + if os.path.exists(path): + return path + raise InputError("Config '%s' not found" % name) + +class Config(object): + def __init__(self): + self.partitions = partition.Table() + self.devices = storage.List() + self.depends = [] + + def __str__(self): + return "'%s' for %s" % (self.name, self.arch) + + @classmethod + def from_name(cls, name): + config = Config() + config.load(name) + return config + + def load(self, name): + filename = findConfig(name) + self.depends.append(filename) + data = json.load(open(filename)) + self.parse_dict(data) + + def parse_dict(self, data): + base_config = data.get('base_config') + if base_config is not None: + self.load(base_config) + del data['base_config'] + + for k, v in data.items(): + if k == 'name': + self.name = v + elif k == 'arch': + self.arch = v + elif k == 'partition_table_offset': + self.partitions.offset = parse_int(v) + elif k == 'devices': + self.devices.parse_dict(v) + elif k == 'comment': + self.comment = v + elif k != 'partitions': + raise InputError("Unknown config key '%s'" % k) + + v = data.get('partitions') + if not v is None: + self.partitions.parse_dict(v, self.devices) + + def dict(self): + res = {} + + res['name'] = self.name + if hasattr(self, 'comment'): + res['comment'] = self.comment + res['arch'] = self.arch; + res['partition_table_offset'] = self.partitions.offset_str() + res['devices'] = self.devices.dict() + res['partitions'] = self.partitions.dict() + return res + + def to_json(self): + return to_json(self.dict()) + + def buildVars(self): + dict = {} + + dict['SMING_ARCH_HW'] = self.arch + dict['PARTITION_TABLE_OFFSET'] = self.partitions.offset_str() + dict['PARTITION_TABLE_LENGTH'] = "0x%04x" % partition.MAX_PARTITION_LENGTH + dict['SPIFLASH_PARTITION_NAMES'] = " ".join(p.name for p in filter(lambda p: p.device == self.devices[0], self.partitions)) + dict['HWCONFIG_DEPENDS'] = " ".join(self.depends) + dict.update(self.devices.buildVars()) + dict.update(self.partitions.buildVars()) + + res = "# Generated from hardware configuration '%s'\r\n" % self.name + for k, v in dict.items(): + res += "%s = %s\r\n" % (k, v) + return res + + def verify(self, secure): + self.partitions.verify(self.arch, secure) + + def map(self): + return partition.Map(self.partitions, self.devices) + + @classmethod + def from_binary(cls, b): + res = Config() + res.name = 'from binary' + res.arch = os.environ.get('SMING_ARCH', 'Unknown') + res.partitions.offset = 0 + res.partitions.parse_binary(b, res.devices) + return res diff --git a/Sming/Components/Storage/Tools/hwconfig/hwconfig.py b/Sming/Components/Storage/Tools/hwconfig/hwconfig.py new file mode 100644 index 0000000000..5a8cfbdf9b --- /dev/null +++ b/Sming/Components/Storage/Tools/hwconfig/hwconfig.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# +# Sming hardware configuration tool +# + +import common, argparse, os, partition +from common import * +from config import Config + +def openOutput(path): + if path == '-': + try: + stdout_binary = sys.stdout.buffer # Python 3 + except AttributeError: + stdout_binary = sys.stdout + return stdout_binary + status("Writing to '%s'" % path) + output_dir = os.path.abspath(os.path.dirname(path)) + os.makedirs(output_dir, exist_ok=True) + return open(path, 'wb') + + +def main(): + parser = argparse.ArgumentParser(description='Sming hardware configuration utility') + + parser.add_argument('--no-verify', help="Don't verify partition table fields", action='store_true') + parser.add_argument('--quiet', '-q', help="Don't print non-critical status messages to stderr", action='store_true') + parser.add_argument('--secure', help="Require app partitions to be suitable for secure boot", action='store_true') + parser.add_argument('--part', help="Name of partition to operate on") + parser.add_argument('command', help='Action to perform', choices=['partgen', 'expr', 'validate', 'flashcheck']) + parser.add_argument('input', help='Name of hardware configuration or path to binary partition table') + parser.add_argument('output', help='Path to output file. Will use stdout if omitted.', nargs='?', default='-') + parser.add_argument('expr', help='Expression to evaluate', nargs='?', default=None) + + args = parser.parse_args() + + common.quiet = args.quiet + + output = None + input_is_binary = False + if os.path.exists(args.input): + inputData = open(args.input, "rb").read() + input_is_binary = inputData[0:2] == partition.Entry.MAGIC_BYTES + if input_is_binary: + config = Config.from_binary(inputData) + else: + raise InputError("File '%s' not recognised as partition table" % args.input) + else: + config = Config.from_name(args.input) + partitions = config.partitions + + # Define local so it's available for eval() + if args.part is not None: + part = partitions.find_by_name(args.part) + if part is None: + return + + if args.command == 'validate': + # Validate resulting hardware configuration against schema + try: + from jsonschema import Draft7Validator + from jsonschema.exceptions import ValidationError + except ImportError: + critical("hwconfig: `jsonschema` is not installed. Please run `make python-requirements`") + sys.exit(1) + inst = json.loads(config.to_json()) + schema = json.load(open(args.expr)) + v = Draft7Validator(schema) + errors = sorted(v.iter_errors(inst), key=lambda e: e.path) + if errors != []: + for e in errors: + critical("%s @ %s" % (e.message, e.path)) + sys.exit(3) + elif args.command == 'flashcheck': + # Expect list of chunks, such as "0x100000=/out/Esp8266/debug/firmware/spiff_rom.bin 0x200000=custom.bin" + list = args.expr.split() + if len(list) == 0: + raise InputError("No chunks to flash!") + for e in list: + addr, filename = e.split('=') + addr = int(addr, 0) + part = config.partitions.find_by_address(addr) + if part is None: + raise InputError("No partition contains address 0x%08x" % addr) + if part.address != addr: + raise InputError("Address 0x%08x is within partition '%s', not at start (0x%08x)" % (addr, part.name, part.address)) + filesize = os.path.getsize(filename) + if filesize > part.size: + raise InputError("File '%s' is 0x%08x bytes, too big for partition '%s' (0x%08x bytes)" % (os.path.basename(filename), filesize, part.name, part.size)) + elif args.command == 'partgen': + # Generate partition table binary + if not args.no_verify: + status("Verifying partition table...") + config.verify(args.secure) + output = config.partitions.to_binary(config.devices) + elif args.command == 'expr': + # Evaluate expression against configuration data + output = str(eval(args.expr)).encode() + else: + raise InputError('Unknown command: %s' % args.command) + + if output is not None: + openOutput(args.output).write(output) + + +if __name__ == '__main__': + try: + main() + except InputError as e: + print(e, file=sys.stderr) + sys.exit(2) diff --git a/Sming/Components/Storage/Tools/hwconfig/partition.py b/Sming/Components/Storage/Tools/hwconfig/partition.py new file mode 100644 index 0000000000..018167d60a --- /dev/null +++ b/Sming/Components/Storage/Tools/hwconfig/partition.py @@ -0,0 +1,541 @@ +#!/usr/bin/env python3 +# +# From Espressif gen_esp32part.py +# +# Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import struct, hashlib, storage, binascii +from common import * + +MAX_PARTITION_LENGTH = 0xC00 # 3K for partition data (96 entries) leaves 1K in a 4K sector for signature +MD5_PARTITION_BEGIN = b"\xEB\xEB" + b"\xFF" * 14 # The first 2 bytes are like magic numbers for MD5 sum +PARTITION_TABLE_SIZE = 0x1000 # Size of partition table +PARTITION_ENTRY_SIZE = 32 + +MIN_PARTITION_SUBTYPE_APP_OTA = 0x10 +NUM_PARTITION_SUBTYPE_APP_OTA = 16 + +__version__ = '1.2' + +APP_TYPE = 0x00 +DATA_TYPE = 0x01 +STORAGE_TYPE = 0x02 # Reference to storage device +USER_TYPE = 0x40 # First user-defined type + +# Default is 4 +ALIGNMENT = { + "Esp32": { + APP_TYPE: 0x10000, + }, + "Esp8266": { + APP_TYPE: 0x1000, + }, + "Host": { + APP_TYPE: 0x1000, + } +} + +TYPES = { + "app": APP_TYPE, + "data": DATA_TYPE, + "storage": STORAGE_TYPE, + "user": USER_TYPE, +} + +# Keep this map in sync with esp_partition_subtype_t enum in esp_partition.h +SUBTYPES = { + APP_TYPE: { + "factory": 0x00, + "ota_0": 0x10, + "ota_1": 0x11, + "ota_2": 0x12, + "ota_3": 0x13, + "ota_4": 0x14, + "ota_5": 0x15, + "ota_6": 0x16, + "ota_7": 0x17, + "ota_8": 0x18, + "ota_9": 0x19, + "ota_10": 0x1a, + "ota_11": 0x1b, + "ota_12": 0x1c, + "ota_13": 0x1d, + "ota_14": 0x1e, + "ota_15": 0x1f, + "test": 0x20, + }, + DATA_TYPE: { + "ota": 0x00, + "phy": 0x01, + "nvs": 0x02, + "coredump": 0x03, + "nvs_keys": 0x04, + "efuse": 0x05, + "sysparam": 0x40, + "esphttpd": 0x80, + "fat": 0x81, + "spiffs": 0x82, + }, + STORAGE_TYPE: storage.TYPES +} + + +def parse_type(value): + if isinstance(value, str): + if value == "": + raise InputError("Field 'type' can't be left empty.") + return parse_int(value, TYPES) + return value + + +def parse_subtype(ptype, value): + if isinstance(value, str): + if value == "": + return 0 # default + return parse_int(value, SUBTYPES.get(ptype, {})) + return value + + +class Table(list): + + def __init__(self): + super(Table, self).__init__(self) + + def parse_dict(self, data, devices): + partnames = [] + for name, entry in data.items(): + if name in partnames: + raise InputError("Duplicate partition '%s'" % name) + if contains_whitespace(name): + raise InputError("Partition names may not contain spaces '%s'" % name) + partnames += name + part = self.find_by_name(name) + if part is None: + part = Entry(devices[0], name) + self.append(part) + part.parse_dict(entry, devices) + self.sort() + + def sort(self): + super(Table, self).sort(key=lambda p: p.device.name + p.address_str()) + + def dict(self): + res = {} + for entry in self: + res[entry.name] = entry.dict() + return res + + def to_json(self): + return to_json(self.dict()); + + def to_csv(self): + res = "Device Start End Size Type SubType Name Filename\n" + res += "---------------- ---------- ---------- ---------- -------- -------- ---------------- ------------\n" + for p in self: + res += "%-16s %10s %10s %10s %-8s %-8s %-16s %s\n" \ + % (p.device.name, p.address_str(), p.end_str(), p.size_str(), p.type_str(), p.subtype_str(), p.name, p.filename) + return res + + def offset_str(self): + return addr_format(self.offset) + + def buildVars(self): + dict = {} + dict['PARTITION_NAMES'] = " ".join(p.name for p in self) + for p in self: + dict.update(p.buildVars()) + return dict + + def __getitem__(self, item): + """Allow partition table access by name or index + """ + if isinstance(item, str): + p = self.find_by_name(item) + if p is None: + raise ValueError("No partition entry named '%s'" % item) + return p + return super(Table, self).__getitem__(item) + + def find_by_type(self, ptype, subtype): + """Return a partition by type & subtype, returns None if not found + """ + # convert ptype & subtypes names (if supplied this way) to integer values + try: + ptype = TYPES[ptype] + except KeyError: + try: + ptype = int(ptype, 0) + except TypeError: + pass + try: + subtype = SUBTYPES[int(ptype)][subtype] + except KeyError: + try: + subtype = int(subtype, 0) + except TypeError: + pass + + for p in self: + if p.type == ptype and p.subtype == subtype: + yield p + return + + def find_by_name(self, name): + for p in self: + if p.name == name: + return p + return None + + def find_by_address(self, addr): + for p in self: + if p.contains(addr): + return p + return None + + def verify(self, arch, secure): + """Verify partition layout + """ + # verify each partition individually + for p in self: + p.verify(arch, secure) + + # check on duplicate name + names = [p.name for p in self] + duplicates = set(n for n in names if names.count(n) > 1) + + # print sorted duplicate partitions by name + if len(duplicates) != 0: + print("A list of partitions that have the same name:") + for p in sorted(self, key=lambda x:x.name): + if len(duplicates.intersection([p.name])) != 0: + print("%s" % (p.to_csv())) + raise InputError("Partition names must be unique") + + # check for overlaps + minPartitionAddress = self.offset + PARTITION_TABLE_SIZE + dev = '' + last = None + for p in self: + if p.device != dev: + last = None + dev = p.device + if dev == self[0].device and p.address < minPartitionAddress: + raise InputError("Partition '%s' @ %s-%s must be located after @ %s" \ + % (p.name, p.address_str(), p.end_str(), addr_format(minPartitionAddress))) + if last is not None and p.address <= last.end(): + raise InputError("Partition '%s' @ %s-%s overlaps '%s' @ %s-%s" \ + % (p.name, p.address_str(), p.end_str(), \ + last.name, last.address_str(), last.end_str())) + last = p + + def parse_binary(self, b, devices): + """Construct partition table object from binary image + """ + dev = None + md5 = hashlib.md5() + for o in range(0, len(b), PARTITION_ENTRY_SIZE): + data = b[o:o + PARTITION_ENTRY_SIZE] + if len(data) != PARTITION_ENTRY_SIZE: + raise InputError("Partition table length must be a multiple of %d bytes" % PARTITION_ENTRY_SIZE) + if data == b'\xFF' * PARTITION_ENTRY_SIZE: + return # got end marker + + if data[:2] == MD5_PARTITION_BEGIN[:2]: # check only the magic number part + if data[16:] == md5.digest(): + md5.update(data) + continue # the next iteration will check for the end marker + else: + raise InputError("MD5 checksums don't match! (computed: 0x%s, parsed: 0x%s)" % (md5.hexdigest(), binascii.hexlify(data[16:]))) + + md5.update(data) + + if data[:2] == b"\xff\xff": # Pseudo-end marker to keep esp32 bootloader happy + continue + + e = Entry.from_binary(data) + if e.type == STORAGE_TYPE: + dev = storage.Device(e.name, e.subtype, e.size) + devices.append(dev) + else: + e.device = dev + self.append(e) + raise InputError("Partition table is missing an end-of-table marker") + + def to_binary(self, devices): + """Create binary image of partition table + """ + dev_count = 0 + dev = None + result = b"" + for e in self: + if e.device != dev: + if dev_count == 1: + result += MD5_PARTITION_BEGIN + hashlib.md5(result).digest() + # esp32 bootloader will see this as end of partition table + result += struct.pack(Entry.STRUCT_FORMAT, + b"\xff\xff", + 0xff, 0xff, + 0, 0, + b"SMING EXTENSIONS", + 0) + dev = e.device + s = Entry(dev, dev.name, 0, dev.size, STORAGE_TYPE, dev.type) + result += s.to_binary() + dev_count += 1 + result += e.to_binary() + + result += MD5_PARTITION_BEGIN + hashlib.md5(result).digest() + + if len(result) >= MAX_PARTITION_LENGTH: + raise InputError("Binary partition table length (%d) longer than max" % len(result)) + result += b"\xFF" * (MAX_PARTITION_LENGTH - len(result)) # pad the sector, for signing + return result + + +class Entry(object): + MAGIC_BYTES = b"\xAA\x50" + + # dictionary maps flag name (as used in CSV flags list, property name) + # to bit set in flags words in binary format + FLAGS = { + "encrypted": 0, + "readonly": 31, + } + + def __init__(self, device=None, name="", address=None, size=None, ptype=None, subtype=None): + self.device = device + self.name = name + self.address = address + self.size = size + self.type = parse_type(ptype) + self.subtype = parse_subtype(self.type, subtype) + self.readonly = False + self.encrypted = False + self.filename = '' + self.build = None + + + def parse_dict(self, data, devices): + """Construct a partition object from JSON definition + """ + try: + v = data.get('type') + if not v is None: + self.type = parse_type(v) + v = data.get('subtype') + if not v is None: + self.subtype = parse_subtype(self.type, v) + + for k, v in data.items(): + if k == 'device': + self.device = devices.find_by_name(v) + elif k == 'address': + self.address = parse_int(v) + elif k == 'size': + self.size = parse_int(v) + elif k == 'filename': + self.filename = v + elif k == 'build': + if self.build is None: + self.build = v + else: + self.build.update(v) + elif k != 'type' and k != 'subtype': + setattr(self, k, v) + + if self.type is None or self.subtype is None: + raise InputError("type/subtype missing") + if self.address is None or self.size is None: + raise InputError("address/size missing") + if self.end() >= self.device.size: + raise InputError("Partition '%s' %s-%s too big for %s size %s" \ + % (self.name, self.address_str(), self.end_str(), self.device.name, self.device.size_str())) + except InputError as e: + raise InputError("Error in partition entry '%s': %s" % (self.name, e)) + + + def dict(self): + res = {} + for k, v in self.__dict__.items(): + if k == 'device': + res[k] = v.name + elif k == 'address': + res[k] = self.address_str() + elif k == 'size': + res[k] = self.size_str() + elif k == 'type': + res[k] = stringnum(self.type_str()) + elif k == 'subtype': + res[k] = stringnum(self.subtype_str()) + elif v is not None and k != 'name': + res[k] = v + return res + + def buildVars(self): + res = {} + + dict = self.dict() + dict['size_bytes'] = "0x%x" % self.size + dict.pop('build', None) + for k, v in dict.items(): + k = "PARTITION_%s_%s" % (self.name, k.upper()) + res[k] = int(v) if type(v) is bool else v + + return res + + def to_json(self): + return to_json(self.dict()); + + def end(self): + return self.address + self.size - 1 + + def end_str(self): + return addr_format(self.end()) + + def address_str(self): + return addr_format(self.address) + + def size_str(self): + return size_format(self.size) + + def contains(self, addr): + return addr >= self.address and addr <= self.end() + + def type_str(self): + return "" if self.type == 0xff else lookup_keyword(self.type, TYPES) + + def type_is(self, t): + return self.type_str() == t if isinstance(t, str) else self.type == t + + def subtype_str(self): + return "" if self.subtype == 0xff else lookup_keyword(self.subtype, SUBTYPES.get(self.type, {})) + + def subtype_is(self, subtype): + return self.subtype_str() == subtype if isinstance(subtype, str) else self.subtype == subtype + + def __eq__(self, other): + if isinstance(other, str): + return self.name == other + else: + return self.name == other.name and self.type == other.type \ + and self.subtype == other.subtype and self.address == other.address \ + and self.size == other.size + + def __repr__(self): + + def maybe_hex(x): + return "0x%x" % x if x is not None else "None" + + return "Entry('%s', 0x%x, 0x%x, %s, %s)" % (self.name, self.type, self.subtype or 0, + maybe_hex(self.address), maybe_hex(self.size)) + + def __str__(self): + return "Part '%s' %s/%s @ 0x%x size 0x%x" % (self.name, self.type_str(), self.subtype_str(), self.address or -1, self.size or -1) + + def alignment(self, arch): + return ALIGNMENT[arch].get(self.type, 4) + + def verify(self, arch, secure): + if self.type is None: + raise ValidationError(self, "Type field is not set") + if self.subtype is None: + raise ValidationError(self, "Subtype field is not set") + if self.address is None: + raise ValidationError(self, "Address field is not set") + align = self.alignment(arch) + if self.address % align: + raise ValidationError(self, "Offset 0x%x is not aligned to 0x%x" % (self.address, align)) + if self.size % align and secure: + raise ValidationError(self, "Size 0x%x is not aligned to 0x%x" % (self.size, align)) + if self.size is None: + raise ValidationError(self, "Size field is not set") + + if self.name in TYPES and TYPES.get(self.name, "") != self.type: + critical("WARNING: Partition has name '%s' which is a partition type, but does not match this partition's " + "type (0x%x). Mistake in partition table?" % (self.name, self.type)) + all_subtype_names = [] + for names in (t.keys() for t in SUBTYPES.values()): + all_subtype_names += names + if self.name in all_subtype_names and SUBTYPES.get(self.type, {}).get(self.name, "") != self.subtype: + critical("WARNING: Partition has name '%s' which is a partition subtype, but this partition has " + "non-matching type 0x%x and subtype 0x%x. Mistake in partition table?" % (self.name, self.type, self.subtype)) + + STRUCT_FORMAT = b"<2sBBLL16sL" + + @classmethod + def from_binary(cls, b): + if len(b) != PARTITION_ENTRY_SIZE: + raise InputError("Partition entry size incorrect. Expected %d bytes, got %d." % (PARTITION_ENTRY_SIZE, len(b))) + res = cls() + (magic, res.type, res.subtype, res.address, + res.size, res.name, flags) = struct.unpack(cls.STRUCT_FORMAT, b) + if b"\x00" in res.name: # strip null byte padding from name string + res.name = res.name[:res.name.index(b"\x00")] + res.name = res.name.decode() + if magic != cls.MAGIC_BYTES: + raise InputError("Invalid magic bytes (%r) for partition definition" % magic) + for flag, bit in cls.FLAGS.items(): + if flags & (1 << bit): + setattr(res, flag, True) + flags &= ~(1 << bit) + if flags != 0: + critical("WARNING: Partition definition had unknown flag(s) 0x%08x. Newer binary format?" % flags) + return res + + def to_binary(self): + flags = 0 + for key in self.FLAGS: + if getattr(self, key): + flags |= (1 << self.FLAGS[key]) + return struct.pack(self.STRUCT_FORMAT, + self.MAGIC_BYTES, + self.type, self.subtype, + self.address, self.size, + self.name.encode(), + flags) + + +class Map(Table): + """Contiguous map of flash memory + """ + def __init__(self, table, devices): + device = devices[0] + + def add(name, address, size): + entry = Entry(device, name, address, size, 0xff, 0xff) + self.append(entry) + return entry + + def add_unused(address, last_end): + return add('(unused)', last_end + 1, address - last_end - 1) + + if table.offset == 0: + last = None + else: + add("Boot Sector", 0, table.offset) + last = add("Partition Table", table.offset, PARTITION_TABLE_SIZE) + + for p in table: + if last is not None: + if p.device != last.device: + add_unused(last.device.size, last.end()) + device = p.device + elif p.address > last.end() + 1: + add_unused(p.address, last.end()) + self.append(p) + last = p + + if last.end() + 1 < device.size: + add_unused(device.size, last.end()) diff --git a/Sming/Components/Storage/Tools/hwconfig/storage.py b/Sming/Components/Storage/Tools/hwconfig/storage.py new file mode 100644 index 0000000000..719f8393dc --- /dev/null +++ b/Sming/Components/Storage/Tools/hwconfig/storage.py @@ -0,0 +1,116 @@ +# +# Storage devices +# + +from common import * + +TYPES = { + "unknown": 0x00, + "flash": 0x01, + "spiram": 0x02, + "sdcard": 0x03, + "disk": 0x04, + "file": 0x05, +} + + +def parse_type(value): + if value == "": + raise InputError("Field 'type' can't be left empty.") + return parse_int(value, TYPES) + + +class List(list): + def parse_dict(self, data): + for name, v in data.items(): + if contains_whitespace(name): + raise InputError("Device names may not contain spaces '%s'" % name) + dev = self.find_by_name(name) + if dev is None: + dev = Device(name) + self.append(dev) + dev.parse_dict(v) + + def dict(self): + res = {} + for dev in self: + res[dev.name] = dev.dict() + return res + + def to_json(self): + return to_json(self.dict()) + + def buildVars(self): + dict = {} + dict['STORAGE_DEVICE_NAMES'] = " ".join(p.name for p in self) + for p in self: + dict.update(p.buildVars()) + return dict + + def __getitem__(self, item): + if isinstance(item, str): + d = self.find_by_name(item) + if d is None: + raise ValueError("No device named '%s'" % item) + return d + return super(List, self).__getitem__(item) + + def find_by_name(self, name): + for d in self: + if d.name == name: + return d + return None + + +class Device(object): + def __init__(self, name, stype = 0, size = 0): + self.name = name + self.type = parse_type(stype) + self.size = parse_int(size) + + def parse_dict(self, data): + for k, v in data.items(): + if k == 'type': + self.type = parse_int(v, TYPES) + elif k == 'size': + self.size = parse_int(v) + elif k == 'mode': + self.mode = v + elif k == 'speed': + self.speed = v + else: + raise InputError("Unknown storage field '%s'" % k) + + def dict(self): + res = {} + + # Some fields are optional + def tryAdd(k): + if hasattr(self, k): + res[k] = getattr(self, k) + + res['type'] = self.type_str() + res['size'] = self.size_str() + tryAdd('mode') + tryAdd('speed') + return res + + def to_json(self): + return to_json(self.dict()) + + def buildVars(self): + res = {} + + dict = self.dict() + dict['size_bytes'] = "0x%x" % self.size + for k, v in dict.items(): + k = "STORAGE_DEVICE_%s_%s" % (self.name, k.upper()) + res[k] = v + + return res + + def type_str(self): + return "" if self.type == 0 else lookup_keyword(self.type, TYPES) + + def size_str(self): + return size_format(self.size) diff --git a/Sming/Components/esptool/blank.bin b/Sming/Components/Storage/blank.bin similarity index 100% rename from Sming/Components/esptool/blank.bin rename to Sming/Components/Storage/blank.bin diff --git a/Sming/Components/Storage/component.mk b/Sming/Components/Storage/component.mk new file mode 100644 index 0000000000..af0307ec78 --- /dev/null +++ b/Sming/Components/Storage/component.mk @@ -0,0 +1,186 @@ +COMPONENT_INCDIRS := src/include +COMPONENT_SRCDIRS := src +COMPONENT_DOXYGEN_INPUT := src/include + +CONFIG_VARS += HWCONFIG +ifndef HWCONFIG +override HWCONFIG := standard +$(info Using configuration '$(HWCONFIG)') +endif + +# Directories to search for hardware config +HWCONFIG_DIRS := $(PROJECT_DIR) $(COMPONENT_SEARCH_DIRS) $(ARCH_BASE) $(SMING_HOME) + +# List of all hardware configurations +ALL_HWCONFIG_PATHS := $(foreach d,$(HWCONFIG_DIRS),$(wildcard $d/*.hw)) +ALL_HWCONFIG := $(sort $(notdir $(ALL_HWCONFIG_PATHS))) +ALL_HWCONFIG := $(ALL_HWCONFIG:.hw=) + +# Path to selected hardware config file +HWCONFIG_PATH := $(firstword $(foreach d,$(HWCONFIG_DIRS),$(wildcard $d/$(HWCONFIG).hw))) + +ifeq (,$(wildcard $(HWCONFIG_PATH))) +ifeq (,$(MAKE_CLEAN)) +$(info $(HWCONFIG_DIRS)) +$(eval $(call PrintVariable,ALL_HWCONFIG,Available configurations)) +$(error Hardware configuration '$(HWCONFIG)' not found) +endif +endif + +PARTITION_PATH := $(COMPONENT_PATH) +PARTITION_TOOLS := $(PARTITION_PATH)/Tools +HWCONFIG_TOOL := \ + HWCONFIG_DIRS="$(HWCONFIG_DIRS)" \ + BUILD_BASE=$(BUILD_BASE) \ + $(PYTHON) $(PARTITION_TOOLS)/hwconfig/hwconfig.py + +ifeq (,$(MAKE_DOCS)$(MAKE_CLEAN)) + +# Generate build variables from hardware configuration +HWCONFIG_MK := $(PROJECT_DIR)/$(OUT_BASE)/hwconfig.mk +$(shell $(HWCONFIG_TOOL) --quiet expr $(HWCONFIG) $(HWCONFIG_MK) "config.buildVars()") +include $(HWCONFIG_MK) +ifeq ($(SMING_ARCH_HW),) +$(error Hardware configuration error) +else ifneq ($(SMING_ARCH),$(SMING_ARCH_HW)) +$(error Hardware configuration is for arch $(SMING_ARCH_HW), does not match SMING_ARCH ($(SMING_ARCH))) +endif +COMPONENT_CXXFLAGS := -DPARTITION_TABLE_OFFSET=$(PARTITION_TABLE_OFFSET) + +# Function to evaluate expression against config +HWEXPR := $(HWCONFIG_TOOL) $(if $(PART),--part "$(PART)") expr $(HWCONFIG) - + +define HwExpr +$(shell $(HWEXPR) "$1") +endef + + +##@Configuration + +.PHONY: map +map: $(HWCONFIG_PATH) ##Show partition map + @echo "Partition map:" + $(Q) $(HWEXPR) "config.map().to_csv()" + @echo + +.PHONY: hwexpr +hwexpr: $(HWCONFIG_PATH) ##Evaluate expression against hardware configuration (use EXPR= and optionally PART=) + $(Q) $(HWEXPR) "$(EXPR)" + +.PHONY: hwconfig +hwconfig: $(HWCONFIG_PATH) ##Show current hardware configuration + @echo + $(Q) $(HWEXPR) "config.to_json()" + @echo +ifneq ($(V),) + @echo "$(HWCONFIG): $(foreach c,$(HWCONFIG_DEPENDS),\n $c)" +endif + +.PHONY: hwconfig-list +hwconfig-list: ##List available hardware configurations + @echo "Available configurations: $(foreach c,$(ALL_HWCONFIG),\n $(if $(subst $c,,$(HWCONFIG)), ,*) $(shell printf "%-25s" "$c") $(filter %/$c.hw,$(ALL_HWCONFIG_PATHS)))" + @echo + +.PHONY: hwconfig-validate +hwconfig-validate: $(HWCONFIG_PATH) ##Validate current hardware configuration + @echo "Validating hardware config '$(HWCONFIG)'" + $(Q) $(HWCONFIG_TOOL) validate $(HWCONFIG) - $(PARTITION_PATH)/schema.json + + +##@Building + +# The partition table +PARTITIONS_BIN := $(FW_BASE)/partitions.bin +CUSTOM_TARGETS += $(PARTITIONS_BIN) + +$(PARTITIONS_BIN): $(HWCONFIG_DEPENDS) + $(Q) $(MAKE) --no-print-directory hwconfig-validate + $(Q) $(HWCONFIG_TOOL) partgen $(HWCONFIG) $@ + + +# Create build target for a partition +# $1 -> Partition name +define PartitionTarget +PTARG := $(shell $(HWCONFIG_TOOL) --part $1 expr $(HWCONFIG) - part.filename) +$$(PTARG): + $$(Q) $$(MAKE) --no-print-directory $$(shell $$(HWCONFIG_TOOL) --part $1 expr $$(HWCONFIG) - "part.build['target']") PART=$1 +CUSTOM_TARGETS += $$(PTARG) +endef + +# Create build targets for all partitions with 'build' property +DEBUG_VARS += PARTITIONS_WITH_TARGETS +PARTITIONS_WITH_TARGETS := $(call HwExpr,(' '.join([part.name for part in filter(lambda part: part.build is not None, config.partitions)]))) + +# Must be invoked from project.mk after all Components have been processed +# This allows partition definitions to include variables which may not yet be defined +define PartitionCreateTargets +$(foreach p,$(PARTITIONS_WITH_TARGETS),$(eval $(call PartitionTarget,$p))) +endef + + +##@Flashing + +# Get chunks for a list of partitions, ignore any without filenames +# $1 -> List of partition names +define PartChunks +$(foreach p,$1,$(if $(PARTITION_$p_FILENAME),$(PARTITION_$p_ADDRESS)=$(PARTITION_$p_FILENAME))) +endef + +# Get regions for list of partitions +# $1 -> List of partition names +define PartRegions +$(foreach p,$1,$(PARTITION_$p_ADDRESS),$(PARTITION_$p_SIZE_BYTES)) +endef + +# One flash sector of 0xFF +BLANK_BIN := $(PARTITION_PATH)/blank.bin + +# Just the application chunks +SPIFLASH_APP_PARTITION_NAMES := $(foreach p,$(SPIFLASH_PARTITION_NAMES),$(if $(filter app,$(PARTITION_$p_TYPE)),$p)) +FLASH_APP_CHUNKS = $(call PartChunks,$(SPIFLASH_APP_PARTITION_NAMES)) +# Partition map chunk +FLASH_MAP_CHUNK := $(PARTITION_TABLE_OFFSET)=$(PARTITIONS_BIN) +# All partitions with image files +FLASH_PARTITION_CHUNKS = $(call PartChunks,$(SPIFLASH_PARTITION_NAMES)) + +# User-selected partition(s) +FLASH_PART_CHUNKS = $(call PartChunks,$(PART)) +FLASH_PART_REGION = $(call PartRegions,$(PART)) + +# Where to store read partition map file +READMAP_BIN := $(OUT_BASE)/partition-table.read.bin +PARTITION_TABLE_REGION = $(PARTITION_TABLE_OFFSET),$(PARTITION_TABLE_LENGTH) + +.PHONY: readmap +readmap:##Read partition map from device + $(Q) $(call ReadFlash,$(PARTITION_TABLE_REGION),$(READMAP_BIN)) + @echo + @echo "Partition map read from device:" + $(Q) $(HWCONFIG_TOOL) expr $(READMAP_BIN) - "config.map().to_csv()" + @echo + +# Run sanity checks against a list of partitions before flashing commences +# $1 -> List of partition names +define CheckPartitionChunks +$(HWCONFIG_TOOL) flashcheck $(HWCONFIG) - "$1" +endef + +.PHONY: flashpart +flashpart: all kill_term ##Flash a specific partition, set PART=name + $(Q) $(call CheckPartitionChunks,$(FLASH_PART_CHUNKS)) + $(call WriteFlash,$(FLASH_PART_CHUNKS)) + +.PHONY: erasepart +erasepart: kill_term ##Erase a specific partition, set PART=name + $(call EraseFlashRegion,$(FLASH_PART_REGION)) + +.PHONY: readpart +readpart: kill_term ##Read partition from device, set PART=name + $(call ReadFlash,$(FLASH_PART_REGION),$(OUT_BASE)/$(PART).read.bin) + +.PHONY: flashmap +flashmap: $(PARTITIONS_BIN) kill_term ##Write partition table to device + $(call WriteFlash,$(FLASH_MAP_CHUNK)) + + +endif # MAKE_DOCS diff --git a/Sming/Components/Storage/notes.rst b/Sming/Components/Storage/notes.rst new file mode 100644 index 0000000000..3b4765aa06 --- /dev/null +++ b/Sming/Components/Storage/notes.rst @@ -0,0 +1,69 @@ +Storage Partition notes +======================= + +External devices +---------------- + +Support for 'external' storage devices is implemented by marking groups of entries +with a ``storage`` type partition entry. This is intended to support designs where the +memory devices are fixed (such as additional SPI RAM or flash chips) rather than +removable (such as SD cards). When these devices are registered with the Storage subsystem, +the primary partition table is scanned for related entries. + +Devices may contain their own partition table, but such tables are not loaded automatically. +This is done by calling :cpp:func:`Storage::Device::loadPartitions`. +The application must also consider how the partition table will be initialised on external +devices as this is not handled by the build system. + + + +But it also has some disadvantages: + +- Adding/removing storage devices dynamically does not fit well. + For example, when an SD Card is inserted the partition layout must be established + dynamically. + +The main purpose of the partition table is to manage the system's fixed storage. +Mostly that will just be the main flash device, but could also be 'external' devices +such as SPI RAM or flash. + +Devices can be dynamically registered/un-registered to the partition table: existing Partition objects remain valid but calls will fail. + +Dynamic partitions have additional issues. + +Partition identification +------------------------ + +Two devices may have identical partition names so these would need +to be qualified, e.g. ``spiFlash/spiffs0``. Without qualification the first matching +name would be used. + +Alternatively, each :cpp:class:`Storage::Device` object could manage its own partitions, +rather than having them in a single table. The existing layout scheme is fine, we can +just extend it so that devices can support their own partitions. + +Device serial numbers should be exposed so that devices can be uniquely identified +by applications. + +Device partitions +----------------- + +A global :cpp:class:`Storage::DeviceManager` object replaces ``partitionTable``. + +Storage each device manages its own partitions, therefore spiFlash loads its own partition table. + + + +Partitions in devices ? +----------------------- + +JSON may be more logically arranged so that partitions are defined within each device. + +This implies that partition_table_offset is also defined within the device, +so adding this entry to external devices allows definition of a partition table there also. +The application now only has to handle how to write any supplementary tables to external devices. + +Problem: Way too complicated. + +So, if we want external partition tables how to do that? +Leave for another time. \ No newline at end of file diff --git a/Sming/Components/Storage/requirements.txt b/Sming/Components/Storage/requirements.txt new file mode 100644 index 0000000000..d89304b1a8 --- /dev/null +++ b/Sming/Components/Storage/requirements.txt @@ -0,0 +1 @@ +jsonschema diff --git a/Sming/Components/Storage/schema.json b/Sming/Components/Storage/schema.json new file mode 100644 index 0000000000..df887e75e2 --- /dev/null +++ b/Sming/Components/Storage/schema.json @@ -0,0 +1,178 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Config", + "definitions": { + "Config": { + "title": "Hardware Configuration", + "description": "Defines memory devices and partitions for a specific hardware implementation", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "base_config": { + "type": "string", + "title": "Base configuration", + "description": "Inherit a previously-defined configuration" + }, + "comment": { + "type": "string" + }, + "arch": { + "type": "string", + "title": "Target architecture", + "description": "Defined *only* in the base 'standard' spec" + }, + "partition_table_offset": { + "type": "string", + "description": "Location of partition table in spiFlash" + }, + "devices": { + "$ref": "#/definitions/Devices" + }, + "partitions": { + "$ref": "#/definitions/Partitions" + } + }, + "required": [ + "name", + "arch", + "partition_table_offset", + "devices", + "partitions" + ] + }, + "Devices": { + "title": "Devices", + "type": "object", + "additionalProperties": false, + "properties": { + "spiFlash": { + "$ref": "#/definitions/Device", + "description": "Main flash memory device" + } + }, + "patternProperties": { + "^[A-Za-z_][A-Za-z0-9_]*$": { + "type": "object", + "$ref": "#/definitions/Device" + } + }, + "required": [ + "spiFlash" + ] + }, + "Device": { + "title": "Storage device definition", + "type": "object", + "additionalProperties": false, + "properties": { + "size": { + "type": "string" + }, + "type": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "qio", + "qout", + "dio", + "dout" + ] + }, + "speed": { + "type": "integer" + } + }, + "required": [ + "size", + "type" + ] + }, + "Partitions": { + "title": "Partitions", + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[A-Za-z_][A-Za-z0-9_]*$": { + "type": "object", + "$ref": "#/definitions/Partition" + } + } + }, + "Partition": { + "title": "Partition definition", + "type": "object", + "additionalProperties": false, + "properties": { + "device": { + "type": "string", + "description": "ID of device this partition relates to" + }, + "address": { + "type": [ + "string", + "integer" + ] + }, + "size": { + "type": [ + "string", + "integer" + ] + }, + "type": { + "type": [ + "string", + "integer" + ] + }, + "subtype": { + "type": [ + "string", + "integer" + ] + }, + "readonly": { + "type": "boolean" + }, + "encrypted": { + "type": "boolean" + }, + "filename": { + "type": "string", + "description": "Location of file to write to this partition" + }, + "build": { + "type": "object", + "$ref": "#/definitions/Build", + "description": "If present, used to build 'filename'" + } + }, + "required": [ + "address", + "size", + "type", + "subtype" + ] + }, + "Build": { + "title": "Build specification", + "description": "Additional properties as required by build target", + "type": "object", + "additionalProperties": true, + "properties": { + "target": { + "type": "string", + "description": "Makefile target for this build" + } + }, + "required": [ + "target" + ] + } + } +} \ No newline at end of file diff --git a/Sming/Components/Storage/src/CustomDevice.cpp b/Sming/Components/Storage/src/CustomDevice.cpp new file mode 100644 index 0000000000..a960528961 --- /dev/null +++ b/Sming/Components/Storage/src/CustomDevice.cpp @@ -0,0 +1,42 @@ +/* + * CustomDevice.cpp + */ + +#include "include/Storage/CustomDevice.h" +#include + +namespace Storage +{ +namespace +{ +static constexpr size_t maxPartitions{16}; ///< Hard limit on partition table size + +class Partitions : public PartitionTable +{ +public: + Partition add(const Partition::Info& info) + { + if(!mEntries) { + mEntries.reset(new Partition::Info[maxPartitions]); + } else + assert(mCount < maxPartitions); + + auto i = mCount++; + mEntries.get()[i] = info; + return operator[](i); + } +}; + +} // namespace + +Partition CustomDevice::createPartition(const Partition::Info& info) +{ + if(mPartitions.count() >= maxPartitions) { + debug_e("Partition table is full for '%s'", getName().c_str()); + return Partition{}; + } + + return reinterpret_cast(mPartitions).add(info); +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/Device.cpp b/Sming/Components/Storage/src/Device.cpp new file mode 100644 index 0000000000..6f2c6ec428 --- /dev/null +++ b/Sming/Components/Storage/src/Device.cpp @@ -0,0 +1,112 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Device.h - external storage device API + * + ****/ + +#include "include/Storage.h" +#include "include/Storage/Device.h" +#include "include/Storage/partition_info.h" +#include +#include + +namespace +{ +#define XX(type, value, desc) DEFINE_FSTR_LOCAL(typestr_##type, #type) +STORAGE_TYPE_MAP(XX) +#undef XX + +#define XX(type, value, desc) &typestr_##type, +DEFINE_FSTR_VECTOR_LOCAL(typeStrings, FlashString, STORAGE_TYPE_MAP(XX)) +#undef XX + +#define XX(type, value, desc) DEFINE_FSTR_LOCAL(long_typestr_##type, desc) +STORAGE_TYPE_MAP(XX) +#undef XX + +#define XX(type, value, desc) &long_typestr_##type, +DEFINE_FSTR_VECTOR_LOCAL(longTypeStrings, FlashString, STORAGE_TYPE_MAP(XX)) +#undef XX + +} // namespace + +String toString(Storage::Device::Type type) +{ + return typeStrings[unsigned(type)]; +} + +String toLongString(Storage::Device::Type type) +{ + return longTypeStrings[unsigned(type)]; +} + +namespace Storage +{ +Device::~Device() +{ + unRegisterDevice(this); +} + +bool Device::loadPartitions(Device& source, uint32_t tableOffset) +{ + constexpr size_t maxEntries = ESP_PARTITION_TABLE_MAX_LEN / sizeof(esp_partition_info_t); + esp_partition_info_t buffer[maxEntries]; + if(!source.read(tableOffset, buffer, sizeof(buffer))) { + debug_e("[Partition] Failed to read partition table at offset 0x%08x", tableOffset); + return false; + } + + if(buffer[0].type != Partition::Type::storage) { + debug_e("[Partition] Bad partition table for device '%s' @ 0x%08x", getName().c_str(), tableOffset); + return false; + } + + String devname = getName(); + for(unsigned i = 0; i < maxEntries; ++i) { + auto entry = &buffer[i]; + if(entry->magic != ESP_PARTITION_MAGIC) { + continue; + } + if(entry->type != Partition::Type::storage) { + continue; + } + + auto len = devname.length(); + if(len > Partition::nameSize) { + continue; + } + if(strncmp(entry->name, devname.c_str(), len) != 0) { + continue; + } + + if(entry->subtype != uint8_t(getType())) { + debug_w("[Device] '%s' type mismatch, '%s' in partition table but device reports '%s'", getName().c_str(), + toString(Device::Type(entry->subtype)).c_str(), toString(getType()).c_str()); + } + if(entry->size != getSize()) { + debug_w("[Device] '%s' size mismatch, 0x%08x in partition table but device reports 0x%08x", + getName().c_str(), entry->size, getSize()); + } + + // Skip the storage entry, not required + ++entry; + ++i; + unsigned count{0}; + while(i < maxEntries && buffer[i].magic == ESP_PARTITION_MAGIC) { + ++i; + ++count; + } + + mPartitions.load(entry, count); + return true; + } + + // No partitions found + return false; +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/Iterator.cpp b/Sming/Components/Storage/src/Iterator.cpp new file mode 100644 index 0000000000..a082b4778d --- /dev/null +++ b/Sming/Components/Storage/src/Iterator.cpp @@ -0,0 +1,68 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Iterator.cpp + * + ****/ + +#include "include/Storage/Iterator.h" +#include "include/Storage/SpiFlash.h" +#include + +namespace Storage +{ +Iterator::Iterator(Device& device, uint8_t partitionIndex) + : mSearch{&device, Partition::Type::any, Partition::SubType::any}, mDevice(&device), mPos(partitionIndex) + +{ + if(partitionIndex >= device.partitions().count()) { + mDevice = nullptr; + mPos = afterEnd; + } +} + +Iterator::Iterator(Partition::Type type, uint8_t subtype) : mSearch{nullptr, type, subtype} +{ + mDevice = spiFlash; + next(); +} + +bool Iterator::next() +{ + while(mDevice != nullptr) { + while(uint8_t(++mPos) < mDevice->partitions().count()) { + auto entry = mDevice->partitions()[mPos]; + + if(mSearch.type != Partition::Type::any && mSearch.type != entry.type()) { + continue; + } + + if(mSearch.subType != Partition::SubType::any && mSearch.subType != entry.subType()) { + continue; + } + + return true; + } + + mPos = afterEnd; + if(mSearch.device != nullptr) { + mDevice = nullptr; + break; + } + + mDevice = mDevice->getNext(); + mPos = beforeStart; + } + + return false; +} + +Partition Iterator::operator*() const +{ + return mDevice ? mDevice->partitions()[mPos] : Partition{}; +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/ObjectList.cpp b/Sming/Components/Storage/src/ObjectList.cpp new file mode 100644 index 0000000000..dae8a07716 --- /dev/null +++ b/Sming/Components/Storage/src/ObjectList.cpp @@ -0,0 +1,65 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * ObjectList.cpp + * + ****/ + +#include "include/Storage/ObjectList.h" + +namespace Storage +{ +bool ObjectList::add(Object* object) +{ + if(object == nullptr) { + return false; + } + + Object* prev = nullptr; + auto it = mHead; + while(it != nullptr) { + if(it == object) { + // Already in list + return true; + } + prev = it; + it = it->mNext; + } + + if(prev == nullptr) { + mHead = object; + } else { + prev->mNext = object; + } + object->mNext = it; + return true; +} + +bool ObjectList::remove(Object* object) +{ + if(object == nullptr) { + return false; + } + + if(mHead == object) { + mHead = object->mNext; + return true; + } + + auto it = mHead; + while(it->mNext != nullptr) { + if(it->mNext == object) { + it->mNext = object->mNext; + object->mNext = nullptr; + return true; + } + it = it->mNext; + } + + return false; +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/Partition.cpp b/Sming/Components/Storage/src/Partition.cpp new file mode 100644 index 0000000000..d66c128fbb --- /dev/null +++ b/Sming/Components/Storage/src/Partition.cpp @@ -0,0 +1,251 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Partition.cpp - Partition support for all architectures + * + ****/ + +#include "include/Storage/Partition.h" +#include "include/Storage/Device.h" +#include +#include + +using namespace Storage; + +namespace +{ +/* APP type strings */ + +#define XX(subtype, value, desc) DEFINE_FSTR_LOCAL(app_subTypeStr_##subtype, #subtype) +PARTITION_APP_SUBTYPE_MAP(XX) +#undef XX + +#define XX(subtype, value, desc) {Partition::SubType::App::subtype, &app_subTypeStr_##subtype}, +DEFINE_FSTR_MAP_LOCAL(appSubTypeStrings, Partition::SubType::App, FlashString, PARTITION_APP_SUBTYPE_MAP(XX)) +#undef XX + +#define XX(subtype, value, desc) DEFINE_FSTR_LOCAL(app_subTypeStr_long_##subtype, desc) +PARTITION_APP_SUBTYPE_MAP(XX) +#undef XX + +#define XX(subtype, value, desc) {Partition::SubType::App::subtype, &app_subTypeStr_long_##subtype}, +DEFINE_FSTR_MAP_LOCAL(longAppSubTypeStrings, Partition::SubType::App, FlashString, PARTITION_APP_SUBTYPE_MAP(XX)) +#undef XX + +/* DATA subtype strings */ + +#define XX(subtype, value, desc) DEFINE_FSTR_LOCAL(data_subTypeStr_##subtype, #subtype) +PARTITION_DATA_SUBTYPE_MAP(XX) +#undef XX + +#define XX(subtype, value, desc) {Partition::SubType::Data::subtype, &data_subTypeStr_##subtype}, +DEFINE_FSTR_MAP_LOCAL(dataSubTypeStrings, Partition::SubType::Data, FlashString, PARTITION_DATA_SUBTYPE_MAP(XX)) +#undef XX + +#define XX(subtype, value, desc) DEFINE_FSTR_LOCAL(data_subTypeStr_long_##subtype, desc) +PARTITION_DATA_SUBTYPE_MAP(XX) +#undef XX + +#define XX(subtype, value, desc) {Partition::SubType::Data::subtype, &data_subTypeStr_long_##subtype}, +DEFINE_FSTR_MAP_LOCAL(longDataSubTypeStrings, Partition::SubType::Data, FlashString, PARTITION_DATA_SUBTYPE_MAP(XX)) +#undef XX + +} // namespace + +String toString(Partition::Type type, uint8_t subType) +{ + String s; + switch(type) { + case Partition::Type::app: + s = F("app/"); + if(auto v = appSubTypeStrings[Partition::SubType::App(subType)]) { + s += String(v); + } else { + s += subType; + } + break; + case Partition::Type::data: + s = F("data/"); + if(auto v = dataSubTypeStrings[Partition::SubType::Data(subType)]) { + s += String(v); + } else { + s += subType; + } + break; + + case Partition::Type::storage: + s = F("storage/"); + if(auto v = toString(Device::Type(subType))) { + s += v; + } else { + s += subType; + } + break; + + default: + s = unsigned(type); + s += '.'; + s += subType; + } + return s; +} + +String toLongString(Partition::Type type, uint8_t subType) +{ + switch(type) { + case Partition::Type::app: + if(auto v = longAppSubTypeStrings[Partition::SubType::App(subType)]) { + return F("App: ") + String(v); + } + break; + case Partition::Type::data: + if(auto v = longDataSubTypeStrings[Partition::SubType::Data(subType)]) { + return F("Data: ") + String(v); + } + break; + + case Partition::Type::storage: + if(auto s = toLongString(Device::Type(subType))) { + return F("Storage: ") + s; + } + break; + + default:; + // Unknown + } + + return toString(type, subType); +} + +namespace Storage +{ +String Partition::typeString() const +{ + return toString(type(), subType()); +} + +String Partition::longTypeString() const +{ + return toLongString(type(), subType()); +} + +bool Partition::verify(Partition::Type type, uint8_t subtype) const +{ + if(mPart == nullptr) { + debug_e("[Partition] invalid"); + return false; + } + + if(type != mPart->type) { + debug_e("[Partition] type mismatch, expected %u got %u", unsigned(type), unsigned(mPart->type)); + return false; + } + + if(mPart->subtype != subtype) { + debug_e("[Partition] subtype mismatch, expected %u got %u", subtype, mPart->subtype); + return false; + } + + return true; +} + +bool Partition::getDeviceAddress(uint32_t& address, size_t size) const +{ + if(mDevice == nullptr || mPart == nullptr) { + debug_e("[Partition] Invalid"); + return false; + } + + if(address >= mPart->size || (address + size - 1) >= mPart->size) { + debug_e("[Partition] Invalid range, address: 0x%08x, size: 0x%08x", address, size); + return false; + } + + // Storage partitions refer directly to the underlying device + if(type() != Partition::Type::storage) { + address += mPart->offset; + } + + return true; +} + +String Partition::getDeviceName() const +{ + return mDevice ? mDevice->getName() : nullptr; +} + +size_t Partition::getBlockSize() const +{ + return mDevice ? mDevice->getBlockSize() : 0; +} + +bool Partition::allowRead() +{ + if(mDevice == nullptr || mPart == nullptr) { + debug_e("[Partition] Invalid"); + return false; + } + + return true; +} + +bool Partition::allowWrite() +{ + if(!allowRead()) { + return false; + } + + if(mPart->flags[Flag::readOnly]) { + debug_e("[Partition] %s is read-only", mPart ? mPart->name.c_str() : "?"); + return false; + } + + return true; +} + +bool Partition::read(size_t offset, void* dst, size_t size) +{ + if(!allowRead()) { + return false; + } + + uint32_t addr = offset; + if(!getDeviceAddress(addr, size)) { + return false; + } + + return mDevice ? mDevice->read(addr, dst, size) : false; +} + +bool Partition::write(size_t offset, const void* src, size_t size) +{ + if(!allowWrite()) { + return false; + } + + uint32_t addr = offset; + if(!getDeviceAddress(addr, size)) { + return false; + } + + return mDevice ? mDevice->write(addr, src, size) : false; +} + +bool Partition::erase_range(size_t offset, size_t size) +{ + if(!allowWrite()) { + return false; + } + + uint32_t addr = offset; + if(!getDeviceAddress(addr, size)) { + return false; + } + + return mDevice ? mDevice->erase_range(addr, size) : false; +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/PartitionStream.cpp b/Sming/Components/Storage/src/PartitionStream.cpp new file mode 100644 index 0000000000..86ad9b037c --- /dev/null +++ b/Sming/Components/Storage/src/PartitionStream.cpp @@ -0,0 +1,61 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * PartitionStream.cpp + * + ****/ + +#include "include/Storage/PartitionStream.h" + +namespace Storage +{ +uint16_t PartitionStream::readMemoryBlock(char* data, int bufSize) +{ + int len = std::min(bufSize, available()); + return partition.read(startOffset + readPos, data, len) ? len : 0; +} + +int PartitionStream::seekFrom(int offset, SeekOrigin origin) +{ + size_t newPos; + switch(origin) { + case SeekOrigin::Start: + newPos = 0; + break; + case SeekOrigin::Current: + newPos = readPos + offset; + break; + case SeekOrigin::End: + newPos = size + offset; + break; + default: + return -1; + } + + if(newPos > size) { + return -1; + } + + readPos = newPos; + return readPos; +} + +size_t PartitionStream::write(const uint8_t* data, size_t length) +{ + auto len = std::min(size - writePos, length); + if(len != 0) { + if(!partition.write(startOffset + writePos, data, len)) { + len = 0; + } else { + writePos += len; + } + } + + // Return amount actually written + return len; +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/PartitionTable.cpp b/Sming/Components/Storage/src/PartitionTable.cpp new file mode 100644 index 0000000000..793cd061bf --- /dev/null +++ b/Sming/Components/Storage/src/PartitionTable.cpp @@ -0,0 +1,37 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * PartitionTable.cpp + * + ****/ + +#include "include/Storage/PartitionTable.h" +#include "include/Storage/partition_info.h" +#include + +namespace Storage +{ +void PartitionTable::load(const esp_partition_info_t* entry, unsigned count) +{ + if(count == 0) { + mEntries.reset(); + mCount = count; + return; + } + + mCount = count; + mEntries.reset(new Partition::Info[count]); + for(unsigned i = 0; i < count; ++i) { + auto& e = entry[i]; + // name may not be zero-terminated + char name[Partition::nameSize + 1]; + memcpy(name, e.name, Partition::nameSize); + name[Partition::nameSize] = '\0'; + mEntries.get()[i] = Partition::Info{name, e.type, e.subtype, e.offset, e.size, e.flags}; + } +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/ProgMem.cpp b/Sming/Components/Storage/src/ProgMem.cpp new file mode 100644 index 0000000000..8d95f594cc --- /dev/null +++ b/Sming/Components/Storage/src/ProgMem.cpp @@ -0,0 +1,35 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * ProgMem.cpp + * + ****/ + +#include "include/Storage/ProgMem.h" +#include + +namespace Storage +{ +ProgMem progMem; + +bool ProgMem::read(uint32_t address, void* dst, size_t size) +{ + size_t readCount = flashmem_read(dst, address, size); + return readCount == size; +} + +Partition ProgMem::createPartition(const String& name, const void* flashPtr, size_t size, Partition::Type type, + uint8_t subtype) +{ + auto addr = flashmem_get_address(flashPtr); + if(addr == 0) { + return Partition{}; + } + + return createPartition(name, type, subtype, addr, size, Partition::Flag::readOnly); +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/SpiFlash.cpp b/Sming/Components/Storage/src/SpiFlash.cpp new file mode 100644 index 0000000000..a308d65c49 --- /dev/null +++ b/Sming/Components/Storage/src/SpiFlash.cpp @@ -0,0 +1,66 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * SpiFlash.cpp + * + ****/ + +#include "include/Storage/SpiFlash.h" +#include "include/Storage/partition_info.h" +#include + +namespace Storage +{ +DEFINE_FSTR(FS_SPIFLASH, "spiFlash") +SpiFlash* spiFlash; + +String SpiFlash::getName() const +{ + return FS_SPIFLASH; +} + +size_t SpiFlash::getBlockSize() const +{ + return SPI_FLASH_SEC_SIZE; +} + +size_t SpiFlash::getSize() const +{ + return flashmem_get_size_bytes(); +} + +bool SpiFlash::read(uint32_t address, void* dst, size_t size) +{ + size_t readCount = flashmem_read(dst, address, size); + return readCount == size; +} + +bool SpiFlash::write(uint32_t address, const void* src, size_t size) +{ + size_t writeCount = flashmem_write(src, address, size); + return writeCount == size; +} + +bool SpiFlash::erase_range(uint32_t address, size_t size) +{ + if(address % SPI_FLASH_SEC_SIZE != 0 || size % SPI_FLASH_SEC_SIZE != 0) { + debug_e("[Partition] erase address/size misaligned: 0x%08x / 0x%08x", address, size); + return false; + } + + auto sec = address / SPI_FLASH_SEC_SIZE; + auto end = (address + size) / SPI_FLASH_SEC_SIZE; + while(sec < end) { + if(!flashmem_erase_sector(sec)) { + return false; + } + ++sec; + } + + return true; +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/Storage.cpp b/Sming/Components/Storage/src/Storage.cpp new file mode 100644 index 0000000000..98b6c8ed18 --- /dev/null +++ b/Sming/Components/Storage/src/Storage.cpp @@ -0,0 +1,75 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * DeviceManager.cpp + * + ****/ + +#include "include/Storage.h" +#include "include/Storage/SpiFlash.h" +#include + +namespace Storage +{ +void initialize() +{ + if(spiFlash == nullptr) { + spiFlash = new SpiFlash; + registerDevice(spiFlash); + spiFlash->loadPartitions(PARTITION_TABLE_OFFSET); + } +} + +const Device::List getDevices() +{ + return Device::List(spiFlash); +} + +bool registerDevice(Device* device) +{ + if(device == nullptr) { + return false; + } + auto devname = device->getName(); + + Device::List devices(spiFlash); + auto it = std::find(devices.begin(), devices.end(), devname); + if(!it) { + devices.add(device); + device->loadPartitions(*spiFlash, PARTITION_TABLE_OFFSET); + debug_i("[Storage] Device '%s' registered", devname.c_str()); + } else if(*it != *device) { + debug_e("[Storage] Another device is already registered with name '%s'", devname.c_str()); + return false; + } + + return true; +} + +bool unRegisterDevice(Device* device) +{ + return Device::List(spiFlash).remove(device); +} + +Device* findDevice(const String& name) +{ + Device::List devices(spiFlash); + return std::find(devices.begin(), devices.end(), name); +} + +Partition findPartition(const String& name) +{ + for(auto& dev : getDevices()) { + auto part = dev.partitions().find(name); + if(part) { + return part; + } + } + + return Partition{}; +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/SysMem.cpp b/Sming/Components/Storage/src/SysMem.cpp new file mode 100644 index 0000000000..5281259f62 --- /dev/null +++ b/Sming/Components/Storage/src/SysMem.cpp @@ -0,0 +1,25 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * SysMem.cpp + * + ****/ + +#include "include/Storage/SysMem.h" +#include + +namespace Storage +{ +SysMem sysMem; + +Partition SysMem::createPartition(const String& name, const FSTR::ObjectBase& fstr, Partition::Type type, + uint8_t subtype) +{ + return createPartition(name, type, subtype, reinterpret_cast(fstr.data()), fstr.size(), + Partition::Flag::readOnly); +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage.h b/Sming/Components/Storage/src/include/Storage.h new file mode 100644 index 0000000000..81977f4d85 --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage.h @@ -0,0 +1,62 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Storage.h + * + ****/ +#pragma once + +#include "Storage/Device.h" + +namespace Storage +{ +/** + * @brief Called early in the startup phase + */ +void initialize(); + +/** + * @brief Get read-only reference to device list + */ +const Device::List getDevices(); + +/** + * @brief Register a storage device + * @retval bool true on success, false if another device already registered with same name + */ +bool registerDevice(Device* device); + +/** + * @brief Unregister a storage device + * + * Use extreme care: behaviour is unpredictable if partitions are in use + */ +bool unRegisterDevice(Device* device); + +/** + * @brief Find a registered device + */ +Device* findDevice(const String& name); + +/** + * @brief Find the first partition matching the given name + */ +Partition findPartition(const String& name); + +/** + * @brief Find partitions of the given type + */ +inline Iterator findPartition(Partition::Type type = Partition::Type::any, uint8_t subType = Partition::SubType::any) +{ + return Iterator(type, subType); +} + +template Iterator findPartition(T subType) +{ + return Iterator(Partition::Type(T::partitionType), uint8_t(subType)); +} + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/CustomDevice.h b/Sming/Components/Storage/src/include/Storage/CustomDevice.h new file mode 100644 index 0000000000..82222f2667 --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/CustomDevice.h @@ -0,0 +1,35 @@ +/* + * CustomDevice.h + */ + +#pragma once + +#include "Device.h" + +namespace Storage +{ +/** + * @brief Class to support dynamic partitions + * + * Call `createPartition` to add partitions up to a maximum of 16 entries. + */ +class CustomDevice : public Device +{ +public: + Partition createPartition(const Partition::Info& info); + + Partition createPartition(const String& name, Partition::Type type, uint8_t subtype, uint32_t offset, size_t size, + Partition::Flags flags = 0) + { + return createPartition(Partition::Info{name, type, subtype, offset, size, flags}); + } + + template + Partition createPartition(const String& name, SubType subtype, uint32_t offset, size_t size, + Partition::Flags flags = 0) + { + return createPartition(name, Partition::Type(SubType::partitionType), uint8_t(subtype), offset, size, flags); + } +}; + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/Device.h b/Sming/Components/Storage/src/include/Storage/Device.h new file mode 100644 index 0000000000..cf54588b19 --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/Device.h @@ -0,0 +1,140 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Device.h - external storage device API + * + ****/ +#pragma once + +#include +#include "ObjectList.h" +#include "PartitionTable.h" + +#define STORAGE_TYPE_MAP(XX) \ + XX(unknown, 0x00, "Other storage device") \ + XX(flash, 0x01, "SPI flash") \ + XX(spiram, 0x02, "SPI RAM") \ + XX(sdcard, 0x03, "SD Card") \ + XX(disk, 0x04, "Physical disk") \ + XX(file, 0x05, "Backing file on separate filesystem") \ + XX(sysmem, 0x06, "System Memory") + +namespace Storage +{ +class SpiFlash; + +/** + * @brief Represents a storage device (e.g. flash memory) + */ +class Device : public ObjectTemplate +{ +public: + using List = ObjectListTemplate; + using OwnedList = OwnedObjectListTemplate; + + /** + * @brief Storage type + */ + enum class Type : uint8_t { +#define XX(type, value, desc) type = value, + STORAGE_TYPE_MAP(XX) +#undef XX + }; + + Device() : mPartitions(*this) + { + } + + ~Device(); + + bool operator==(const String& name) const + { + return getName() == name; + } + + PartitionTable& partitions() + { + return mPartitions; + } + + const PartitionTable& partitions() const + { + return mPartitions; + } + + /** + * @brief Load partition table entries + * @tableOffset Location of partition table to read + * @retval bool true on success, false on failure + */ + bool loadPartitions(uint32_t tableOffset) + { + return loadPartitions(*this, tableOffset); + } + + /** + * @brief Load partition table entries from another table + * @param source Device to load entries from + * @tableOffset Location of partition table to read + * @retval bool true on success, false on failure + */ + bool loadPartitions(Device& source, uint32_t tableOffset); + + /** + * @brief Obtain unique device name + */ + virtual String getName() const = 0; + + /** + * @brief Obtain smallest allocation unit for erase operations + */ + virtual size_t getBlockSize() const = 0; + + /** + * @brief Obtain addressable size of this device + * @retval size_t Must be at least as large as the value declared in the partition table + */ + virtual size_t getSize() const = 0; + + /** + * @brief Obtain device type + */ + virtual Type getType() const = 0; + + /** + * @brief Read data from the storage device + * @param address Where to start reading + * @param dst Buffer to store data + * @param size Size of data to be read, in bytes. + * @retval bool true on success, false on error + */ + virtual bool read(uint32_t address, void* dst, size_t size) = 0; + + /** + * @brief Write data to the storage device + * @param address Where to start writing + * @param src Data to write + * @param size Size of data to be written, in bytes. + * @retval bool true on success, false on error + */ + virtual bool write(uint32_t address, const void* src, size_t size) = 0; + + /** + * @brief Erase a region of storage in preparation for writing + * @param address Where to start erasing + * @param size Size of region to erase, in bytes + * @retval bool true on success, false on error + */ + virtual bool erase_range(uint32_t address, size_t size) = 0; + +protected: + PartitionTable mPartitions; +}; + +} // namespace Storage + +String toString(Storage::Device::Type type); +String toLongString(Storage::Device::Type type); diff --git a/Sming/Components/Storage/src/include/Storage/Iterator.h b/Sming/Components/Storage/src/include/Storage/Iterator.h new file mode 100644 index 0000000000..dcd3b7ca88 --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/Iterator.h @@ -0,0 +1,78 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Iterator.h + * + ****/ +#pragma once + +#include "Partition.h" + +namespace Storage +{ +class Device; + +class Iterator : public std::iterator +{ +public: + Iterator(Device& device, uint8_t partitionIndex); + + Iterator(Device& device, Partition::Type type, uint8_t subtype) : mSearch{&device, type, subtype} + { + mDevice = &device; + next(); + } + + Iterator(Partition::Type type, uint8_t subtype); + + explicit operator bool() const + { + return (mDevice != nullptr) && (mPos > beforeStart) && (mPos < afterEnd); + } + + Iterator operator++(int) + { + auto result = *this; + next(); + return result; + } + + Iterator& operator++() + { + next(); + return *this; + } + + bool operator==(const Iterator& other) const + { + return (mDevice == other.mDevice) && (mPos == other.mPos); + } + + bool operator!=(const Iterator& other) const + { + return !operator==(other); + } + + Partition operator*() const; + +private: + static constexpr int8_t beforeStart{-1}; + static constexpr int8_t afterEnd{0x7f}; + + bool seek(uint8_t pos); + bool next(); + + struct Search { + Device* device; + Partition::Type type; + uint8_t subType; + }; + Search mSearch{}; + Device* mDevice{nullptr}; + int8_t mPos{beforeStart}; +}; + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/Object.h b/Sming/Components/Storage/src/include/Storage/Object.h new file mode 100644 index 0000000000..0d6914f643 --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/Object.h @@ -0,0 +1,144 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Object.h - Base Storage object definition + * + ****/ +#pragma once + +#include +#include + +namespace Storage +{ +class ObjectList; + +class Object +{ +public: + virtual ~Object() + { + } + + virtual Object* next() const + { + return mNext; + } + + Object* getNext() const + { + return mNext; + } + + bool operator==(const Object& other) const + { + return this == &other; + } + + bool operator!=(const Object& other) const + { + return this != &other; + } + +private: + friend class ObjectList; + Object* mNext{nullptr}; +}; + +/** + * @brief Base class template for linked items with type casting + */ +template class ObjectTemplate : public Object +{ +public: + template + class IteratorTemplate : public std::iterator + { + public: + IteratorTemplate(TPtr x) : mObject(x) + { + } + + IteratorTemplate(TRef& x) : mObject(&x) + { + } + + IteratorTemplate(const IteratorTemplate& other) : mObject(other.mObject) + { + } + + IteratorTemplate& operator++() + { + mObject = mObject->getNext(); + return *this; + } + + IteratorTemplate operator++(int) + { + Iterator tmp(*this); + operator++(); + return tmp; + } + + bool operator==(const IteratorTemplate& rhs) const + { + return mObject == rhs.mObject; + } + + bool operator!=(const IteratorTemplate& rhs) const + { + return mObject != rhs.mObject; + } + + TRef operator*() + { + return *mObject; + } + + TPtr operator->() + { + return mObject; + } + + operator TPtr() + { + return mObject; + } + + private: + TPtr mObject; + }; + + using Iterator = IteratorTemplate; + using ConstIterator = IteratorTemplate; + + ObjectType* getNext() const + { + return reinterpret_cast(this->next()); + } + + Iterator begin() const + { + return Iterator(this); + } + + Iterator end() const + { + return Iterator(nullptr); + } + + Iterator cbegin() const + { + return ConstIterator(this); + } + + Iterator cend() const + { + return ConstIterator(nullptr); + } +}; + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/ObjectList.h b/Sming/Components/Storage/src/include/Storage/ObjectList.h new file mode 100644 index 0000000000..da015dd0d8 --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/ObjectList.h @@ -0,0 +1,137 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Object.h - Base Storage object definition + * + ****/ +#pragma once + +#include "Object.h" +#include + +namespace Storage +{ +/** + * @brief Singly-linked list of objects + * @note We don't own the items, just keep references to them + */ +class ObjectList +{ +public: + ObjectList() + { + } + + ObjectList(Object* object) : mHead(object) + { + } + + bool add(Object* object); + + bool add(const Object* object) + { + return add(const_cast(object)); + } + + bool remove(Object* object); + + void clear() + { + mHead = nullptr; + } + + Object* head() + { + return mHead; + } + + const Object* head() const + { + return mHead; + } + + bool isEmpty() const + { + return mHead == nullptr; + } + +protected: + Object* mHead{nullptr}; +}; + +template class ObjectListTemplate : public ObjectList +{ +public: + ObjectListTemplate() = default; + + ObjectListTemplate(ObjectType* object) : ObjectList(object) + { + } + + ObjectType* head() + { + return reinterpret_cast(mHead); + } + + const ObjectType* head() const + { + return reinterpret_cast(mHead); + } + + typename ObjectType::Iterator begin() + { + return head(); + } + + typename ObjectType::Iterator end() + { + return nullptr; + } + + typename ObjectType::ConstIterator begin() const + { + return head(); + } + + typename ObjectType::ConstIterator end() const + { + return nullptr; + } + + size_t count() const + { + return std::count(begin(), end(), true); + } + + bool contains(const ObjectType& object) const + { + return std::find(begin(), end(), object); + } +}; + +/** + * @brief Class template for singly-linked list of objects + * @note We own the objects so are responsible for destroying them when removed + */ +template class OwnedObjectListTemplate : public ObjectListTemplate +{ +public: + bool remove(ObjectType* object) + { + bool res = ObjectList::remove(object); + delete object; + return res; + } + + void clear() + { + while(remove(this->head())) { + // + } + } +}; + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/Partition.h b/Sming/Components/Storage/src/include/Storage/Partition.h new file mode 100644 index 0000000000..5428e1e2a7 --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/Partition.h @@ -0,0 +1,376 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Partition.h - C++ wrapper for universal partition table support + * + * Original license for IDF code: + * + * Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + ****/ +#pragma once + +#include +#include +#include +#include + +#define PARTITION_APP_SUBTYPE_MAP(XX) \ + XX(factory, 0x00, "Factory application") \ + XX(ota0, 0x10, "OTA #0") \ + XX(ota1, 0x11, "OTA #1") \ + XX(ota2, 0x12, "OTA #2") \ + XX(ota3, 0x13, "OTA #3") \ + XX(ota4, 0x14, "OTA #4") \ + XX(ota5, 0x15, "OTA #5") \ + XX(ota6, 0x16, "OTA #6") \ + XX(ota7, 0x17, "OTA #7") \ + XX(ota8, 0x18, "OTA #8") \ + XX(ota9, 0x19, "OTA #9") \ + XX(ota10, 0x1a, "OTA #10") \ + XX(ota11, 0x1b, "OTA #11") \ + XX(ota12, 0x1c, "OTA #12") \ + XX(ota13, 0x1d, "OTA #13") \ + XX(ota14, 0x1e, "OTA #14") \ + XX(ota15, 0x1f, "OTA #15") \ + XX(test, 0x20, "Test application") + +#define PARTITION_DATA_SUBTYPE_MAP(XX) \ + XX(ota, 0x00, "OTA selection") \ + XX(phy, 0x01, "PHY init data") \ + XX(nvs, 0x02, "NVS") \ + XX(coreDump, 0x03, "Core Dump data") \ + XX(nvsKeys, 0x04, "NVS key information") \ + XX(eFuseEm, 0x05, "eFuse emulation") \ + XX(sysParam, 0x40, "System Parameters") \ + XX(espHttpd, 0x80, "ESPHTTPD") \ + XX(fat, 0x81, "FAT") \ + XX(spiffs, 0x82, "SPIFFS") + +namespace Storage +{ +class Device; +class PartitionTable; +struct esp_partition_info_t; + +/** + * @brief Represents a flash partition + */ +class Partition +{ +public: + enum class Type : uint8_t { + app = 0x00, + data = 0x01, + storage = 0x02, + userMin = 0x40, + userMax = 0xFE, + invalid = 0xff, + any = 0xff, + }; + + struct SubType { + static constexpr uint8_t any{0xff}; + static constexpr uint8_t invalid{0xff}; + + /** + * @brief Application partition type + */ + enum class App : uint8_t { + partitionType = uint8_t(Type::app), +#define XX(type, value, desc) type = value, + PARTITION_APP_SUBTYPE_MAP(XX) +#undef XX + ota_min = ota0, + ota_max = ota15, + any = 0xff + }; + + /** + * @brief Data partition type + */ + enum class Data : uint8_t { + partitionType = uint8_t(Type::data), +#define XX(subtype, value, desc) subtype = value, + PARTITION_DATA_SUBTYPE_MAP(XX) +#undef XX + any = 0xff + }; + }; + + enum class Flag { + encrypted = 0, + readOnly = 31, ///< Write/erase prohibited + }; + + static constexpr size_t nameSize{16}; + using Name = char[nameSize]; + using Flags = BitSet; + + /** + * @brief Partition information + */ + struct Info { + CString name; + uint32_t offset{0}; + uint32_t size{0}; + Type type{Type::invalid}; + uint8_t subtype{SubType::invalid}; + Flags flags; + + Info() + { + } + + Info(const String& name, Type type, uint8_t subtype, uint32_t offset, uint32_t size, Flags flags) + : name(name), offset(offset), size(size), type(type), subtype(subtype), flags(flags) + { + } + }; + + Partition() + { + } + + Partition(const Partition& other) : mDevice(other.mDevice), mPart(other.mPart) + { + } + + Partition(Device& device, const Info& info) : mDevice(&device), mPart(&info) + { + } + + /** + * @name Confirm partition is of the expected type + * @param type Expected partition type + * @param subtype Expected partition sub-type + * @retval bool true if type is OK, false if not. + * Logs debug messages on failure. + * @{ + */ + bool verify(Type type, uint8_t subtype) const; + + bool verify(uint8_t type, uint8_t subtype) const + { + return verify(Type(type), subtype); + } + + template bool verify(T subType) const + { + return verify(Type(T::partitionType), uint8_t(subType)); + } + + /** @} */ + + /** + * @brief Convenience function to get SubType value for the i-th OTA partition + */ + static inline SubType::App apptypeOta(uint8_t i) + { + auto subtype = SubType::App(uint8_t(SubType::App::ota_min) + i); + assert(subtype >= SubType::App::ota_min && subtype <= SubType::App::ota_max); + return subtype; + } + + explicit operator bool() const + { + return mDevice != nullptr && mPart != nullptr; + } + + /** + * @brief Read data from the partition + * @param offset Where to start reading, relative to start of partition + * @param dst Buffer to store data + * @param size Size of data to be read, in bytes. + * @retval bool true on success, false on error + */ + bool read(size_t offset, void* dst, size_t size); + + template typename std::enable_if::value, bool>::type read(size_t offset, T& value) + { + return read(offset, &value, sizeof(value)); + } + + /** + * @brief Write data to the partition + * @param offset Where to start writing, relative to start of partition + * @param src Data to write + * @param size Size of data to be written, in bytes. + * @retval bool true on success, false on error + * @note Flash region must be erased first + */ + bool write(size_t offset, const void* src, size_t size); + + /** + * @brief Erase part of the partition + * @param offset Where to start erasing, relative to start of partition + * @param size Size of region to erase, in bytes + * @retval bool true on success, false on error + * @note Both offset and size must be aligned to flash sector size (4Kbytes) + */ + bool erase_range(size_t offset, size_t size); + + /** + * @brief Obtain partition type + */ + Partition::Type type() const + { + return mPart ? Partition::Type(mPart->type) : Type::invalid; + } + + /** + * @brief Obtain partition sub-type + */ + uint8_t subType() const + { + return mPart ? mPart->subtype : SubType::invalid; + } + + /** + * @brief Obtain partition starting address + * @param uint32_t Device address + */ + uint32_t address() const + { + return (mPart && mPart->type != Partition::Type::storage) ? mPart->offset : 0; + } + + /** + * @brief Obtain address of last byte in this this partition + * @param uint32_t Device address + */ + uint32_t lastAddress() const + { + return mPart ? (mPart->offset + mPart->size - 1) : 0; + } + + /** + * @brief Obtain partition size + * @retval uint32_t Size in bytes + */ + uint32_t size() const + { + return mPart ? mPart->size : 0; + } + + /** + * @brief Get partition name + */ + String name() const + { + return mPart ? mPart->name.c_str() : nullptr; + } + + /** + * @brief Get partition flags + */ + Flags flags() const + { + return mPart ? mPart->flags : 0; + } + + /** + * @brief Check state of partition `encrypted` flag + */ + bool isEncrypted() const + { + return flags()[Flag::encrypted]; + } + + /** + * @brief Check state of partition `readOnly` flag + */ + bool isReadOnly() const + { + return mPart ? mPart->flags[Flag::readOnly] : true; + } + + /** + * @name Get partition type expressed as a string + * @{ + */ + String typeString() const; + String longTypeString() const; + /** @} */ + + /** + * @brief Get corresponding storage device address for a given partition offset + * @param address IN: Zero-based offset within partition, OUT: Device address + * @param size Size of data to be accessed + * @retval bool true on success, false on failure + * Fails if the given offset/size combination is out of range, or the partition is undefined. + */ + bool getDeviceAddress(uint32_t& address, size_t size) const; + + /** + * @brief Get name of storage device for this partition + * @retval String + */ + String getDeviceName() const; + + /** + * @brief Get storage device containing this partition + * @retval Device* null if device isn't registered + */ + Device* getDevice() const + { + return mDevice; + } + + /** + * @brief Determine if given address contained within this partition + */ + bool contains(uint32_t addr) const + { + return mPart ? (addr >= mPart->offset && addr <= lastAddress()) : false; + } + + bool operator==(const Partition& other) const + { + return this == &other; + } + + bool operator==(const char* name) const + { + return mPart ? mPart->name.equals(name) : false; + } + + bool operator==(const String& name) const + { + return mPart ? mPart->name.equals(name) : false; + } + + /** + * @brief Obtain smallest allocation unit for erase operations + */ + size_t getBlockSize() const; + +protected: + Device* mDevice{nullptr}; + const Info* mPart{nullptr}; + +private: + bool allowRead(); + bool allowWrite(); +}; + +} // namespace Storage + +String toString(Storage::Partition::Type type, uint8_t subType); +String toLongString(Storage::Partition::Type type, uint8_t subType); diff --git a/Sming/Components/Storage/src/include/Storage/PartitionStream.h b/Sming/Components/Storage/src/include/Storage/PartitionStream.h new file mode 100644 index 0000000000..93debaeaff --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/PartitionStream.h @@ -0,0 +1,61 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * PartitionStream.h + * + ****/ + +#pragma once + +#include +#include "Partition.h" + +namespace Storage +{ +/** + * @brief Stream operating directory on a Storage partition + * + * To support write operations, the target region must be erased first. + * + * @ingroup stream + */ +class PartitionStream : public ReadWriteStream +{ +public: + PartitionStream(Partition partition, uint32_t offset, size_t size) + : partition(partition), startOffset(offset), size(size) + { + } + + PartitionStream(Partition partition) : partition(partition), startOffset(0), size(partition.size()) + { + } + + int available() override + { + return size - readPos; + } + + uint16_t readMemoryBlock(char* data, int bufSize) override; + + int seekFrom(int offset, SeekOrigin origin) override; + + size_t write(const uint8_t* buffer, size_t size) override; + + bool isFinished() override + { + return available() <= 0; + } + +private: + Partition partition; + uint32_t startOffset; + size_t size; + uint32_t writePos{0}; + uint32_t readPos{0}; +}; + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/PartitionTable.h b/Sming/Components/Storage/src/include/Storage/PartitionTable.h new file mode 100644 index 0000000000..1dd4ee7b16 --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/PartitionTable.h @@ -0,0 +1,92 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * PartitionTable.h + * + ****/ +#pragma once + +#include "Partition.h" +#include "Iterator.h" + +namespace Storage +{ +// Used Partition table entries cached in RAM, initialised on first request +class PartitionTable +{ +public: + PartitionTable(Device& device) : mDevice(device) + { + } + + /** + * @name Partition search + * @{ + * + * @brief Find partitions based on one or more parameters + * @param type Partition type + * @param subtype Partition sub-type + * @retval Iterator Forward-iterator for matching partitions + */ + Iterator find(Partition::Type type = Partition::Type::any, uint8_t subType = Partition::SubType::any) const + { + return Iterator(mDevice, type, subType); + } + + template Iterator find(T subType) const + { + return find(Partition::Type(T::partitionType), uint8_t(subType)); + } + + /** @} */ + + /** + * @brief Find partition by name + * @param Name to search for, case-sensitive + * @retval Partition + * + * Names are unique so at most only one match + */ + Partition find(const String& name) const + { + return *std::find(begin(), end(), name); + } + + Iterator begin() const + { + return Iterator(mDevice, 0); + } + + Iterator end() const + { + return Iterator(mDevice, mCount); + } + + uint8_t count() const + { + return mCount; + } + + Device& device() const + { + return mDevice; + } + + Partition operator[](unsigned index) const + { + return (index < mCount) ? Partition(mDevice, mEntries.get()[index]) : Partition(); + } + +protected: + friend Device; + void load(const esp_partition_info_t* entry, unsigned count); + + Device& mDevice; + std::unique_ptr mEntries; + uint8_t mCount{0}; +}; + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/ProgMem.h b/Sming/Components/Storage/src/include/Storage/ProgMem.h new file mode 100644 index 0000000000..d8758244d0 --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/ProgMem.h @@ -0,0 +1,89 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * ProgMem.h + * + ****/ +#pragma once + +#include "CustomDevice.h" + +namespace Storage +{ +/** + * @brief Storage device to access PROGMEM using flash API + */ +class ProgMem : public CustomDevice +{ +public: + String getName() const override + { + return F("ProgMem"); + } + + size_t getBlockSize() const override + { + return sizeof(uint32_t); + } + + size_t getSize() const override + { + return 0x80000000; + } + + Type getType() const override + { + return Type::flash; + } + + bool read(uint32_t address, void* dst, size_t size) override; + + bool write(uint32_t address, const void* src, size_t size) override + { + return false; + } + + bool erase_range(uint32_t address, size_t size) override + { + return false; + } + + using CustomDevice::createPartition; + + /** + * @brief Create partition for PROGMEM data access + * @param name Name for partition + * @param flashPtr PROGMEM pointer + * @param size Size of PROGMEM data + * @param type Partition type + * @param subtype Partition sub-type + * @retval Partition Invalid if data is not progmem + */ + Partition createPartition(const String& name, const void* flashPtr, size_t size, Partition::Type type, + uint8_t subtype); + + template Partition createPartition(const String& name, const void* flashPtr, size_t size, T subType) + { + return createPartition(name, flashPtr, size, Partition::Type(T::partitionType), uint8_t(subType)); + } + + /** + * @brief Create partition for FlashString data access + */ + Partition createPartition(const String& name, const FSTR::ObjectBase& fstr, Partition::Type type, uint8_t subtype) + { + return createPartition(name, fstr.data(), fstr.size(), type, subtype); + } + + template Partition createPartition(const String& name, const FSTR::ObjectBase& fstr, T subType) + { + return createPartition(name, fstr, Partition::Type(T::partitionType), uint8_t(subType)); + } +}; + +extern ProgMem progMem; + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/SpiFlash.h b/Sming/Components/Storage/src/include/Storage/SpiFlash.h new file mode 100644 index 0000000000..a5f0319362 --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/SpiFlash.h @@ -0,0 +1,38 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * SpiFlash.h + * + ****/ +#pragma once + +#include "Device.h" + +namespace Storage +{ +extern SpiFlash* spiFlash; + +/** + * @brief Main flash storage device + */ +class SpiFlash : public Device +{ +public: + String getName() const override; + size_t getBlockSize() const override; + size_t getSize() const override; + + Type getType() const override + { + return Type::flash; + } + + bool read(uint32_t address, void* dst, size_t size) override; + bool write(uint32_t address, const void* src, size_t size) override; + bool erase_range(uint32_t address, size_t size) override; +}; + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/StreamDevice.h b/Sming/Components/Storage/src/include/Storage/StreamDevice.h new file mode 100644 index 0000000000..18ca2d64cf --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/StreamDevice.h @@ -0,0 +1,67 @@ +/* + * StreamDevice.h + */ + +#include "CustomDevice.h" +#include + +namespace Storage +{ +/** + * @brief Read-only partition on a stream object + * @note Writes not possible as streams always append data, cannot do random writes + */ +class StreamDevice : public CustomDevice +{ +public: + StreamDevice(IDataSourceStream* stream, size_t size) : CustomDevice(nameOf(stream), size), mStream(stream) + { + } + + StreamDevice(IDataSourceStream* stream) : StreamDevice(stream, size_t(stream->available())) + { + } + + static String nameOf(IDataSourceStream* stream) + { + String s; + if(stream != nullptr) { + s = stream->getName(); + } + if(!s) { + s = F("stream_") + String(uint32_t(stream), HEX); + } + return s; + } + + Type getType() const override + { + return Type::stream; + } + + bool read(uint32_t address, void* buffer, size_t len) override + { + if(mStream == nullptr) { + return false; + } + if(mStream->seekFrom(address, SeekOrigin::Start) != int(address)) { + return false; + } + return mStream->readBytes(static_cast(buffer), len) == len; + } + + bool write(uint32_t address, const void* data, size_t len) override + { + return false; + } + + bool erase_range(uint32_t address, size_t len) override + { + return false; + } + +private: + std::unique_ptr mStream; +}; + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/SysMem.h b/Sming/Components/Storage/src/include/Storage/SysMem.h new file mode 100644 index 0000000000..9f49a3fa3e --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/SysMem.h @@ -0,0 +1,88 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * SysMem.h + * + ****/ + +#pragma once + +#include "CustomDevice.h" + +namespace Storage +{ +/** + * @brief Storage device to access system memory, e.g. RAM + */ +class SysMem : public CustomDevice +{ +public: + String getName() const override + { + return F("SysMem"); + } + + size_t getBlockSize() const override + { + return sizeof(uint32_t); + } + + size_t getSize() const override + { + return 0x80000000; + } + + Type getType() const override + { + return Type::sysmem; + } + + bool read(uint32_t address, void* buffer, size_t len) override + { + if(isFlashPtr(reinterpret_cast(address))) { + memcpy_P(buffer, reinterpret_cast(address), len); + } else { + memcpy(buffer, reinterpret_cast(address), len); + } + return true; + } + + bool write(uint32_t address, const void* data, size_t len) override + { + if(isFlashPtr(reinterpret_cast(address))) { + return false; + } + + memcpy(reinterpret_cast(address), data, len); + return true; + } + + bool erase_range(uint32_t address, size_t len) override + { + if(isFlashPtr(reinterpret_cast(address))) { + return false; + } + + memset(&address, 0xFF, len); + return true; + } + + using CustomDevice::createPartition; + + /** + * @brief Create partition for FlashString data access + */ + Partition createPartition(const String& name, const FSTR::ObjectBase& fstr, Partition::Type type, uint8_t subtype); + + template Partition createPartition(const String& name, const FSTR::ObjectBase& fstr, T subType) + { + return createPartition(name, fstr, Partition::Type(T::partitionType), uint8_t(subType)); + } +}; + +extern SysMem sysMem; + +} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/partition_info.h b/Sming/Components/Storage/src/include/Storage/partition_info.h new file mode 100644 index 0000000000..a4b54c7f62 --- /dev/null +++ b/Sming/Components/Storage/src/include/Storage/partition_info.h @@ -0,0 +1,32 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * partition_info.h + * + ****/ + +#include "Partition.h" + +namespace Storage +{ +/** + * @brief Internal structure describing the binary layout of a partition table entry. + */ +struct esp_partition_info_t { + uint16_t magic; ///< Fixed value to identify valid entry, appears as 0xFFFF at end of table + Partition::Type type; ///< Main type of partition + uint8_t subtype; ///< Sub-type for partition (interpretation dependent upon type) + uint32_t offset; ///< Start offset + uint32_t size; ///< Size of partition in bytes + Storage::Partition::Name name; ///< Unique identifer for entry + Storage::Partition::Flags flags; ///< Various option flags +}; + +constexpr uint16_t ESP_PARTITION_MAGIC{0x50AA}; ///< Identifies a valid partition +constexpr uint16_t ESP_PARTITION_MAGIC_MD5{0xEBEB}; ///< Identifies an MD5 hash block +constexpr size_t ESP_PARTITION_TABLE_MAX_LEN{0xC00}; // Maximum length of partition table data + +} // namespace Storage diff --git a/Sming/Components/esptool/README.rst b/Sming/Components/esptool/README.rst index df11b055fa..43acb7c806 100644 --- a/Sming/Components/esptool/README.rst +++ b/Sming/Components/esptool/README.rst @@ -8,18 +8,26 @@ Options .. envvar:: SPI_SPEED + [read-only] Set by :ref:`hardware_config`. + Clock speed for flash memory (20, 26, 40 or 80). Default is 40. .. envvar:: SPI_MODE - Flash memory operating mode (quot, dio, dout, qio). Default is qio. + [read-only] Set by :ref:`hardware_config`. + Flash memory operating mode (quot, dio, dout, qio). Default is qio. .. envvar:: SPI_SIZE + [read-only] Set by :ref:`hardware_config`. + Size of flash memory chip (256KB, 512KB, 1MB, 2MB, 4MB). Default is 512K bytes. + The default hardware profile ``standard`` sets this to 1MB. + You can set ``HWCONFIG=standard-4m`` to increase it or create a custom :ref:`hardware_config` for your project. + .. envvar:: ESPTOOL diff --git a/Sming/Components/esptool/component.mk b/Sming/Components/esptool/component.mk index c2f8d6d22c..2a4cf0bbc0 100644 --- a/Sming/Components/esptool/component.mk +++ b/Sming/Components/esptool/component.mk @@ -1,44 +1,11 @@ COMPONENT_LIBNAME := -CONFIG_VARS += SPI_SPEED SPI_MODE SPI_SIZE +DEBUG_VARS += SPI_SPEED SPI_MODE SPI_SIZE +SPI_SPEED = $(STORAGE_DEVICE_spiFlash_SPEED) +SPI_MODE = $(STORAGE_DEVICE_spiFlash_MODE) +SPI_SIZE = $(STORAGE_DEVICE_spiFlash_SIZE) -# SPI_SPEED = 40, 26, 20, 80 -SPI_SPEED ?= 40 -# SPI_MODE: qio, qout, dio, dout -SPI_MODE ?= dio -# SPI_SIZE: 512K, 256K, 1M, 2M, 4M -SPI_SIZE ?= 1M - -ifeq ($(SPI_SPEED), 26) - flashimageoptions := -ff 26m -else ifeq ($(SPI_SPEED), 20) - flashimageoptions := -ff 20m -else ifeq ($(SPI_SPEED), 80) - flashimageoptions := -ff 80m -else - flashimageoptions := -ff 40m -endif - -ifeq ($(SPI_MODE), qout) - flashimageoptions += -fm qout -else ifeq ($(SPI_MODE), dio) - flashimageoptions += -fm dio -else ifeq ($(SPI_MODE), dout) - flashimageoptions += -fm dout -else - flashimageoptions += -fm qio -endif - -# Calculate parameters from SPI_SIZE value (esptool will check validity) -ifeq ($(SPI_SIZE),detect) -flashimageoptions += -fs detect -else -flashimageoptions += -fs $(SPI_SIZE)B -endif -FLASH_SIZE := $(subst M,*1024K,$(SPI_SIZE)) -FLASH_SIZE := $(subst K,*1024,$(FLASH_SIZE)) -FlashOffset = $$(($(FLASH_SIZE)-$1)) -BLANK_BIN := $(COMPONENT_PATH)/blank.bin +flashimageoptions += -fs $(SPI_SIZE)B -ff $(SPI_SPEED)m -fm $(SPI_MODE) # Default COM port and speed used for flashing CACHE_VARS += COM_PORT_ESPTOOL COM_SPEED_ESPTOOL @@ -74,6 +41,8 @@ else ESPTOOL_EXECUTE = $(ESPTOOL_CMDLINE) $1 endif +comma := , + # Write file contents to Flash # $1 -> List of `Offset=File` chunks define WriteFlash @@ -83,6 +52,21 @@ define WriteFlash ) endef +# Read flash memory into file +# $1 -> `Offset,Size` chunk +# $2 -> Output filename +define ReadFlash + $(info ReadFlash $1,$2) + $(call ESPTOOL_EXECUTE,read_flash $(subst $(comma), ,$1) $2) +endef + +# Erase a region of Flash +# $1 -> Offset,Size +define EraseFlashRegion + $(info EraseFlashRegion $1) + $(call ESPTOOL_EXECUTE,erase_region $(subst $(comma), ,$1)) +endef + # Erase flash memory contents define EraseFlash $(call ESPTOOL_EXECUTE,erase_flash) diff --git a/Sming/Components/rboot/.patches/rboot.patch b/Sming/Components/rboot/.patches/rboot.patch index ee164e45ac..b8cd1ce078 100644 --- a/Sming/Components/rboot/.patches/rboot.patch +++ b/Sming/Components/rboot/.patches/rboot.patch @@ -1,5 +1,5 @@ diff --git a/Makefile b/Makefile -index 0b43474..5176f8f 100644 +index 638a8f7..5176f8f 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,19 @@ endif @@ -22,17 +22,7 @@ index 0b43474..5176f8f 100644 ifneq ($(RBOOT_EXTRA_INCDIR),) CFLAGS += $(addprefix -I,$(RBOOT_EXTRA_INCDIR)) endif -@@ -75,6 +88,10 @@ else ifeq ($(SPI_SIZE), 2Mb) - E2_OPTS += -2048b - else ifeq ($(SPI_SIZE), 4M) - E2_OPTS += -4096 -+else ifeq ($(SPI_SIZE), 8M) -+ E2_OPTS += -8192 -+else ifeq ($(SPI_SIZE), 16M) -+ E2_OPTS += -16384 - endif - ifeq ($(SPI_MODE), qio) - E2_OPTS += -qio + diff --git a/rboot.c b/rboot.c index d622f97..5e254fa 100644 --- a/rboot.c @@ -309,7 +299,7 @@ index d622f97..5e254fa 100644 ets_memcpy((void*)_text_addr, _text_data, _text_len); // return address to load from diff --git a/appcode/rboot-api.c b/appcode/rboot-api.c -index eb4d028..490a763 100644 +index eb4d028..bf276a7 100644 --- a/appcode/rboot-api.c +++ b/appcode/rboot-api.c @@ -9,7 +9,7 @@ @@ -356,6 +346,15 @@ index eb4d028..490a763 100644 return true; } +@@ -103,7 +103,7 @@ bool ICACHE_FLASH_ATTR rboot_write_end(rboot_write_status *status) { + + // function to do the actual writing to flash + // call repeatedly with more data (max len per write is the flash sector size (4k)) +-bool ICACHE_FLASH_ATTR rboot_write_flash(rboot_write_status *status, uint8_t *data, uint16_t len) { ++bool ICACHE_FLASH_ATTR rboot_write_flash(rboot_write_status *status, const uint8_t *data, uint16_t len) { + + bool ret = false; + uint8_t *buffer; @@ -114,7 +114,7 @@ bool ICACHE_FLASH_ATTR rboot_write_flash(rboot_write_status *status, uint8_t *da } @@ -387,3 +386,17 @@ index eb4d028..490a763 100644 return ret; } +diff --git a/appcode/rboot-api.h b/appcode/rboot-api.h +index a98c209..ec4f0e5 100644 +--- a/appcode/rboot-api.h ++++ b/appcode/rboot-api.h +@@ -93,7 +93,7 @@ bool ICACHE_FLASH_ATTR rboot_write_end(rboot_write_status *status); + * of OTA data is received over the network. + * @note Call rboot_write_init before calling this function to get the rboot_write_status structure + */ +-bool ICACHE_FLASH_ATTR rboot_write_flash(rboot_write_status *status, uint8_t *data, uint16_t len); ++bool ICACHE_FLASH_ATTR rboot_write_flash(rboot_write_status *status, const uint8_t *data, uint16_t len); + + #ifdef BOOT_RTC_ENABLED + /** @brief Get rBoot status/control data from RTC data area + diff --git a/Sming/Components/rboot/README.rst b/Sming/Components/rboot/README.rst index df119b1787..50cd239c2a 100644 --- a/Sming/Components/rboot/README.rst +++ b/Sming/Components/rboot/README.rst @@ -7,7 +7,12 @@ Introduction rBoot is a second-stage bootloader that allows booting application images from several pre-configured flash memory addresses, called "slots". Sming supports up to three slots. -Sming uses rBoot exclusively because of its flexibility, reliability and ease of use. +.. note:: + + With Sming 4.3 partitions are used to manage flash storage. + A "slot" refers to a specific application partition, namely ``rom0``, ``rom1`` or ``rom2``. + + The location or size of these partitions is determined by the :ref:`hardware_config`. .. attention:: @@ -19,38 +24,40 @@ Sming uses rBoot exclusively because of its flexibility, reliability and ease of Slot 0 ------ -This is the default slot which is always used. +This is the default slot (``rom0``, the primary application partition) which is always used. .. envvar:: RBOOT_ROM0_ADDR - default: 0x2000. + [read-only] - This is the start address for slot 0. The default is sector 3, immediately after rBoot and its configuration data. + This is the start address for slot 0. - Except for the use case described in `Slot2`_ below, you should not need to set this value. + Except for the use case described in `Slot2`_ below, you should not need to change this. Slot 1 ------ .. envvar:: RBOOT_ROM1_ADDR - default: disabled + [read-only] default: disabled - The start address of slot 1. If you don't need it, leave unconfigured (empty). + The start address of slot 1. If your application includes any kind of Over-the-Air (OTA) firmware update functionality, you will need a second memory slot to store the received update image while the update routines execute from the first slot. +.. note:: + + The ``spiffs-two-roms`` configuration can be used for this purpose. + Upon successful completion of the update, the second slot is activated, such that on next reset rBoot boots into the uploaded application. While now running from slot 1, the next update will be stored to slot 0 again, i.e. the roles of slot 0 and slot 1 are flipped with every update. -For devices with more than 1MB of flash memory, it is advisable to choose an address with the same -offset within its 1MB block as :envvar:`RBOOT_ROM0_ADDR`, e.g. 0x102000 for the default -slot 0 address (0x2000). This way, the same application image can be used for both -slots. See :ref:`single_vs_dual` for further details. +For devices with more than 1MB of flash memory, it is advisable to choose an address +for ``rom1`` with the same offset within its 1MB block as ``rom0``. .. _Slot2: @@ -69,23 +76,61 @@ To enable slot 2, set these values: .. envvar:: RBOOT_ROM2_ADDR - Address for slot 2 + [read-only] + + Address for slot 2. You must create a custom :ref:`hardware_config` for your project + with a definition for ``rom2``. + + .. code-block:: json + + { + ... + "partitions": { + "rom2": { + "address": "0x100000", + "size": "512K", + "type": "app", + "subtype": "ota_1" + } + } + } + +.. note:: + + At present, this will only configure rBoot. + Sming will not create an application image for slot 2. + + You can, however, use a second Sming project to build a recovery application image as follows: + + - Create a new Sming project for your recovery application. This will be a simple + single-slot project. Create a new :ref:`hardware_config` and configure the + ``rom0`` start address and size to the same as the ``rom2`` partition of the main project. + + option (a) + + - Build and flash the recovery project as usual by typing ``make flash``. This will + install the recovery ROM (into slot 2 of the main project) and a temporary + bootloader, which will be overwritten in the next step. + + - Go back to your main project. Build and flash it with ``make flash``. This will + install the main application (into slot 0) and the final bootloader. You are + now all set for booting into the recovery image if the need arises. + + option (b) + + - Run a normal ``make`` for your recovery project + + - Locate the firmware image file, typically ``out/Esp8266/release/firmware/rom0.bin`` + (adjust accordingly if using a debug build). + Copy this image file as ``rom2.bin`` into your main project directory. -Note that this will only configure rBoot. -Sming will not create an application image for slot 2. -You can, however, use a second Sming project to build a recovery application image as follows: + - Add an additional property to the ``rom2`` partition entry in your main project: -1. Create a new Sming project for your recovery application. This will be a simple - single-slot project. Set :envvar:`RBOOT_ROM0_ADDR` of the recovery project to the value - of :envvar:`RBOOT_ROM2_ADDR` of the main project. + .. code-block:: json -2. Build and flash the recovery project as usual by typing ``make flash``. This will - install the recovery ROM (into slot 2 of the main project) and a temporary - bootloader, which will be overwritten in the next step. + "filename": "rom2.bin" -3. Go back to your main project. Build and flash it with ``make flash``. This will - install the main application (into slot 0) and the final bootloader. You are - now all set for booting into the recovery image if the need arises. + When you run ``make flash`` in this will get written along with the other partitions. Automatically derived settings ------------------------------ @@ -122,8 +167,9 @@ the same address offsets within their 1MB blocks, i.e. ``(RBOOT_ROM0_ADDR & 0xFF Consequently, for such configurations, the Sming build system generates only one ROM image. In all other cases, two distinct application images must be linked with different addresses -for the 'irom0_0_seg' memory region. The Sming build system takes care of all the details, -including linker script generation. +for the 'irom0_0_seg' memory region. +You should use the ``two-rom-mode`` :ref:`hardware_config` for this. +The Sming build system will handle everything else, including linker script generation. .. envvar:: RBOOT_TWO_ROMS diff --git a/Sming/Components/rboot/appcode/rboot-overrides.c b/Sming/Components/rboot/appcode/rboot-overrides.c deleted file mode 100644 index 04132ad0cf..0000000000 --- a/Sming/Components/rboot/appcode/rboot-overrides.c +++ /dev/null @@ -1,43 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/SmingHub/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * rboot-overrides.c - * - ****/ - -#include "spiffs_sming.h" -#include - -/* - * rBoot uses different spiffs organization and we need to override this method - * during application compile time in order to make automatic - * mounting with `spiffs_mount()` work as expected. - */ -spiffs_config spiffs_get_storage_config() -{ - spiffs_config cfg = {0}; - u32_t max_allowed_sector, requested_sector; - -#ifdef RBOOT_SPIFFS_0 - cfg.phys_addr = RBOOT_SPIFFS_0; -#elif RBOOT_SPIFFS_1 - cfg.phys_addr = RBOOT_SPIFFS_1; -#else -#error "Define either RBOOT_SPIFFS_0 or RBOOT_SPIFFS_1" -#endif - - cfg.phys_addr &= 0xFFFFF000; // get the start address of the sector - - max_allowed_sector = flashmem_get_sector_of_address(INTERNAL_FLASH_SIZE - 1); - requested_sector = flashmem_get_sector_of_address((cfg.phys_addr + SPIFF_SIZE) - 1); - if(requested_sector > max_allowed_sector) { - debug_w("The requested SPIFFS size is too big."); - requested_sector = max_allowed_sector; - } - // get the max size until the sector end - cfg.phys_size = ((requested_sector + 1) * INTERNAL_FLASH_SECTOR_SIZE) - cfg.phys_addr; - return cfg; -} diff --git a/Sming/Components/rboot/component.mk b/Sming/Components/rboot/component.mk index 7cd47cbdeb..0d7617b94e 100644 --- a/Sming/Components/rboot/component.mk +++ b/Sming/Components/rboot/component.mk @@ -7,8 +7,8 @@ RBOOT_EMULATION := 1 endif COMPONENT_SUBMODULES := rboot -COMPONENT_INCDIRS := rboot appcode rboot/appcode include -COMPONENT_SRCDIRS := src +COMPONENT_INCDIRS := rboot rboot/appcode include +COMPONENT_SRCDIRS := src src/Arch/$(SMING_ARCH) RBOOT_DIR := $(COMPONENT_PATH) @@ -35,23 +35,22 @@ $(error Cannot enable RBOOT_GPIO_ENABLED and RBOOT_GPIO_SKIP_ENABLED at the same endif ### ROM Addresses ### -# Make sure that your ROM slots and SPIFFS slot(s) do not overlap! -CONFIG_VARS += RBOOT_ROM0_ADDR RBOOT_ROM1_ADDR RBOOT_ROM2_ADDR +DEBUG_VARS += RBOOT_ROM0_ADDR RBOOT_ROM1_ADDR RBOOT_ROM2_ADDR -# Loation of first ROM (default is sector 2 after rboot and rboot config sector) -RBOOT_ROM0_ADDR ?= 0x002000 +# Location of first ROM +RBOOT_ROM0_ADDR := $(PARTITION_rom0_ADDRESS) # The parameter below specifies the location of the second ROM. # You need a second slot for any kind of firmware update mechanism. # Leave empty if you don't need a second ROM slot. -RBOOT_ROM1_ADDR ?= +RBOOT_ROM1_ADDR := $(PARTITION_rom1_ADDRESS) # The parameter below specifies the location of the GPIO ROM. # It is only used when RBOOT_GPIO_ENABLED = 1 # Note that setting this parameter will only configure rboot. # The Sming build system does not create a ROM for this slot. -RBOOT_ROM2_ADDR ?= +RBOOT_ROM2_ADDR := $(PARTITION_rom2_ADDRESS) ifeq ($(RBOOT_GPIO_ENABLED),0) ifneq ($(RBOOT_ROM2_ADDR),) @@ -86,15 +85,6 @@ RBOOT_LD_TEMPLATE ?= $(RBOOT_DIR)/rboot.rom0.ld RBOOT_LD_0 := $(BUILD_BASE)/$(RBOOT_ROM_0).ld RBOOT_LD_1 := $(BUILD_BASE)/$(RBOOT_ROM_1).ld -# -CONFIG_VARS += RBOOT_SPIFFS_0 RBOOT_SPIFFS_1 -RBOOT_SPIFFS_0 ?= 0x100000 -RBOOT_SPIFFS_1 ?= 0x300000 -APP_CFLAGS += -DRBOOT_SPIFFS_0=$(RBOOT_SPIFFS_0) -APP_CFLAGS += -DRBOOT_SPIFFS_1=$(RBOOT_SPIFFS_1) - -SPIFF_START_ADDR ?= $(RBOOT_SPIFFS_0) - # filenames and options for generating rBoot rom images with esptool2 RBOOT_E2_SECTS ?= .text .text1 .data .rodata RBOOT_E2_USER_ARGS ?= -quiet -bin -boot2 @@ -103,7 +93,7 @@ RBOOT_ROM_0_BIN := $(FW_BASE)/$(RBOOT_ROM_0).bin RBOOT_ROM_1_BIN := $(FW_BASE)/$(RBOOT_ROM_1).bin -COMPONENT_APPCODE := appcode rboot/appcode $(if $(RBOOT_EMULATION),host) +COMPONENT_APPCODE := rboot/appcode APP_CFLAGS += -DRBOOT_INTEGRATION # these are exported for use by the rBoot Makefile @@ -133,11 +123,19 @@ ifeq ($(RBOOT_GPIO_SKIP_ENABLED),1) APP_CFLAGS += -DBOOT_GPIO_SKIP_ENABLED endif -ifndef RBOOT_EMULATION +COMPONENT_CXXFLAGS += \ + -DRBOOT_ROM0_ADDR=$(RBOOT_ROM0_ADDR) \ + -DRBOOT_ROM1_ADDR=$(RBOOT_ROM1_ADDR) + +ifdef RBOOT_EMULATION +FLASH_BOOT_CHUNKS = 0x00000=$(BLANK_BIN) +else +export RBOOT_ROM0_ADDR +export RBOOT_ROM1_ADDR RBOOT_BIN := $(FW_BASE)/rboot.bin CUSTOM_TARGETS += $(RBOOT_BIN) $(RBOOT_BIN): - $(Q) $(MAKE) -C $(RBOOT_DIR)/rboot + $(Q) $(MAKE) -C $(RBOOT_DIR)/rboot $(RBOOT_CFLAGS) EXTRA_LDFLAGS := -u Cache_Read_Enable_New @@ -151,14 +149,9 @@ endef LIBMAIN_COMMANDS += $(RBOOT_LIBMAIN_COMMANDS) endif -endif # RBOOT_EMULATION - # Define our flash chunks -FLASH_RBOOT_BOOT_CHUNKS := 0x00000=$(RBOOT_BIN) -FLASH_RBOOT_APP_CHUNKS := $(RBOOT_ROM0_ADDR)=$(RBOOT_ROM_0_BIN) -FLASH_RBOOT_ERASE_CONFIG_CHUNKS := 0x01000=$(SDK_BASE)/bin/blank.bin - -ifndef RBOOT_EMULATION +FLASH_BOOT_CHUNKS := 0x00000=$(RBOOT_BIN) +FLASH_RBOOT_ERASE_CONFIG_CHUNKS := 0x01000=$(SDK_BASE)/bin/blank.bin # => Automatic linker script generation from template # $1 -> application target diff --git a/Sming/Components/rboot/include/Network/RbootHttpUpdater.h b/Sming/Components/rboot/include/Network/RbootHttpUpdater.h index 3a6ac65c46..c7ed16a43c 100644 --- a/Sming/Components/rboot/include/Network/RbootHttpUpdater.h +++ b/Sming/Components/rboot/include/Network/RbootHttpUpdater.h @@ -39,7 +39,7 @@ class RbootHttpUpdater : protected HttpClient cleanup(); } - bool addItem(int offset, const String& firmwareFileUrl, size_t maxSize = 0); + bool addItem(uint32_t offset, const String& firmwareFileUrl, size_t maxSize = 0); bool addItem(const String& firmwareFileUrl, RbootOutputStream* stream = nullptr); void start(); diff --git a/Sming/Components/rboot/appcode/rboot-integration.h b/Sming/Components/rboot/include/rboot-integration.h similarity index 100% rename from Sming/Components/rboot/appcode/rboot-integration.h rename to Sming/Components/rboot/include/rboot-integration.h diff --git a/Sming/Components/rboot/rboot b/Sming/Components/rboot/rboot index 24f6021da8..614f33685d 160000 --- a/Sming/Components/rboot/rboot +++ b/Sming/Components/rboot/rboot @@ -1 +1 @@ -Subproject commit 24f6021da82b303262a939c955f6a984bce6208f +Subproject commit 614f33685d0dd990fc4202f2409b0d2365eeaef3 diff --git a/Sming/Components/rboot/host/rboot.cpp b/Sming/Components/rboot/src/Arch/Host/rboot.cpp similarity index 89% rename from Sming/Components/rboot/host/rboot.cpp rename to Sming/Components/rboot/src/Arch/Host/rboot.cpp index 755d942c8d..96c5719d88 100644 --- a/Sming/Components/rboot/host/rboot.cpp +++ b/Sming/Components/rboot/src/Arch/Host/rboot.cpp @@ -40,12 +40,12 @@ void host_init_bootloader() romconf.magic = BOOT_CONFIG_MAGIC; romconf.version = BOOT_CONFIG_VERSION; romconf.count = 2; - romconf.roms[0] = SECTOR_SIZE * (BOOT_CONFIG_SECTOR + 1); + romconf.roms[0] = RBOOT_ROM0_ADDR; #ifdef BOOT_ROM1_ADDR - romconf.roms[1] = BOOT_ROM1_ADDR; + romconf.roms[1] = RBOOT_ROM1_ADDR; #else size_t flashsize = flashmem_get_size_bytes(); - romconf.roms[1] = (flashsize / 2) + (SECTOR_SIZE * (BOOT_CONFIG_SECTOR + 1)); + romconf.roms[1] = RBOOT_ROM0_ADDR + (flashsize / 2); #endif bool ok = rboot_set_config(&romconf); hostmsg("Write default config: %s", ok ? "OK" : "FAIL"); diff --git a/Sming/Components/rboot/src/RbootHttpUpdater.cpp b/Sming/Components/rboot/src/RbootHttpUpdater.cpp index 077d9697da..07d28bf31d 100644 --- a/Sming/Components/rboot/src/RbootHttpUpdater.cpp +++ b/Sming/Components/rboot/src/RbootHttpUpdater.cpp @@ -15,7 +15,7 @@ #include -bool RbootHttpUpdater::addItem(int offset, const String& firmwareFileUrl, size_t maxSize) +bool RbootHttpUpdater::addItem(uint32_t offset, const String& firmwareFileUrl, size_t maxSize) { RbootHttpUpdaterItem add; add.targetOffset = offset; diff --git a/Sming/Components/rboot/src/RbootOutputStream.cpp b/Sming/Components/rboot/src/RbootOutputStream.cpp index 5fea78dff2..e7b71e4eb5 100644 --- a/Sming/Components/rboot/src/RbootOutputStream.cpp +++ b/Sming/Components/rboot/src/RbootOutputStream.cpp @@ -33,12 +33,12 @@ size_t RbootOutputStream::write(const uint8_t* data, size_t size) initialized = true; } - if(maxLength && (written + size > maxLength)) { + if(maxLength != 0 && (written + size > maxLength)) { debug_e("The ROM size is bigger than the maximum allowed"); return 0; } - if(!rboot_write_flash(&rBootWriteStatus, (uint8_t*)data, size)) { + if(!rboot_write_flash(&rBootWriteStatus, data, size)) { debug_e("rboot_write_flash: Failed. Size: %d", size); return 0; } diff --git a/Sming/Components/spiffs/README.rst b/Sming/Components/spiffs/README.rst index 7f2792eb57..13e5852abc 100644 --- a/Sming/Components/spiffs/README.rst +++ b/Sming/Components/spiffs/README.rst @@ -1,36 +1,47 @@ SPIFFS for Sming ================ -This Component provides additional functionality to support SPIFFS running on both Esp8266 and Host architectures. +This Component provides SPIFFS filesystem support for all architectures. -.. envvar:: DISABLE_SPIFFS +A single SPIFFS partition is defined using :envvar:`HWCONFIG` ``=spiffs``, which supports these build variables: - 0 (default) - Enable filesystem generation - - 1 - Disable filesystem generation + .. envvar:: DISABLE_SPIFFS - The value is also #defined for application use. + [deprecated and removed] - Note this doesn't actually disable SPIFFS support in the application! + This value is no longer supported. Please remove it from your project's component.mk file. -.. envvar:: SPIFF_SIZE + .. envvar:: SPIFF_SIZE - Size (in bytes) of the SPIFFS area in Flash memory. + [deprecated and removed] + Size (in bytes) of the SPIFFS area in Flash memory. To change this, edit the :ref:`hardware_config`. -.. envvar:: SPIFF_FILES + .. envvar:: SPIFF_FILES - default: ``files`` + .. envvar:: SPIFF_FILES - The SPIFFS image is built using files from this directory. + default: ``files`` + The SPIFFS image is built using files from this directory, which must exist or the build will fail. -.. envvar:: SPIFF_BIN + If you set this to an empty value, then an empty filesystem will be created. + + + .. envvar:: SPIFF_BIN + + Filename to use for the generated SPIFFS filesystem image. The default is ``spiff_rom``. + + + .. envvar:: SPIFF_BIN_OUT + + [read-only] Shows the full path to the generated image file. + + +For more control over the SPIFFS partition you can create your own partition definition in a +custom :ref:`hardware_config`. - Path to the generated SPIFFS filesystem image. .. envvar:: SPIFF_FILEDESC_COUNT diff --git a/Sming/Components/spiffs/blankfs.bin b/Sming/Components/spiffs/blankfs.bin deleted file mode 100644 index 171fa2bbce..0000000000 --- a/Sming/Components/spiffs/blankfs.bin +++ /dev/null @@ -1 +0,0 @@ -ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ \ No newline at end of file diff --git a/Sming/Components/spiffs/component.mk b/Sming/Components/spiffs/component.mk index e9019a6382..f82bfa9608 100644 --- a/Sming/Components/spiffs/component.mk +++ b/Sming/Components/spiffs/component.mk @@ -14,31 +14,14 @@ $(COMPONENT_RULE)$(SPIFFY): ## Application -# This controls filesystem generation, it doesn't actually disable SPIFFS support in the application -CONFIG_VARS += DISABLE_SPIFFS -DISABLE_SPIFFS ?= 0 -APP_CFLAGS += -DDISABLE_SPIFFS=$(DISABLE_SPIFFS) +ifdef DISABLE_SPIFFS +$(error DISABLE_SPIFFS is no longer supported; please remove this option from your component.mk file) +endif CACHE_VARS += SPIFF_FILES SPIFF_BIN SPIFF_FILES ?= files SPIFF_BIN ?= spiff_rom SPIFF_BIN_OUT := $(FW_BASE)/$(SPIFF_BIN).bin -CUSTOM_TARGETS += $(SPIFF_BIN_OUT) - -CONFIG_VARS += SPIFF_SIZE -ifeq ($(SPI_SIZE), 256K) - SPIFF_SIZE ?= 131072 #128K -else ifeq ($(SPI_SIZE), 1M) - SPIFF_SIZE ?= 524288 #512K -else ifeq ($(SPI_SIZE), 2M) - SPIFF_SIZE ?= 524288 #512K -else ifeq ($(SPI_SIZE), 4M) - SPIFF_SIZE ?= 524288 #512K -else - SPIFF_SIZE ?= 196608 #192K -endif -APP_CFLAGS += -DSPIFF_SIZE=$(SPIFF_SIZE) - COMPONENT_RELINK_VARS += SPIFF_FILEDESC_COUNT SPIFF_FILEDESC_COUNT ?= 7 @@ -46,57 +29,20 @@ COMPONENT_CFLAGS += -DSPIFF_FILEDESC_COUNT=$(SPIFF_FILEDESC_COUNT) COMPONENT_CFLAGS += -Wno-tautological-compare - -ifeq ($(SMING_ARCH),Esp32) -SPIFF_START_ADDR ?= 0x200000 -APP_CFLAGS += -DSPIFF_SIZE=$(SPIFF_SIZE) -DSPIFF_START_ADDR=$(SPIFF_START_ADDR) -endif - ##@Building .PHONY: spiffs-image-update spiffs-image-update: spiffs-image-clean $(SPIFF_BIN_OUT) ##Rebuild the SPIFFS filesystem image -##@Cleaning - -.PHONY: spiffs-image-clean -spiffs-image-clean: ##Remove SPIFFS image file - $(info Cleaning $(SPIFF_BIN_OUT)) - $(Q) rm -f $(SPIFF_BIN_OUT) - -# Generating spiffs_bin -$(SPIFF_BIN_OUT): $(SPIFFY) -ifeq ($(DISABLE_SPIFFS), 1) - $(info (!) Spiffs support disabled. Remove 'DISABLE_SPIFFS' make argument to enable spiffs.) -else - $(Q) mkdir -p $(dir $(SPIFF_BIN_OUT)) - $(info Checking for spiffs files) - $(Q) if [ -d "$(SPIFF_FILES)" ]; then \ - echo "$(SPIFF_FILES) directory exists. Creating $(SPIFF_BIN_OUT)"; \ - $(SPIFFY) $(SPIFF_SIZE) $(SPIFF_FILES) $(SPIFF_BIN_OUT); \ - else \ - echo "No files found in ./$(SPIFF_FILES)."; \ - echo "Creating empty $(SPIFF_BIN_OUT)"; \ - $(SPIFFY) $(SPIFF_SIZE) dummy.dir $(SPIFF_BIN_OUT); \ - fi +# Target invoked via partition table +ifneq (,$(filter spiffsgen,$(MAKECMDGOALS))) +PART_TARGET := $(PARTITION_$(PART)_FILENAME) +$(eval PART_FILES := $(call HwExpr,part.build['files'])) +SPIFFY_ARGS := $(PARTITION_$(PART)_SIZE_BYTES) "$(or $(PART_FILES),dummy.dir)" +.PHONY: spiffsgen +spiffsgen: $(SPIFFY) +ifneq (,$(PART_TARGET)) + $(Q) mkdir -p $(dir $(PART_TARGET)) + $(Q) $(SPIFFY) $(SPIFFY_ARGS) $(PART_TARGET) endif - -##@Flashing - -BLANKFS_BIN := $(COMPONENT_PATH)/blankfs.bin - -# If enabled, add the SPIFFS image to the chunks to write -ifeq ($(DISABLE_SPIFFS), 1) -FLASH_SPIFFS_CHUNKS := -else -FLASH_SPIFFS_CHUNKS := $(SPIFF_START_ADDR)=$(SPIFF_BIN_OUT) -FLASH_INIT_CHUNKS += $(SPIFF_START_ADDR)=$(BLANKFS_BIN) -endif - -.PHONY: flashfs -flashfs: $(SPIFF_BIN_OUT) ##Write just the SPIFFS filesystem image -ifeq ($(DISABLE_SPIFFS), 1) - $(info SPIFFS image creation disabled!) -else - $(call WriteFlash,$(FLASH_SPIFFS_CHUNKS)) endif diff --git a/Sming/Components/spiffs/spiffs_config.h b/Sming/Components/spiffs/spiffs_config.h index fe7c0b6dd7..1cd0560bcc 100644 --- a/Sming/Components/spiffs/spiffs_config.h +++ b/Sming/Components/spiffs/spiffs_config.h @@ -181,9 +181,7 @@ #endif // Enable this if you want the HAL callbacks to be called with the spiffs struct -#ifndef SPIFFS_HAL_CALLBACK_EXTRA -#define SPIFFS_HAL_CALLBACK_EXTRA 0 -#endif +#define SPIFFS_HAL_CALLBACK_EXTRA 1 // Enable this if you want to add an integer offset to all file handles // (spiffs_file). This is useful if running multiple instances of spiffs on diff --git a/Sming/Components/spiffs/spiffs_sming.cpp b/Sming/Components/spiffs/spiffs_sming.cpp index 0168d93b2b..b138e3517e 100644 --- a/Sming/Components/spiffs/spiffs_sming.cpp +++ b/Sming/Components/spiffs/spiffs_sming.cpp @@ -10,10 +10,13 @@ #include "spiffs_sming.h" #include +#include extern "C" { #include "spiffs/src/spiffs_nucleus.h" } +#define LOG_PAGE_SIZE 256 + spiffs _filesystemStorageHandle; #ifndef SPIFF_FILEDESC_COUNT @@ -26,32 +29,51 @@ uint16_t spiffs_work_buf[LOG_PAGE_SIZE]; spiffs_fd spiffs_fds[SPIFF_FILEDESC_COUNT]; uint32_t spiffs_cache_buf[LOG_PAGE_SIZE + 32]; -s32_t api_spiffs_read(u32_t addr, u32_t size, u8_t* dst) +#define GET_DEVICE() \ + if(fs == nullptr || fs->user_data == nullptr) { \ + debug_e("[SPIFFS] NO DEVICE"); \ + return 0; \ + } \ + auto device = static_cast(fs->user_data); + +s32_t api_spiffs_read(struct spiffs_t* fs, u32_t addr, u32_t size, u8_t* dst) { - return (flashmem_read(dst, addr, size) == size) ? SPIFFS_OK : SPIFFS_ERR_INTERNAL; + GET_DEVICE(); + return device->read(addr, dst, size) ? SPIFFS_OK : SPIFFS_ERR_INTERNAL; } -s32_t api_spiffs_write(u32_t addr, u32_t size, u8_t* src) +s32_t api_spiffs_write(struct spiffs_t* fs, u32_t addr, u32_t size, u8_t* src) { //debugf("api_spiffs_write"); - return (flashmem_write(src, addr, size) == size) ? SPIFFS_OK : SPIFFS_ERR_INTERNAL; + GET_DEVICE(); + return device->write(addr, src, size) ? SPIFFS_OK : SPIFFS_ERR_INTERNAL; } -s32_t api_spiffs_erase(u32_t addr, u32_t size) +s32_t api_spiffs_erase(struct spiffs_t* fs, u32_t addr, u32_t size) { - debugf("api_spiffs_erase"); - uint32_t sect_first = flashmem_get_sector_of_address(addr); - uint32_t sect_last = sect_first; - while(sect_first <= sect_last) { - if(!flashmem_erase_sector(sect_first++)) { - return SPIFFS_ERR_INTERNAL; - } - } - return SPIFFS_OK; + debugf("api_spiffs_erase(0x%08x, 0x%08x)", addr, size); + GET_DEVICE(); + return device->erase_range(addr, size) ? SPIFFS_OK : SPIFFS_ERR_INTERNAL; } -bool tryMount(const spiffs_config& cfg) +spiffs_config initConfig(Storage::Partition& partition) { + _filesystemStorageHandle.user_data = partition.getDevice(); + return spiffs_config{ + .hal_read_f = api_spiffs_read, + .hal_write_f = api_spiffs_write, + .hal_erase_f = api_spiffs_erase, + .phys_size = partition.size(), + .phys_addr = partition.address(), + .phys_erase_block = INTERNAL_FLASH_SECTOR_SIZE, + .log_block_size = INTERNAL_FLASH_SECTOR_SIZE * 2, + .log_page_size = LOG_PAGE_SIZE, + }; +} + +bool tryMount(Storage::Partition& partition) +{ + auto cfg = initConfig(partition); int res = SPIFFS_mount(&_filesystemStorageHandle, const_cast(&cfg), reinterpret_cast(spiffs_work_buf), reinterpret_cast(spiffs_fds), sizeof(spiffs_fds), spiffs_cache_buf, sizeof(spiffs_cache_buf), nullptr); @@ -60,10 +82,10 @@ bool tryMount(const spiffs_config& cfg) return res >= 0; } -bool spiffs_format_internal(const spiffs_config& cfg) +bool spiffs_format_internal(Storage::Partition& partition) { spiffs_unmount(); - if(tryMount(cfg)) { + if(tryMount(partition)) { spiffs_unmount(); } @@ -71,22 +93,23 @@ bool spiffs_format_internal(const spiffs_config& cfg) return res >= 0; } -bool spiffs_mount_internal(const spiffs_config& cfg) +bool spiffs_mount_internal(Storage::Partition& partition) { + auto cfg = initConfig(partition); debugf("fs.start: size:%u Kb, offset:0x%X\n", cfg.phys_size / 1024U, cfg.phys_addr); // Simple check of the erase count to see if flash looks like it's already been formatted - spiffs_obj_id dat; - flashmem_read(&dat, cfg.phys_addr + cfg.log_page_size - sizeof(spiffs_obj_id), sizeof(spiffs_obj_id)); + spiffs_obj_id dat{UINT16_MAX}; + partition.read(cfg.log_page_size - sizeof(spiffs_obj_id), &dat, sizeof(dat)); //debugf("%X", dat); - bool isFormatted = (dat != spiffs_obj_id(UINT32_MAX)); + bool isFormatted = (dat != UINT16_MAX); if(!isFormatted) { debugf("First init file system"); - spiffs_format_internal(cfg); + spiffs_format_internal(partition); } - if(!tryMount(cfg)) { + if(!tryMount(partition)) { return false; } @@ -102,42 +125,30 @@ bool spiffs_mount_internal(const spiffs_config& cfg) return true; } -bool initConfig(spiffs_config& cfg, uint32_t phys_addr, uint32_t phys_size) +Storage::Partition findDefaultPartition() { - if(phys_addr == 0) { - SYSTEM_ERROR("SPIFFS: Start address invalid"); - return false; + auto it = Storage::findPartition(Storage::Partition::SubType::Data::spiffs); + if(!it) { + debug_e("No SPIFFS partition found"); } - - cfg = spiffs_config{ - .hal_read_f = api_spiffs_read, - .hal_write_f = api_spiffs_write, - .hal_erase_f = api_spiffs_erase, - .phys_size = phys_size, - .phys_addr = phys_addr, - .phys_erase_block = INTERNAL_FLASH_SECTOR_SIZE, - .log_block_size = INTERNAL_FLASH_SECTOR_SIZE * 2, - .log_page_size = LOG_PAGE_SIZE, - }; - - return true; + return *it; } } // namespace bool spiffs_mount() { - spiffs_config cfg = spiffs_get_storage_config(); - return spiffs_mount_manual(cfg.phys_addr, cfg.phys_size); + auto part = findDefaultPartition(); + return part ? spiffs_mount_internal(part) : false; } -bool spiffs_mount_manual(uint32_t phys_addr, uint32_t phys_size) +bool spiffs_mount(Storage::Partition partition) { - spiffs_config cfg; - if(!initConfig(cfg, phys_addr, phys_size)) { + if(!partition.verify(Storage::Partition::SubType::Data::spiffs)) { return false; } - return spiffs_mount_internal(cfg); + + return spiffs_mount_internal(partition); } void spiffs_unmount() @@ -147,16 +158,21 @@ void spiffs_unmount() bool spiffs_format() { - auto cfg = spiffs_get_storage_config(); - return spiffs_format_manual(cfg.phys_addr, cfg.phys_size); + auto part = findDefaultPartition(); + if(!part) { + return false; + } + + spiffs_format_internal(part); + return spiffs_mount_internal(part); } -bool spiffs_format_manual(uint32_t phys_addr, uint32_t phys_size) +bool spiffs_format(Storage::Partition& partition) { - spiffs_config cfg; - if(!initConfig(cfg, phys_addr, phys_size)) { + if(!partition.verify(Storage::Partition::SubType::Data::spiffs)) { return false; } - spiffs_format_internal(cfg); - return spiffs_mount_internal(cfg); + + spiffs_format_internal(partition); + return spiffs_mount_internal(partition); } diff --git a/Sming/Components/spiffs/spiffs_sming.h b/Sming/Components/spiffs/spiffs_sming.h index c8619f9d81..3b969a7d91 100644 --- a/Sming/Components/spiffs/spiffs_sming.h +++ b/Sming/Components/spiffs/spiffs_sming.h @@ -9,14 +9,8 @@ ****/ #pragma once -#if defined(__cplusplus) -extern "C" { -#endif - #include "spiffs.h" -#include - -#define LOG_PAGE_SIZE 256 +#include /** * @brief Mount the SPIFFS volume using default configuration @@ -27,46 +21,31 @@ extern "C" { bool spiffs_mount(); /** - * @brief Mount a SPIFFS volume using custom location and size - * @param phys_addr The flash memory address (offset) for the volume - * @param phys_size The volume size, in bytes - * @retval bool true on success, false on failure - * @note If the given flash memory range appears to be empty then it is - * formatted, erasing any existing content. + * @brief Mount SPIFFS volume from a specific partition */ -bool spiffs_mount_manual(uint32_t phys_addr, uint32_t phys_size); +bool spiffs_mount(Storage::Partition partition); /** - * @brief Unmount a previously mounted volume + * @brief unmount SPIFFS filesystem + * @deprecated use fileFreeFileSystem() instead + * @note this will do nothing if the active filesystem is not SPIFFS */ void spiffs_unmount(); -/** - * @brief Format and mount a SPIFFS volume using default configuration - * @retval bool true on success +/** @brief Format and mount a SPIFFS filesystem + * @deprecated use fileSystemFormat() instead + * @note this will fail if the active filesystem is not SPIFFS */ bool spiffs_format(); /** - * @brief Format and mount a SPIFFS volume using custom location and size - * @param phys_addr The flash memory address (offset) for the volume - * @param phys_size The volume size, in bytes + * @brief Format and mount a SPIFFS volume using given partition + * @param partition * @retval bool true on success */ -bool spiffs_format_manual(uint32_t phys_addr, uint32_t phys_size); - -/** - * @brief Obtain the default SPIFFS configuration information - * @retval spiffs_config - * @note Only `phys_addr` and `phys_size` are used, all other parameters are overridden. - */ -spiffs_config spiffs_get_storage_config(); +bool spiffs_format(Storage::Partition& partition); /** * @brief Global SPIFFS instance used by FileSystem API */ extern spiffs _filesystemStorageHandle; - -#if defined(__cplusplus) -} -#endif diff --git a/Sming/Components/spiffs/spiffy/spiffy.c b/Sming/Components/spiffs/spiffy/spiffy.c index 485e4a87c5..6dab9cedac 100644 --- a/Sming/Components/spiffs/spiffy/spiffy.c +++ b/Sming/Components/spiffs/spiffy/spiffy.c @@ -33,7 +33,7 @@ void hexdump_mem(u8_t *b, u32_t len) { if ((i % 16) != 0) S_DBG("\n"); } -static s32_t my_spiffs_read(u32_t addr, u32_t size, u8_t *dst) { +static s32_t my_spiffs_read(struct spiffs_t* fs, u32_t addr, u32_t size, u8_t *dst) { int res; @@ -53,7 +53,7 @@ static s32_t my_spiffs_read(u32_t addr, u32_t size, u8_t *dst) { return SPIFFS_OK; } -static s32_t my_spiffs_write(u32_t addr, u32_t size, u8_t *src) { +static s32_t my_spiffs_write(struct spiffs_t* fs, u32_t addr, u32_t size, u8_t *src) { int ret = SPIFFS_OK; u8_t *buff = 0; @@ -63,7 +63,7 @@ static s32_t my_spiffs_write(u32_t addr, u32_t size, u8_t *src) { printf("Unable to malloc %d bytes.\n", size); ret = SPIFFS_ERR_INTERNAL; } else { - ret = my_spiffs_read(addr, size, buff); + ret = my_spiffs_read(fs, addr, size, buff); if (ret == SPIFFS_OK) { int i; for(i = 0; i < size; i++) buff[i] &= src[i]; @@ -87,7 +87,7 @@ static s32_t my_spiffs_write(u32_t addr, u32_t size, u8_t *src) { return ret; } -static s32_t my_spiffs_erase(u32_t addr, u32_t size) { +static s32_t my_spiffs_erase(struct spiffs_t* fs, u32_t addr, u32_t size) { int i; diff --git a/Sming/Core/Data/CString.h b/Sming/Core/Data/CString.h index 9dea4b4d9b..4335b95bc7 100644 --- a/Sming/Core/Data/CString.h +++ b/Sming/Core/Data/CString.h @@ -13,6 +13,7 @@ #pragma once #include +#include #include /** @@ -27,13 +28,21 @@ class CString : public std::unique_ptr public: CString() = default; - CString(const CString& src) = default; + CString(const CString& src) + { + assign(src.get()); + } CString(const String& src) { assign(src); } + CString(const char* src) + { + assign(src); + } + void assign(const String& src) { if(src) { @@ -59,6 +68,12 @@ class CString : public std::unique_ptr } } + CString& operator=(const CString& src) + { + assign(src.get()); + return *this; + } + CString& operator=(const String& src) { assign(src); @@ -81,14 +96,55 @@ class CString : public std::unique_ptr return get() ?: ""; } - bool operator==(const CString& other) const + bool equals(const CString& other) const { return strcmp(c_str(), other.c_str()) == 0; } + bool equals(const String& other) const + { + return other.equals(c_str()); + } + + bool equals(const char* other) const + { + if(other == nullptr) { + return length() == 0; + } + return strcmp(c_str(), other) == 0; + } + + bool equalsIgnoreCase(const CString& other) const + { + return strcasecmp(c_str(), other.c_str()) == 0; + } + + bool equalsIgnoreCase(const String& other) const + { + return other.equalsIgnoreCase(c_str()); + } + + bool equalsIgnoreCase(const char* other) const + { + if(other == nullptr) { + return length() == 0; + } + return strcasecmp(c_str(), other) == 0; + } + + bool operator==(const CString& other) const + { + return equals(other); + } + bool operator==(const String& other) const { - return strcmp(c_str(), other.c_str()) == 0; + return equals(other); + } + + bool operator==(const char* other) const + { + return equals(other); } size_t length() const diff --git a/Sming/Core/SmingCore.h b/Sming/Core/SmingCore.h index 402665df6c..7420d1ce26 100644 --- a/Sming/Core/SmingCore.h +++ b/Sming/Core/SmingCore.h @@ -57,3 +57,5 @@ #include "DateTime.h" #include "fatfs/ff.h" + +#include diff --git a/Sming/Libraries/.patches/Arduino_TensorFlowLite/README.rst b/Sming/Libraries/.patches/Arduino_TensorFlowLite/README.rst deleted file mode 100644 index 514a20a70a..0000000000 --- a/Sming/Libraries/.patches/Arduino_TensorFlowLite/README.rst +++ /dev/null @@ -1,8 +0,0 @@ -Arduino TensorFlow Lite -======================= - -This library runs TensorFlow machine learning models on microcontrollers, allowing you to build AI/ML applications powered by deep learning and neural networks. - -With the included examples, you can recognize speech, detect people using a camera, and recognise "magic wand" gestures using an accelerometer. - -The examples work best with the Arduino Nano 33 BLE Sense board, which has a microphone and accelerometer. diff --git a/Sming/Libraries/ArduCAM/ArduCAMStream.cpp b/Sming/Libraries/ArduCAM/ArduCAMStream.cpp index 91564aa79c..11a491cc14 100644 --- a/Sming/Libraries/ArduCAM/ArduCAMStream.cpp +++ b/Sming/Libraries/ArduCAM/ArduCAMStream.cpp @@ -15,7 +15,7 @@ #define BMPIMAGEOFFSET 66 -const char bmp_header[BMPIMAGEOFFSET] = +const uint8_t bmp_header[BMPIMAGEOFFSET] = { 0x42, 0x4D, 0x36, 0x58, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x03, 0x00, diff --git a/Sming/Libraries/Arduino_TensorFlowLite b/Sming/Libraries/Arduino_TensorFlowLite index 521b889982..9f00dd38f4 160000 --- a/Sming/Libraries/Arduino_TensorFlowLite +++ b/Sming/Libraries/Arduino_TensorFlowLite @@ -1 +1 @@ -Subproject commit 521b8899822ccce6188d7fe1fb246c06aec9ef17 +Subproject commit 9f00dd38f4329b8e884c417aa1bcfbfb15f47913 diff --git a/Sming/Libraries/CS5460/samples/generic/component.mk b/Sming/Libraries/CS5460/samples/generic/component.mk index 51f1520d50..391ca50c73 100644 --- a/Sming/Libraries/CS5460/samples/generic/component.mk +++ b/Sming/Libraries/CS5460/samples/generic/component.mk @@ -1,2 +1 @@ ARDUINO_LIBRARIES := CS5460 -DISABLE_SPIFFS = 1 diff --git a/Sming/Libraries/DIAL b/Sming/Libraries/DIAL index 712c3d4087..4d54bde7a1 160000 --- a/Sming/Libraries/DIAL +++ b/Sming/Libraries/DIAL @@ -1 +1 @@ -Subproject commit 712c3d4087fca7c90336b559edf912e2bcf9b846 +Subproject commit 4d54bde7a1620b85bf53fcb5670f2321ee562835 diff --git a/Sming/Libraries/HueEmulator b/Sming/Libraries/HueEmulator index 20553f6e9e..79d7e79754 160000 --- a/Sming/Libraries/HueEmulator +++ b/Sming/Libraries/HueEmulator @@ -1 +1 @@ -Subproject commit 20553f6e9e223588595e626fc7aa060847df57eb +Subproject commit 79d7e797540519a4fcd9e73a6ca7c6e04715794e diff --git a/Sming/Libraries/ModbusMaster/samples/generic/component.mk b/Sming/Libraries/ModbusMaster/samples/generic/component.mk index 6b0689bce9..2797abfd8b 100644 --- a/Sming/Libraries/ModbusMaster/samples/generic/component.mk +++ b/Sming/Libraries/ModbusMaster/samples/generic/component.mk @@ -1,2 +1 @@ ARDUINO_LIBRARIES := ModbusMaster -DISABLE_SPIFFS = 1 diff --git a/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.cpp b/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.cpp index ca549eac12..c1ac6be9e4 100644 --- a/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.cpp +++ b/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.cpp @@ -184,7 +184,7 @@ size_t BasicStream::write(const uint8_t* data, size_t size) break; case State::WriteRom: { - bool ok = rboot_write_flash(&rbootWriteStatus, const_cast(data), std::min(remainingBytes, size)); + bool ok = rboot_write_flash(&rbootWriteStatus, data, std::min(remainingBytes, size)); if(ok) { if(consume(data, size)) { ok = slot.updated = rboot_write_end(&rbootWriteStatus); diff --git a/Sming/Libraries/RapidXML b/Sming/Libraries/RapidXML index 0b56a775b7..cab0af2fda 160000 --- a/Sming/Libraries/RapidXML +++ b/Sming/Libraries/RapidXML @@ -1 +1 @@ -Subproject commit 0b56a775b7b3603631734506be4984f094df3685 +Subproject commit cab0af2fda5d52a365502750db6e6bd01f585053 diff --git a/Sming/Libraries/RingTone b/Sming/Libraries/RingTone index 65ebef1efe..a62f39ea08 160000 --- a/Sming/Libraries/RingTone +++ b/Sming/Libraries/RingTone @@ -1 +1 @@ -Subproject commit 65ebef1efe71f56ee8d01f158420d4ca07a91fe6 +Subproject commit a62f39ea08867b26c7ace53aedb43769f02cc74f diff --git a/Sming/Libraries/SmingTest b/Sming/Libraries/SmingTest index b52ffcd329..36913db2ce 160000 --- a/Sming/Libraries/SmingTest +++ b/Sming/Libraries/SmingTest @@ -1 +1 @@ -Subproject commit b52ffcd3297490a39135d50824374d073c4e384b +Subproject commit 36913db2ce3a5be82abb90d2140988307adfc38d diff --git a/Sming/Libraries/TFT_S1D13781 b/Sming/Libraries/TFT_S1D13781 index e83437794c..4940599366 160000 --- a/Sming/Libraries/TFT_S1D13781 +++ b/Sming/Libraries/TFT_S1D13781 @@ -1 +1 @@ -Subproject commit e83437794c493cb4bb1c486aa5181a227ed2408b +Subproject commit 49405993668fc4dfbd51109e836f3eaa7b0b838f diff --git a/Sming/Libraries/UPnP b/Sming/Libraries/UPnP index 0dc7d07fe8..e04f503c15 160000 --- a/Sming/Libraries/UPnP +++ b/Sming/Libraries/UPnP @@ -1 +1 @@ -Subproject commit 0dc7d07fe8436c96f8c0072acea4d4eca0074139 +Subproject commit e04f503c151f9cd67d7ee06f156ede583b5f06e5 diff --git a/Sming/Libraries/modbusino/samples/generic/component.mk b/Sming/Libraries/modbusino/samples/generic/component.mk index 4163e3ffed..068e9f51a6 100644 --- a/Sming/Libraries/modbusino/samples/generic/component.mk +++ b/Sming/Libraries/modbusino/samples/generic/component.mk @@ -1,5 +1,4 @@ ARDUINO_LIBRARIES := modbusino -DISABLE_SPIFFS = 1 CONFIG_VARS += MB_SLAVE_ADDR MB_SLAVE_ADDR ?= 1 diff --git a/Sming/Makefile b/Sming/Makefile index ee0c49f580..4ad8476a79 100644 --- a/Sming/Makefile +++ b/Sming/Makefile @@ -22,7 +22,7 @@ all: ##@Cleaning .PHONY: dist-clean -dist-clean: submodules-clean samples-clean docs-clean ##Clean everything for all arch/build types +dist-clean: submodules-clean samples-clean docs-clean tests-clean ##Clean everything for all arch/build types -$(Q) rm -rf out .PHONY: clean @@ -103,8 +103,8 @@ docs: submodules ##Build the Sming documentation # For integration testing both samples and tests are moved outside of the repo. SMING_PROJECTS_DIR ?= $(abspath $(SMING_HOME)/..) -SAMPLES_DIR := $(call FixPath,$(SMING_PROJECTS_DIR)/samples) -TESTS_DIR := $(call FixPath,$(SMING_PROJECTS_DIR)/tests) +SMING_PROJECTS_DIR := $(call FixPath, $(SMING_PROJECTS_DIR)) +SAMPLES_DIR := $(SMING_PROJECTS_DIR)/samples SAMPLE_NAMES = $(shell ls -1 $(SAMPLES_DIR)) @@ -112,17 +112,41 @@ SAMPLE_NAMES = $(shell ls -1 $(SAMPLES_DIR)) samples: | $(SAMPLE_NAMES) ##Build all sample applications $(SAMPLE_NAMES): - $(Q) $(MAKE) --no-print-directory -C $(SAMPLES_DIR)/$@ + $(Q) $(MAKE) --no-print-directory -C $(SAMPLES_DIR)/$@ PIP_ARGS=-q python-requirements all + +# Build component samples +.PHONY: component-samples +component-samples: submodules ##Build all samples contained in components + $(Q) $(MAKE) --no-print-directory build-component-samples + +# Marks sample as build complete for faster re-testing +BUILT_SUFFIX := /out/.built +COMPONENT_SAMPLE_TARGETS = $(addsuffix $(BUILT_SUFFIX),$(wildcard $(foreach c,$(COMPONENT_SEARCH_DIRS),$c/*/samples/*))) + +.PHONY: build-component-samples +build-component-samples: $(COMPONENT_SAMPLE_TARGETS) + +$(COMPONENT_SAMPLE_TARGETS): + $(Q) $(MAKE) --no-print-directory -C $(@:$(BUILT_SUFFIX)=) PIP_ARGS=-q python-requirements all + $(Q) touch $@ -TESTS_COMPLETED = $(addsuffix /.complete,$(call ListSubDirs,$(TESTS_DIR))) PHONY: tests -tests: $(TESTS_COMPLETED) ##Build and run all test applications +tests: submodules ##Build and run all tests + $(Q) $(MAKE) --no-print-directory build-and-run-tests + +COMPLETED_SUFFIX := /.complete +COMPONENT_TESTS = $(call dirx,$(wildcard $(foreach c,$(COMPONENT_SEARCH_DIRS),$c/*/test/component.mk))) +TESTS_COMPLETED = $(addsuffix $(COMPLETED_SUFFIX),$(wildcard $(SMING_PROJECTS_DIR)/tests/* $(COMPONENT_TESTS))) + +PHONY: build-and-run-tests +build-and-run-tests: $(TESTS_COMPLETED) ##Build and run all test applications $(TESTS_COMPLETED): $(Q) $(MAKE) -C $(@D) execute $(Q) touch $@ + ##@Cleaning .PHONY: samples-clean @@ -130,15 +154,20 @@ samples-clean: ##Clean all sample applications (all arch/build types) @echo Cleaning all samples... -$(Q) cd $(SAMPLES_DIR) && rm -rf $(addsuffix /out,$(SAMPLE_NAMES)) +.PHONY: component-samples-clean +component-samples-clean: ##Clean all component samples + @echo Cleaning all component samples... + -$(Q) rm -rf $(dir $(COMPONENT_SAMPLE_TARGETS)) + CLEAN_TESTS := $(TESTS_COMPLETED:complete=clean) .PHONY: tests-clean -tests-clean: $(CLEAN_TESTS) ##Clean all test applications (all arch/build types) +tests-clean: $(wildcard $(CLEAN_TESTS)) ##Clean all test applications (all arch/build types) -$(Q) rm -f $(TESTS_COMPLETED) .PHONY: $(CLEAN_TESTS) $(CLEAN_TESTS): @echo Cleaning '$(@D)' - $(Q) $(MAKE) -C $(@D) clean + -$(Q) $(MAKE) -C $(@D) clean ##@Tools diff --git a/Sming/build.mk b/Sming/build.mk index 98f2889422..d252d0f730 100644 --- a/Sming/build.mk +++ b/Sming/build.mk @@ -280,20 +280,22 @@ endef # Display variable and list values, e.g. $(call PrintVariable,LIBS) # $1 -> Name of variable containing values +# $2 -> (optional) tag to use instead of variable name define PrintVariable - $(info $1) + $(info $(if $2,$2,$1):) $(foreach item,$($1),$(info - $(item))) endef define PrintVariableSorted - $(info $1) + $(info $(if $2,$2,$1):) $(foreach item,$(sort $($1)),$(info - $(value item))) endef # Display list of variable references with their values e.g. $(call PrintVariableRefs,DEBUG_VARS) # $1 -> Name of variable containing list of variable names +# $2 -> (optional) tag to use instead of variable name define PrintVariableRefs - $(info $1) + $(info $(if $2,$2,$1):) $(foreach item,$(sort $($1)),$(info - $(item) = $(value $(item))) ) endef diff --git a/Sming/building.rst b/Sming/building.rst index 4904002d34..e5c6513b50 100644 --- a/Sming/building.rst +++ b/Sming/building.rst @@ -127,6 +127,13 @@ To switch to a different build architecture, for example: To inspect the current build configuration, type ``make list-config``. +Hardware configuration +~~~~~~~~~~~~~~~~~~~~~~ + +The appropriate hardware configuration should be selected in the +project's component.mk file. Use one of the standard configurations +or create your own. See :ref:`hardware-config`. + Configuration variables ~~~~~~~~~~~~~~~~~~~~~~~ @@ -140,7 +147,7 @@ sessions, and will override any values set in your project’s - Type ``make SPIFF_BIN=test-rom`` to build the project and (if enabled) create a SPIFFS image file called ``test-rom.bin`` -- Type ``make flash COM_PORT=COM4 SPI_MODE=dio SPI_SIZE=4M`` to flash +- Type ``make flash COM_PORT=COM4`` to flash the project and ``test-rom`` SPIFFS image using the provided flash memory settings - Next time you type ``make flash``, the same settings will be used, no diff --git a/Sming/component-wrapper.mk b/Sming/component-wrapper.mk index 81af7e389e..ae7d2c4cfd 100644 --- a/Sming/component-wrapper.mk +++ b/Sming/component-wrapper.mk @@ -15,6 +15,7 @@ all: $(error Internal makefile) include $(SMING_HOME)/build.mk +include $(PROJECT_DIR)/$(OUT_BASE)/hwconfig.mk # Makefile runs in the build directory COMPONENT_BUILD_DIR := $(CURDIR) diff --git a/Sming/component.mk b/Sming/component.mk index 62a9f18a22..9685debd3a 100644 --- a/Sming/component.mk +++ b/Sming/component.mk @@ -13,6 +13,7 @@ COMPONENT_INCDIRS := \ . COMPONENT_DEPENDS := \ + Storage \ sming-arch \ FlashString \ spiffs \ @@ -147,8 +148,26 @@ GLOBAL_CFLAGS += -DSTRING_OBJECT_SIZE=$(STRING_OBJECT_SIZE) ##@Flashing .PHONY: flashinit -flashinit: $(ESPTOOL) $(FLASH_INIT_DATA) | $(FW_BASE) ##Erase your device's flash memory and reset system configuration area to defaults +flashinit: $(ESPTOOL) $(FLASH_INIT_DATA) | $(FW_BASE) ##Erase your device's flash memory $(info Flash init data default and blank data) - $(info DISABLE_SPIFFS = $(DISABLE_SPIFFS)) $(Q) $(EraseFlash) $(call WriteFlash,$(FLASH_INIT_CHUNKS)) + +.PHONY: flashboot +flashboot: $(FLASH_BOOT_LOADER) kill_term ##Write just the Bootloader + $(call WriteFlash,$(FLASH_BOOT_CHUNKS)) + +.PHONY: flashapp +flashapp: all kill_term ##Write just the application image(s) + $(Q) $(call CheckPartitionChunks,$(FLASH_APP_CHUNKS)) + $(call WriteFlash,$(FLASH_APP_CHUNKS)) + +.PHONY: flash +flash: all kill_term ##Write the boot loader and all defined partition images + $(Q) $(call CheckPartitionChunks,$(FLASH_PARTITION_CHUNKS)) + $(call WriteFlash,$(FLASH_BOOT_CHUNKS) $(FLASH_MAP_CHUNK) $(FLASH_PARTITION_CHUNKS)) +ifeq ($(ENABLE_GDB), 1) + $(GDB_CMDLINE) +else ifneq ($(SMING_ARCH),Host) + $(TERMINAL) +endif diff --git a/Sming/project.mk b/Sming/project.mk index ec9440cbc1..91b7db27df 100644 --- a/Sming/project.mk +++ b/Sming/project.mk @@ -55,6 +55,7 @@ CACHE_VARS := # Use PROJECT_DIR to identify the project source directory, from where this makefile must be included DEBUG_VARS += PROJECT_DIR PROJECT_DIR := $(CURDIR) +export PROJECT_DIR ifeq ($(MAKELEVEL),0) $(info ) @@ -318,6 +319,8 @@ CMP_App_VARS := $(CONFIG_VARS) CMP_App_ALL_VARS := $(CONFIG_VARS) $(foreach c,$(COMPONENTS),$(eval $(call ParseComponentLibs,$c))) +$(PartitionCreateTargets) + export COMPONENTS_EXTRA_INCDIR export APPCODE export APP_CFLAGS diff --git a/Sming/spiffs.hw b/Sming/spiffs.hw new file mode 100644 index 0000000000..f3c0ee4e93 --- /dev/null +++ b/Sming/spiffs.hw @@ -0,0 +1,17 @@ +{ + "name": "Single SPIFFS partition", + "base_config": "standard-4m", + "partitions": { + "spiffs0": { + "address": "0x200000", + "size": "512K", + "type": "data", + "subtype": "spiffs", + "filename": "$(SPIFF_BIN_OUT)", + "build": { + "target": "spiffsgen", + "files": "$(SPIFF_FILES)" + } + } + } +} \ No newline at end of file diff --git a/Sming/standard-4m.hw b/Sming/standard-4m.hw new file mode 100644 index 0000000000..c259654b19 --- /dev/null +++ b/Sming/standard-4m.hw @@ -0,0 +1,9 @@ +{ + "name": "Standard config with 4M flash", + "base_config": "standard", + "devices": { + "spiFlash": { + "size": "4M" + } + } +} diff --git a/Tools/requirements.txt b/Tools/requirements.txt new file mode 100644 index 0000000000..ee96c8f8e1 --- /dev/null +++ b/Tools/requirements.txt @@ -0,0 +1,2 @@ +pyserial +jsonschema diff --git a/docs/Makefile b/docs/Makefile index 6fe6b5c76a..e8cf78e6f5 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -9,6 +9,7 @@ AWK ?= POSIXLY_CORRECT= awk SMINGDIR := .. override SMING_HOME := $(SMINGDIR)/Sming +override SMING_ARCH := Host # You can set these variables from the command line, and also from the environment for the first two. SPHINXOPTS ?= @@ -16,6 +17,7 @@ SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build +ARCH_BASE := $(SMING_HOME)/Arch/$(SMING_ARCH) BUILD_TYPE := release OUT_BASE := out/$(SMING_ARCH)/$(BUILD_TYPE) BUILD_BASE = $(OUT_BASE)/build @@ -115,7 +117,7 @@ DOXYGEN_PREDEFINED := \ ICACHE_FLASH_ATTR= \ __forceinline= \ SMING_DEPRECATED= \ - SMING_ARCH=Host \ + SMING_ARCH=$(SMING_ARCH) \ ARCH_HOST=1 diff --git a/docs/source/arch/esp8266/getting-started/config.rst b/docs/source/arch/esp8266/getting-started/config.rst index 920e5125d2..1798cd59d6 100644 --- a/docs/source/arch/esp8266/getting-started/config.rst +++ b/docs/source/arch/esp8266/getting-started/config.rst @@ -3,26 +3,16 @@ Configuring your Esp8266 device .. highlight:: bash -You may need to configure your project to support the specific device being programmed. +You may need to configure your project to support the specific device being programmed: -.. note:: - - Arduino has a system which allows these values to be set based on a *board* - selection. At present Sming does not have such a system so this must be done - manually. - -Here are a few important ones: - -* :envvar:`SPI_MODE` Change this if your device fails to program or run -* :envvar:`SPI_SIZE` If using a device with reduced memory you may need to change this value. Note also that samples using :component:`spiffs` may not work. * :envvar:`COM_PORT` If you haven't set this already, it will need to match the port you're using to talk to the Esp8266. * :envvar:`COM_SPEED_ESPTOOL` The default value should work fine but you can usually set a much faster speed. You can set these initially on the command line, like this:: - make SMING_ARCH=Esp8266 SPI_MODE=dio SPI_SIZE=4M COM_SPEED_ESPTOOL=921600 + make SMING_ARCH=Esp8266 COM_PORT=/dev/ttyUSB3 COM_SPEED_ESPTOOL=921600 -Once you're happy with the settings, you can add them to your ``project.mk`` file. +For Windows expect to use COM2, COM3, etc. You can list the current set of configuration variables like this:: @@ -32,14 +22,17 @@ If you want to reset back to default values, do this:: make config-clean -.. note:: +Other hardware-specific settings are stored in the hardware configuration file. +You can examine the current configuration like this:: + + make hwconfig + +The standard config should work with all ESP8266 variants. +If you want to use SPIFFS then you should add this line to your component.mk file:: - When :envvar:`SPI_MODE` or :envvar:`SPI_SIZE` are changed, you must update the on-flash configuration - data by running:: - - make flashinit + HWCONFIG = spiffs - You should do this before uploading your program code. +This expects your device to have at least 4MBytes of flash. * See :doc:`/_inc/Sming/building` for further details about configuring your project. * See :doc:`/features` for configuring Sming options. diff --git a/docs/source/information/flash.rst b/docs/source/information/flash.rst index a5a92348d7..90eb70b700 100644 --- a/docs/source/information/flash.rst +++ b/docs/source/information/flash.rst @@ -9,33 +9,50 @@ Introduction ESP8266 flash memory sizes vary from 512Kbytes on the ESP-01 up to 4Mbytes on the ESP12F. Up to 16MBytes are supported for custom designs. -You can find general details for the memory layout in the `ESP8266 Wiki `__. +Sming version 4.3 introduced partition tables to support multiple architectures, +different hardware variants and custom flash layouts without restriction. -This is the layout for Sming with a 4MByte flash device: +See :ref:`hardware-config` for details. -======= =============== ==== ========================= =================================================== -Address Config variable Size Source filename Description -(hex) (if any) (KB) (if applicable) -======= =============== ==== ========================= =================================================== -000000 1 rboot.bin Boot loader -001000 4 rBoot configuration -002000 ROM_0_ADDR rom0.bin First ROM image -100000 RBOOT_SPIFFS_0 -202000 ROM_1_ADDR rom1.bin Second ROM image -300000 RBOOT_SPIFFS_1 -3FB000 4 blank.bin RF Calibration data (Initialised to FFh) -3FC000 4 esp_init_data_default.bin PHY configuration data -3FD000 12 blank.bin System parameter area -======= =============== ==== ========================= =================================================== +A typical layout for a 4MByte device might look like this: + ======= =============== ==== ========================= =================================================== + Address Config variable Size Source filename Description + (hex) (if any) (KB) (if applicable) + ======= =============== ==== ========================= =================================================== + 000000 1 rboot.bin Boot loader + 001000 4 rBoot configuration + 002000 4 Partition table + 003000 4 esp_init_data_default.bin PHY configuration data + 004000 12 blank.bin System parameter area + 006000 4 blank.bin RF Calibration data (Initialised to FFh) + 006000 4 Reserved + 008000 ROM_0_ADDR rom0.bin First ROM image + 100000 RBOOT_SPIFFS_0 + 208000 ROM_1_ADDR rom1.bin Second ROM image + 300000 RBOOT_SPIFFS_1 + ======= =============== ==== ========================= =================================================== + -Partition Tables ----------------- +.. note:: -{ todo } + This was the previous layout for a 4MByte flash device: -Whilst SDK version 3 requires a partition table, previous versions do not but this can be added so that we -can use it as a common reference for all the above locations. + ======= =============== ==== ========================= =================================================== + Address Config variable Size Source filename Description + (hex) (if any) (KB) (if applicable) + ======= =============== ==== ========================= =================================================== + 000000 1 rboot.bin Boot loader + 001000 4 rBoot configuration + 002000 ROM_0_ADDR rom0.bin First ROM image + 100000 RBOOT_SPIFFS_0 + 202000 ROM_1_ADDR rom1.bin Second ROM image + 300000 RBOOT_SPIFFS_1 + 3FB000 4 blank.bin RF Calibration data (Initialised to FFh) + 3FC000 4 esp_init_data_default.bin PHY configuration data + 3FD000 12 blank.bin System parameter area + ======= =============== ==== ========================= =================================================== + Speed and caching diff --git a/docs/source/information/rboot-ota.rst b/docs/source/information/rboot-ota.rst index a2953df1f8..5f22026f90 100644 --- a/docs/source/information/rboot-ota.rst +++ b/docs/source/information/rboot-ota.rst @@ -44,13 +44,16 @@ Configuring for two ROM mode ============================ If you have a 1MB flash, you will need to have two 512KB ROM slots, both -in the same 1MB block of flash. Set the following options in your project's -``component.mk`` file: +in the same 1MB block of flash. You can accommodate this by setting the +appropriate hardware configuration in your project's component.mk file:: .. code-block:: make - RBOOT_ROM1_ADDR = 0x80000 - SPI_SIZE = 1M + HWCONFIG = two-rom-mode + +See ``Sming/Arch/Esp8266/two-rom-mode.hw`` for details. +You can copy this and customise it in your project. + SPIFFS ====== @@ -60,9 +63,10 @@ To use SPIFFS think about where you want your SPIFFS to sit on the flash. If you have a 4MB flash the default position is for the first ROM to be placed in the first 1MB block and the second ROM to be placed in the third 1MB block of flash. This leaves a whole 1MB spare after each -ROM in which you can put your SPIFFS. +ROM in which you can put your SPIFFS. This is the behaviour when you +set ``HWCONFIG = spiffs`` in your project's component.mk file. -If you have to a smaller flash the SPIFFS will have to share the 1MB block with the ROM. +If you have a smaller flash the SPIFFS will have to share the 1MB block with the ROM. For example, the first part of each 1MB block may contain the ROM, and the second part the SPIFFS (but does *not* have to be split equally in half). So for the 4MB example you could put the SPIFFS for your first ROM at flash address at 0x100000 @@ -74,9 +78,14 @@ To mount your SPIFFS at boot time add the following code to init: .. code-block:: c++ int slot = rboot_get_current_rom(); - uint32_t address = (slot == 0) ? RBOOT_SPIFFS_0 : RBOOT_SPIFFS_1; - //debugf("trying to mount SPIFFS at %x, length %d", address, SPIFF_SIZE); - spiffs_mount_manual(address, SPIFF_SIZE); + // Find the n'th SPIFFS partition + auto part = PartitionTable().find(Partition::SubType::Data::spiffs, slot); + if(part) { + //debugf("trying to mount SPIFFS at %x, length %d", part.address(), part.size()); + spiffs_mount(part); + } else { + debug_e("SPIFFS partition missing for slot #%u", slot); + } Over-the-air (OTA) updates ========================== @@ -98,64 +107,6 @@ products. A more lightweight solution is provided by :cpp:class:`RbootOutputStream`, which is just a thin wrapper around rBoot's flash API, in combination with :cpp:class:`RbootHttpUpdater`, -which pulls individual ROM image from an HTTP server. Add the following code: - -.. code-block:: c++ - - RbootHttpUpdater* otaUpdater = nullptr; - - void OtaUpdate_CallBack(RbootHttpUpdater& client, bool result) - { - if (result) { - // success - switch slot - uint8_t slot = rboot_get_current_rom(); - if (slot == 0) { - slot = 1; - } else { - slot = 0; - } - // set to boot new ROM and then reboot - Serial.printf("Firmware updated, rebooting to ROM %d...\r\n", slot); - rboot_set_current_rom(slot); - System.restart(); - } else { - // fail - Serial.println("Firmware update failed!"); - } - } - - void OtaUpdate() - { - // need a clean object, otherwise if run before and failed will not run again - delete otaUpdater; - otaUpdater = new RbootHttpUpdater(); - - // select ROM slot to flash - rboot_config bootconf = rboot_get_config(); - uint8_t slot = bootconf.current_rom; - if (slot == 0) { - slot = 1; - } else { - slot = 0; - } - - #ifndef RBOOT_TWO_ROMS - // flash ROM to position indicated in the rBoot config ROM table - otaUpdater->addItem(bootconf.roms[slot], ROM_0_URL); - #else - // flash appropriate ROM - otaUpdater->addItem(bootconf.roms[slot], (slot == 0) ? ROM_0_URL : ROM_1_URL); - #endif - - // use user supplied values (defaults for 4MB flash in makefile) - otaUpdater->addItem((slot == 0) ? RBOOT_SPIFFS_0 : RBOOT_SPIFFS_1, SPIFFS_URL); - - // set a callback - otaUpdater->setCallback(OtaUpdate_CallBack); - - // start update - otaUpdater->start(); - } +which pulls individual ROM image from an HTTP server. -You will need to define ``ROM_0_URL``, ``ROM_1_URL`` and ``SPIFFS_URL`` -with http urls for the files to download. +For details, refer to the `OtaUpdate()` function in the :sample:`Basic_rBoot` sample. diff --git a/docs/source/troubleshooting/random-restart.rst b/docs/source/troubleshooting/random-restart.rst index 818dfe9d9c..3076a189fe 100644 --- a/docs/source/troubleshooting/random-restart.rst +++ b/docs/source/troubleshooting/random-restart.rst @@ -1,9 +1,7 @@ Random Restarts =============== -First try setting the baud rate to ``74880``. Example for Linux: - -:: +First try setting the baud rate to ``74880``. Example for Linux:: python -m serial.tools.miniterm /dev/ttyUSB0 74880 @@ -15,25 +13,16 @@ device. To achieve this do the following: -1) Set the SPI_SIZE of your flash memory. Example: If you have device - with 4 megabytes make sure that the following is set: - -:: - - export SPI_SIZE=4M - -2) Run ``flashinit``. +1) Check the :ref:`hardware-config` especially ``flash_size`` setting. -Run the following commands. +2) Run ``flashinit``:: -:: + cd $SMING_HOME/../samples/Basic_Blink + make flashinit - cd $SMING_HOME/../samples/Basic_Blink - make flashinit + This resets the flash memory to a default state, erasing any existing + application, configuration or data stored there. -``flashinit`` is erasing the current flash memory and populating some -areas on it with the bytes that your SDK / BootROM is expecting to be -present. +3) Re-program your device:: -This command needs to be executed only when you change SDKs or memory -layouts. + make flash diff --git a/docs/source/upgrading/4.2-4.3.rst b/docs/source/upgrading/4.2-4.3.rst new file mode 100644 index 0000000000..9d52386086 --- /dev/null +++ b/docs/source/upgrading/4.2-4.3.rst @@ -0,0 +1,98 @@ +***************** +From v4.2 to v4.3 +***************** + +.. highlight:: c++ + + +Storage Management +------------------ + +The layout of flash memory has been revised and is now managed via partition table. +Hardware configuration is stored in a JSON file (with .hw extension). + +If your project has minimal customisation then you may only need to change +the :envvar:`HWCONFIG` setting. + +You can find full details in the :component:`Storage` library. +See also background on :doc:`/information/flash`. + +New and updated build targets + hwconfig + Displays the current configuration in JSON format + hwconfig-list + Show available hardware configs + map + Display the current flash memory map + readmap + Read partition table from device and display the map + readpart + Read contents of partition into a file, e.g. ``make readpart PART=spiffs0`` + will create ``out/Esp8266/debug/spiffs0.read.bin`` + flash + Flash partition table and all partitions. Previously it was necessary to run + ``make flashinit`` to write system parameter information. Failure to do this + was a common problem and should now be a thing of the past. + flashinit + This now just erases the flash memory, and is no longer a pre-requisite for ``make flash``. + + The ESP8266 system parameter information has been moved into registered + partitions (phy_init and sys_param) near the beginning of flash memory. + It now gets written when running ``make flash``. + + Previously, this information was kept right at the end of the flash memory, + so the location would vary depending on the setting of ``SPI_SIZE``. + This was a frequent cause of problems as the system would fail to start if this + was set incorrectly. + flashmap + Flash just the partition map + flashpart + Flash a single partition, e.g. ``make flashpart PART=spiffs0`` + erasepart + Erase a partition, e.g. ``make erasepart PART=spiffs0`` + +Configuration variables + A number of configuration variables have been removed or made read-only, as these are now + generated from the :ref:`hardware_config`. + + :envvar:`SPI_MODE`, :envvar:`SPI_SIZE`, :envvar:`SPI_SPEED` + The variables are still used internally but are read-only; they cannot be set at the command line. + Values are read from the hardware configuration under ``/devices/spiFlash``. + + :envvar:`RBOOT_ROM0_ADDR`, :envvar:`RBOOT_ROM1_ADDR`, :envvar:`RBOOT_ROM2_ADDR` + Used by :component:`rboot`, and are now read-only. + Values are read from the ``address`` property of ``rom0-2`` in the hardware configuration. + + :envvar:`RBOOT_SPIFFS_0`, :envvar:`RBOOT_SPIFFS_1` + Removed. + + :envvar:`SPIFF_SIZE` + Removed. Attempting to set this automatically within a hardware configuration is + liable to cause more problems than it solves, so updating the hardware config is + the now only way to change this setting. + + :envvar:`SPIFF_FILES` + [deprecated] + + You can still use this to specify the source location for the primary + SPIFFS partition (spiffs0). The preferred method is to set the ``files`` + property in a partition ``build`` key. + + The default SPIFFS partition settings can be overridden in a custom profile. + For example: + + .. code-block:: json + + { + ... + "base_config": "spiffs", + "partitions": { + "spiffs0": { + "size": "128K", + "build": { + "files": "some/other/folder" + } + } + } + } + diff --git a/docs/source/upgrading/index.rst b/docs/source/upgrading/index.rst index 9afa71358d..4b024707b8 100644 --- a/docs/source/upgrading/index.rst +++ b/docs/source/upgrading/index.rst @@ -7,6 +7,7 @@ For newer versions we have dedicated pages. .. toctree:: :maxdepth: 1 + 4.2-4.3 4.1-4.2 4.0-4.1 3.8-4.0 diff --git a/samples/Accelerometer_MMA7455/component.mk b/samples/Accelerometer_MMA7455/component.mk index ce1725567a..2d423a2a56 100644 --- a/samples/Accelerometer_MMA7455/component.mk +++ b/samples/Accelerometer_MMA7455/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := MMA_7455 - -DISABLE_SPIFFS = 1 diff --git a/samples/Arducam/component.mk b/samples/Arducam/component.mk index 3d7a1074ba..961e2d7ea9 100644 --- a/samples/Arducam/component.mk +++ b/samples/Arducam/component.mk @@ -1,3 +1,3 @@ ARDUINO_LIBRARIES := ArduCAM - +HWCONFIG = spiffs SPIFF_FILES = web/build diff --git a/samples/Basic_APA102/component.mk b/samples/Basic_APA102/component.mk index 75ebf23720..3c540d9c36 100644 --- a/samples/Basic_APA102/component.mk +++ b/samples/Basic_APA102/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := APA102 - -DISABLE_SPIFFS = 1 diff --git a/samples/Basic_AWS/component.mk b/samples/Basic_AWS/component.mk index bcb042181f..6e25c28d7d 100644 --- a/samples/Basic_AWS/component.mk +++ b/samples/Basic_AWS/component.mk @@ -1,4 +1,3 @@ -DISABLE_SPIFFS := 1 ENABLE_SSL := 1 MQTT_NO_COMPAT := 1 ENABLE_CUSTOM_HEAP := 1 diff --git a/samples/Basic_Audio/component.mk b/samples/Basic_Audio/component.mk index 44197b4522..3da06ba826 100644 --- a/samples/Basic_Audio/component.mk +++ b/samples/Basic_Audio/component.mk @@ -1,5 +1,3 @@ -DISABLE_SPIFFS=1 - # Required if compiling with ENABLE_GDB=1, you must connect debug terminal to alternative serial pins GDB_UART_SWAP=1 diff --git a/samples/Basic_Blink/component.mk b/samples/Basic_Blink/component.mk index ec6697542e..fcff50daf3 100644 --- a/samples/Basic_Blink/component.mk +++ b/samples/Basic_Blink/component.mk @@ -29,11 +29,11 @@ # COMPONENT_CFLAGS := # COMPONENT_CXXFLAGS := -## Configure flash parameters (for ESP12-E and other new boards): -# SPI_MODE := dio +## Configure hardware +# Default is 'standard' (no spiffs), can also provide your own +#HWCONFIG := spiffs -## SPIFFS options -DISABLE_SPIFFS := 1 +## Select source of content for default `spiffs` partition when built # SPIFF_FILES = files ## Refer to Basic_rBoot sample for options relating to rBoot diff --git a/samples/Basic_Capsense/component.mk b/samples/Basic_Capsense/component.mk index 4652678d1e..7b80efa885 100644 --- a/samples/Basic_Capsense/component.mk +++ b/samples/Basic_Capsense/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := CapacitiveSensor - -DISABLE_SPIFFS = 1 diff --git a/samples/Basic_DateTime/component.mk b/samples/Basic_DateTime/component.mk index 7df8c5b1c2..3596ab71d1 100644 --- a/samples/Basic_DateTime/component.mk +++ b/samples/Basic_DateTime/component.mk @@ -1,4 +1,2 @@ -DISABLE_SPIFFS = 1 - # Emulate UART 0 ENABLE_HOST_UARTID := 0 diff --git a/samples/Basic_Delegates/component.mk b/samples/Basic_Delegates/component.mk index 2e02dba4b6..7e4d03795a 100644 --- a/samples/Basic_Delegates/component.mk +++ b/samples/Basic_Delegates/component.mk @@ -1,2 +1 @@ -DISABLE_SPIFFS = 1 DEBUG_VERBOSE_LEVEL = 3 diff --git a/samples/Basic_HwPWM/component.mk b/samples/Basic_HwPWM/component.mk index 5794c096f3..81bf5015ae 100644 --- a/samples/Basic_HwPWM/component.mk +++ b/samples/Basic_HwPWM/component.mk @@ -1,4 +1,2 @@ -DISABLE_SPIFFS = 1 - # Uncomment the line below if you want to use Espressif's PWM library. #ENABLE_CUSTOM_PWM=0 diff --git a/samples/Basic_Interrupts/component.mk b/samples/Basic_Interrupts/component.mk deleted file mode 100644 index 973dfe71cf..0000000000 --- a/samples/Basic_Interrupts/component.mk +++ /dev/null @@ -1 +0,0 @@ -DISABLE_SPIFFS = 1 diff --git a/samples/Basic_NFC/component.mk b/samples/Basic_NFC/component.mk index 6de711f481..61d0cfc5e2 100644 --- a/samples/Basic_NFC/component.mk +++ b/samples/Basic_NFC/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := MFRC522 - -DISABLE_SPIFFS = 1 diff --git a/samples/Basic_Neopixel/component.mk b/samples/Basic_Neopixel/component.mk index 7728b3185e..379801d9b5 100644 --- a/samples/Basic_Neopixel/component.mk +++ b/samples/Basic_Neopixel/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := Adafruit_NeoPixel - -DISABLE_SPIFFS = 1 diff --git a/samples/Basic_ProgMem/component.mk b/samples/Basic_ProgMem/component.mk deleted file mode 100644 index 973dfe71cf..0000000000 --- a/samples/Basic_ProgMem/component.mk +++ /dev/null @@ -1 +0,0 @@ -DISABLE_SPIFFS = 1 diff --git a/samples/Basic_ScannerI2C/component.mk b/samples/Basic_ScannerI2C/component.mk index 973dfe71cf..e69de29bb2 100644 --- a/samples/Basic_ScannerI2C/component.mk +++ b/samples/Basic_ScannerI2C/component.mk @@ -1 +0,0 @@ -DISABLE_SPIFFS = 1 diff --git a/samples/Basic_Serial/component.mk b/samples/Basic_Serial/component.mk index ecfe972d0a..96def499d3 100644 --- a/samples/Basic_Serial/component.mk +++ b/samples/Basic_Serial/component.mk @@ -1,4 +1,4 @@ -SPIFF_SIZE ?= 65536 +HWCONFIG := spiffs CUSTOM_TARGETS += files/README.md @@ -10,3 +10,4 @@ files/README.md: $(SMING_HOME)/../README.md # Emulate both serial ports ENABLE_HOST_UARTID := 0 1 +HOST_NETWORK_OPTIONS := --pause diff --git a/samples/Basic_Servo/component.mk b/samples/Basic_Servo/component.mk index 08990693e8..b3fea3a944 100644 --- a/samples/Basic_Servo/component.mk +++ b/samples/Basic_Servo/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := Servo - -DISABLE_SPIFFS = 1 diff --git a/samples/Basic_SmartConfig/component.mk b/samples/Basic_SmartConfig/component.mk index 30a787416e..c24c9ac9d9 100644 --- a/samples/Basic_SmartConfig/component.mk +++ b/samples/Basic_SmartConfig/component.mk @@ -1,2 +1 @@ -DISABLE_SPIFFS = 1 ENABLE_SMART_CONFIG = 1 diff --git a/samples/Basic_Ssl/component.mk b/samples/Basic_Ssl/component.mk index ad42913baa..b994d8280f 100644 --- a/samples/Basic_Ssl/component.mk +++ b/samples/Basic_Ssl/component.mk @@ -1,12 +1,7 @@ -DISABLE_SPIFFS = 1 - # ifeq ($(ENABLE_MALLOC_COUNT),1) COMPONENT_DEPENDS += malloc_count COMPONENT_CXXFLAGS += -DENABLE_MALLOC_COUNT=1 endif -## size of the flash chip -SPI_SIZE ?= 4M - ENABLE_SSL = 1 diff --git a/samples/Basic_Storage/.cproject b/samples/Basic_Storage/.cproject new file mode 100644 index 0000000000..1c32c9a8a2 --- /dev/null +++ b/samples/Basic_Storage/.cproject @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + make + -f ${ProjDirPath}/Makefile + all + true + true + true + + + make + -f ${ProjDirPath}/Makefile + clean + true + true + true + + + make + -f ${ProjDirPath}/Makefile + flash + true + true + true + + + make + -f ${ProjDirPath}/Makefile + flashonefile + true + true + true + + + make + -f ${ProjDirPath}/Makefile + flashinit + true + true + true + + + make + -f ${ProjDirPath}/Makefile + flashboot + true + true + true + + + make + -f ${ProjDirPath}/Makefile + rebuild + true + true + true + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Basic_Storage/.project b/samples/Basic_Storage/.project new file mode 100644 index 0000000000..96b9a5cf41 --- /dev/null +++ b/samples/Basic_Storage/.project @@ -0,0 +1,28 @@ + + + Basic_Storage + + + Sming + + + + org.eclipse.cdt.managedbuilder.core.genmakebuilder + clean,full,incremental, + + + + + org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder + full,incremental, + + + + + + org.eclipse.cdt.core.cnature + org.eclipse.cdt.core.ccnature + org.eclipse.cdt.managedbuilder.core.managedBuildNature + org.eclipse.cdt.managedbuilder.core.ScannerConfigNature + + diff --git a/samples/Basic_Storage/Makefile b/samples/Basic_Storage/Makefile new file mode 100644 index 0000000000..ff51b6c3a7 --- /dev/null +++ b/samples/Basic_Storage/Makefile @@ -0,0 +1,9 @@ +##################################################################### +#### Please don't change this file. Use component.mk instead #### +##################################################################### + +ifndef SMING_HOME +$(error SMING_HOME is not set: please configure it as an environment variable) +endif + +include $(SMING_HOME)/project.mk diff --git a/samples/Basic_Storage/README.rst b/samples/Basic_Storage/README.rst new file mode 100644 index 0000000000..873299a0fd --- /dev/null +++ b/samples/Basic_Storage/README.rst @@ -0,0 +1,7 @@ +Basic Storage +============= + +This sample application demonstrates various ways to manage and access flash memory +using the :component:`Storage` system. + +It also shows how to create and partition custom storage devices. diff --git a/samples/Basic_Storage/app/application.cpp b/samples/Basic_Storage/app/application.cpp new file mode 100644 index 0000000000..97c40d6eb4 --- /dev/null +++ b/samples/Basic_Storage/app/application.cpp @@ -0,0 +1,99 @@ +#include +#include +#include +#include + +IMPORT_FSTR(FS_app, PROJECT_DIR "/app/application.cpp") + +void listSpiffsPartitions() +{ + Serial.println(_F("** Enumerate registered SPIFFS partitions")); + for(auto it = Storage::findPartition(Storage::Partition::SubType::Data::spiffs); it; ++it) { + Serial.print(F(">> Mounting '")); + Serial.print((*it).name()); + Serial.println("' ..."); + bool ok = spiffs_mount(*it); + Serial.println(ok ? "OK, listing files:" : "Mount failed!"); + if(ok) { + auto list = fileList(); + for(auto& f : fileList()) { + Serial.print(" "); + Serial.println(f); + } + Serial.print(list.count()); + Serial.println(F(" files found")); + Serial.println(); + } + } +} + +void printPart(Storage::Partition part) +{ + size_t bufSize = std::min(4096U, part.size()); + char buf[bufSize]; + OneShotFastUs timer; + if(!part.read(0, buf, bufSize)) { + debug_e("Error reading from partition '%s'", part.name().c_str()); + } else { + auto elapsed = timer.elapsedTime(); + String s = part.getDeviceName(); + s += '/'; + s += part.name(); + m_printHex(s.c_str(), buf, std::min(128U, bufSize)); + m_printf(_F("Elapsed: %s\r\n"), elapsed.toString().c_str()); + if(elapsed != 0) { + m_printf(_F("Speed: %u KB/s\r\n\r\n"), 1000 * bufSize / elapsed); + } + } + Serial.println(); +} + +void printPart(const String& partitionName) +{ + auto part = Storage::findPartition(partitionName); + if(!part) { + debug_e("Partition '%s' not found", partitionName.c_str()); + } else { + printPart(part); + } +} + +void init() +{ + Serial.begin(SERIAL_BAUD_RATE); + Serial.systemDebugOutput(true); + + listSpiffsPartitions(); + + printPart(F("user0")); + + auto part = Storage::findPartition(F("user1")); + printPart(part); + Serial.println(_F("Writing some stuff to partition...")); + String s = F("Some test stuff to write..."); + part.write(32, s.c_str(), s.length() + 1); + uint8_t buf[32]; + os_get_random(buf, sizeof buf); + part.write(64, buf, sizeof buf); + printPart(part); + + Serial.println(_F("** Reading tests, repeat 3 times to show effect of caching (if any)")); + + Serial.println(_F("** Reading SysMem device (flash)")); + part = Storage::sysMem.createPartition(F("fs_app"), FS_app, Storage::Partition::Type::data, 100); + printPart(part); + printPart(part); + printPart(part); + + Serial.println(_F("** Reading SysMem device (RAM)")); + part = Storage::sysMem.createPartition(F("fs_app"), FS_app, Storage::Partition::Type::data, 100); + printPart(part); + printPart(part); + printPart(part); + + Serial.println(_F("** Reading ProgMem device")); + part = Storage::progMem.createPartition(F("fs_app"), FS_app, Storage::Partition::Type::data, 100); + printPart(part); + printPart(part); + printPart(part); +} diff --git a/samples/Basic_Storage/basic_storage.hw b/samples/Basic_Storage/basic_storage.hw new file mode 100644 index 0000000000..c878ea9713 --- /dev/null +++ b/samples/Basic_Storage/basic_storage.hw @@ -0,0 +1,53 @@ +{ + "name": "Basic Storage sample", + "base_config": "spiffs", + "devices": { + "spiFlash": { + "mode": "qio", + "speed": 80 + } + }, + "partitions": { + "user0": { + "address": "0x1F0000", + "size": "16K", + "type": "user", + "subtype": 0, + "filename": "user0.bin" + }, + "user1": { + "address": "0x1F4000", + "size": "16K", + "type": "user", + "subtype": 1 + }, + "spiffs0": { + "address": "0x200000", + "build": { + "files": "files/spiffs0" + } + }, + "spiffs1": { + "address": "0x280000", + "size": "256K", + "type": "data", + "subtype": "spiffs", + "filename": "$(FW_BASE)/spiffs1_rom.bin", + "build": { + "target": "spiffsgen", + "files": "files/spiffs1" + } + }, + "spiffs2": { + "address": "0x2C0000", + "size": "256K", + "type": "data", + "subtype": "spiffs", + "filename": "$(FW_BASE)/spiffs2_rom.bin", + "build": { + "target": "spiffsgen", + "files": "files/spiffs2" + } + } + } +} \ No newline at end of file diff --git a/samples/Basic_Storage/component.mk b/samples/Basic_Storage/component.mk new file mode 100644 index 0000000000..9bb23018f7 --- /dev/null +++ b/samples/Basic_Storage/component.mk @@ -0,0 +1,4 @@ +# Use our custom hardware configuration +HWCONFIG := basic_storage + +HOST_NETWORK_OPTIONS := --nonet diff --git a/samples/Basic_Storage/files/spiffs0/file for spiffs0.txt b/samples/Basic_Storage/files/spiffs0/file for spiffs0.txt new file mode 100644 index 0000000000..1de357938e --- /dev/null +++ b/samples/Basic_Storage/files/spiffs0/file for spiffs0.txt @@ -0,0 +1,7 @@ +Basic Storage +============= + +This sample application demonstrates various ways to manage and access flash memory +using the :cpp:namespace:`Storage` API. + +It also shows how to create and partition custom storage devices. diff --git a/samples/Basic_Storage/files/spiffs1/file for spiffs1.txt b/samples/Basic_Storage/files/spiffs1/file for spiffs1.txt new file mode 100644 index 0000000000..1de357938e --- /dev/null +++ b/samples/Basic_Storage/files/spiffs1/file for spiffs1.txt @@ -0,0 +1,7 @@ +Basic Storage +============= + +This sample application demonstrates various ways to manage and access flash memory +using the :cpp:namespace:`Storage` API. + +It also shows how to create and partition custom storage devices. diff --git a/samples/Basic_Storage/files/spiffs2/file for spiffs2.txt b/samples/Basic_Storage/files/spiffs2/file for spiffs2.txt new file mode 100644 index 0000000000..1de357938e --- /dev/null +++ b/samples/Basic_Storage/files/spiffs2/file for spiffs2.txt @@ -0,0 +1,7 @@ +Basic Storage +============= + +This sample application demonstrates various ways to manage and access flash memory +using the :cpp:namespace:`Storage` API. + +It also shows how to create and partition custom storage devices. diff --git a/samples/Basic_Storage/user0.bin b/samples/Basic_Storage/user0.bin new file mode 100644 index 0000000000..2a8925f954 --- /dev/null +++ b/samples/Basic_Storage/user0.bin @@ -0,0 +1 @@ +This is some us \ No newline at end of file diff --git a/samples/Basic_Tasks/component.mk b/samples/Basic_Tasks/component.mk index 6599e8dede..ba03b6dbd5 100644 --- a/samples/Basic_Tasks/component.mk +++ b/samples/Basic_Tasks/component.mk @@ -1,4 +1,3 @@ -DISABLE_SPIFFS := 1 ARDUINO_LIBRARIES := ArduinoFFT SignalGenerator ENABLE_TASK_COUNT := 1 diff --git a/samples/Basic_Utility/component.mk b/samples/Basic_Utility/component.mk index 2c74f83110..bde909c92d 100644 --- a/samples/Basic_Utility/component.mk +++ b/samples/Basic_Utility/component.mk @@ -5,8 +5,5 @@ APP_NAME := utility # We don't need heap monitoring for utility applications ENABLE_MALLOC_COUNT := 0 -# Don't attempt to build any filesystem images -DISABLE_SPIFFS := 1 - # HOST_NETWORK_OPTIONS := --nonet diff --git a/samples/Basic_WebSkeletonApp/README.rst b/samples/Basic_WebSkeletonApp/README.rst index f6cc42df8b..f46ed941c7 100644 --- a/samples/Basic_WebSkeletonApp/README.rst +++ b/samples/Basic_WebSkeletonApp/README.rst @@ -34,6 +34,6 @@ To test this out, build the application without a filesystem image: .. code-block:: bash - make DISABLE_SPIFFS=1 + make HWCONFIG=standard ENABLE_FLASHSTRING_MAP=1 See *webserver.cpp* for the details. diff --git a/samples/Basic_WebSkeletonApp/app/application.cpp b/samples/Basic_WebSkeletonApp/app/application.cpp index 173a10f07f..187bd579bf 100644 --- a/samples/Basic_WebSkeletonApp/app/application.cpp +++ b/samples/Basic_WebSkeletonApp/app/application.cpp @@ -38,7 +38,9 @@ void init() Serial.systemDebugOutput(true); Serial.commandProcessing(false); +#ifndef ENABLE_FLASHSTRING_MAP spiffs_mount(); // Mount file system, in order to work with files +#endif //SET higher CPU freq & disable wifi sleep // system_update_cpu_freq(SYS_CPU_160MHZ); diff --git a/samples/Basic_WebSkeletonApp/app/webserver.cpp b/samples/Basic_WebSkeletonApp/app/webserver.cpp index 358dfb9884..5e19982205 100644 --- a/samples/Basic_WebSkeletonApp/app/webserver.cpp +++ b/samples/Basic_WebSkeletonApp/app/webserver.cpp @@ -9,7 +9,7 @@ namespace bool serverStarted = false; HttpServer server; -#if DISABLE_SPIFFS +#ifdef ENABLE_FLASHSTRING_MAP // If a filesystem image hasn't been provided, serve the files using a FlashString map #define FILE_LIST(XX) \ diff --git a/samples/Basic_WebSkeletonApp/component.mk b/samples/Basic_WebSkeletonApp/component.mk index 590d42b363..de15dbd41b 100644 --- a/samples/Basic_WebSkeletonApp/component.mk +++ b/samples/Basic_WebSkeletonApp/component.mk @@ -1,5 +1,9 @@ ARDUINO_LIBRARIES := ArduinoJson6 +HWCONFIG := spiffs -SPIFF_SIZE ?= 196608 - -DISABLE_SPIFFS ?= 0 +# Use to store files in a FlashString map object instead of SPIFFS +CONFIG_VARS += ENABLE_FLASHSTRING_MAP +ENABLE_FLASHSTRING_MAP ?= 0 +ifeq ($(ENABLE_FLASHSTRING_MAP),1) +COMPONENT_CXXFLAGS += -DENABLE_FLASHSTRING_MAP=1 +endif diff --git a/samples/Basic_WebSkeletonApp_LTS/component.mk b/samples/Basic_WebSkeletonApp_LTS/component.mk index e00a805aa3..8621049159 100644 --- a/samples/Basic_WebSkeletonApp_LTS/component.mk +++ b/samples/Basic_WebSkeletonApp_LTS/component.mk @@ -26,7 +26,8 @@ # Com port speed # COM_SPEED = 115200 -SPIFF_SIZE = 196608 +# +HWCONFIG = spiffs # ARDUINO_LIBRARIES := OneWire ArduinoJson5 diff --git a/samples/Basic_WiFi/component.mk b/samples/Basic_WiFi/component.mk deleted file mode 100644 index 973dfe71cf..0000000000 --- a/samples/Basic_WiFi/component.mk +++ /dev/null @@ -1 +0,0 @@ -DISABLE_SPIFFS = 1 diff --git a/samples/Basic_rBoot/README.rst b/samples/Basic_rBoot/README.rst index 68307498a1..2fdcbf7e2d 100644 --- a/samples/Basic_rBoot/README.rst +++ b/samples/Basic_rBoot/README.rst @@ -44,12 +44,20 @@ Technical Notes ``spiffs_mount_manual(address, length)`` must be called from init. +.. note:: + + This method is now deprecated. Please configure partitions appropriately, + use PartitionTable methods to locate the desired partition, then mount it:: + + auto part = PartitionTable().find('spiffs0'); + spiffs_mount(part); + + See :ref:`hardware-config` for further details. + Important compiler flags used: - BOOT_BIG_FLASH - when using big flash mode, ensures flash mapping code is built in to the rom. - RBOOT_INTEGRATION - ensures Sming specific options are pulled in to the rBoot source at compile time. -- SPIFF_SIZE=value - passed through to code for mounting the filesystem. - Also used in the Makefile to create the SPIFFS. Flash layout considerations --------------------------- @@ -58,12 +66,10 @@ If you want to use, for example, two 512k roms in the first 1MB block of flash (old style) then Sming will automatically create two separately linked roms. If you are flashing a single rom to multiple 1MB flash blocks, all using the same offset inside their 1MB blocks, only a single rom is created. -See the rBoot readme for further details. +See :component:`rboot` for further details. - If using a very small flash (e.g. 512k) there may be no room for a - spiffs fileystem, disable it with *DISABLE_SPIFFS = 1* -- If you are using spiffs set *RBOOT_SPIFFS_0* & *RBOOT_SPIFFS_1* to - indicate where the filesystems are located on the flash. + spiffs fileystem, so use *HWCONFIG = standard* - After building copy all the rom*.bin files to the root of your web server. diff --git a/samples/Basic_rBoot/app/application.cpp b/samples/Basic_rBoot/app/application.cpp index 945dc409fa..19d0a683c9 100644 --- a/samples/Basic_rBoot/app/application.cpp +++ b/samples/Basic_rBoot/app/application.cpp @@ -1,5 +1,4 @@ #include -#include #include // download urls, set appropriately @@ -13,19 +12,32 @@ #define WIFI_PWD "PleaseEnterPass" #endif -RbootHttpUpdater* otaUpdater = 0; +RbootHttpUpdater* otaUpdater; +Storage::Partition spiffsPartition; -void OtaUpdate_CallBack(RbootHttpUpdater& client, bool result) +Storage::Partition findSpiffsPartition(uint8_t slot) +{ + String name = F("spiffs"); + name += slot; + auto part = Storage::findPartition(name); + if(!part) { + debug_w("Partition '%s' not found", name.c_str()); + } + return part; +} + +void otaUpdateCallBack(RbootHttpUpdater& client, bool result) { Serial.println("In callback..."); if(result == true) { // success uint8 slot; slot = rboot_get_current_rom(); - if(slot == 0) + if(slot == 0) { slot = 1; - else + } else { slot = 0; + } // set to boot new rom and then reboot Serial.printf("Firmware updated, rebooting to rom %d...\r\n", slot); rboot_set_current_rom(slot); @@ -44,43 +56,38 @@ void OtaUpdate() Serial.println("Updating..."); // need a clean object, otherwise if run before and failed will not run again - if(otaUpdater) + if(otaUpdater) { delete otaUpdater; + } otaUpdater = new RbootHttpUpdater(); // select rom slot to flash bootconf = rboot_get_config(); slot = bootconf.current_rom; - if(slot == 0) + if(slot == 0) { slot = 1; - else + } else { slot = 0; + } #ifndef RBOOT_TWO_ROMS // flash rom to position indicated in the rBoot config rom table otaUpdater->addItem(bootconf.roms[slot], ROM_0_URL); #else - // flash appropriate rom - if(slot == 0) { - otaUpdater->addItem(bootconf.roms[slot], ROM_0_URL); - } else { - otaUpdater->addItem(bootconf.roms[slot], ROM_1_URL); - } + // flash appropriate ROM + otaUpdater->addItem(bootconf.roms[slot], (slot == 0) ? ROM_0_URL : ROM_1_URL); #endif -#if !DISABLE_SPIFFS - // use user supplied values (defaults for 4mb flash in makefile) - if(slot == 0) { - otaUpdater->addItem(RBOOT_SPIFFS_0, SPIFFS_URL); - } else { - otaUpdater->addItem(RBOOT_SPIFFS_1, SPIFFS_URL); + auto part = findSpiffsPartition(slot); + if(part) { + // use user supplied values (defaults for 4mb flash in hardware config) + otaUpdater->addItem(part.address(), SPIFFS_URL, part.size()); } -#endif // request switch and reboot on success //otaUpdater->switchToRom(slot); // and/or set a callback (called on failure or success without switching requested) - otaUpdater->setCallback(OtaUpdate_CallBack); + otaUpdater->setCallback(otaUpdateCallBack); // start update otaUpdater->start(); @@ -90,10 +97,11 @@ void Switch() { uint8 before, after; before = rboot_get_current_rom(); - if(before == 0) + if(before == 0) { after = 1; - else + } else { after = 0; + } Serial.printf("Swapping from rom %d to rom %d.\r\n", before, after); rboot_set_current_rom(after); Serial.println("Restarting...\r\n"); @@ -173,10 +181,10 @@ void serialCallBack(Stream& stream, char arrivedChar, unsigned short availableCh Serial.println(" switch - switch to the other rom and reboot"); Serial.println(" ota - perform ota update, switch rom and reboot"); Serial.println(" info - show esp8266 info"); -#if !DISABLE_SPIFFS - Serial.println(" ls - list files in spiffs"); - Serial.println(" cat - show first file in spiffs"); -#endif + if(spiffsPartition) { + Serial.println(" ls - list files in spiffs"); + Serial.println(" cat - show first file in spiffs"); + } Serial.println(); } else { Serial.println("unknown command"); @@ -190,28 +198,14 @@ void init() Serial.systemDebugOutput(true); // Debug output to serial // mount spiffs - int slot = rboot_get_current_rom(); -#if !DISABLE_SPIFFS - if(slot == 0) { -#ifdef RBOOT_SPIFFS_0 - debugf("trying to mount spiffs at 0x%08x, length %d", RBOOT_SPIFFS_0, SPIFF_SIZE); - spiffs_mount_manual(RBOOT_SPIFFS_0, SPIFF_SIZE); -#else - debugf("trying to mount spiffs at 0x%08x, length %d", 0x100000, SPIFF_SIZE); - spiffs_mount_manual(0x100000, SPIFF_SIZE); -#endif - } else { -#ifdef RBOOT_SPIFFS_1 - debugf("trying to mount spiffs at 0x%08x, length %d", RBOOT_SPIFFS_1, SPIFF_SIZE); - spiffs_mount_manual(RBOOT_SPIFFS_1, SPIFF_SIZE); -#else - debugf("trying to mount spiffs at 0x%08x, length %d", 0x300000, SPIFF_SIZE); - spiffs_mount_manual(0x300000, SPIFF_SIZE); -#endif + auto slot = rboot_get_current_rom(); + spiffsPartition = findSpiffsPartition(slot); + if(spiffsPartition) { + debugf("trying to mount '%s' at 0x%08x, length %d", spiffsPartition.name().c_str(), spiffsPartition.address(), + spiffsPartition.size()); + spiffs_mount(spiffsPartition); } -#else - debugf("spiffs disabled"); -#endif + WifiAccessPoint.enable(false); Serial.printf("\r\nCurrently running rom %d.\r\n", slot); diff --git a/samples/Basic_rBoot/basic_rboot.hw b/samples/Basic_rBoot/basic_rboot.hw new file mode 100644 index 0000000000..64465e1b3a --- /dev/null +++ b/samples/Basic_rBoot/basic_rboot.hw @@ -0,0 +1,19 @@ +{ + "name": "Two ROM slots, two SPIFFS", + "base_config": "spiffs", + "partitions": { + "rom1": { + "address": "0x108000", + "size": "992K", + "type": "app", + "subtype": "ota_0", + "filename": "$(RBOOT_ROM_1_BIN)" + }, + "spiffs1": { + "address": "0x300000", + "size": "512K", + "type": "data", + "subtype": "spiffs" + } + } +} diff --git a/samples/Basic_rBoot/component.mk b/samples/Basic_rBoot/component.mk index 1574232c70..d77a3c8c2f 100644 --- a/samples/Basic_rBoot/component.mk +++ b/samples/Basic_rBoot/component.mk @@ -1,31 +1,13 @@ #### overridable rBoot options #### ## use rboot build mode -RBOOT_ENABLED ?= 1 - -## size of the flash chip -SPI_SIZE ?= 4M - -## address of ROM slots 0 & 1 -#RBOOT_ROM0_ADDR ?= 0x002000 -RBOOT_ROM1_ADDR ?= 0x102000 - -## output file for first rom (.bin will be appended) -#RBOOT_ROM_0 ?= rom0 - -## these next options only needed when using two rom mode -#RBOOT_ROM_1 ?= rom1 - -## size of the spiffs to create -SPIFF_SIZE ?= 65536 - -## option to completely disable spiffs -#DISABLE_SPIFFS ?= 1 - -## flash offsets for spiffs, set if using two rom mode or not on a 4mb flash -## (spiffs location defaults to the mb after the rom slot on 4mb flash) -#RBOOT_SPIFFS_0 ?= 0x100000 -#RBOOT_SPIFFS_1 ?= 0x300000 +RBOOT_ENABLED := 1 +## Use standard hardware config with two ROM slots and two SPIFFS partitions +ifeq ($(SMING_ARCH),Esp8266) +HWCONFIG := basic_rboot +else +HWCONFIG := spiffs # Emulate UART 0 ENABLE_HOST_UARTID := 0 +endif diff --git a/samples/CanBus/component.mk b/samples/CanBus/component.mk index 5db38ef1f8..668373c3fc 100644 --- a/samples/CanBus/component.mk +++ b/samples/CanBus/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := MCP_CAN_lib - -DISABLE_SPIFFS = 1 diff --git a/samples/CommandProcessing_Debug/component.mk b/samples/CommandProcessing_Debug/component.mk index 973dfe71cf..298d44e80b 100644 --- a/samples/CommandProcessing_Debug/component.mk +++ b/samples/CommandProcessing_Debug/component.mk @@ -1 +1 @@ -DISABLE_SPIFFS = 1 +HWCONFIG := spiffs diff --git a/samples/Compass_HMC5883L/component.mk b/samples/Compass_HMC5883L/component.mk index 763fcb3a77..bfebb2a7dc 100644 --- a/samples/Compass_HMC5883L/component.mk +++ b/samples/Compass_HMC5883L/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := HMC5883L - -DISABLE_SPIFFS = 1 diff --git a/samples/DFPlayerMini/component.mk b/samples/DFPlayerMini/component.mk index ccda75fa45..fe1938596e 100644 --- a/samples/DFPlayerMini/component.mk +++ b/samples/DFPlayerMini/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := DFRobotDFPlayerMini - -DISABLE_SPIFFS = 1 diff --git a/samples/DS3232RTC_NTP_Setter/component.mk b/samples/DS3232RTC_NTP_Setter/component.mk index 4534b60e67..9f593e41f9 100644 --- a/samples/DS3232RTC_NTP_Setter/component.mk +++ b/samples/DS3232RTC_NTP_Setter/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := DS3232RTC - -DISABLE_SPIFFS = 1 diff --git a/samples/Display_TM1637/component.mk b/samples/Display_TM1637/component.mk index 1f36cbd482..76b8ba3655 100644 --- a/samples/Display_TM1637/component.mk +++ b/samples/Display_TM1637/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := TM1637 - -DISABLE_SPIFFS = 1 diff --git a/samples/Distance_Vl53l0x/component.mk b/samples/Distance_Vl53l0x/component.mk index d61fceb68e..a944fc3b10 100644 --- a/samples/Distance_Vl53l0x/component.mk +++ b/samples/Distance_Vl53l0x/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := Adafruit_VL53L0X - -DISABLE_SPIFFS = 1 diff --git a/samples/DnsCaptivePortal/component.mk b/samples/DnsCaptivePortal/component.mk deleted file mode 100644 index 973dfe71cf..0000000000 --- a/samples/DnsCaptivePortal/component.mk +++ /dev/null @@ -1 +0,0 @@ -DISABLE_SPIFFS = 1 diff --git a/samples/Echo_Ssl/component.mk b/samples/Echo_Ssl/component.mk index 1e5eedd2a0..ae82bad52a 100644 --- a/samples/Echo_Ssl/component.mk +++ b/samples/Echo_Ssl/component.mk @@ -1,6 +1 @@ -DISABLE_SPIFFS = 1 - -## size of the flash chip -SPI_SIZE ?= 4M - ENABLE_SSL = 1 diff --git a/samples/FtpServer_Files/component.mk b/samples/FtpServer_Files/component.mk new file mode 100644 index 0000000000..ab4cb6eed9 --- /dev/null +++ b/samples/FtpServer_Files/component.mk @@ -0,0 +1,2 @@ +HWCONFIG := spiffs +SPIFF_FILES := diff --git a/samples/Gesture_APDS-9960/component.mk b/samples/Gesture_APDS-9960/component.mk index 29be2f65a4..6dc722623d 100644 --- a/samples/Gesture_APDS-9960/component.mk +++ b/samples/Gesture_APDS-9960/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := SparkFun_APDS9960 - -DISABLE_SPIFFS = 1 diff --git a/samples/HttpClient/component.mk b/samples/HttpClient/component.mk index f3ee8603fd..b5818865f6 100644 --- a/samples/HttpClient/component.mk +++ b/samples/HttpClient/component.mk @@ -1,5 +1,4 @@ -## size of the flash chip -SPI_SIZE ?= 4M +HWCONFIG = spiffs ## Prefer BearSSL as it can handle more gracefully big SSL packets. ENABLE_SSL ?= Bearssl diff --git a/samples/HttpClient_Instapush/component.mk b/samples/HttpClient_Instapush/component.mk index c304e7f8f4..8786c2333d 100644 --- a/samples/HttpClient_Instapush/component.mk +++ b/samples/HttpClient_Instapush/component.mk @@ -1,2 +1,3 @@ -DISABLE_SPIFFS = 1 +HWCONFIG := spiffs +SPIFF_FILES := ARDUINO_LIBRARIES := ArduinoJson6 diff --git a/samples/HttpClient_ThingSpeak/component.mk b/samples/HttpClient_ThingSpeak/component.mk index 973dfe71cf..ab4cb6eed9 100644 --- a/samples/HttpClient_ThingSpeak/component.mk +++ b/samples/HttpClient_ThingSpeak/component.mk @@ -1 +1,2 @@ -DISABLE_SPIFFS = 1 +HWCONFIG := spiffs +SPIFF_FILES := diff --git a/samples/HttpServer_AJAX/component.mk b/samples/HttpServer_AJAX/component.mk index 6e7cdbc0c9..50337d80c6 100644 --- a/samples/HttpServer_AJAX/component.mk +++ b/samples/HttpServer_AJAX/component.mk @@ -1,2 +1,3 @@ +HWCONFIG := spiffs SPIFF_FILES = web/build ARDUINO_LIBRARIES := ArduinoJson6 diff --git a/samples/HttpServer_Bootstrap/component.mk b/samples/HttpServer_Bootstrap/component.mk new file mode 100644 index 0000000000..ab4cb6eed9 --- /dev/null +++ b/samples/HttpServer_Bootstrap/component.mk @@ -0,0 +1,2 @@ +HWCONFIG := spiffs +SPIFF_FILES := diff --git a/samples/HttpServer_ConfigNetwork/component.mk b/samples/HttpServer_ConfigNetwork/component.mk index ef3cde28e8..2574c69f39 100644 --- a/samples/HttpServer_ConfigNetwork/component.mk +++ b/samples/HttpServer_ConfigNetwork/component.mk @@ -1,3 +1,4 @@ +HWCONFIG := spiffs SPIFF_FILES = web/build ARDUINO_LIBRARIES := ArduinoJson6 diff --git a/samples/HttpServer_FirmwareUpload/README.rst b/samples/HttpServer_FirmwareUpload/README.rst index c483906a07..1a6be3e966 100644 --- a/samples/HttpServer_FirmwareUpload/README.rst +++ b/samples/HttpServer_FirmwareUpload/README.rst @@ -46,15 +46,7 @@ documentation for further advice on how to use the security features properly. Usage instructions ------------------ - -1. Configure your flash memory layout: - - - Set :envvar:`SPI_SIZE` to the flash memory size of your device. - - If necessary, modify :envvar:`RBOOT_ROM0_ADDR`, :envvar:`RBOOT_ROM1_ADDR`, - :envvar:`RBOOT_SPIFFS_0` and :envvar:`SPIFF_SIZE` to fit both ROM slots and - the file system into the available flash memory. Make sure that the - flash areas do not overlap with each other or any the reserved regions. - Refer to the :component:`rboot` documentation for further details. +1. Configure your flash memory layout. See :ref:`hardware_config`. 2. Build the example by running:: diff --git a/samples/HttpServer_FirmwareUpload/component.mk b/samples/HttpServer_FirmwareUpload/component.mk index 66466ad9b0..5bba481389 100644 --- a/samples/HttpServer_FirmwareUpload/component.mk +++ b/samples/HttpServer_FirmwareUpload/component.mk @@ -1,3 +1,4 @@ +HWCONFIG := spiffs SPIFF_FILES = web/ ARDUINO_LIBRARIES := MultipartParser OtaUpgrade diff --git a/samples/HttpServer_WebSockets/component.mk b/samples/HttpServer_WebSockets/component.mk new file mode 100644 index 0000000000..298d44e80b --- /dev/null +++ b/samples/HttpServer_WebSockets/component.mk @@ -0,0 +1 @@ +HWCONFIG := spiffs diff --git a/samples/Humidity_AM2321/component.mk b/samples/Humidity_AM2321/component.mk index ed804579fb..a1e9eff658 100644 --- a/samples/Humidity_AM2321/component.mk +++ b/samples/Humidity_AM2321/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := AM2321 - -DISABLE_SPIFFS = 1 diff --git a/samples/Humidity_DHT22/component.mk b/samples/Humidity_DHT22/component.mk index da47456b52..6a0e948fe4 100644 --- a/samples/Humidity_DHT22/component.mk +++ b/samples/Humidity_DHT22/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := DHTesp - -DISABLE_SPIFFS = 1 diff --git a/samples/Humidity_SI7021/component.mk b/samples/Humidity_SI7021/component.mk index e01c1c0c49..d570b9b3f4 100644 --- a/samples/Humidity_SI7021/component.mk +++ b/samples/Humidity_SI7021/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := SI7021 - -DISABLE_SPIFFS = 1 diff --git a/samples/IR_lib/component.mk b/samples/IR_lib/component.mk index 10e306665b..bf1221327c 100644 --- a/samples/IR_lib/component.mk +++ b/samples/IR_lib/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := IR - -DISABLE_SPIFFS = 1 diff --git a/samples/LED_WS2812/component.mk b/samples/LED_WS2812/component.mk index c3b9fd7313..19ece9de25 100644 --- a/samples/LED_WS2812/component.mk +++ b/samples/LED_WS2812/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := WS2812 - -DISABLE_SPIFFS = 1 diff --git a/samples/LED_YeelightBulb/component.mk b/samples/LED_YeelightBulb/component.mk index 8b99c0071c..68b70f2b6b 100644 --- a/samples/LED_YeelightBulb/component.mk +++ b/samples/LED_YeelightBulb/component.mk @@ -1,2 +1 @@ -DISABLE_SPIFFS = 1 ARDUINO_LIBRARIES := Yeelight diff --git a/samples/Light_BH1750/component.mk b/samples/Light_BH1750/component.mk index 0ff62be148..7f0ae9c735 100644 --- a/samples/Light_BH1750/component.mk +++ b/samples/Light_BH1750/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := BH1750FVI - -DISABLE_SPIFFS = 1 diff --git a/samples/LiquidCrystal_44780/component.mk b/samples/LiquidCrystal_44780/component.mk index 2eaa53c03c..1424c543b2 100644 --- a/samples/LiquidCrystal_44780/component.mk +++ b/samples/LiquidCrystal_44780/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := LiquidCrystal - -DISABLE_SPIFFS = 1 diff --git a/samples/LiveDebug/component.mk b/samples/LiveDebug/component.mk index f82ee2d9d9..e54600cb3c 100644 --- a/samples/LiveDebug/component.mk +++ b/samples/LiveDebug/component.mk @@ -1,5 +1,3 @@ -DISABLE_SPIFFS = 1 - ENABLE_GDB = 1 ENABLE_GDB_CONSOLE ?= 1 diff --git a/samples/MeteoControl/component.mk b/samples/MeteoControl/component.mk index 253cf6cb30..b847e6d1b0 100644 --- a/samples/MeteoControl/component.mk +++ b/samples/MeteoControl/component.mk @@ -1,3 +1,4 @@ ARDUINO_LIBRARIES := LiquidCrystal DHTesp ArduinoJson6 +HWCONFIG := spiffs SPIFF_FILES = web diff --git a/samples/MeteoControl_mqtt/component.mk b/samples/MeteoControl_mqtt/component.mk index bdb4b87fa1..5b00021804 100644 --- a/samples/MeteoControl_mqtt/component.mk +++ b/samples/MeteoControl_mqtt/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := BMP180 SI7021 - -DISABLE_SPIFFS = 1 diff --git a/samples/MqttClient_Hello/component.mk b/samples/MqttClient_Hello/component.mk index 0b3208d9a7..10ef45cc95 100644 --- a/samples/MqttClient_Hello/component.mk +++ b/samples/MqttClient_Hello/component.mk @@ -1,8 +1,5 @@ -DISABLE_SPIFFS = 1 +HWCONFIG := standard-4m ifdef MQTT_URL USER_CFLAGS += -DMQTT_URL=\"$(MQTT_URL)\" endif - -## size of the flash chip -SPI_SIZE ?= 4M diff --git a/samples/Nextion_Button/component.mk b/samples/Nextion_Button/component.mk index c7cc0b302c..538e725470 100644 --- a/samples/Nextion_Button/component.mk +++ b/samples/Nextion_Button/component.mk @@ -1,5 +1,3 @@ ARDUINO_LIBRARIES := ITEADLIB_Arduino_Nextion -DISABLE_SPIFFS = 1 - ENABLE_NEXTION = 1 diff --git a/samples/PortExpander_MCP23017/component.mk b/samples/PortExpander_MCP23017/component.mk index e59988fa90..decdab0b14 100644 --- a/samples/PortExpander_MCP23017/component.mk +++ b/samples/PortExpander_MCP23017/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := MCP23017 - -DISABLE_SPIFFS = 1 diff --git a/samples/Pressure_BMP180/component.mk b/samples/Pressure_BMP180/component.mk index edad90d026..bc6de35d78 100644 --- a/samples/Pressure_BMP180/component.mk +++ b/samples/Pressure_BMP180/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := BMP180 - -DISABLE_SPIFFS = 1 diff --git a/samples/Radio_RCSwitch/component.mk b/samples/Radio_RCSwitch/component.mk index 664aaee609..cd2c1300e9 100644 --- a/samples/Radio_RCSwitch/component.mk +++ b/samples/Radio_RCSwitch/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := RCSwitch - -DISABLE_SPIFFS = 1 diff --git a/samples/Radio_nRF24L01/component.mk b/samples/Radio_nRF24L01/component.mk index 5b82a43543..9a478c319a 100644 --- a/samples/Radio_nRF24L01/component.mk +++ b/samples/Radio_nRF24L01/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := RF24 - -DISABLE_SPIFFS = 1 diff --git a/samples/Radio_si4432/component.mk b/samples/Radio_si4432/component.mk index 2ad241b2cc..fdf6ee7962 100644 --- a/samples/Radio_si4432/component.mk +++ b/samples/Radio_si4432/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := si4432 - -DISABLE_SPIFFS = 1 diff --git a/samples/SDCard/component.mk b/samples/SDCard/component.mk index 0811609ed1..e93d57393d 100644 --- a/samples/SDCard/component.mk +++ b/samples/SDCard/component.mk @@ -1,2 +1 @@ ARDUINO_LIBRARIES := SDCard -DISABLE_SPIFFS := 1 diff --git a/samples/ScreenLCD_5110/component.mk b/samples/ScreenLCD_5110/component.mk index 9e06efca69..2855fb3158 100644 --- a/samples/ScreenLCD_5110/component.mk +++ b/samples/ScreenLCD_5110/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := Adafruit_PCD8544 - -DISABLE_SPIFFS = 1 diff --git a/samples/ScreenOLED_SSD1306/component.mk b/samples/ScreenOLED_SSD1306/component.mk index c30fac01ee..72278b67fd 100644 --- a/samples/ScreenOLED_SSD1306/component.mk +++ b/samples/ScreenOLED_SSD1306/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := Adafruit_SSD1306 - -DISABLE_SPIFFS = 1 diff --git a/samples/ScreenTFT_ILI9163C/component.mk b/samples/ScreenTFT_ILI9163C/component.mk index 9494698445..a6c5efa5b8 100644 --- a/samples/ScreenTFT_ILI9163C/component.mk +++ b/samples/ScreenTFT_ILI9163C/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := TFT_ILI9163C - -DISABLE_SPIFFS = 1 diff --git a/samples/ScreenTFT_ILI9340-ILI9341/component.mk b/samples/ScreenTFT_ILI9340-ILI9341/component.mk index 77706c8ff2..29bda5a46a 100644 --- a/samples/ScreenTFT_ILI9340-ILI9341/component.mk +++ b/samples/ScreenTFT_ILI9340-ILI9341/component.mk @@ -1 +1,2 @@ +HWCONFIG := spiffs ARDUINO_LIBRARIES := Adafruit_ILI9341 diff --git a/samples/ScreenTFT_ST7735/component.mk b/samples/ScreenTFT_ST7735/component.mk index 444739bbdd..cb9740b8f7 100644 --- a/samples/ScreenTFT_ST7735/component.mk +++ b/samples/ScreenTFT_ST7735/component.mk @@ -1 +1,2 @@ +HWCONFIG := spiffs ARDUINO_LIBRARIES := Adafruit_ST7735 diff --git a/samples/SmtpClient/component.mk b/samples/SmtpClient/component.mk index 738d384dcc..943f6630c9 100644 --- a/samples/SmtpClient/component.mk +++ b/samples/SmtpClient/component.mk @@ -1 +1,2 @@ +HWCONFIG := spiffs ENABLE_SSL ?= 1 diff --git a/samples/SystemClock_NTP/component.mk b/samples/SystemClock_NTP/component.mk index 5e0ebcaf7f..2d668a76f1 100644 --- a/samples/SystemClock_NTP/component.mk +++ b/samples/SystemClock_NTP/component.mk @@ -1,5 +1,3 @@ -DISABLE_SPIFFS = 1 - ARDUINO_LIBRARIES = \ Timezone \ SolarCalculator diff --git a/samples/TcpClient_NarodMon/component.mk b/samples/TcpClient_NarodMon/component.mk deleted file mode 100644 index 973dfe71cf..0000000000 --- a/samples/TcpClient_NarodMon/component.mk +++ /dev/null @@ -1 +0,0 @@ -DISABLE_SPIFFS = 1 diff --git a/samples/Telnet_TCPServer_TCPClient/component.mk b/samples/Telnet_TCPServer_TCPClient/component.mk deleted file mode 100644 index 973dfe71cf..0000000000 --- a/samples/Telnet_TCPServer_TCPClient/component.mk +++ /dev/null @@ -1 +0,0 @@ -DISABLE_SPIFFS = 1 diff --git a/samples/Temperature_DS1820/component.mk b/samples/Temperature_DS1820/component.mk index 087c1b4647..d5d59373b7 100644 --- a/samples/Temperature_DS1820/component.mk +++ b/samples/Temperature_DS1820/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := DS18S20 - -DISABLE_SPIFFS = 1 diff --git a/samples/UdpServer_Echo/component.mk b/samples/UdpServer_Echo/component.mk deleted file mode 100644 index 973dfe71cf..0000000000 --- a/samples/UdpServer_Echo/component.mk +++ /dev/null @@ -1 +0,0 @@ -DISABLE_SPIFFS = 1 diff --git a/samples/UdpServer_mDNS/component.mk b/samples/UdpServer_mDNS/component.mk index 75719a495d..ed2eaeb314 100644 --- a/samples/UdpServer_mDNS/component.mk +++ b/samples/UdpServer_mDNS/component.mk @@ -1,2 +1,3 @@ -DISABLE_SPIFFS = 1 +HWCONFIG := spiffs +SPIFF_FILES := COMPONENT_DEPENDS += mdns diff --git a/samples/Ultrasonic_HCSR04/component.mk b/samples/Ultrasonic_HCSR04/component.mk index c21f2d42fa..793a94abd4 100644 --- a/samples/Ultrasonic_HCSR04/component.mk +++ b/samples/Ultrasonic_HCSR04/component.mk @@ -1,3 +1 @@ ARDUINO_LIBRARIES := Ultrasonic - -DISABLE_SPIFFS = 1 diff --git a/samples/Websocket_Client/component.mk b/samples/Websocket_Client/component.mk index a2dcdb1632..38bc642725 100644 --- a/samples/Websocket_Client/component.mk +++ b/samples/Websocket_Client/component.mk @@ -1,7 +1,4 @@ -DISABLE_SPIFFS = 1 +HWCONFIG := standard-4m # Uncomment the option below if you want SSL support #ENABLE_SSL=1 - -## size of the flash chip -SPI_SIZE ?= 4M diff --git a/samples/Wifi_Sniffer/component.mk b/samples/Wifi_Sniffer/component.mk deleted file mode 100644 index 973dfe71cf..0000000000 --- a/samples/Wifi_Sniffer/component.mk +++ /dev/null @@ -1 +0,0 @@ -DISABLE_SPIFFS = 1 diff --git a/tests/HostTests/component.mk b/tests/HostTests/component.mk index 136f4813d1..284b9f0aab 100644 --- a/tests/HostTests/component.mk +++ b/tests/HostTests/component.mk @@ -1,6 +1,5 @@ -DISABLE_SPIFFS = 1 +HWCONFIG = host-tests DEBUG_VERBOSE_LEVEL = 2 -SPI_SIZE = 4M COMPONENT_INCDIRS := include COMPONENT_SRCDIRS := app modules diff --git a/tests/HostTests/host-tests.hw b/tests/HostTests/host-tests.hw new file mode 100644 index 0000000000..954c4865ff --- /dev/null +++ b/tests/HostTests/host-tests.hw @@ -0,0 +1,42 @@ +{ + "name": "Host Tests profile", + "base_config": "spiffs", + "devices": { + "testDevice": { + "type": "spiram", + "size": "0x40000000" + } + }, + "partitions": { + "spiffs0": { + "size": "0x10000", + "filename": "" + }, + "external1": { + "device": "testDevice", + "address": 0, + "size": "4M", + "type": "data", + "subtype": "spiffs", + "filename": "$(FW_BASE)/test-spiffs.bin", + "build": { + "target": "spiffsgen", + "files": "resource" + } + }, + "external2": { + "device": "testDevice", + "address": "0x600000", + "size": "240K", + "type": "data", + "subtype": 37 + }, + "external3": { + "device": "testDevice", + "address": "0x800000", + "size": "240M", + "type": "data", + "subtype": "nvs" + } + } +} \ No newline at end of file diff --git a/tests/HostTests/include/modules.h b/tests/HostTests/include/modules.h index ba9bfc2520..97583b28b3 100644 --- a/tests/HostTests/include/modules.h +++ b/tests/HostTests/include/modules.h @@ -19,6 +19,7 @@ XX(Url) \ XX(ArduinoJson5) \ XX(ArduinoJson6) \ + XX(Storage) \ XX(Files) \ XX(SpiFlash) \ XX(Spiffs) \ diff --git a/tests/HostTests/modules/ArduinoJson6.cpp b/tests/HostTests/modules/ArduinoJson6.cpp index 8dbc74d63b..9c3690045b 100644 --- a/tests/HostTests/modules/ArduinoJson6.cpp +++ b/tests/HostTests/modules/ArduinoJson6.cpp @@ -292,15 +292,17 @@ class JsonTest6 : public TestGroup loadStream(fs); } - void speedChecks() + template void check(T func) { -#define CHECK(func) \ - for(unsigned i = 0; i < 4; ++i) { \ - ElapseTimer timer; \ - func; \ - debug_i("Time: %s", timer.elapsedTime().toString().c_str()); \ + for(unsigned i = 0; i < 4; ++i) { + ElapseTimer timer; + func(); + debug_i("Time: %s", timer.elapsedTime().toString().c_str()); + } } + void speedChecks() + { PSTR_ARRAY(filename, "test.json"); if(!fileExist(filename)) { FileStream fs(filename, eFO_CreateNewAlways | eFO_WriteOnly); @@ -312,28 +314,28 @@ class JsonTest6 : public TestGroup TEST_CASE("loadBuffer") { LOAD_FSTR(buffer, Resource::test_json); - CHECK(loadBuffer(buffer, Resource::test_json.length())); + check([&] { loadBuffer(buffer, Resource::test_json.length()); }); } TEST_CASE("loadFlashString") { - CHECK(loadFlashString()); + check([&] { loadFlashString(); }); } TEST_CASE("loadFlashString via Stream (cached)") { - CHECK(loadFlashStringViaStream(false)); + check([&] { loadFlashStringViaStream(false); }); } TEST_CASE("loadFlashString via Stream (un-cached)") { - CHECK(loadFlashStringViaStream(true)); + check([&] { loadFlashStringViaStream(true); }); } TEST_CASE("loadFile") { - CHECK(loadFile(filename)); + check([&] { loadFile(filename); }); } TEST_CASE("loadStream") { FileStream fs(filename); - CHECK(loadStream(fs)); + check([&]() { loadStream(fs); }); } } diff --git a/tests/HostTests/modules/Spiffs.cpp b/tests/HostTests/modules/Spiffs.cpp index f7bdf9b98a..67e73475ee 100644 --- a/tests/HostTests/modules/Spiffs.cpp +++ b/tests/HostTests/modules/Spiffs.cpp @@ -1,5 +1,5 @@ #include -#include +#include class SpiffsTest : public TestGroup { @@ -32,7 +32,7 @@ class SpiffsTest : public TestGroup DEFINE_FSTR_LOCAL(testFile, "testfile"); DEFINE_FSTR_LOCAL(testContent, "Some test content to write to a file"); - auto cfg = spiffs_get_storage_config(); + auto part = *Storage::findPartition(Storage::Partition::SubType::Data::spiffs); // Write to filesystem until sector #0 gets erased unsigned writeCount = 0; @@ -44,8 +44,8 @@ class SpiffsTest : public TestGroup break; } ++writeCount; - if(flashmem_read(&word0, cfg.phys_addr, sizeof(word0)) != sizeof(word0)) { - debug_e("flashmem_read failed"); + if(!part.read(0, word0)) { + debug_e("part.read() failed"); TEST_ASSERT(false); break; } diff --git a/tests/HostTests/modules/Storage.cpp b/tests/HostTests/modules/Storage.cpp new file mode 100644 index 0000000000..238af5076b --- /dev/null +++ b/tests/HostTests/modules/Storage.cpp @@ -0,0 +1,110 @@ +#include +#include + +class TestDevice : public Storage::Device +{ +public: + String getName() const override + { + return F("testDevice"); + } + + size_t getBlockSize() const override + { + return sizeof(uint32_t); + } + + size_t getSize() const override + { + return 0x40000000; + } + + Type getType() const override + { + return Type::unknown; + } + + bool read(uint32_t address, void* dst, size_t size) override + { + for(unsigned i = 0; i < size; ++i) { + static_cast(dst)[i] = address + i; + } + return true; + } + + bool write(uint32_t address, const void* src, size_t size) override + { + return false; + } + + bool erase_range(uint32_t address, size_t size) override + { + return false; + } +}; + +class PartitionTest : public TestGroup +{ +public: + PartitionTest() : TestGroup(_F("Partition")) + { + } + + void execute() override + { + auto dev = new TestDevice; + Storage::registerDevice(dev); + + listPartitions(); + + delete dev; + } + + void listPartitions() + { + for(auto it = Storage::findPartition(); it; ++it) { + auto part = *it; + + Serial.println(); + Serial.print(_F("Device '")); + Serial.print(part.getDeviceName()); + Serial.print(_F("', partition '")); + Serial.print(part.name()); + Serial.print(_F("', type ")); + Serial.print(int(part.type())); + Serial.print('/'); + Serial.print(int(part.subType())); + Serial.print(" ("); + Serial.print(part.longTypeString()); + Serial.print(_F("), address 0x")); + Serial.print(part.address(), HEX); + Serial.print(_F(", size 0x")); + Serial.print(part.size(), HEX); + Serial.println(); + + testRead(part, 0xE0, 0x20, true); + testRead(part, 10, 20, true); + testRead(part, part.size() - 10, 10, true); + testRead(part, part.size() - 10, 11, false); + } + } + + void testRead(Storage::Partition& part, uint32_t address, size_t size, bool shouldSucceed) + { + auto buf = new uint8_t[size]; + bool success = part.read(address, buf, size); + if(success) { + m_printHex("READ", buf, size, address); + REQUIRE(shouldSucceed == true); + } else { + debug_e("read(0x%08x, %u) failed", address, size); + REQUIRE(shouldSucceed == false); + } + delete[] buf; + } +}; + +void REGISTER_TEST(Storage) +{ + registerGroup(); +} diff --git a/tests/SharedComponent/Project/component.mk b/tests/SharedComponent/Project/component.mk index 9aaec6713e..80934857ab 100644 --- a/tests/SharedComponent/Project/component.mk +++ b/tests/SharedComponent/Project/component.mk @@ -1,12 +1,7 @@ ## Local build configuration ## Parameters configured here will override default and ENV values. -## SPIFFS options -DISABLE_SPIFFS = 1 -# SPIFF_FILES = files - DEBUG_VERBOSE_LEVEL = 3 -SPI_SIZE = 4M # Tell Sming about any Components we need COMPONENT_DEPENDS := shared-test From d9e8c9c977c39b93f198b59be720275fe8adf0ee Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 9 Feb 2021 09:35:15 +0000 Subject: [PATCH 12/91] Storage fixes (#2216) * HOST_PARAMETERS missing from valgrind and gdb cmdline * RingTone sample requires HWCONFIG=spiffs * Storage missing nullptr check * Wrong device name in error message in `Device::loadPartitions` --- Sming/Arch/Host/Components/gdbstub/component.mk | 2 +- Sming/Arch/Host/app.mk | 2 +- Sming/Components/Storage/src/Device.cpp | 2 +- Sming/Components/Storage/src/ObjectList.cpp | 2 +- Sming/Libraries/RingTone | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sming/Arch/Host/Components/gdbstub/component.mk b/Sming/Arch/Host/Components/gdbstub/component.mk index d2f160a6f7..46fd3ee841 100644 --- a/Sming/Arch/Host/Components/gdbstub/component.mk +++ b/Sming/Arch/Host/Components/gdbstub/component.mk @@ -3,4 +3,4 @@ DEBUG_VARS += GDBSTUB_DIR GDBSTUB_DIR := $(COMPONENT_PATH) CACHE_VARS += GDB_CMDLINE -GDB_CMDLINE = trap '' INT; $(GDB) -x $(GDBSTUB_DIR)/gdbcmds --args $(TARGET_OUT_0) $(CLI_TARGET_OPTIONS) --pause +GDB_CMDLINE = trap '' INT; $(GDB) -x $(GDBSTUB_DIR)/gdbcmds --args $(TARGET_OUT_0) $(CLI_TARGET_OPTIONS) --pause -- $(HOST_PARAMETERS) diff --git a/Sming/Arch/Host/app.mk b/Sming/Arch/Host/app.mk index 9e4f858af4..d5484569cf 100644 --- a/Sming/Arch/Host/app.mk +++ b/Sming/Arch/Host/app.mk @@ -29,7 +29,7 @@ $(TARGET_OUT_0): $(COMPONENTS_AR) valgrind: all ##Run the application under valgrind to detect memory issues. Requires `valgrind` to be installed on the host system. $(Q) valgrind --track-origins=yes --leak-check=full \ $(foreach id,$(ENABLE_HOST_UARTID),echo '$(call RunHostTerminal,$(id))' >> $@;) \ - $(TARGET_OUT_0) $(CLI_TARGET_OPTIONS) + $(TARGET_OUT_0) $(CLI_TARGET_OPTIONS) -- $(HOST_PARAMETERS) RUN_SCRIPT := $(FW_BASE)/run.sh diff --git a/Sming/Components/Storage/src/Device.cpp b/Sming/Components/Storage/src/Device.cpp index 6f2c6ec428..127396828a 100644 --- a/Sming/Components/Storage/src/Device.cpp +++ b/Sming/Components/Storage/src/Device.cpp @@ -61,7 +61,7 @@ bool Device::loadPartitions(Device& source, uint32_t tableOffset) } if(buffer[0].type != Partition::Type::storage) { - debug_e("[Partition] Bad partition table for device '%s' @ 0x%08x", getName().c_str(), tableOffset); + debug_e("[Partition] Bad partition table for device '%s' @ 0x%08x", source.getName().c_str(), tableOffset); return false; } diff --git a/Sming/Components/Storage/src/ObjectList.cpp b/Sming/Components/Storage/src/ObjectList.cpp index dae8a07716..4ec5cc3d15 100644 --- a/Sming/Components/Storage/src/ObjectList.cpp +++ b/Sming/Components/Storage/src/ObjectList.cpp @@ -40,7 +40,7 @@ bool ObjectList::add(Object* object) bool ObjectList::remove(Object* object) { - if(object == nullptr) { + if(object == nullptr || mHead == nullptr) { return false; } diff --git a/Sming/Libraries/RingTone b/Sming/Libraries/RingTone index a62f39ea08..56bdf070f7 160000 --- a/Sming/Libraries/RingTone +++ b/Sming/Libraries/RingTone @@ -1 +1 @@ -Subproject commit a62f39ea08867b26c7ace53aedb43769f02cc74f +Subproject commit 56bdf070f7e4eebebe15646079aa709af10e248c From 9b5143fbf9c63933398dfbf33860f59f3d639fea Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 10 Feb 2021 15:04:50 +0000 Subject: [PATCH 13/91] Installable File System (#1579) * Add IFS Component * Use new style of file open flags * File/Directory streams and Formatter classes Core IFS streams allow use with any file system Add base Formatter class, headers and standard/html/json within Format namespace * Use Host filesystem instead of raw API * Implement GdbFileStream using GdbFileSystem * Basic_IFS sample * Add upgrade notes [scan:coverity] --- .gitmodules | 4 + .../Esp8266/Components/gdbstub/gdbhostio.cpp | 12 +- .../hostlib/include/hostlib/hostlib.h | 6 +- .../Arch/Host/Components/hostlib/options.cpp | 5 + Sming/Arch/Host/Components/hostlib/options.h | 1 + .../Arch/Host/Components/hostlib/startup.cpp | 3 +- .../Host/Components/spi_flash/flashmem.cpp | 52 +- .../Host/Core/Data/Stream/HostFileStream.cpp | 166 +------ .../Host/Core/Data/Stream/HostFileStream.h | 116 +---- Sming/Arch/Host/Tools/ci/build.run.cmd | 2 +- Sming/Components/IFS | 1 + .../Storage/Tools/hwconfig/partition.py | 1 + .../Storage/src/include/Storage/Partition.h | 3 +- Sming/Components/spiffs/spiffs_config.h | 2 +- Sming/Components/spiffs/spiffs_sming.cpp | 157 ++---- Sming/Components/spiffs/spiffs_sming.h | 7 +- Sming/Core/Data/Stream/Directory.h | 28 ++ Sming/Core/Data/Stream/FileStream.cpp | 155 ------ Sming/Core/Data/Stream/FileStream.h | 152 +----- Sming/Core/Data/Stream/GdbFileStream.cpp | 130 ----- Sming/Core/Data/Stream/GdbFileStream.h | 120 +---- Sming/Core/Data/Stream/IFS/Directory.cpp | 102 ++++ Sming/Core/Data/Stream/IFS/Directory.h | 111 +++++ .../Data/Stream/IFS/DirectoryTemplate.cpp | 118 +++++ .../Core/Data/Stream/IFS/DirectoryTemplate.h | 82 +++ Sming/Core/Data/Stream/IFS/FileStream.cpp | 223 +++++++++ Sming/Core/Data/Stream/IFS/FileStream.h | 146 ++++++ Sming/Core/Data/Stream/IFS/FsBase.h | 72 +++ .../Data/Stream/IFS/HtmlDirectoryTemplate.cpp | 67 +++ .../Data/Stream/IFS/HtmlDirectoryTemplate.h | 36 ++ .../Data/Stream/IFS/JsonDirectoryTemplate.h | 33 ++ Sming/Core/FileSystem.cpp | 209 +------- Sming/Core/FileSystem.h | 468 ++++++++++++++---- Sming/Core/Network/Ftp/FtpDataRetrieve.h | 2 +- Sming/Core/Network/Ftp/FtpDataStore.h | 2 +- Sming/Core/Network/FtpServer.cpp | 2 +- Sming/Core/Network/Http/HttpResponse.cpp | 40 +- Sming/Core/Network/Http/HttpResponse.h | 6 + Sming/Core/Network/HttpClient.cpp | 2 +- Sming/Libraries/Adafruit_GFX/BMPDraw.h | 2 +- .../ArduinoJson6/include/ArduinoJson.h | 2 +- Sming/component.mk | 1 + docs/source/upgrading/4.2-4.3.rst | 24 + samples/Basic_IFS/.cproject | 154 ++++++ samples/Basic_IFS/.project | 29 ++ samples/Basic_IFS/Makefile | 9 + samples/Basic_IFS/README.rst | 24 + samples/Basic_IFS/app/application.cpp | 220 ++++++++ samples/Basic_IFS/basic_ifs.hw | 21 + samples/Basic_IFS/component.mk | 12 + samples/Basic_IFS/files/.auth.json | 19 + samples/Basic_IFS/files/.network.json | 8 + samples/Basic_IFS/files/.time.json | 4 + ...ever want to do this, but hey, why not.txt | 7 + .../A Subdirectory/a/b/c/d/e/f/lonely.txt | 1 + .../files/apple-touch-icon-180x180.png | Bin 0 -> 17250 bytes samples/Basic_IFS/files/bootstrap.min.css | 7 + samples/Basic_IFS/files/error.html | 26 + samples/Basic_IFS/files/favicon.ico | Bin 0 -> 15086 bytes samples/Basic_IFS/files/styles.css | 165 ++++++ samples/Basic_IFS/fsimage.ini | 32 ++ samples/Basic_IFS/resource/listing.html | 76 +++ samples/Basic_IFS/resource/listing.json | 24 + samples/Basic_IFS/resource/listing.txt | 26 + samples/Basic_Serial/app/application.cpp | 2 +- samples/HttpClient/app/application.cpp | 2 +- samples/LiveDebug/app/application.cpp | 26 +- tests/HostTests/modules/ArduinoJson6.cpp | 2 +- tests/HostTests/modules/Files.cpp | 6 +- tests/HostTests/modules/TemplateStream.cpp | 21 + 70 files changed, 2499 insertions(+), 1297 deletions(-) create mode 160000 Sming/Components/IFS create mode 100644 Sming/Core/Data/Stream/Directory.h delete mode 100644 Sming/Core/Data/Stream/FileStream.cpp delete mode 100644 Sming/Core/Data/Stream/GdbFileStream.cpp create mode 100644 Sming/Core/Data/Stream/IFS/Directory.cpp create mode 100644 Sming/Core/Data/Stream/IFS/Directory.h create mode 100644 Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp create mode 100644 Sming/Core/Data/Stream/IFS/DirectoryTemplate.h create mode 100644 Sming/Core/Data/Stream/IFS/FileStream.cpp create mode 100644 Sming/Core/Data/Stream/IFS/FileStream.h create mode 100644 Sming/Core/Data/Stream/IFS/FsBase.h create mode 100644 Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.cpp create mode 100644 Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.h create mode 100644 Sming/Core/Data/Stream/IFS/JsonDirectoryTemplate.h create mode 100644 samples/Basic_IFS/.cproject create mode 100644 samples/Basic_IFS/.project create mode 100644 samples/Basic_IFS/Makefile create mode 100644 samples/Basic_IFS/README.rst create mode 100644 samples/Basic_IFS/app/application.cpp create mode 100644 samples/Basic_IFS/basic_ifs.hw create mode 100644 samples/Basic_IFS/component.mk create mode 100644 samples/Basic_IFS/files/.auth.json create mode 100644 samples/Basic_IFS/files/.network.json create mode 100644 samples/Basic_IFS/files/.time.json create mode 100644 samples/Basic_IFS/files/A Subdirectory/The name of this file is excessively long. Can't say I'd ever want to do this, but hey, why not.txt create mode 100644 samples/Basic_IFS/files/A Subdirectory/a/b/c/d/e/f/lonely.txt create mode 100644 samples/Basic_IFS/files/apple-touch-icon-180x180.png create mode 100644 samples/Basic_IFS/files/bootstrap.min.css create mode 100644 samples/Basic_IFS/files/error.html create mode 100644 samples/Basic_IFS/files/favicon.ico create mode 100644 samples/Basic_IFS/files/styles.css create mode 100644 samples/Basic_IFS/fsimage.ini create mode 100644 samples/Basic_IFS/resource/listing.html create mode 100644 samples/Basic_IFS/resource/listing.json create mode 100644 samples/Basic_IFS/resource/listing.txt diff --git a/.gitmodules b/.gitmodules index ad249f7315..9bb87d3574 100644 --- a/.gitmodules +++ b/.gitmodules @@ -49,6 +49,10 @@ path = Sming/Components/http-parser url = https://github.com/nodejs/http-parser.git ignore = dirty +[submodule "IFS"] + path = Sming/Components/IFS + url = https://github.com/mikee47/IFS.git + ignore = dirty [submodule "libyuarel"] path = Sming/Components/libyuarel url = https://github.com/jacketizer/libyuarel.git diff --git a/Sming/Arch/Esp8266/Components/gdbstub/gdbhostio.cpp b/Sming/Arch/Esp8266/Components/gdbstub/gdbhostio.cpp index fc29b4a100..df984d9e4a 100644 --- a/Sming/Arch/Esp8266/Components/gdbstub/gdbhostio.cpp +++ b/Sming/Arch/Esp8266/Components/gdbstub/gdbhostio.cpp @@ -57,23 +57,23 @@ void ATTR_GDBEXTERNFN gdbHandleHostIo(char* commandBuffer, unsigned cmdLen) filename[len] = '\0'; ++data; // skip , unsigned flags = GdbPacket::readHexValue(data); - ++data; // Skip , + ++data; // Skip , unsigned mode = GdbPacket::readHexValue(data); // Skip mode (not used) (void)mode; FileOpenFlags openFlags; if((flags & 0xff) == O_RDWR) { - openFlags = eFO_ReadWrite; + openFlags = File::ReadWrite; } else if((flags & 0xff) == O_WRONLY) { - openFlags = eFO_WriteOnly; + openFlags = File::WriteOnly; } else { - openFlags = eFO_ReadOnly; + openFlags = File::ReadOnly; } if(flags & O_CREAT) { - openFlags = openFlags | eFO_CreateIfNotExist; + openFlags |= File::Create; } if(flags & O_TRUNC) { - openFlags = openFlags | eFO_Truncate; + openFlags |= File::Truncate; } int fd = fileOpen(filename, openFlags); diff --git a/Sming/Arch/Host/Components/hostlib/include/hostlib/hostlib.h b/Sming/Arch/Host/Components/hostlib/include/hostlib/hostlib.h index c2e52a10f5..b30b0cec12 100644 --- a/Sming/Arch/Host/Components/hostlib/include/hostlib/hostlib.h +++ b/Sming/Arch/Host/Components/hostlib/include/hostlib/hostlib.h @@ -19,7 +19,7 @@ #pragma once -// Required for sleep(), ftruncate(), probably others +// Required for sleep(), probably others #undef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200112L @@ -35,10 +35,6 @@ #include #include -#ifndef O_BINARY -#define O_BINARY 0 -#endif - #ifndef ARRAY_SIZE #define ARRAY_SIZE(x) (int)(sizeof(x) / sizeof((x)[0])) #endif diff --git a/Sming/Arch/Host/Components/hostlib/options.cpp b/Sming/Arch/Host/Components/hostlib/options.cpp index b46908e07c..bc40b88e12 100644 --- a/Sming/Arch/Host/Components/hostlib/options.cpp +++ b/Sming/Arch/Host/Components/hostlib/options.cpp @@ -92,3 +92,8 @@ option_tag_t get_option(int argc, char* argv[], const char*& arg) arg = optarg; return option_tag_t(option_index); } + +int get_first_non_option() +{ + return optind; +} diff --git a/Sming/Arch/Host/Components/hostlib/options.h b/Sming/Arch/Host/Components/hostlib/options.h index 85a31f75b8..f148b70847 100644 --- a/Sming/Arch/Host/Components/hostlib/options.h +++ b/Sming/Arch/Host/Components/hostlib/options.h @@ -58,4 +58,5 @@ enum option_tag_t { }; option_tag_t get_option(int argc, char* argv[], const char*& arg); +int get_first_non_option(); void print_help(); diff --git a/Sming/Arch/Host/Components/hostlib/startup.cpp b/Sming/Arch/Host/Components/hostlib/startup.cpp index e9f95c91b2..dc918e8fd8 100644 --- a/Sming/Arch/Host/Components/hostlib/startup.cpp +++ b/Sming/Arch/Host/Components/hostlib/startup.cpp @@ -201,7 +201,8 @@ int main(int argc, char* argv[]) } } - commandLine.parse(argc - optind, &argv[optind]); + auto i = get_first_non_option(); + commandLine.parse(argc - i, &argv[i]); if(!host_flashmem_init(config.flash)) { return 1; diff --git a/Sming/Arch/Host/Components/spi_flash/flashmem.cpp b/Sming/Arch/Host/Components/spi_flash/flashmem.cpp index 8489fe1ac5..d1d645c78a 100644 --- a/Sming/Arch/Host/Components/spi_flash/flashmem.cpp +++ b/Sming/Arch/Host/Components/spi_flash/flashmem.cpp @@ -21,17 +21,21 @@ #include "flashmem.h" #include #include +#include -static int flashFile = -1; -static size_t flashFileSize = 0x400000U; -static char flashFileName[256]; -static const char defaultFlashFileName[] = "flash.bin"; +namespace +{ +IFS::Host::FileSystem fileSys; +IFS::File::Handle flashFile{-1}; +size_t flashFileSize{0x400000U}; +char flashFileName[256]; +const char defaultFlashFileName[]{"flash.bin"}; // Top bit of flash address is set to indicate it's actually program memory -static constexpr uint32_t FLASHMEM_REAL_BIT = 0x80000000U; -static constexpr uint32_t FLASHMEM_REAL_MASK = ~FLASHMEM_REAL_BIT; +constexpr uint32_t FLASHMEM_REAL_BIT{0x80000000U}; +constexpr uint32_t FLASHMEM_REAL_MASK{~FLASHMEM_REAL_BIT}; -#define SPI_FLASH_SEC_SIZE 4096 +} // namespace #define CHECK_ALIGNMENT(_x) assert(((uint32_t)(_x)&0x00000003) == 0) @@ -55,26 +59,26 @@ bool host_flashmem_init(FlashmemConfig& config) config.filename = flashFileName; } - flashFile = open(flashFileName, O_CREAT | O_RDWR | O_BINARY, 0644); + flashFile = fileSys.open(flashFileName, IFS::File::Create | IFS::File::ReadWrite); if(flashFile < 0) { hostmsg("Error opening \"%s\"", flashFileName); return false; } - int res = lseek(flashFile, 0, SEEK_END); + int res = fileSys.lseek(flashFile, 0, SeekOrigin::End); if(res < 0) { - hostmsg("Error seeking \"%s\"", flashFileName); - close(flashFile); + hostmsg("Error seeking \"%s\": %s", flashFileName, fileSys.getErrorString(res).c_str()); + fileSys.close(flashFile); flashFile = -1; return false; } if(res == 0) { size_t size = config.createSize ?: flashFileSize; - res = lseek(flashFile, size, SEEK_SET); + res = fileSys.lseek(flashFile, size, SeekOrigin::Start); if(res != int(size)) { hostmsg("Error seeking beyond end of file \"%s\"", flashFileName); - } else if(ftruncate(flashFile, size) < 0) { + } else if(fileSys.truncate(flashFile, size) < 0) { hostmsg("Error truncating \"%s\" to %u bytes", flashFileName, size); } else { hostmsg("Created blank \"%s\", %u bytes", flashFileName, size); @@ -91,7 +95,7 @@ bool host_flashmem_init(FlashmemConfig& config) void host_flashmem_cleanup() { - close(flashFile); + fileSys.close(flashFile); flashFile = -1; hostmsg("Closed \"%s\"", flashFileName); } @@ -101,8 +105,14 @@ static int readFlashFile(uint32_t offset, void* buffer, size_t count) if(flashFile < 0) { return -1; } - int res = lseek(flashFile, offset, SEEK_SET); - return (res < 0) ? res : read(flashFile, buffer, count); + int res = fileSys.lseek(flashFile, offset, SeekOrigin::Start); + if(res >= 0) { + res = fileSys.read(flashFile, buffer, count); + } + if(res < 0) { + debug_w("readFlashFile(0x%08x, %u) failed: %s", offset, count, fileSys.getErrorString(res).c_str()); + } + return res; } static int writeFlashFile(uint32_t offset, const void* data, size_t count) @@ -110,8 +120,14 @@ static int writeFlashFile(uint32_t offset, const void* data, size_t count) if(flashFile < 0) { return -1; } - int res = lseek(flashFile, offset, SEEK_SET); - return (res < 0) ? res : write(flashFile, data, count); + int res = fileSys.lseek(flashFile, offset, SeekOrigin::Start); + if(res >= 0) { + res = fileSys.write(flashFile, data, count); + } + if(res < 0) { + debug_w("writeFlashFile(0x%08x, %u) failed: %s", offset, count, fileSys.getErrorString(res).c_str()); + } + return res; } SPIFlashInfo flashmem_get_info() diff --git a/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp b/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp index 3f1eb40952..a467733794 100644 --- a/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp +++ b/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp @@ -4,174 +4,18 @@ * http://github.com/SmingHub/Sming * All files of the Sming Core are provided under the LGPL v3 license. * - * HostFileStream.h + * HostFileStream.cpp * ****/ -// Required for ftruncate() -#define _POSIX_C_SOURCE 200112L - #include "HostFileStream.h" -#include -#include -#include - -#ifndef O_BINARY -#define O_BINARY 0 -#endif - -#define CHECK(res) check(res, __FUNCTION__) - -static int mapFlags(FileOpenFlags flags) -{ - int f = 0; - if(flags & eFO_Append) { - f |= O_APPEND; - } - if(flags & eFO_CreateIfNotExist) { - f |= O_CREAT; - } - if(flags & eFO_ReadOnly) { - f |= O_RDONLY; - } - if(flags & eFO_Truncate) { - f |= O_TRUNC; - } - if(flags & eFO_WriteOnly) { - f |= O_WRONLY; - } - return f; -} - -bool HostFileStream::check(int res, const char* func) -{ - if(res >= 0) { - return true; - } - - lastError = errno; - - debug_e("check(%s) = %d", func, lastError); - - return false; -} - -bool HostFileStream::open(const String& fileName, FileOpenFlags openFlags) -{ - close(); - - handle = ::open(fileName.c_str(), O_BINARY | mapFlags(openFlags), 0644); - if(!CHECK(handle)) { - debug_e("Failed to open '%s'", fileName.c_str()); - return false; - } - - int len = ::lseek(handle, 0, SEEK_END); - if(len < 0) { - len = 0; - } else if(openFlags & eFO_Append) { - pos = len; - } else { - ::lseek(handle, 0, SEEK_SET); - } - size = len; - - debug_i("Opened file '%s', %u bytes", fileName.c_str(), size); - filename = fileName; - return true; -} - -void HostFileStream::close() -{ - if(!isValid()) { - return; - } - - ::close(handle); - handle = -1; - size = 0; - pos = 0; - filename = nullptr; - lastError = 0; -} - -size_t HostFileStream::write(const uint8_t* buffer, size_t size) -{ - if(!isValid()) { - return 0; - } - - ::lseek(handle, 0, SEEK_END); - - int res = ::write(handle, buffer, size); - if(!CHECK(res)) { - return 0; - } - pos += res; - if(pos > this->size) { - this->size = pos; - } - return res; -} - -uint16_t HostFileStream::readMemoryBlock(char* data, int bufSize) -{ - if(!isValid()) { - return 0; - } - - int res = ::lseek(handle, pos, SEEK_SET); - if(!CHECK(res)) { - return 0; - } - - res = ::read(handle, data, bufSize); - if(!CHECK(res)) { - return 0; - } - - return res; -} - -int HostFileStream::seekFrom(int offset, SeekOrigin origin) -{ - if(!isValid()) { - return -1; - } - - if(origin == SeekOrigin::Current) { - origin = SeekOrigin::Start; - offset += pos; - } - int newPos = ::lseek(handle, offset, int(origin)); - if(!CHECK(newPos)) { - return -1; - } - - pos = newPos; - if(newPos > int(size)) { - size = newPos; - } - - return true; -} +#include -bool HostFileStream::isFinished() +namespace { - return pos == size; +IFS::Host::FileSystem hostFileSystem; } -bool HostFileStream::truncate(size_t newSize) +HostFileStream::HostFileStream() : IFS::FileStream(&hostFileSystem) { - if(!isValid()) { - return false; - } - if(::ftruncate(handle, newSize) < 0) { - return false; - } - size = newSize; - if(pos > newSize) { - pos = newSize; - } - return true; } diff --git a/Sming/Arch/Host/Core/Data/Stream/HostFileStream.h b/Sming/Arch/Host/Core/Data/Stream/HostFileStream.h index 8532cc05ed..0332203d5c 100644 --- a/Sming/Arch/Host/Core/Data/Stream/HostFileStream.h +++ b/Sming/Arch/Host/Core/Data/Stream/HostFileStream.h @@ -10,127 +10,19 @@ #pragma once -#include -#include +#include /** * @brief Host File stream class * @ingroup stream data - * - * @{ */ - -class HostFileStream : public ReadWriteStream +class HostFileStream : public IFS::FileStream { public: - HostFileStream() - { - } + HostFileStream(); - /** @brief Create a file stream - * @param fileName Name of file to open - * @param openFlags - */ - HostFileStream(const String& fileName, FileOpenFlags openFlags = eFO_ReadOnly) + HostFileStream(const String& fileName, IFS::File::OpenFlags openFlags = IFS::File::ReadOnly) : HostFileStream() { open(fileName, openFlags); } - - ~HostFileStream() - { - close(); - } - - /** @brief Open a file and attach this stream object to it - * @param fileName - * @param openFlags - * @retval bool true on success, false on error - * @note call getLastError() to determine cause of failure - */ - bool open(const String& fileName, FileOpenFlags openFlags = eFO_ReadOnly); - - /** @brief Close file - */ - void close(); - - StreamType getStreamType() const override - { - return eSST_File; - } - - size_t write(const uint8_t* buffer, size_t size) override; - - uint16_t readMemoryBlock(char* data, int bufSize) override; - - int seekFrom(int offset, SeekOrigin origin) override; - - bool isFinished() override; - - String getName() const override - { - return filename; - } - - bool isValid() const override - { - return handle >= 0; - } - - /** @brief Get the offset of cursor from beginning of data - * @retval size_t Cursor offset - */ - size_t getPos() const - { - return pos; - } - - /** @brief Get the total file size - * @retval size_t File size - */ - size_t getSize() const - { - return size; - } - - /** @brief Return the maximum bytes available to read, from current position - * @retval int -1 is returned when the size cannot be determined - */ - int available() override - { - return size - pos; - } - - /** @brief determine if an error occurred during operation - * @retval int filesystem error code - */ - int getLastError() - { - return lastError; - } - - /** @brief Reduce the file size - * @param newSize - * @retval bool true on success - */ - bool truncate(size_t newSize); - - /** @brief Truncate file at current position - * @retval bool true on success - */ - bool truncate() - { - return truncate(pos); - } - -private: - bool check(int res, const char* func); - -private: - int handle = -1; - String filename; - size_t pos = 0; - size_t size = 0; - int lastError = 0; }; - -/** @} */ diff --git a/Sming/Arch/Host/Tools/ci/build.run.cmd b/Sming/Arch/Host/Tools/ci/build.run.cmd index 735cc871f0..55174dc850 100644 --- a/Sming/Arch/Host/Tools/ci/build.run.cmd +++ b/Sming/Arch/Host/Tools/ci/build.run.cmd @@ -1,7 +1,7 @@ REM Host build.run.cmd REM Build a couple of basic applications -%MAKE_PARALLEL% Basic_Serial Basic_ProgMem STRICT=1 V=1 || goto :error +%MAKE_PARALLEL% Basic_Serial Basic_ProgMem Basic_IFS STRICT=1 V=1 || goto :error REM Run basic tests %MAKE_PARALLEL% tests || goto :error diff --git a/Sming/Components/IFS b/Sming/Components/IFS new file mode 160000 index 0000000000..ac268fb17a --- /dev/null +++ b/Sming/Components/IFS @@ -0,0 +1 @@ +Subproject commit ac268fb17a562a6b94fb16df64d43639a3e5aefe diff --git a/Sming/Components/Storage/Tools/hwconfig/partition.py b/Sming/Components/Storage/Tools/hwconfig/partition.py index 018167d60a..fcd6d67d5b 100644 --- a/Sming/Components/Storage/Tools/hwconfig/partition.py +++ b/Sming/Components/Storage/Tools/hwconfig/partition.py @@ -87,6 +87,7 @@ "esphttpd": 0x80, "fat": 0x81, "spiffs": 0x82, + "fwfs": 0xf1, }, STORAGE_TYPE: storage.TYPES } diff --git a/Sming/Components/Storage/src/include/Storage/Partition.h b/Sming/Components/Storage/src/include/Storage/Partition.h index 5428e1e2a7..6affd80cba 100644 --- a/Sming/Components/Storage/src/include/Storage/Partition.h +++ b/Sming/Components/Storage/src/include/Storage/Partition.h @@ -61,7 +61,8 @@ XX(sysParam, 0x40, "System Parameters") \ XX(espHttpd, 0x80, "ESPHTTPD") \ XX(fat, 0x81, "FAT") \ - XX(spiffs, 0x82, "SPIFFS") + XX(spiffs, 0x82, "SPIFFS") \ + XX(fwfs, 0xF1, "FWFS") namespace Storage { diff --git a/Sming/Components/spiffs/spiffs_config.h b/Sming/Components/spiffs/spiffs_config.h index 1cd0560bcc..502ecc0e64 100644 --- a/Sming/Components/spiffs/spiffs_config.h +++ b/Sming/Components/spiffs/spiffs_config.h @@ -117,7 +117,7 @@ // logical_page_size - (SPIFFS_OBJ_NAME_LEN + sizeof(spiffs_page_header) + // spiffs_object_ix_header fields + at least some LUT entries) #ifndef SPIFFS_OBJ_META_LEN -#define SPIFFS_OBJ_META_LEN (0) +#define SPIFFS_OBJ_META_LEN (16) #endif // Size of buffer allocated on stack used when copying data. diff --git a/Sming/Components/spiffs/spiffs_sming.cpp b/Sming/Components/spiffs/spiffs_sming.cpp index b138e3517e..10e99448ed 100644 --- a/Sming/Components/spiffs/spiffs_sming.cpp +++ b/Sming/Components/spiffs/spiffs_sming.cpp @@ -9,122 +9,12 @@ ****/ #include "spiffs_sming.h" -#include +#include +#include #include -extern "C" { -#include "spiffs/src/spiffs_nucleus.h" -} - -#define LOG_PAGE_SIZE 256 - -spiffs _filesystemStorageHandle; - -#ifndef SPIFF_FILEDESC_COUNT -#define SPIFF_FILEDESC_COUNT 7 -#endif namespace { -uint16_t spiffs_work_buf[LOG_PAGE_SIZE]; -spiffs_fd spiffs_fds[SPIFF_FILEDESC_COUNT]; -uint32_t spiffs_cache_buf[LOG_PAGE_SIZE + 32]; - -#define GET_DEVICE() \ - if(fs == nullptr || fs->user_data == nullptr) { \ - debug_e("[SPIFFS] NO DEVICE"); \ - return 0; \ - } \ - auto device = static_cast(fs->user_data); - -s32_t api_spiffs_read(struct spiffs_t* fs, u32_t addr, u32_t size, u8_t* dst) -{ - GET_DEVICE(); - return device->read(addr, dst, size) ? SPIFFS_OK : SPIFFS_ERR_INTERNAL; -} - -s32_t api_spiffs_write(struct spiffs_t* fs, u32_t addr, u32_t size, u8_t* src) -{ - //debugf("api_spiffs_write"); - GET_DEVICE(); - return device->write(addr, src, size) ? SPIFFS_OK : SPIFFS_ERR_INTERNAL; -} - -s32_t api_spiffs_erase(struct spiffs_t* fs, u32_t addr, u32_t size) -{ - debugf("api_spiffs_erase(0x%08x, 0x%08x)", addr, size); - GET_DEVICE(); - return device->erase_range(addr, size) ? SPIFFS_OK : SPIFFS_ERR_INTERNAL; -} - -spiffs_config initConfig(Storage::Partition& partition) -{ - _filesystemStorageHandle.user_data = partition.getDevice(); - return spiffs_config{ - .hal_read_f = api_spiffs_read, - .hal_write_f = api_spiffs_write, - .hal_erase_f = api_spiffs_erase, - .phys_size = partition.size(), - .phys_addr = partition.address(), - .phys_erase_block = INTERNAL_FLASH_SECTOR_SIZE, - .log_block_size = INTERNAL_FLASH_SECTOR_SIZE * 2, - .log_page_size = LOG_PAGE_SIZE, - }; -} - -bool tryMount(Storage::Partition& partition) -{ - auto cfg = initConfig(partition); - int res = SPIFFS_mount(&_filesystemStorageHandle, const_cast(&cfg), - reinterpret_cast(spiffs_work_buf), reinterpret_cast(spiffs_fds), - sizeof(spiffs_fds), spiffs_cache_buf, sizeof(spiffs_cache_buf), nullptr); - debugf("mount res: %d", res); - - return res >= 0; -} - -bool spiffs_format_internal(Storage::Partition& partition) -{ - spiffs_unmount(); - if(tryMount(partition)) { - spiffs_unmount(); - } - - int res = SPIFFS_format(&_filesystemStorageHandle); - return res >= 0; -} - -bool spiffs_mount_internal(Storage::Partition& partition) -{ - auto cfg = initConfig(partition); - debugf("fs.start: size:%u Kb, offset:0x%X\n", cfg.phys_size / 1024U, cfg.phys_addr); - - // Simple check of the erase count to see if flash looks like it's already been formatted - spiffs_obj_id dat{UINT16_MAX}; - partition.read(cfg.log_page_size - sizeof(spiffs_obj_id), &dat, sizeof(dat)); - //debugf("%X", dat); - bool isFormatted = (dat != UINT16_MAX); - - if(!isFormatted) { - debugf("First init file system"); - spiffs_format_internal(partition); - } - - if(!tryMount(partition)) { - return false; - } - - if(!isFormatted) { - spiffs_file fd = SPIFFS_open(&_filesystemStorageHandle, _F("initialize_fs_header.dat"), - SPIFFS_CREAT | SPIFFS_TRUNC | SPIFFS_RDWR, 0); - uint8_t c{1}; - SPIFFS_write(&_filesystemStorageHandle, fd, &c, 1); - SPIFFS_fremove(&_filesystemStorageHandle, fd); - SPIFFS_close(&_filesystemStorageHandle, fd); - } - - return true; -} - Storage::Partition findDefaultPartition() { auto it = Storage::findPartition(Storage::Partition::SubType::Data::spiffs); @@ -136,43 +26,52 @@ Storage::Partition findDefaultPartition() } // namespace -bool spiffs_mount() -{ - auto part = findDefaultPartition(); - return part ? spiffs_mount_internal(part) : false; -} - bool spiffs_mount(Storage::Partition partition) { - if(!partition.verify(Storage::Partition::SubType::Data::spiffs)) { + auto fs = new IFS::SPIFFS::FileSystem(partition); + int err = fs->mount(); + if(err < 0) { + debug_e("SPIFFS mount failed: %s", fs->getErrorString(err).c_str()); + delete fs; return false; } - return spiffs_mount_internal(partition); + fileSetFileSystem(fs); + return true; +} + +bool spiffs_mount() +{ + auto part = findDefaultPartition(); + return part ? spiffs_mount(part) : false; } void spiffs_unmount() { - SPIFFS_unmount(&_filesystemStorageHandle); + if(fileSystemType() == IFS::IFileSystem::Type::SPIFFS) { + fileFreeFileSystem(); + } } bool spiffs_format() { - auto part = findDefaultPartition(); - if(!part) { + if(fileSystemType() != IFS::IFileSystem::Type::SPIFFS) { return false; } - - spiffs_format_internal(part); - return spiffs_mount_internal(part); + return fileSystemFormat() == FS_OK; } bool spiffs_format(Storage::Partition& partition) { - if(!partition.verify(Storage::Partition::SubType::Data::spiffs)) { + spiffs_unmount(); + auto fs = new IFS::SPIFFS::FileSystem(partition); + int err = fs->format(); + if(err < 0) { + debug_e("SPIFFS format failed: %s", fs->getErrorString(err).c_str()); + delete fs; return false; } - spiffs_format_internal(partition); - return spiffs_mount_internal(partition); + fileSetFileSystem(fs); + return true; } diff --git a/Sming/Components/spiffs/spiffs_sming.h b/Sming/Components/spiffs/spiffs_sming.h index 3b969a7d91..566bd91287 100644 --- a/Sming/Components/spiffs/spiffs_sming.h +++ b/Sming/Components/spiffs/spiffs_sming.h @@ -9,7 +9,7 @@ ****/ #pragma once -#include "spiffs.h" +#include #include /** @@ -44,8 +44,3 @@ bool spiffs_format(); * @retval bool true on success */ bool spiffs_format(Storage::Partition& partition); - -/** - * @brief Global SPIFFS instance used by FileSystem API - */ -extern spiffs _filesystemStorageHandle; diff --git a/Sming/Core/Data/Stream/Directory.h b/Sming/Core/Data/Stream/Directory.h new file mode 100644 index 0000000000..e1c203d876 --- /dev/null +++ b/Sming/Core/Data/Stream/Directory.h @@ -0,0 +1,28 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/anakod/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Directory.h + * + * @author mikee47 May 2019 + * + ****/ + +#pragma once + +#include "IFS/Directory.h" +#include + +/** + * @brief Directory stream class + * @ingroup stream data + */ +class Directory : public IFS::Directory +{ +public: + Directory() : IFS::Directory(::getFileSystem()) + { + } +}; diff --git a/Sming/Core/Data/Stream/FileStream.cpp b/Sming/Core/Data/Stream/FileStream.cpp deleted file mode 100644 index 8837e4bb6a..0000000000 --- a/Sming/Core/Data/Stream/FileStream.cpp +++ /dev/null @@ -1,155 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/SmingHub/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * FileStream.cpp - * - ****/ - -#include "FileStream.h" - -/* FileStream */ - -void FileStream::attach(file_t file, size_t size) -{ - close(); - if(file >= 0) { - handle = file; - this->size = size; - fileSeek(handle, 0, SeekOrigin::Start); - pos = 0; - debug_d("attached file: '%s' (%u bytes) #0x%08X", fileName().c_str(), size, this); - } -} - -bool FileStream::open(const String& fileName, FileOpenFlags openFlags) -{ - lastError = SPIFFS_OK; - - file_t file = fileOpen(fileName, openFlags); - if(!check(file)) { - debug_w("File wasn't found: %s", fileName.c_str()); - return false; - } - - // Get size - int size = fileSeek(file, 0, SeekOrigin::End); - if(check(size)) { - attach(file, size); - return true; - } - - fileClose(file); - return false; -} - -void FileStream::close() -{ - if(handle >= 0) { - fileClose(handle); - handle = -1; - } - size = 0; - pos = 0; - lastError = SPIFFS_OK; -} - -size_t FileStream::readBytes(char* buffer, size_t length) -{ - if(buffer == nullptr || length == 0 || pos >= size) { - return 0; - } - - int available = fileRead(handle, buffer, std::min(size - pos, length)); - if(!check(available)) { - return 0; - } - - pos += size_t(available); - - return available; -} - -uint16_t FileStream::readMemoryBlock(char* data, int bufSize) -{ - assert(bufSize >= 0); - size_t startPos = pos; - size_t count = readBytes(data, bufSize); - - // Move cursor back to start position - (void)fileSeek(handle, startPos, SeekOrigin::Start); - - return count; -} - -size_t FileStream::write(const uint8_t* buffer, size_t size) -{ - if(!fileExist()) { - return 0; - } - - if(pos != this->size) { - int writePos = fileSeek(handle, 0, SeekOrigin::End); - if(!check(writePos)) { - return 0; - } - - pos = size_t(writePos); - } - - int written = fileWrite(handle, buffer, size); - if(check(written)) { - pos += size_t(written); - this->size = pos; - } - - return written > 0 ? written : 0; -} - -int FileStream::seekFrom(int offset, SeekOrigin origin) -{ - // Cannot rely on return value from fileSeek - failure does not mean position hasn't changed - fileSeek(handle, offset, origin); - int newpos = fileTell(handle); - if(check(newpos)) { - pos = size_t(newpos); - if(pos > size) { - size = pos; - } - } - return newpos; -} - -String FileStream::fileName() const -{ - spiffs_stat stat; - fileStats(handle, &stat); - return String(reinterpret_cast(stat.name)); -} - -String FileStream::id() const -{ - spiffs_stat stat; - fileStats(handle, &stat); - -#define ETAG_SIZE 16 - char buf[ETAG_SIZE]; - m_snprintf(buf, ETAG_SIZE, _F("00f-%x-%x0-%x"), stat.obj_id, stat.size, - strlen(reinterpret_cast(stat.name))); - - return String(buf); -} - -bool FileStream::truncate(size_t newSize) -{ - bool res = check(fileTruncate(handle, newSize)); - if(res) { - size = newSize; - if(pos > size) { - pos = size; - } - } - return res; -} diff --git a/Sming/Core/Data/Stream/FileStream.h b/Sming/Core/Data/Stream/FileStream.h index d5b0b01037..d391a2aa42 100644 --- a/Sming/Core/Data/Stream/FileStream.h +++ b/Sming/Core/Data/Stream/FileStream.h @@ -10,17 +10,17 @@ #pragma once -#include "ReadWriteStream.h" -#include "FileSystem.h" +#include "IFS/FileStream.h" +#include /** * @brief File stream class * @ingroup stream data */ -class FileStream : public ReadWriteStream +class FileStream : public IFS::FileStream { public: - FileStream() + FileStream() : IFS::FileStream(::getFileSystem()) { } @@ -28,155 +28,21 @@ class FileStream : public ReadWriteStream * @param fileName Name of file to open * @param openFlags */ - FileStream(const String& fileName, FileOpenFlags openFlags = eFO_ReadOnly) + FileStream(const String& fileName, FileOpenFlags openFlags = File::ReadOnly) : FileStream() { open(fileName, openFlags); } - ~FileStream() + FileStream(const FileStat& stat, FileOpenFlags openFlags = File::ReadOnly) : FileStream() { - close(); + open(stat, openFlags); } - /** @brief Attach this stream object to an open file handle - * @param file - * @param size - */ - void attach(file_t file, size_t size); + using IFS::FileStream::attach; /** @deprecated Use `open()` instead */ - bool attach(const String& fileName, FileOpenFlags openFlags = eFO_ReadOnly) SMING_DEPRECATED + bool attach(const String& fileName, FileOpenFlags openFlags = File::ReadOnly) SMING_DEPRECATED { return open(fileName, openFlags); } - - /** @brief Open a file and attach this stream object to it - * @param fileName - * @param openFlags - * @retval bool true on success, false on error - * @note call getLastError() to determine cause of failure - */ - bool open(const String& fileName, FileOpenFlags openFlags = eFO_ReadOnly); - - /** @brief Close file - */ - void close(); - - StreamType getStreamType() const override - { - return eSST_File; - } - - size_t write(const uint8_t* buffer, size_t size) override; - - int read() override - { - char c; - return readBytes(&c, 1) ? static_cast(c) : -1; - } - - size_t readBytes(char* buffer, size_t length) override; - - uint16_t readMemoryBlock(char* data, int bufSize) override; - - int seekFrom(int offset, SeekOrigin origin) override; - - bool isFinished() override - { - return fileIsEOF(handle); - } - - /** @brief Filename of file stream is attached to - * @retval String invalid if stream isn't open - */ - String fileName() const; - - /** @brief Determine if file exists - * @retval bool true if stream contains valid file - */ - bool fileExist() const - { - return handle >= 0; - } - - String getName() const override - { - return fileName(); - } - - bool isValid() const override - { - return fileExist(); - } - - /** @brief Get the offset of cursor from beginning of data - * @retval size_t Cursor offset - */ - size_t getPos() const - { - return pos; - } - - /** @brief Get the total file size - * @retval size_t File size - */ - size_t getSize() const - { - return size; - } - - /** @brief Return the maximum bytes available to read, from current position - * @retval int -1 is returned when the size cannot be determined - */ - int available() override - { - return size - pos; - } - - String id() const override; - - /** @brief determine if an error occurred during operation - * @retval int filesystem error code - */ - int getLastError() - { - return lastError; - } - - /** @brief Reduce the file size - * @param newSize - * @retval bool true on success - */ - bool truncate(size_t newSize); - - /** @brief Truncate file at current position - * @retval bool true on success - */ - bool truncate() - { - return truncate(pos); - } - -private: - /** @brief Check file operation result and note error code - * @param res result of fileXXX() operation to check - * @retval bool true if operation was successful, false if error occurred - */ - bool check(int res) - { - if(res >= 0) { - return true; - } - - if(lastError >= 0) { - lastError = res; - } - return false; - } - -private: - file_t handle = -1; - size_t pos = 0; - size_t size = 0; - int lastError = SPIFFS_OK; }; diff --git a/Sming/Core/Data/Stream/GdbFileStream.cpp b/Sming/Core/Data/Stream/GdbFileStream.cpp deleted file mode 100644 index ae7d03c967..0000000000 --- a/Sming/Core/Data/Stream/GdbFileStream.cpp +++ /dev/null @@ -1,130 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/SmingHub/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * GdbFileStream.cpp - * - ****/ - -#include "GdbFileStream.h" -#include - -bool GdbFileStream::open(const String& fileName, FileOpenFlags openFlags) -{ - lastError = 0; - - int flags = 0; - if((openFlags & eFO_ReadWrite) == eFO_ReadWrite) { - flags = O_RDWR; - } else if(openFlags & eFO_WriteOnly) { - flags = O_WRONLY; - } else { - flags = O_RDONLY; - } - if(openFlags & eFO_CreateIfNotExist) { - flags |= O_CREAT; - } - if(openFlags & eFO_Truncate) { - flags |= O_TRUNC; - } - - int fd = gdb_syscall_open(fileName.c_str(), flags, S_IRWXU); - if(!check(fd)) { - debug_w("File wasn't found: %s", fileName.c_str()); - return false; - } - - // Get size - int size = gdb_syscall_lseek(fd, 0, SEEK_END); - if(check(size)) { - close(); - if(fd >= 0) { - handle = fd; - this->size = size; - gdb_syscall_lseek(fd, 0, SEEK_SET); - pos = 0; - debug_d("opened file: '%s' (%u bytes) #0x%08X", fileName.c_str(), size, this); - } - return true; - } - - gdb_syscall_close(fd); - return false; -} - -void GdbFileStream::close() -{ - if(handle >= 0) { - gdb_syscall_close(handle); - handle = -1; - } - size = 0; - pos = 0; - lastError = 0; -} - -uint16_t GdbFileStream::readMemoryBlock(char* data, int bufSize) -{ - if(data == nullptr || bufSize <= 0 || pos >= size) { - return 0; - } - - int available = gdb_syscall_read(handle, data, std::min(size - pos, size_t(bufSize))); - (void)check(available); - - // Don't move cursor now (waiting seek) - (void)gdb_syscall_lseek(handle, pos, SEEK_SET); - - return available > 0 ? available : 0; -} - -size_t GdbFileStream::write(const uint8_t* buffer, size_t size) -{ - if(!fileExist()) { - return 0; - } - - int writePos = gdb_syscall_lseek(handle, 0, SEEK_END); - if(!check(writePos)) { - return 0; - } - - pos = size_t(writePos); - - int written = gdb_syscall_write(handle, buffer, size); - if(check(written)) { - pos += size_t(written); - if(pos > this->size) { - this->size = pos; - } - } - - return written; -} - -int GdbFileStream::seekFrom(int offset, SeekOrigin origin) -{ - int newpos = gdb_syscall_lseek(handle, offset, int(origin)); - if(check(newpos)) { - pos = size_t(newpos); - if(pos > size) { - size = pos; - } - } - - return newpos; -} - -String GdbFileStream::id() const -{ - gdb_stat_t stat; - gdb_syscall_fstat(handle, &stat); - -#define ETAG_SIZE 16 - char buf[ETAG_SIZE]; - m_snprintf(buf, ETAG_SIZE, _F("00f-%x-%x0-%x"), stat.st_ino, stat.st_size, fileName.length()); - - return String(buf); -} diff --git a/Sming/Core/Data/Stream/GdbFileStream.h b/Sming/Core/Data/Stream/GdbFileStream.h index fbc47e2b3e..e7f564a41b 100644 --- a/Sming/Core/Data/Stream/GdbFileStream.h +++ b/Sming/Core/Data/Stream/GdbFileStream.h @@ -10,130 +10,20 @@ #pragma once -#include "ReadWriteStream.h" -#include "FileSystem.h" +#include "IFS/FileStream.h" +#include /** * @brief GDB File stream class to provide access to host files whilst running under debugger * @ingroup stream gdb_syscall */ -class GdbFileStream : public ReadWriteStream +class GdbFileStream : public IFS::FileStream { public: - GdbFileStream() + GdbFileStream() : IFS::FileStream(&fileSystem) { } - /** @brief Create a file stream - * @param fileName Name of file to open - * @param openFlags - */ - GdbFileStream(const String& fileName, FileOpenFlags openFlags = eFO_ReadOnly) - { - open(fileName, openFlags); - } - - ~GdbFileStream() - { - close(); - } - - /** @brief Open a file and attach this stream object to it - * @param fileName - * @param openFlags - * @retval bool true on success, false on error - * @note call getLastError() to determine cause of failure - */ - bool open(const String& fileName, FileOpenFlags openFlags = eFO_ReadOnly); - - /** @brief Close file - */ - void close(); - - size_t write(const uint8_t* buffer, size_t size) override; - - uint16_t readMemoryBlock(char* data, int bufSize) override; - - int seekFrom(int offset, SeekOrigin origin) override; - - bool isFinished() override - { - return pos == size; - } - - /** @brief Filename of file stream is attached to - * @retval String invalid if stream isn't open - */ - String getFileName() const - { - return fileName; - } - - /** @brief Determine if file exists - * @retval bool true if stream contains valid file - */ - bool fileExist() const - { - return handle >= 0; - } - - String getName() const override - { - return fileName; - } - - bool isValid() const override - { - return fileExist(); - } - - /** @brief Get the offset of cursor from beginning of data - * @retval size_t Cursor offset - */ - size_t getPos() const - { - return pos; - } - - /** @brief Return the total length of the stream - * @retval int -1 is returned when the size cannot be determined - */ - int available() override - { - return size - pos; - } - - String id() const override; - - /** @brief determine if an error occurred during operation - * @retval int filesystem error code - */ - int getLastError() - { - return lastError; - } - -private: - /** @brief Check file operation result and note error code - * @param res result of fileXXX() operation to check - * @retval bool true if operation was successful, false if error occurred - */ - bool check(int res) - { - if(res >= 0) { - return true; - } - - if(lastError >= 0) { - lastError = res; - } - return false; - } - private: - String fileName; - int handle = -1; - size_t pos = 0; - size_t size = 0; - int lastError = 0; + IFS::Gdb::FileSystem fileSystem; }; diff --git a/Sming/Core/Data/Stream/IFS/Directory.cpp b/Sming/Core/Data/Stream/IFS/Directory.cpp new file mode 100644 index 0000000000..2405e208d7 --- /dev/null +++ b/Sming/Core/Data/Stream/IFS/Directory.cpp @@ -0,0 +1,102 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/anakod/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Directory.cpp + * + * @author mikee47 May 2019 + * + ****/ + +#include "Directory.h" + +namespace IFS +{ +bool Directory::open(const String& dirName) +{ + auto fs = getFileSystem(); + if(fs == nullptr) { + return false; + } + + DirHandle dir; + int err = fs->opendir(dirName, dir); + if(!check(err)) { + debug_w("Directory '%s' open error: %s", dirName.c_str(), fs->getErrorString(err).c_str()); + return false; + } + + close(); + name = dirName ?: ""; + this->dir = dir; + + return true; +} + +void Directory::close() +{ + if(dir != nullptr) { + auto fs = getFileSystem(); + assert(fs != nullptr); + fs->closedir(dir); + dir = nullptr; + } + lastError = FS_OK; +} + +bool Directory::rewind() +{ + auto fs = getFileSystem(); + if(fs == nullptr) { + return false; + } + + int err = fs->rewinddir(dir); + return err == FS_OK; +} + +String Directory::getPath() const +{ + String path('/'); + path += name; + if(name.length() != 0 && name[name.length() - 1] != '/') { + path += '/'; + } + return path; +} + +String Directory::getParent() const +{ + if(name.length() == 0 || name == "/") { + return nullptr; + } + String path('/'); + int i = name.lastIndexOf('/'); + if(i >= 0) { + path.concat(name.c_str(), i); + } + return path; +} + +bool Directory::next() +{ + auto fs = getFileSystem(); + if(fs == nullptr) { + return false; + } + + int err = fs->readdir(dir, dirStat); + if(check(err)) { + totalSize += dirStat.size; + ++currentIndex; + return true; + } + + debug_w("Directory '%s' read error: %s", name.c_str(), getErrorString(err).c_str()); + + return false; +} + +} // namespace IFS diff --git a/Sming/Core/Data/Stream/IFS/Directory.h b/Sming/Core/Data/Stream/IFS/Directory.h new file mode 100644 index 0000000000..5b1af25adf --- /dev/null +++ b/Sming/Core/Data/Stream/IFS/Directory.h @@ -0,0 +1,111 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/anakod/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Directory.h + * + * @author mikee47 May 2019 + * + ****/ + +#pragma once + +#include "FsBase.h" + +namespace IFS +{ +/** + * @brief Wrapper class for enumerating a directory + */ +class Directory : public FsBase +{ +public: + using FsBase::FsBase; + + ~Directory() + { + close(); + } + + /** + * @brief Open a directory and attach this stream object to it + * @param dirName + * @retval bool true on success, false on error + * @note call getLastError() to determine cause of failure + */ + bool open(const String& dirName); + + /** + * @brief Close directory + */ + void close(); + + /** + * @brief Rewind directory stream to start so it can be re-enumerated + * @retval bool true on success, false on error + * @note call getLastError() to determine cause of failure + */ + bool rewind(); + + /** + * @brief Name of directory stream is attached to + * @retval String invalid if stream isn't open + */ + const String& getDirName() const + { + return name; + } + + /** + * @brief Determine if directory exists + * @retval bool true if stream is attached to a directory + */ + bool dirExist() const + { + return dir != nullptr; + } + + /** + * @brief Get path with leading separator /path/to/dir + */ + String getPath() const; + + /** + * @brief Get parent directory + * @retval String invalid if there is no parent directory + */ + String getParent() const; + + int index() const + { + return currentIndex; + } + + bool isValid() const + { + return currentIndex >= 0; + } + + size_t size() const + { + return totalSize; + } + + const FileStat& stat() const + { + return dirStat; + } + + bool next(); + +private: + String name; + DirHandle dir{}; + FileNameStat dirStat; + int currentIndex{-1}; + size_t totalSize{0}; +}; + +} // namespace IFS diff --git a/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp new file mode 100644 index 0000000000..460aa885a5 --- /dev/null +++ b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp @@ -0,0 +1,118 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/anakod/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * DirectoryStream.cpp + * + * @author mikee47 May 2019 + * + ****/ + +#include "DirectoryTemplate.h" +#include +#include + +namespace IFS +{ +namespace +{ +#define XX(name, comment) DEFINE_FSTR_LOCAL(fieldstr_##name, #name) +DIRSTREAM_FIELD_MAP(XX) +#undef XX + +#define XX(name, comment) &fieldstr_##name, +DEFINE_FSTR_VECTOR(fieldStrings, FSTR::String, DIRSTREAM_FIELD_MAP(XX)) +#undef XX + +} // namespace + +String DirectoryTemplate::getValue(const char* name) +{ + String value = SectionTemplate::getValue(name); + if(value) { + return value; + } + + int i = fieldStrings.indexOf(name); + auto field = Field(i + 1); + + auto& d = dir(); + auto& s = d.stat(); + + bool statValid = d.isValid(); + + switch(field) { + case Field::unknown: + break; + + case Field::file_id: + return statValid ? String(s.id) : nullptr; + + case Field::name: + if(statValid) { + value.setString(s.name.buffer, s.name.length); + formatter().escape(value); + return value; + } else { + return nullptr; + } + + case Field::modified: + return statValid ? DateTime(s.mtime).toISO8601() : nullptr; + + case Field::size: + return statValid ? String(s.size) : nullptr; + + case Field::original_size: + return statValid ? String(s.originalSize) : nullptr; + + case Field::attr: + return statValid ? IFS::File::getAttributeString(s.attr) : nullptr; + + case Field::attr_long: + return statValid ? toString(s.attr) : nullptr; + + case Field::compression: + if(!statValid) { + return nullptr; + } else if(!s.attr[File::Attribute::Compressed]) { + return ""; + } else { + return toString(s.compression); + } + + case Field::access: + return statValid ? IFS::File::getAclString(s.acl) : nullptr; + + case Field::access_long: + return statValid ? toString(s.acl) : nullptr; + + case Field::index: + return String(d.index()); + + case Field::total_size: + return String(d.size()); + + case Field::path: + return d.getPath(); + + case Field::parent: { + value = d.getParent(); + if(value.length() != 0) { + formatter().escape(value); + } else { + value = ""; + } + return value; + } + + case Field::last_error: + return d.getLastErrorString(); + } + + return nullptr; +} + +} // namespace IFS diff --git a/Sming/Core/Data/Stream/IFS/DirectoryTemplate.h b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.h new file mode 100644 index 0000000000..4e88dd5d0c --- /dev/null +++ b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.h @@ -0,0 +1,82 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/anakod/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * DirectoryTemplate.h + * + * @author mikee47 May 2019 + * + * + ****/ + +#pragma once + +#include "../SectionTemplate.h" +#include "Directory.h" + +#define DIRSTREAM_FIELD_MAP(XX) \ + XX(file_id, "File identifier") \ + XX(name, "Filename") \ + XX(modified, "Date/time of file modification") \ + XX(size, "File size in bytes") \ + XX(original_size, "Original size of compressed file") \ + XX(attr, "File attributes (brief)") \ + XX(attr_long, "File attributes") \ + XX(compression, "Compression type") \ + XX(access, "File access information (brief)") \ + XX(access_long, "File access information") \ + XX(index, "Zero-based index of current file") \ + XX(total_size, "Total size of files processed (in bytes)") \ + XX(path, "Path to containing directory") \ + XX(parent, "Path to parent directory (if any)") \ + XX(last_error, "Last error message") + +namespace IFS +{ +/** + * @brief Directory stream class + * @ingroup stream data + */ +class DirectoryTemplate : public SectionTemplate +{ +public: + enum class Field { + unknown = 0, +#define XX(name, comment) name, + DIRSTREAM_FIELD_MAP(XX) +#undef XX + }; + + DirectoryTemplate(IDataSourceStream* source, Directory* dir) : SectionTemplate(source), directory(dir) + { + } + + ~DirectoryTemplate() + { + delete directory; + } + + Directory& dir() + { + return *directory; + } + + bool nextRecord() override + { + if(sectionIndex() == 1) { + return directory->next(); + } + + return recordIndex() < 0; + } + +protected: + String getValue(const char* name) override; + +private: + Directory* directory; +}; + +} // namespace IFS diff --git a/Sming/Core/Data/Stream/IFS/FileStream.cpp b/Sming/Core/Data/Stream/IFS/FileStream.cpp new file mode 100644 index 0000000000..66caa0c383 --- /dev/null +++ b/Sming/Core/Data/Stream/IFS/FileStream.cpp @@ -0,0 +1,223 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * FileStream.cpp + * + ****/ + +#include "FileStream.h" + +#define ETAG_SIZE 16 + +namespace IFS +{ +void FileStream::attach(File::Handle file, size_t size) +{ + close(); + if(file < 0) { + return; + } + + auto fs = getFileSystem(); + if(fs == nullptr) { + return; + } + handle = file; + this->size = size; + fs->lseek(handle, 0, SeekOrigin::Start); + pos = 0; + + debug_d("attached file: '%s' (%u bytes) #0x%08X", fileName().c_str(), size, this); +} + +bool FileStream::open(const FileStat& stat, File::OpenFlags openFlags) +{ + auto fs = getFileSystem(); + if(fs == nullptr) { + return false; + } + + lastError = FS_OK; + + File::Handle file = fs->fopen(stat, openFlags); + if(!check(file)) { + return false; + } + + attach(file, stat.size); + return true; +} + +bool FileStream::open(const String& fileName, File::OpenFlags openFlags) +{ + auto fs = getFileSystem(); + if(fs == nullptr) { + return false; + } + + lastError = FS_OK; + + File::Handle file = fs->open(fileName, openFlags); + if(!check(file)) { + debug_w("File '%s' open error: %s", fileName.c_str(), fs->getErrorString(file).c_str()); + return false; + } + + // Get size + int size = fs->lseek(file, 0, SeekOrigin::End); + if(check(size)) { + attach(file, size); + return true; + } + + fs->close(file); + return false; +} + +void FileStream::close() +{ + if(handle >= 0) { + auto fs = getFileSystem(); + assert(fs != nullptr); + fs->close(handle); + handle = -1; + } + size = 0; + pos = 0; + lastError = FS_OK; +} + +size_t FileStream::readBytes(char* buffer, size_t length) +{ + if(buffer == nullptr || length == 0 || pos >= size) { + return 0; + } + + auto fs = getFileSystem(); + if(fs == nullptr) { + return 0; + } + + int available = fs->read(handle, buffer, std::min(size - pos, length)); + if(!check(available)) { + return 0; + } + + pos += size_t(available); + + return available; +} + +uint16_t FileStream::readMemoryBlock(char* data, int bufSize) +{ + auto fs = getFileSystem(); + if(fs == nullptr) { + return 0; + } + + assert(bufSize >= 0); + size_t startPos = pos; + size_t count = readBytes(data, bufSize); + + // Move cursor back to start position + (void)fs->lseek(handle, startPos, SeekOrigin::Start); + + return count; +} + +size_t FileStream::write(const uint8_t* buffer, size_t size) +{ + auto fs = getFileSystem(); + if(fs == nullptr) { + return 0; + } + + if(pos != this->size) { + int writePos = fs->lseek(handle, 0, SeekOrigin::End); + if(!check(writePos)) { + return 0; + } + + pos = size_t(writePos); + } + + int written = fs->write(handle, buffer, size); + if(check(written)) { + pos += size_t(written); + this->size = pos; + } + + return written > 0 ? written : 0; +} + +int FileStream::seekFrom(int offset, SeekOrigin origin) +{ + auto fs = getFileSystem(); + if(fs == nullptr) { + return 0; + } + + // Cannot rely on return value from fileSeek - failure does not mean position hasn't changed + fs->lseek(handle, offset, origin); + int newpos = fs->tell(handle); + if(check(newpos)) { + pos = size_t(newpos); + if(pos > size) { + size = pos; + } + } + return newpos; +} + +String FileStream::fileName() const +{ + auto fs = getFileSystem(); + if(fs == nullptr) { + return nullptr; + } + + FileNameStat stat; + int res = fs->fstat(handle, stat); + return (res < 0 || stat.name.length == 0) ? nullptr : stat.name.buffer; +} + +String FileStream::id() const +{ + auto fs = getFileSystem(); + if(fs == nullptr) { + return 0; + } + + FileStat stat; + int res = fs->fstat(handle, stat); + if(res < 0) { + return nullptr; + } + + char buf[ETAG_SIZE]; + m_snprintf(buf, ETAG_SIZE, _F("00f-%x-%x0-%x"), stat.id, stat.size, stat.name.length); + + return String(buf); +} + +bool FileStream::truncate(size_t newSize) +{ + auto fs = getFileSystem(); + if(fs == nullptr) { + return 0; + } + + bool res = check(fs->truncate(handle, newSize)); + if(res) { + size = newSize; + if(pos > size) { + pos = size; + } + } + return res; +} + +} // namespace IFS diff --git a/Sming/Core/Data/Stream/IFS/FileStream.h b/Sming/Core/Data/Stream/IFS/FileStream.h new file mode 100644 index 0000000000..9fb9491317 --- /dev/null +++ b/Sming/Core/Data/Stream/IFS/FileStream.h @@ -0,0 +1,146 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * FileStream.h + * + ****/ + +#pragma once + +#include "../ReadWriteStream.h" +#include "FsBase.h" + +namespace IFS +{ +/** + * @brief File stream class + * @ingroup stream data + */ +class FileStream : public FsBase, public ReadWriteStream +{ +public: + using FsBase::FsBase; + + ~FileStream() + { + close(); + } + + /** @brief Attach this stream object to an open file handle + * @param file + * @param size + */ + void attach(File::Handle file, size_t size); + + bool open(const FileStat& stat, File::OpenFlags openFlags = File::ReadOnly); + + /** @brief Open a file and attach this stream object to it + * @param fileName + * @param openFlags + * @retval bool true on success, false on error + * @note call getLastError() to determine cause of failure + */ + bool open(const String& fileName, File::OpenFlags openFlags = File::ReadOnly); + + /** @brief Close file + */ + void close(); + + StreamType getStreamType() const override + { + return eSST_File; + } + + size_t write(const uint8_t* buffer, size_t size) override; + + int read() override + { + char c; + return readBytes(&c, 1) ? static_cast(c) : -1; + } + + size_t readBytes(char* buffer, size_t length) override; + + uint16_t readMemoryBlock(char* data, int bufSize) override; + + int seekFrom(int offset, SeekOrigin origin) override; + + bool isFinished() override + { + auto fs = getFileSystem(); + return fs == nullptr || fs->eof(handle) != 0; + } + + /** @brief Filename of file stream is attached to + * @retval String invalid if stream isn't open + */ + String fileName() const; + + /** @brief Determine if file exists + * @retval bool true if stream contains valid file + */ + bool fileExist() const + { + return handle >= 0; + } + + String getName() const override + { + return fileName(); + } + + bool isValid() const override + { + return fileExist(); + } + + /** @brief Get the offset of cursor from beginning of data + * @retval size_t Cursor offset + */ + size_t getPos() const + { + return pos; + } + + /** @brief Get the total file size + * @retval size_t File size + */ + size_t getSize() const + { + return size; + } + + /** @brief Return the maximum bytes available to read, from current position + * @retval int -1 is returned when the size cannot be determined + */ + int available() override + { + return size - pos; + } + + String id() const override; + + /** @brief Reduce the file size + * @param newSize + * @retval bool true on success + */ + bool truncate(size_t newSize); + + /** @brief Truncate file at current position + * @retval bool true on success + */ + bool truncate() + { + return truncate(pos); + } + +private: + File::Handle handle{-1}; + size_t pos{0}; + size_t size{0}; +}; + +} // namespace IFS diff --git a/Sming/Core/Data/Stream/IFS/FsBase.h b/Sming/Core/Data/Stream/IFS/FsBase.h new file mode 100644 index 0000000000..98d00f4cf0 --- /dev/null +++ b/Sming/Core/Data/Stream/IFS/FsBase.h @@ -0,0 +1,72 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * FsBase.h - common base for file system classes + * + ****/ + +#pragma once + +#include + +namespace IFS +{ +class FsBase +{ +public: + FsBase(IFileSystem* filesys) : fileSystem(filesys) + { + } + + /** @brief determine if an error occurred during operation + * @retval int filesystem error code + */ + int getLastError() + { + return lastError; + } + + String getErrorString(int err) const + { + return fileSystem == nullptr ? Error::toString(err) : fileSystem->getErrorString(err); + } + + String getLastErrorString() const + { + return getErrorString(lastError); + } + + IFileSystem* getFileSystem() const + { + lastError = (fileSystem == nullptr) ? Error::NoFileSystem : FS_OK; + return fileSystem; + } + +protected: + /** @brief Check file operation result and note error code + * @param res result of fileXXX() operation to check + * @retval bool true if operation was successful, false if error occurred + */ + bool check(int res) + { + if(res >= 0) { + return true; + } + + if(lastError >= 0) { + lastError = res; + } + return false; + } + +protected: + mutable int lastError{FS_OK}; + +private: + IFileSystem* fileSystem; +}; + +} // namespace IFS diff --git a/Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.cpp b/Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.cpp new file mode 100644 index 0000000000..05cdeb47c7 --- /dev/null +++ b/Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.cpp @@ -0,0 +1,67 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * HtmlDirectoryTemplate.h + * + * @author mikee47 May 2019 + * + ****/ + +#include "HtmlDirectoryTemplate.h" +#include +#include + +namespace IFS +{ +String HtmlDirectoryTemplate::getValue(const char* name) +{ + String text = DirectoryTemplate::getValue(name); + if(text) { + return text; + } + + if(dir().isValid()) { + auto& stat = dir().stat(); + + if(FS("icon") == name) { + if(stat.attr[File::Attribute::Directory]) { + return F("📁"); + } + + auto mimeType = ContentType::fromFullFileName(stat.name, MIME_UNKNOWN); + // https://html-css-js.com/html/character-codes/icons/ + switch(mimeType) { + case MIME_TEXT: + return F("📄"); + case MIME_JS: + case MIME_CSS: + case MIME_HTML: + case MIME_XML: + case MIME_JSON: + return F("📑"); + + case MIME_JPEG: + case MIME_GIF: + case MIME_PNG: + case MIME_SVG: + case MIME_ICO: + return F("🍓"); + + case MIME_GZIP: + case MIME_ZIP: + return F("🗃"); + + case MIME_UNKNOWN: + default: + return F("•"); + } + } + } + + return nullptr; +} + +} // namespace IFS diff --git a/Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.h b/Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.h new file mode 100644 index 0000000000..ec60d66ba4 --- /dev/null +++ b/Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.h @@ -0,0 +1,36 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * HtmlDirectoryTemplate.h + * + * @author mikee47 May 2019 + * + ****/ + +#pragma once + +#include "../../Format.h" +#include "DirectoryTemplate.h" + +namespace IFS +{ +/** + * @brief Read-only stream access to directory listing with HTML output + * @ingroup stream data +*/ +class HtmlDirectoryTemplate : public DirectoryTemplate +{ +public: + HtmlDirectoryTemplate(IDataSourceStream* source, Directory* dir) : DirectoryTemplate(source, dir) + { + setFormatter(Format::html); + } + +protected: + String getValue(const char* name) override; +}; + +} // namespace IFS diff --git a/Sming/Core/Data/Stream/IFS/JsonDirectoryTemplate.h b/Sming/Core/Data/Stream/IFS/JsonDirectoryTemplate.h new file mode 100644 index 0000000000..61ec5bc438 --- /dev/null +++ b/Sming/Core/Data/Stream/IFS/JsonDirectoryTemplate.h @@ -0,0 +1,33 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * JsonDirectoryTemplate.h + * + * @author mikee47 Nov 2020 + * + ****/ + +#pragma once + +#include "DirectoryTemplate.h" +#include + +namespace IFS +{ +/** + * @brief Read-only stream providing directory listing in JSON format +*/ +class JsonDirectoryTemplate : public DirectoryTemplate +{ +public: + JsonDirectoryTemplate(IDataSourceStream* source, Directory* dir) : DirectoryTemplate(source, dir) + { + setDoubleBraces(true); + setFormatter(Format::json); + } +}; + +} // namespace IFS diff --git a/Sming/Core/FileSystem.cpp b/Sming/Core/FileSystem.cpp index c7e1f1def1..72aaa01289 100644 --- a/Sming/Core/FileSystem.cpp +++ b/Sming/Core/FileSystem.cpp @@ -12,213 +12,40 @@ #include #include -file_t fileOpen(const String& name, FileOpenFlags flags) +namespace SmingInternal { - // Special fix to prevent known spifFS bug: manual delete file - if((flags & eFO_CreateNewAlways) == eFO_CreateNewAlways) { - if(fileExist(name)) { - fileDelete(name); - } - flags = (FileOpenFlags)((int)flags & ~eFO_Truncate); - } - - int res = SPIFFS_open(&_filesystemStorageHandle, name.c_str(), (spiffs_flags)flags, 0); - if(res < 0) { - debugf("open errno %d\n", SPIFFS_errno(&_filesystemStorageHandle)); - } - - return res; -} - -void fileClose(file_t file) -{ - SPIFFS_close(&_filesystemStorageHandle, file); -} - -int fileWrite(file_t file, const void* data, size_t size) -{ - int res = SPIFFS_write(&_filesystemStorageHandle, file, (void*)data, size); - if(res < 0) { - debugf("write errno %d\n", SPIFFS_errno(&_filesystemStorageHandle)); - } - return res; -} - -int fileRead(file_t file, void* data, size_t size) -{ - int res = SPIFFS_read(&_filesystemStorageHandle, file, data, size); - if(res < 0) { - debugf("read errno %d\n", SPIFFS_errno(&_filesystemStorageHandle)); - } - return res; -} - -int fileSeek(file_t file, int offset, SeekOrigin origin) -{ - return SPIFFS_lseek(&_filesystemStorageHandle, file, offset, int(origin)); -} - -bool fileIsEOF(file_t file) -{ - return SPIFFS_eof(&_filesystemStorageHandle, file) != 0; -} - -int32_t fileTell(file_t file) -{ - return SPIFFS_tell(&_filesystemStorageHandle, file); -} - -int fileFlush(file_t file) -{ - return SPIFFS_fflush(&_filesystemStorageHandle, file); -} - -int fileStats(const String& name, spiffs_stat* stat) -{ - return SPIFFS_stat(&_filesystemStorageHandle, name.c_str(), stat); -} - -int fileStats(file_t file, spiffs_stat* stat) -{ - return SPIFFS_fstat(&_filesystemStorageHandle, file, stat); -} - -int fileDelete(const String& name) -{ - return SPIFFS_remove(&_filesystemStorageHandle, name.c_str()); -} - -int fileDelete(file_t file) -{ - return SPIFFS_fremove(&_filesystemStorageHandle, file); +IFS::IFileSystem* activeFileSystem; } -bool fileExist(const String& name) +void fileSetFileSystem(IFS::IFileSystem* fileSystem) { - spiffs_stat stat = {0}; - if(fileStats(name.c_str(), &stat) < 0) { - return false; + if(SmingInternal::activeFileSystem != fileSystem) { + delete SmingInternal::activeFileSystem; + SmingInternal::activeFileSystem = fileSystem; } - return stat.name[0] != '\0'; -} - -int fileLastError(file_t fd) -{ - return SPIFFS_errno(&_filesystemStorageHandle); -} - -void fileClearLastError(file_t fd) -{ - SPIFFS_clearerr(&_filesystemStorageHandle); -} - -int fileSetContent(const String& fileName, const String& content) -{ - return fileSetContent(fileName, content.c_str(), content.length()); -} - -int fileSetContent(const String& fileName, const char* content, int length) -{ - file_t file = fileOpen(fileName.c_str(), eFO_CreateNewAlways | eFO_WriteOnly); - if(file < 0) { - return file; - } - if(length < 0) { - length = strlen(content); - } - int res = fileWrite(file, content, length); - fileClose(file); - return res; -} - -uint32_t fileGetSize(const String& fileName) -{ - file_t file = fileOpen(fileName.c_str(), eFO_ReadOnly); - int size = fileSeek(file, 0, SeekOrigin::End); - fileClose(file); - return (size < 0) ? 0 : size; -} - -int fileRename(const String& oldName, const String& newName) -{ - return SPIFFS_rename(&_filesystemStorageHandle, oldName.c_str(), newName.c_str()); } Vector fileList() { Vector result; - spiffs_DIR d; - spiffs_dirent info; - SPIFFS_opendir(&_filesystemStorageHandle, "/", &d); - while(true) { - if(!SPIFFS_readdir(&d, &info)) { - break; + DirHandle dir; + if(fileOpenDir(nullptr, dir) == FS_OK) { + FileNameStat stat; + while(fileReadDir(dir, stat) >= 0) { + result.add(stat.name.buffer); } - result.add(String((char*)info.name)); + fileCloseDir(dir); } - SPIFFS_closedir(&d); return result; } -String fileGetContent(const String& fileName) +IFS::IFileSystem::Type fileSystemType() { - String res; - file_t file = fileOpen(fileName.c_str(), eFO_ReadOnly); - int size = fileSeek(file, 0, SeekOrigin::End); - if(size == 0) { - res = ""; // Valid String, file is empty - } else if(size > 0) { - fileSeek(file, 0, SeekOrigin::Start); - res.setLength(size); - if(fileRead(file, res.begin(), res.length()) != size) { - res = nullptr; // read failed, invalidate String - } - } - fileClose(file); - return res; -} - -size_t fileGetContent(const String& fileName, char* buffer, size_t bufSize) -{ - if(buffer == nullptr || bufSize == 0) { - return 0; + if(SmingInternal::activeFileSystem == nullptr) { + return IFS::IFileSystem::Type::Unknown; } - - file_t file = fileOpen(fileName.c_str(), eFO_ReadOnly); - int size = fileSeek(file, 0, SeekOrigin::End); - if(size <= 0 || bufSize <= size_t(size)) { - size = 0; - } else { - fileSeek(file, 0, SeekOrigin::Start); - if(fileRead(file, buffer, size) != size) { - size = 0; // Error - } - } - fileClose(file); - buffer[size] = '\0'; - return size; -} - -int fileTruncate(file_t file, size_t newSize) -{ - return SPIFFS_ftruncate(&_filesystemStorageHandle, file, newSize); -} - -int fileTruncate(file_t file) -{ - int pos = fileTell(file); - return (pos < 0) ? pos : fileTruncate(file, pos); -} - -int fileTruncate(const String& fileName, size_t newSize) -{ - file_t file = fileOpen(fileName, eFO_WriteOnly); - if(file < 0) { - return file; - } - - int res = fileTruncate(file, newSize); - fileClose(file); - return res; + IFS::IFileSystem::Info info; + fileGetSystemInfo(info); + return info.type; } diff --git a/Sming/Core/FileSystem.h b/Sming/Core/FileSystem.h index aece4d25a0..e34e5488a5 100644 --- a/Sming/Core/FileSystem.h +++ b/Sming/Core/FileSystem.h @@ -15,46 +15,119 @@ #pragma once +#include #include -#include -#include -#include "Data/Stream/SeekOrigin.h" +#include "WVector.h" ///< @deprecated see fileList() -using file_t = signed short; ///< File handle - -/// File open flags -enum FileOpenFlags { - eFO_ReadOnly = SPIFFS_RDONLY, ///< Read only file - eFO_WriteOnly = SPIFFS_WRONLY, ///< Write only file - eFO_ReadWrite = eFO_ReadOnly | eFO_WriteOnly, ///< Read and write file - eFO_CreateIfNotExist = SPIFFS_CREAT, ///< Create new file if file does not exist - eFO_Append = SPIFFS_APPEND, ///< Append to file - eFO_Truncate = SPIFFS_TRUNC, ///< Truncate file - eFO_CreateNewAlways = eFO_CreateIfNotExist | eFO_Truncate ///< Create new file even if file exists -}; - -static inline FileOpenFlags operator|(FileOpenFlags lhs, FileOpenFlags rhs) +namespace File { - return (FileOpenFlags)((int)lhs | (int)rhs); +using namespace IFS::File; } -/// File seek flags +using file_t = IFS::File::Handle; +typedef SeekOrigin SeekOriginFlags; ///< @deprecated Use `SeekOrigin` instead +using DirHandle = IFS::DirHandle; +using FileOpenFlag = File::OpenFlag; +using FileOpenFlags = File::OpenFlags; +using FileStat = File::Stat; +using FileNameStat = File::NameStat; +constexpr int FS_OK = IFS::FS_OK; + +// Various file flag combinations +constexpr FileOpenFlags eFO_ReadOnly SMING_DEPRECATED{File::ReadOnly}; ///< @deprecated use File::ReadOnly +constexpr FileOpenFlags eFO_WriteOnly SMING_DEPRECATED{File::WriteOnly}; ///< @deprecated use File::WriteOnly +constexpr FileOpenFlags eFO_ReadWrite{File::ReadWrite}; ///< @deprecated use File::ReadWrite +constexpr FileOpenFlags eFO_CreateIfNotExist{File::Create}; ///< @deprecated use File::Create +constexpr FileOpenFlags eFO_Append{File::Append}; ///< @deprecated use File::Append +constexpr FileOpenFlags eFO_Truncate{File::Truncate}; ///< @deprecated use File::Truncate +constexpr FileOpenFlags eFO_CreateNewAlways{File::CreateNewAlways}; ///< @deprecated use File::CreateNewAlways + constexpr SeekOrigin eSO_FileStart{SeekOrigin::Start}; ///< @deprecated use SeekOrigin::Start constexpr SeekOrigin eSO_CurrentPos{SeekOrigin::Current}; ///< @deprecated use SeekOrigin::Current constexpr SeekOrigin eSO_FileEnd{SeekOrigin::End}; ///< @deprecated use SeekOrigin::End +namespace SmingInternal +{ +/** @brief Global file system instance + * + * Filing system implementations should use helper functions to setup and + * initialise a filing system. If successful, call fileSetFileSystem() to make + * it active. This ensures that any active filing system is dismounted destroyed + * first. + * + */ +extern IFS::IFileSystem* activeFileSystem; + +} // namespace SmingInternal + +/* + * Boilerplate check for file function wrappers to catch undefined filesystem. + */ +#define CHECK_FS(_method) \ + auto fileSystem = SmingInternal::activeFileSystem; \ + if(fileSystem == nullptr) { \ + debug_e("ERROR in %s(): No active file system", __FUNCTION__); \ + return file_t(IFS::Error::NoFileSystem); \ + } + +/** + * @brief Get the currently active file system, if any + * @retval IFS::IFileSystem* + */ +inline IFS::IFileSystem* getFileSystem() +{ + if(SmingInternal::activeFileSystem == nullptr) { + debug_e("ERROR: No active file system"); + } + return SmingInternal::activeFileSystem; +} + +/** @brief Sets the currently active file system + * @param fileSystem + * @note Any existing filing system is freed first. + * Typically the filing system implementation has helper functions which + * create and initialise the file system to a valid state. The last step + * is to call this function to make it active. + * Call this function with nullptr to inactivate the filing system. + */ +void fileSetFileSystem(IFS::IFileSystem* fileSystem); + +inline void fileFreeFileSystem() +{ + fileSetFileSystem(nullptr); +} + /** @brief Open file * @param name File name * @param flags Mode to open file * @retval file File ID or negative error code */ -file_t fileOpen(const String& name, FileOpenFlags flags); +inline file_t fileOpen(const char* name, FileOpenFlags flags = File::ReadOnly) +{ + CHECK_FS(open) + return fileSystem->open(name, flags); +} + +inline file_t fileOpen(const String& name, FileOpenFlags flags = File::ReadOnly) +{ + return fileOpen(name.c_str(), flags); +} + +inline file_t fileOpen(const FileStat& stat, FileOpenFlags flags = File::ReadOnly) +{ + CHECK_FS(fopen) + return fileSystem->fopen(stat, flags); +} /** @brief Clode file * @param file ID of file to open * @note File ID is returned from fileOpen function */ -void fileClose(file_t file); +inline int fileClose(file_t file) +{ + CHECK_FS(close) + return fileSystem->close(file); +} /** @brief Write to file * @param file File ID @@ -62,7 +135,20 @@ void fileClose(file_t file); * @param size Quantity of data elements to write to file * @retval int Quantity of data elements actually written to file or negative error code */ -int fileWrite(file_t file, const void* data, size_t size); +inline int fileWrite(file_t file, const void* data, size_t size) +{ + CHECK_FS(write); + return fileSystem->write(file, data, size); +} + +/** @brief Update file modification time + * @param file File ID + * @retval int Error code + */ +inline int fileTouch(file_t file) +{ + return fileWrite(file, nullptr, 0); +} /** @brief Read from file * @param file File ID @@ -70,110 +156,153 @@ int fileWrite(file_t file, const void* data, size_t size); * @param size Quantity of data elements to read from file * @retval int Quantity of data elements actually read from file or negative error code */ -int fileRead(file_t file, void* data, size_t size); +inline int fileRead(file_t file, void* data, size_t size) +{ + CHECK_FS(read) + return fileSystem->read(file, data, size); +} /** @brief Position file cursor * @param file File ID * @param offset Quantity of bytes to move cursor * @param origin Position from where to move cursor - * @retval Offset within file or negative error code + * @retval int Offset within file or negative error code */ -int fileSeek(file_t file, int offset, SeekOrigin origin); +inline int fileSeek(file_t file, int offset, SeekOrigin origin) +{ + CHECK_FS(seek) + return fileSystem->lseek(file, offset, origin); +} /** @brief Check if at end of file * @param file File ID - * @retval bool True if at end of file + * @retval bool true if at end of file */ -bool fileIsEOF(file_t file); +inline bool fileIsEOF(file_t file) +{ + auto fileSystem = getFileSystem(); + return fileSystem ? (fileSystem->eof(file) != 0) : true; +} /** @brief Get position in file * @param file File ID - * @retval int32_t Read / write cursor position + * @retval int32_t Read / write cursor position or error code */ -int32_t fileTell(file_t file); +inline int fileTell(file_t file) +{ + CHECK_FS(tell) + return fileSystem->tell(file); +} /** @brief Flush pending writes * @param file File ID - * @retval int Size of last file written or negative error number + * @retval int Size of last file written or error code */ -int fileFlush(file_t file); - -/** @brief Get last file system error code - * @param fd Not used - * @todo Why does fileLastError have unused fd parameter? - * @retval int Error code of last file system operation - */ -int fileLastError(file_t fd); +inline int fileFlush(file_t file) +{ + CHECK_FS(flush) + return fileSystem->flush(file); +} -/** @brief Clear last file system error - * @param fd Not used - * @todo Why does fileClearLastError have unused fd parameter? +/** @brief get the text for a returned error code + * @param err + * @retval String */ -void fileClearLastError(file_t fd); +inline String fileGetErrorString(int err) +{ + auto fileSystem = getFileSystem(); + if(fileSystem == nullptr) { + return IFS::Error::toString(err); + } + return fileSystem->getErrorString(err); +} /** @brief Create or replace file with defined content * @param fileName Name of file to create or replace * @param content Pointer to c-string containing content to populate file with - * @param length (optional) number of characters to write - * @retval int Positive value (>= 0) represents the numbers of bytes written - * Negative value (< 0) indicates error + * @param length Number of characters to write + * @retval int Number of bytes transferred or error code * @note This function creates a new file or replaces an existing file and * populates the file with the content of a c-string buffer. - If you do not specify `length`, remember to terminate your c-string buffer with a NUL ('\0'). */ -int fileSetContent(const String& fileName, const char* content, int length = -1); +template inline int fileSetContent(const TFileName& fileName, const char* content, size_t length) +{ + CHECK_FS(setContent) + return fileSystem->setContent(fileName, content, length); +} -/** @brief Create or replace file with defined content - * @param fileName Name of file to create or replace - * @param content String containing content to populate file with - * @retval int Positive integer represents the numbers of bytes written, - * negative integer represents the error code of last file system operation. - * @note This function creates a new file or replaces an existing file and - populates the file with the content of a string. - */ -int fileSetContent(const String& fileName, const String& content); +template inline int fileSetContent(const TFileName& fileName, TContent content) +{ + CHECK_FS(setContent) + return fileSystem->setContent(fileName, content); +} /** @brief Get size of file * @param fileName Name of file - * @retval uint32_t Size of file in bytes - * @note Returns 0 if error occurs + * @retval uint32_t Size of file in bytes, 0 on error */ -uint32_t fileGetSize(const String& fileName); +template inline uint32_t fileGetSize(const TFileName& fileName) +{ + auto fileSystem = getFileSystem(); + return fileSystem ? fileSystem->getSize(fileName) : 0; +} /** @brief Truncate (reduce) the size of an open file * @param file Open file handle, must have Write access * @param newSize - * @retval int error code + * @retval int Error code * @note In POSIX `ftruncate()` can also make the file bigger, however SPIFFS can only * reduce the file size and will return an error if newSize > fileSize */ -int fileTruncate(file_t file, size_t newSize); +inline int fileTruncate(file_t file, size_t newSize) +{ + CHECK_FS(truncate); + return fileSystem->truncate(file, newSize); +} /** @brief Truncate an open file at the current cursor position * @param file Open file handle, must have Write access - * @retval int error code + * @retval int Error code */ -int fileTruncate(file_t file); +inline int fileTruncate(file_t file) +{ + CHECK_FS(truncate); + return fileSystem->truncate(file); +} /** @brief Truncate (reduce) the size of a file * @param fileName * @param newSize - * @retval int error code + * @retval int Error code * @note In POSIX `truncate()` can also make the file bigger, however SPIFFS can only * reduce the file size and will return an error if newSize > fileSize */ -int fileTruncate(const String& fileName, size_t newSize); +template int fileTruncate(const TFileName& fileName, size_t newSize) +{ + CHECK_FS(truncate); + return fileSystem->truncate(fileName, newSize); +} /** @brief Rename file * @param oldName Original name of file to rename * @param newName New name for file - * @retval int error code + * @retval int Error code */ -int fileRename(const String& oldName, const String& newName); +inline int fileRename(const char* oldName, const char* newName) +{ + CHECK_FS(rename) + return fileSystem->rename(oldName, newName); +} + +inline int fileRename(const String& oldName, const String& newName) +{ + return fileRename(oldName.c_str(), newName.c_str()); +} /** @brief Get list of files on file system * @retval Vector Vector of strings. Each string element contains the name of a file on the file system + @deprecated use fileOpenDir / fileReadDir / fileCloseDir */ Vector fileList(); @@ -184,7 +313,11 @@ Vector fileList(); * The result will be an invalid String (equates to `false`) if the file could not be read. * If the file exists, but is empty, the result will be an empty string "". */ -String fileGetContent(const String& fileName); +template String fileGetContent(const TFileName& fileName) +{ + auto fileSystem = getFileSystem(); + return fileSystem ? fileSystem->getContent(fileName) : nullptr; +} /** @brief Read content of a file * @param fileName Name of file to read from @@ -192,46 +325,205 @@ String fileGetContent(const String& fileName); * @param bufSize Quantity of bytes to read from file * @retval size_t Quantity of bytes read from file or zero on failure * @note After calling this function the content of the file is placed in to a c-string - Ensure there is sufficient space in the buffer for file content - plus extra trailing null, i.e. at least bufSize + 1 - @note Returns 0 if the file could not be read + * Ensure there is sufficient space in the buffer for file content + * plus extra trailing null, i.e. at least bufSize + 1 + * Always check the return value! + * @note Returns 0 if the file could not be read */ -size_t fileGetContent(const String& fileName, char* buffer, size_t bufSize); +template inline size_t fileGetContent(const TFileName& fileName, char* buffer, size_t bufSize) +{ + auto fileSystem = getFileSystem(); + return fileSystem ? fileSystem->getContent(fileName, buffer, bufSize) : 0; +} -/** brief Get file statistics +template inline size_t fileGetContent(const TFileName& fileName, char* buffer) +{ + auto fileSystem = getFileSystem(); + return fileSystem ? fileSystem->getContent(fileName, buffer) : 0; +} + +/** @brief Get file statistics * @param name File name * @param stat Pointer to SPIFFS statistic structure to populate - * @retval int -1 on error - * @note Pass a pointer to an instantiated fileStats structure - * @todo Document the return value of fileStats + * @retval int Error code */ -int fileStats(const String& name, spiffs_stat* stat); +inline int fileStats(const char* fileName, FileStat& stat) +{ + CHECK_FS(stat) + return fileSystem->stat(fileName, &stat); +} + +inline int fileStats(const String& fileName, FileStat& stat) +{ + return fileStats(fileName.c_str(), stat); +} /** brief Get file statistics * @param file File ID * @param stat Pointer to SPIFFS statistic structure to populate - * @retval int -1 on error - * @note Pass a pointer to an instantiated fileStats structure - * @todo Document the return value of fileStats + * @retval int Error code */ -int fileStats(file_t file, spiffs_stat* stat); +inline int fileStats(file_t file, FileStat& stat) +{ + CHECK_FS(fstat) + return fileSystem->fstat(file, &stat); +} /** @brief Delete file * @param name Name of file to delete - * @retval int error code, 0 on success + * @retval int Error code */ -int fileDelete(const String& name); +inline int fileDelete(const char* fileName) +{ + CHECK_FS(remove) + return fileSystem->remove(fileName); +} + +inline int fileDelete(const String& fileName) +{ + return fileDelete(fileName.c_str()); +} /** @brief Delete file - * @param file ID of file to delete - * @retval int error code, 0 on success + * @param file handle of file to delete + * @retval int Error code */ -int fileDelete(file_t file); +inline int fileDelete(file_t file) +{ + CHECK_FS(fremove) + return fileSystem->fremove(file); +} /** @brief Check if a file exists on file system * @param name Name of file to check for - * @retval bool True if file exists + * @retval bool true if file exists + */ +inline bool fileExist(const char* fileName) +{ + CHECK_FS(stat) + return fileSystem->stat(fileName, nullptr) >= 0; +} + +inline bool fileExist(const String& fileName) +{ + return fileExist(fileName.c_str()); +} + +/** @brief Open a named directory for reading + * @param name Name of directory to open, empty or "/" for root + * @param dir Directory object + * @retval int Error code + * + */ +inline int fileOpenDir(const char* dirName, DirHandle& dir) +{ + CHECK_FS(opendir) + return fileSystem->opendir(dirName, dir); +} + +inline int fileOpenDir(const String& dirName, DirHandle& dir) +{ + return fileOpenDir(dirName.c_str(), dir); +} + +inline int fileOpenRootDir(DirHandle& dir) +{ + return fileOpenDir(nullptr, dir); +} + +/** @brief Open a sub-directory for reading + * @param stat Details of directory to open, nullptr for root directory + * @param dir Directory object + * @retrval 0 on success or negative error */ -bool fileExist(const String& name); +inline int fileOpenDir(const FileStat& stat, DirHandle& dir) +{ + CHECK_FS(opendir) + return fileSystem->fopendir(&stat, dir); +} + +/** @brief close a directory object + * @param dir directory to close + * @retval int Error code + */ +inline int fileCloseDir(DirHandle dir) +{ + CHECK_FS(closedir) + return fileSystem->closedir(dir); +} + +/** @brief Read a directory entry + * @param dir The directory object returned by fileOpenDir() + * @param stat The returned information, owned by DirHandle object + */ +inline int fileReadDir(DirHandle dir, FileStat& stat) +{ + CHECK_FS(readdir) + return fileSystem->readdir(dir, stat); +} + +/** @brief Get basic file system information + * @retval int Error code + */ +inline int fileGetSystemInfo(IFS::IFileSystem::Info& info) +{ + CHECK_FS(getinfo) + return fileSystem->getinfo(info); +} + +/** @brief Get the type of file system currently mounted (if any) + * @retval FileSystemType the file system type + */ +IFS::IFileSystem::Type fileSystemType(); + +/** @brief Format the active file system + * @retval int Error code + */ +inline int fileSystemFormat() +{ + CHECK_FS(format) + return fileSystem->format(); +} + +/** @brief Perform a consistency check/repair on the active file system + * @retval int 0 if OK, < 0 unrecoverable errors, > 0 repairs required + */ +inline int fileSystemCheck() +{ + CHECK_FS(check) + return fileSystem->check(); +} + +/** @brief Set access control information + * @param file File handle + * @param acl + * @retval int Error code + */ +inline int fileSetACL(file_t file, const File::ACL& acl) +{ + CHECK_FS(setacl) + return fileSystem->setacl(file, acl); +} + +/** @brief Set file attributes + * @param file handle to open file, must have write access + * @retval int Error code + */ +inline int fileSetAttr(file_t file, File::Attributes attr) +{ + CHECK_FS(setattr) + return fileSystem->setattr(file, attr); +} + +/** @brief Set access control information for file + * @param file handle to open file, must have write access + * @retval int Error code + * @note any writes to file will reset this to current time + */ +inline int fileSetTime(file_t file, time_t mtime) +{ + CHECK_FS(settime) + return fileSystem->settime(file, mtime); +} /** @} */ diff --git a/Sming/Core/Network/Ftp/FtpDataRetrieve.h b/Sming/Core/Network/Ftp/FtpDataRetrieve.h index 0fecb0821f..70cc360bc3 100644 --- a/Sming/Core/Network/Ftp/FtpDataRetrieve.h +++ b/Sming/Core/Network/Ftp/FtpDataRetrieve.h @@ -17,7 +17,7 @@ class FtpDataRetrieve : public FtpDataStream { public: FtpDataRetrieve(FtpServerConnection& connection, const String& fileName) - : FtpDataStream(connection), file(fileOpen(fileName, eFO_ReadOnly)) + : FtpDataStream(connection), file(fileOpen(fileName, File::ReadOnly)) { } diff --git a/Sming/Core/Network/Ftp/FtpDataStore.h b/Sming/Core/Network/Ftp/FtpDataStore.h index aa33032190..a713b97af0 100644 --- a/Sming/Core/Network/Ftp/FtpDataStore.h +++ b/Sming/Core/Network/Ftp/FtpDataStore.h @@ -17,7 +17,7 @@ class FtpDataStore : public FtpDataStream { public: FtpDataStore(FtpServerConnection& connection, const String& fileName) - : FtpDataStream(connection), file(fileOpen(fileName, eFO_WriteOnly | eFO_CreateNewAlways)) + : FtpDataStream(connection), file(fileOpen(fileName, File::WriteOnly | File::CreateNewAlways)) { } diff --git a/Sming/Core/Network/FtpServer.cpp b/Sming/Core/Network/FtpServer.cpp index b2515665c5..efb0a75918 100644 --- a/Sming/Core/Network/FtpServer.cpp +++ b/Sming/Core/Network/FtpServer.cpp @@ -36,7 +36,7 @@ bool FtpServer::checkUser(const String& login, const String& pass) bool FtpServer::onCommand(String cmd, String data, FtpServerConnection& connection) { if(cmd == _F("FSFORMAT")) { - spiffs_format(); + fileSystemFormat(); connection.response(200, F("File system successfully formatted")); return true; } diff --git a/Sming/Core/Network/Http/HttpResponse.cpp b/Sming/Core/Network/Http/HttpResponse.cpp index 517d2deee7..6990cd6615 100644 --- a/Sming/Core/Network/Http/HttpResponse.cpp +++ b/Sming/Core/Network/Http/HttpResponse.cpp @@ -56,22 +56,40 @@ bool HttpResponse::sendString(String&& text) noexcept return true; } +bool HttpResponse::sendFile(const FileStat& stat) +{ + auto file = new FileStream(stat); + if(stat.compression == File::Compression::GZip) { + headers[HTTP_HEADER_CONTENT_ENCODING] = F("gzip"); + } else if(stat.compression != File::Compression::None) { + debug_e("Unsupported compression type: %u", stat.compression); + } + + return sendDataStream(file, ContentType::fromFullFileName(stat.name)); +} + bool HttpResponse::sendFile(const String& fileName, bool allowGzipFileCheck) { - IDataSourceStream* stream = nullptr; - String compressed = fileName + ".gz"; - if(allowGzipFileCheck && fileExist(compressed)) { - debug_d("found %s", compressed.c_str()); - stream = new FileStream(compressed); - headers[HTTP_HEADER_CONTENT_ENCODING] = _F("gzip"); - } else if(fileExist(fileName)) { - debug_d("found %s", fileName.c_str()); - stream = new FileStream(fileName); + FileStat stat; + + if(allowGzipFileCheck) { + String fnCompressed = fileName + _F(".gz"); + if(fileStats(fnCompressed, stat) >= 0) { + debug_d("found %s", stat.name); + stat.compression = File::Compression::GZip; + stat.name = IFS::NameBuffer(fnCompressed); + return sendFile(stat); + } } - headers[HTTP_HEADER_CONTENT_TYPE] = ContentType::fromFullFileName(fileName); + if(fileStats(fileName, stat) >= 0) { + debug_d("found %s", fileName.c_str()); + stat.name = IFS::NameBuffer((char*)fileName.c_str(), fileName.length(), fileName.length()); + return sendFile(stat); + } - return sendNamedStream(stream); + code = HTTP_STATUS_NOT_FOUND; + return false; } bool HttpResponse::sendNamedStream(IDataSourceStream* newDataStream) diff --git a/Sming/Core/Network/Http/HttpResponse.h b/Sming/Core/Network/Http/HttpResponse.h index 1debc961d0..b852a8233d 100644 --- a/Sming/Core/Network/Http/HttpResponse.h +++ b/Sming/Core/Network/Http/HttpResponse.h @@ -94,6 +94,12 @@ class HttpResponse return this; } + /* + * Send file by stat, indicates whether file is compressed + * A name is required in stat to get the appropriate content type + */ + bool sendFile(const FileStat& stat); + /** * @brief Send file by name * @param fileName diff --git a/Sming/Core/Network/HttpClient.cpp b/Sming/Core/Network/HttpClient.cpp index faff09432a..723788e3fe 100644 --- a/Sming/Core/Network/HttpClient.cpp +++ b/Sming/Core/Network/HttpClient.cpp @@ -54,7 +54,7 @@ bool HttpClient::downloadFile(const Url& url, const String& saveFileName, Reques } auto fileStream = new FileStream(); - if(!fileStream->open(file, eFO_CreateNewAlways | eFO_WriteOnly)) { + if(!fileStream->open(file, File::CreateNewAlways | File::WriteOnly)) { debug_e("HttpClient failed to open \"%s\"", file.c_str()); delete fileStream; return false; diff --git a/Sming/Libraries/Adafruit_GFX/BMPDraw.h b/Sming/Libraries/Adafruit_GFX/BMPDraw.h index 5414d03ad0..4ceb3e0401 100644 --- a/Sming/Libraries/Adafruit_GFX/BMPDraw.h +++ b/Sming/Libraries/Adafruit_GFX/BMPDraw.h @@ -64,7 +64,7 @@ template bool bmpDraw(Adafruit_TFT& tft, String fileName, u uint32_t startTime = millis(); - file_t handle = fileOpen(fileName.c_str(), eFO_ReadOnly); + file_t handle = fileOpen(fileName.c_str(), File::ReadOnly); if(handle < 0) { debug_e("File wasn't found: %s", fileName.c_str()); return false; diff --git a/Sming/Libraries/ArduinoJson6/include/ArduinoJson.h b/Sming/Libraries/ArduinoJson6/include/ArduinoJson.h index 955857231a..567a80a914 100644 --- a/Sming/Libraries/ArduinoJson6/include/ArduinoJson.h +++ b/Sming/Libraries/ArduinoJson6/include/ArduinoJson.h @@ -256,7 +256,7 @@ template String serialize(const TSource& source, Serializatio template bool saveToFile(const TSource& source, const String& filename, SerializationFormat format = JSON_FORMAT_DEFAULT) { - FileStream stream(filename, eFO_WriteOnly | eFO_CreateNewAlways); + FileStream stream(filename, File::WriteOnly | File::CreateNewAlways); if(!stream.isValid()) { return false; } diff --git a/Sming/component.mk b/Sming/component.mk index 9685debd3a..8f3471a83c 100644 --- a/Sming/component.mk +++ b/Sming/component.mk @@ -17,6 +17,7 @@ COMPONENT_DEPENDS := \ sming-arch \ FlashString \ spiffs \ + IFS \ http-parser \ libb64 \ ws_parser \ diff --git a/docs/source/upgrading/4.2-4.3.rst b/docs/source/upgrading/4.2-4.3.rst index 9d52386086..63be9b39f8 100644 --- a/docs/source/upgrading/4.2-4.3.rst +++ b/docs/source/upgrading/4.2-4.3.rst @@ -96,3 +96,27 @@ Configuration variables } } + +Installable File System (IFS) +----------------------------- + +Sming now supports multiple filesystems via :component:`IFS`. + +See :sample:`Basic_IFS` for a demonstration. + +:source:`Core/FileSystem.h` has been modified to use IFS but the API remains largely unchanged, although somewhat expanded. +Functions are now mainly just wrappers around filing system calls. + +A single global IFileSystem instance is used. + +SPIFFS + All access is now managed via the ``IFS::SPIFFS::FileSystem`` implementation. + + Applications should not use SPIFFS functions directly. + + SPIFFS is now built with ``SPIFFS_OBJ_META_LEN=16`` to store extended attribute information, + but existing volumes should still be readable. + +File open flags + e.g. eFO_ReadOnly. These will still work but are now deprecated and should be replaced with their + C++ equivalent such as ``File::ReadOnly``. diff --git a/samples/Basic_IFS/.cproject b/samples/Basic_IFS/.cproject new file mode 100644 index 0000000000..a217ffc882 --- /dev/null +++ b/samples/Basic_IFS/.cproject @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + make + -f ${ProjDirPath}/Makefile + all + true + true + true + + + make + -f ${ProjDirPath}/Makefile + clean + true + true + true + + + make + -f ${ProjDirPath}/Makefile + flash + true + true + true + + + make + -f ${ProjDirPath}/Makefile + flashonefile + true + true + true + + + make + -f ${ProjDirPath}/Makefile + flashinit + true + true + true + + + make + -f ${ProjDirPath}/Makefile + flashboot + true + true + true + + + make + -f ${ProjDirPath}/Makefile + rebuild + true + true + true + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Basic_IFS/.project b/samples/Basic_IFS/.project new file mode 100644 index 0000000000..56fba6c6ed --- /dev/null +++ b/samples/Basic_IFS/.project @@ -0,0 +1,29 @@ + + + Basic_IFS + + + SmingFramework + Sming + + + + org.eclipse.cdt.managedbuilder.core.genmakebuilder + clean,full,incremental, + + + + + org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder + full,incremental, + + + + + + org.eclipse.cdt.core.cnature + org.eclipse.cdt.core.ccnature + org.eclipse.cdt.managedbuilder.core.managedBuildNature + org.eclipse.cdt.managedbuilder.core.ScannerConfigNature + + diff --git a/samples/Basic_IFS/Makefile b/samples/Basic_IFS/Makefile new file mode 100644 index 0000000000..ff51b6c3a7 --- /dev/null +++ b/samples/Basic_IFS/Makefile @@ -0,0 +1,9 @@ +##################################################################### +#### Please don't change this file. Use component.mk instead #### +##################################################################### + +ifndef SMING_HOME +$(error SMING_HOME is not set: please configure it as an environment variable) +endif + +include $(SMING_HOME)/project.mk diff --git a/samples/Basic_IFS/README.rst b/samples/Basic_IFS/README.rst new file mode 100644 index 0000000000..70867537f0 --- /dev/null +++ b/samples/Basic_IFS/README.rst @@ -0,0 +1,24 @@ +Basic IFS +========= + +.. highlight:: bash + +Simple Webserver demonstration using IFS. + +View the filesystem using a web browser. + +To see the data in a different format, add ``?format=XX``. Current supported formats are ``json``, ``text`` and ``html``. + +Building +-------- + +By default, data is stored in a read-only FWFS (Firmware Filesytem) partition. + +This sample also demonstrates how to store the data in a :cpp:class:`FlashString` object:: + + make config-clean + make ENABLE_FLASHSTRING_IMAGE=1 + +Because the data is linked into the program image this is only suitable for small filesystem images. +This could be used to store default recovery data, especially with OTA updates because each program +image is self-contained. diff --git a/samples/Basic_IFS/app/application.cpp b/samples/Basic_IFS/app/application.cpp new file mode 100644 index 0000000000..5828f96d24 --- /dev/null +++ b/samples/Basic_IFS/app/application.cpp @@ -0,0 +1,220 @@ +/* + * Webserver demo using IFS + * + */ + +#include +#include +#include +#include +#include +#include +#include + +// If you want, you can define WiFi settings globally in Eclipse Environment Variables +#ifndef WIFI_SSID +#define WIFI_SSID "PleaseEnterSSID" // Put you SSID and Password here +#define WIFI_PWD "PleaseEnterPass" +#endif + +namespace +{ +#ifdef ENABLE_FLASHSTRING_IMAGE +IMPORT_FSTR(fwfsImage, PROJECT_DIR "/out/fwfs1.bin") +#endif + +IMPORT_FSTR(listing_html, PROJECT_DIR "/resource/listing.html") +IMPORT_FSTR(listing_txt, PROJECT_DIR "/resource/listing.txt") +IMPORT_FSTR(listing_json, PROJECT_DIR "/resource/listing.json") + +HttpServer server; +FtpServer ftp; +int requestCount; + +/* + * Handle any custom fields here + */ +String getValue(const char* name) +{ + if(FS("webpage") == name) { + return "https://github.com/SmingHub/Sming"; + } + + if(FS("request-count") == name) { + return String(requestCount); // Doesn't require escaping + } + + return nullptr; +} + +void onFile(HttpRequest& request, HttpResponse& response) +{ + ++requestCount; + + String file = request.uri.getRelativePath(); + + FileStat stat; + if(fileStats(file, stat) < 0) { + response.code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + return; + } + + if(stat.attr[File::Attribute::Directory]) { + auto dir = new Directory; + IFS::DirectoryTemplate* tmpl; + String fmt = request.uri.Query["format"]; + if(fmt.equalsIgnoreCase("json")) { + auto source = new FlashMemoryStream(listing_json); + tmpl = new IFS::JsonDirectoryTemplate(source, dir); + } else if(fmt.equalsIgnoreCase("text")) { + auto source = new FlashMemoryStream(listing_txt); + tmpl = new IFS::DirectoryTemplate(source, dir); + } else { + auto source = new FlashMemoryStream(listing_html); + tmpl = new IFS::HtmlDirectoryTemplate(source, dir); + } + tmpl->onGetValue(getValue); + dir->open(file); + tmpl->gotoSection(0); + response.sendDataStream(tmpl, tmpl->getMimeType()); + } else { + // response.setCache(86400, true); // It's important to use cache for better performance. + auto stream = new FileStream(stat); + if(stat.compression == File::Compression::GZip) { + response.headers[HTTP_HEADER_CONTENT_ENCODING] = F("gzip"); + } else if(stat.compression != File::Compression::None) { + debug_e("Unsupported compression type: %u", stat.compression); + } + + auto mimeType = ContentType::fromFullFileName(file.c_str(), MIME_TEXT); + response.sendDataStream(stream, mimeType); + } +} + +void startWebServer() +{ + server.listen(80); + server.paths.setDefault(onFile); + + Serial.println("\r\n=== WEB SERVER STARTED ==="); + Serial.println(WifiStation.getIP()); + Serial.println("==============================\r\n"); +} + +void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) +{ + startWebServer(); +} + +bool initFileSystem() +{ + fileFreeFileSystem(); + +#if DEBUG_VERBOSE_LEVEL >= INFO + auto freeheap = system_get_free_heap_size(); +#endif + debug_i("1: heap = %u", freeheap); + +#ifdef ENABLE_FLASHSTRING_IMAGE + // Create a partition wrapping some flashstring data + auto part = Storage::progMem.createPartition(F("fwfsMem"), fwfsImage, Storage::Partition::SubType::Data::fwfs); +#else + auto part = *Storage::findPartition(Storage::Partition::SubType::Data::fwfs); + if(part) { + debug_i("Found '%s'", part.name().c_str()); + } else { + debug_e("No FWFS partition found"); + } +#endif + + IFS::IFileSystem* fs; +#ifdef FWFS_HYBRID + // Create a read/write filesystem + fs = IFS::createHybridFilesystem(part); +#else + // Read-only + fs = IFS::createFirmwareFilesystem(part); +#endif + debug_i("2: heap = -%u", freeheap - system_get_free_heap_size()); + + if(fs == nullptr) { + debug_e("Failed to created filesystem object"); + return false; + } + + int res = fs->mount(); + debug_i("3: heap = -%u", freeheap - system_get_free_heap_size()); + + debug_i("mount() returned %d (%s)", res, fs->getErrorString(res).c_str()); + + if(res < 0) { + delete fs; + return false; + } + + fileSetFileSystem(fs); + + debug_i("File system initialised"); + return true; +} + +void printDirectory(const char* path) +{ + auto printStream = [](IDataSourceStream& stream) { + // Use an intermediate memory stream so debug information doesn't get mixed into output + // MemoryDataStream mem; + // mem.copyFrom(&stream); + // Serial.copyFrom(&mem); + Serial.copyFrom(&stream); + }; + + { + auto dir = new Directory; + if(!dir->open(path)) { + debug_e("Open '%s' failed: %s", path, dir->getLastErrorString().c_str()); + delete dir; + return; + } + + auto source = new FlashMemoryStream(listing_txt); + IFS::DirectoryTemplate tmpl(source, dir); + printStream(tmpl); + } + + { + auto dir = new Directory; + if(!dir->open(path)) { + debug_e("Open '%s' failed: %s", path, dir->getLastErrorString().c_str()); + delete dir; + return; + } + + auto source = new FlashMemoryStream(listing_json); + IFS::JsonDirectoryTemplate tmpl(source, dir); + printStream(tmpl); + } +} +} // namespace + +void init() +{ +#if DEBUG_BUILD + Serial.begin(COM_SPEED_SERIAL); + + Serial.systemDebugOutput(true); + debug_i("\n\n********************************************************\n" + "Hello\n"); +#endif + + initFileSystem(); + + printDirectory(nullptr); + + WifiStation.enable(true); + WifiStation.config(WIFI_SSID, WIFI_PWD); + WifiAccessPoint.enable(false); + + WifiEvents.onStationGotIP(gotIP); + + // testDirectoryStreams(); +} diff --git a/samples/Basic_IFS/basic_ifs.hw b/samples/Basic_IFS/basic_ifs.hw new file mode 100644 index 0000000000..b001259618 --- /dev/null +++ b/samples/Basic_IFS/basic_ifs.hw @@ -0,0 +1,21 @@ +{ + "name": "Basic IFS sample", + "base_config": "spiffs", + "partitions": { + "spiffs0": { + "size": "0x20000", + "build": {} + }, + "fwfs1": { + "address": "0x280000", + "size": "0x60000", + "type": "data", + "subtype": "fwfs", + "filename": "out/fwfs1.bin", + "build": { + "target": "fwfs-build", + "config": "fsimage.ini" + } + } + } +} \ No newline at end of file diff --git a/samples/Basic_IFS/component.mk b/samples/Basic_IFS/component.mk new file mode 100644 index 0000000000..81f4a0ac1d --- /dev/null +++ b/samples/Basic_IFS/component.mk @@ -0,0 +1,12 @@ +# Empty SPIFFS partition please +SPIFF_FILES := + +# Use to store filesystem image in a FlashString object instead of partition +CONFIG_VARS += ENABLE_FLASHSTRING_IMAGE +ENABLE_FLASHSTRING_IMAGE ?= 0 +ifeq ($(ENABLE_FLASHSTRING_IMAGE),1) +COMPONENT_CXXFLAGS += -DENABLE_FLASHSTRING_IMAGE=1 +HWCONFIG := spiffs +else +HWCONFIG := basic_ifs +endif diff --git a/samples/Basic_IFS/files/.auth.json b/samples/Basic_IFS/files/.auth.json new file mode 100644 index 0000000000..beb9ec86b9 --- /dev/null +++ b/samples/Basic_IFS/files/.auth.json @@ -0,0 +1,19 @@ +{ + "users": [ + { + "name":"", + "password":"", + "access":"guest" + }, + { + "name":"user", + "password":"user", + "access":"user" + }, + { + "name":"admin", + "password":"admin", + "access":"admin" + } + ] +} diff --git a/samples/Basic_IFS/files/.network.json b/samples/Basic_IFS/files/.network.json new file mode 100644 index 0000000000..1295805160 --- /dev/null +++ b/samples/Basic_IFS/files/.network.json @@ -0,0 +1,8 @@ +{ + "hostname":"Sming-IFS", + "server-port":80, + "accesspoint": { + "ssid":"Sming IFS", + "password":"welcome" + } +} diff --git a/samples/Basic_IFS/files/.time.json b/samples/Basic_IFS/files/.time.json new file mode 100644 index 0000000000..08b3032c55 --- /dev/null +++ b/samples/Basic_IFS/files/.time.json @@ -0,0 +1,4 @@ +{ + "latitude":52.01486, + "longitude":-0.70126 +} diff --git a/samples/Basic_IFS/files/A Subdirectory/The name of this file is excessively long. Can't say I'd ever want to do this, but hey, why not.txt b/samples/Basic_IFS/files/A Subdirectory/The name of this file is excessively long. Can't say I'd ever want to do this, but hey, why not.txt new file mode 100644 index 0000000000..fb0665dc5b --- /dev/null +++ b/samples/Basic_IFS/files/A Subdirectory/The name of this file is excessively long. Can't say I'd ever want to do this, but hey, why not.txt @@ -0,0 +1,7 @@ +Mary had a little lamb +It's fleece was very red. +The reason for this was, +You see, it had a pickaxe +Though its head. + +Anon. \ No newline at end of file diff --git a/samples/Basic_IFS/files/A Subdirectory/a/b/c/d/e/f/lonely.txt b/samples/Basic_IFS/files/A Subdirectory/a/b/c/d/e/f/lonely.txt new file mode 100644 index 0000000000..a59579b465 --- /dev/null +++ b/samples/Basic_IFS/files/A Subdirectory/a/b/c/d/e/f/lonely.txt @@ -0,0 +1 @@ +I am a lonely file. Where are my fwends? \ No newline at end of file diff --git a/samples/Basic_IFS/files/apple-touch-icon-180x180.png b/samples/Basic_IFS/files/apple-touch-icon-180x180.png new file mode 100644 index 0000000000000000000000000000000000000000..5a9221f4cdd765249bf038f264086db03614c1f9 GIT binary patch literal 17250 zcmV)zK#{+RP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 lIdeqj2>a;|@=DF^@n literal 0 HcmV?d00001 diff --git a/samples/Basic_IFS/files/bootstrap.min.css b/samples/Basic_IFS/files/bootstrap.min.css new file mode 100644 index 0000000000..6561b6f4c9 --- /dev/null +++ b/samples/Basic_IFS/files/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.0.0 (https://getbootstrap.com) + * Copyright 2011-2018 The Bootstrap Authors + * Copyright 2011-2018 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014 \00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-sm-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-sm-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-sm-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-sm-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-sm-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-sm-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-sm-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-sm-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-sm-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-sm-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-sm-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-sm-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-sm-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-sm-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-md-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-md-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-md-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-md-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-md-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-md-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-md-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-md-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-md-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-md-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-md-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-md-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-md-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-md-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-lg-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-lg-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-lg-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-lg-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-lg-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-lg-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-lg-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-lg-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-lg-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-lg-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-lg-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-lg-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-lg-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-lg-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-xl-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-xl-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-xl-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-xl-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-xl-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-xl-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-xl-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-xl-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-xl-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-xl-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-xl-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-xl-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-xl-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-xl-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm,.input-group-lg>.form-control-plaintext.form-control,.input-group-lg>.input-group-append>.form-control-plaintext.btn,.input-group-lg>.input-group-append>.form-control-plaintext.input-group-text,.input-group-lg>.input-group-prepend>.form-control-plaintext.btn,.input-group-lg>.input-group-prepend>.form-control-plaintext.input-group-text,.input-group-sm>.form-control-plaintext.form-control,.input-group-sm>.input-group-append>.form-control-plaintext.btn,.input-group-sm>.input-group-append>.form-control-plaintext.input-group-text,.input-group-sm>.input-group-prepend>.form-control-plaintext.btn,.input-group-sm>.input-group-prepend>.form-control-plaintext.input-group-text{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-sm>.input-group-append>select.btn:not([size]):not([multiple]),.input-group-sm>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:calc(1.8125rem + 2px)}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-lg>.input-group-append>select.btn:not([size]):not([multiple]),.input-group-lg>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:calc(2.875rem + 2px)}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(40,167,69,.8);border-radius:.2rem}.custom-select.is-valid,.form-control.is-valid,.was-validated .custom-select:valid,.was-validated .form-control:valid{border-color:#28a745}.custom-select.is-valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{background-color:#71dd8a}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(40,167,69,.25)}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label::before,.was-validated .custom-file-input:valid~.custom-file-label::before{border-color:inherit}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(220,53,69,.8);border-radius:.2rem}.custom-select.is-invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.was-validated .form-control:invalid{border-color:#dc3545}.custom-select.is-invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{background-color:#efa2a9}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(220,53,69,.25)}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label::before,.was-validated .custom-file-input:invalid~.custom-file-label::before{border-color:inherit}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-check{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}.btn:not(:disabled):not(.disabled).active,.btn:not(:disabled):not(.disabled):active{background-image:none}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-primary{color:#007bff;background-color:transparent;background-image:none;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;background-color:transparent;background-image:none;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;background-color:transparent}.btn-link:hover{color:#0056b3;text-decoration:underline;background-color:transparent;border-color:transparent}.btn-link.focus,.btn-link:focus{text-decoration:underline;border-color:transparent;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}.dropdown,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropup .dropdown-menu{margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;width:0;height:0;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file:focus,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control{margin-left:-1px}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::before{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label,.input-group>.custom-file:not(:first-child) .custom-file-label::before{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-webkit-box;display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:active~.custom-control-label::before{color:#fff;background-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{margin-bottom:0}.custom-control-label::before{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#dee2e6}.custom-control-label::after{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;content:"";background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;background-size:8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:inset 0 1px 2px rgba(0,0,0,.075),0 0 5px rgba(128,189,255,.5)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-control{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:focus~.custom-file-control::before{border-color:#80bdff}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(calc(2.25rem + 2px) - 1px * 2);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:1px solid #ced4da;border-radius:0 .25rem .25rem 0}.nav{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .dropup .dropdown-menu{top:auto;bottom:100%}}.navbar-expand{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .dropup .dropdown-menu{top:auto;bottom:100%}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-header,.card-group>.card:first-child .card-img-top{border-top-right-radius:0}.card-group>.card:first-child .card-footer,.card-group>.card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-header,.card-group>.card:last-child .card-img-top{border-top-left-radius:0}.card-group>.card:last-child .card-footer,.card-group>.card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:.25rem}.card-group>.card:only-child .card-header,.card-group>.card:only-child .card-img-top{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-group>.card:only-child .card-footer,.card-group>.card:only-child .card-img-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child){border-radius:0}.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%}}.breadcrumb{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-webkit-box;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}.badge-primary[href]:focus,.badge-primary[href]:hover{color:#fff;text-decoration:none;background-color:#0062cc}.badge-secondary{color:#fff;background-color:#6c757d}.badge-secondary[href]:focus,.badge-secondary[href]:hover{color:#fff;text-decoration:none;background-color:#545b62}.badge-success{color:#fff;background-color:#28a745}.badge-success[href]:focus,.badge-success[href]:hover{color:#fff;text-decoration:none;background-color:#1e7e34}.badge-info{color:#fff;background-color:#17a2b8}.badge-info[href]:focus,.badge-info[href]:hover{color:#fff;text-decoration:none;background-color:#117a8b}.badge-warning{color:#212529;background-color:#ffc107}.badge-warning[href]:focus,.badge-warning[href]:hover{color:#212529;text-decoration:none;background-color:#d39e00}.badge-danger{color:#fff;background-color:#dc3545}.badge-danger[href]:focus,.badge-danger[href]:hover{color:#fff;text-decoration:none;background-color:#bd2130}.badge-light{color:#212529;background-color:#f8f9fa}.badge-light[href]:focus,.badge-light[href]:hover{color:#212529;text-decoration:none;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}.badge-dark[href]:focus,.badge-dark[href]:hover{color:#fff;text-decoration:none;background-color:#1d2124}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;background-color:#007bff;transition:width .6s ease}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-ms-flex:1;flex:1}.list-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;opacity:.75}.close:not(:disabled):not(.disabled){cursor:pointer}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.show .modal-dialog{-webkit-transform:translate(0,0);transform:translate(0,0)}.modal-dialog-centered{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:calc(100% - (.5rem * 2))}.modal-content{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #e9ecef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - (1.75rem * 2))}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top] .arrow,.bs-popover-top .arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::after,.bs-popover-top .arrow::before{border-width:.5rem .5rem 0}.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::before{bottom:0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-top .arrow::after{bottom:1px;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right] .arrow,.bs-popover-right .arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::after,.bs-popover-right .arrow::before{border-width:.5rem .5rem .5rem 0}.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::before{left:0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-right .arrow::after{left:1px;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom] .arrow,.bs-popover-bottom .arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::after,.bs-popover-bottom .arrow::before{border-width:0 .5rem .5rem .5rem}.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::before{top:0;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-bottom .arrow::after{top:1px;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left] .arrow,.bs-popover-left .arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::after,.bs-popover-left .arrow::before{border-width:.5rem 0 .5rem .5rem}.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::before{right:0;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-left .arrow::after{right:1px;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translateX(100%);transform:translateX(100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translateX(-100%);transform:translateX(-100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-circle{border-radius:50%!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;-webkit-clip-path:inset(50%);clip-path:inset(50%);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal;-webkit-clip-path:none;clip-path:none}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0062cc!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#545b62!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#1e7e34!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#117a8b!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#d39e00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#bd2130!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#dae0e5!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#1d2124!important}.text-muted{color:#6c757d!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/samples/Basic_IFS/files/error.html b/samples/Basic_IFS/files/error.html new file mode 100644 index 0000000000..14c35043cb --- /dev/null +++ b/samples/Basic_IFS/files/error.html @@ -0,0 +1,26 @@ + + + + + + +Sming IFS demo + + + + + + + +

Sming IFS demo

+ +

Sorry, there's been a problem.

+ +

Requested path: {path}

+ +

{code}: {text}

+ +Return to home page + + + diff --git a/samples/Basic_IFS/files/favicon.ico b/samples/Basic_IFS/files/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4ef8ef16286c58dd37617b0d8c71ae4ce91b51dd GIT binary patch literal 15086 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 literal 0 HcmV?d00001 diff --git a/samples/Basic_IFS/files/styles.css b/samples/Basic_IFS/files/styles.css new file mode 100644 index 0000000000..4f03804998 --- /dev/null +++ b/samples/Basic_IFS/files/styles.css @@ -0,0 +1,165 @@ + +/* + Safari (iPhone) will auto-zoom if input elements are less than 16px, and the + default for that platform is 11px. That causes the 'Connect' button to disappear + from the screen when entering login details. + To fix this, change the default text size to the minimum; it also makes things + easier to read. +*/ +body { + font-size: 16px; +} + +input, button { + font-size: inherit; +} + +select { + font-family:"Lucida Console", monospace, serif; + width: 100%; + display: block; +} + +select option { + width:auto; +} + + +div .menu { + display: inline-block; + cursor: pointer; +} + +div .bar { + width: 25px; + height: 5px; + background-color: black; + margin: 4px 0; +} + +.sidenav { + height: 100%; + width: 0; + position: fixed; + z-index: 1; + top: 0; + left: 0; + background-color: rgba(0,0,0,0.85); + overflow-x: hidden; + transition: 0.1s; + padding-top: 60px; +} + +.sidenav a { + padding: 8px 8px 8px 32px; + text-decoration: none; + font-size: 25px; + color: #818181; + display: block; + transition: 0.2s; +} + +.sidenav a:hover { + color: #f1f1f1; +} + +.sidenav .closebtn { + position: absolute; + top: 0; + right: 25px; + font-size: 36px; + margin-left: 50px; +} + +@media screen and (max-height: 450px) { + .sidenav {padding-top: 15px;} + .sidenav a {font-size: 18px;} +} + + + + + + +.accordion { + background-color: #eee; + color: #444; + cursor: pointer; + padding: 18px; + text-align: left; + border: none; + width: 100%; + outline: none; + transition: 0.25s; +} + +/* + Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) +*/ +.active, .accordion:hover { + background-color: #ccc; +} + +.accordion:before { + content: '\02795'; /* Unicode character for "plus" sign (+) */ +} + +.active:before { + content: "\2796"; /* Unicode character for "minus" sign (-) */ +} + + +/* + Style the accordion panel. Note: hidden by default +*/ +.panel { + display: none; + padding: 0 18px; + background-color: white; + overflow: hidden; +} + + +.panel button { + outline: none; + border: none; + border-radius: 8px; + color: black; + background-color: GainsBoro; + padding: 10px 10px; + text-align: left; + text-decoration: none; + display: inline-block; + margin: 2px 2px; + cursor: pointer; +} + +.panel button.on { background-color: Red; color: white; } +.panel button.someon { background-color: Orange; color: white; } +.panel button.off { background-color: DimGray; color: white; } +.panel button.unk { background-color: WhiteSmoke; color: DarkGrey; opacity: 0.6; } +.panel button.pending { background-color: DarkSeaGreen; color: white; } +.panel button.disabled { background-color: WhiteSmoke; color: black; opacity: 0.6 } + +.panel button.accordion { border-radius: 0px; opacity: 0.8; } + + +.content { + display: block; +} + +.group { + display: none; + padding: 6px 12px; + border: 1px solid #ccc; + border-top: none; +} + + +#progress { + width: 100%; + background-color: lightgrey; + text-align: center; + color: white; +} + diff --git a/samples/Basic_IFS/fsimage.ini b/samples/Basic_IFS/fsimage.ini new file mode 100644 index 0000000000..6e23dc647a --- /dev/null +++ b/samples/Basic_IFS/fsimage.ini @@ -0,0 +1,32 @@ + +# Filesystem builder configuration +[config] + +volumeName = Sming IFS demo volume +volumeID = 0x12345678 + +# Where to read files from +[source] +/ = files +readme.md = ${SMING_HOME}/Components/IFS/README.rst +sming.png = ${SMING_HOME}/../docs/api-logo.png +Data = ${SMING_HOME}/Core/Data +framework = ${SMING_HOME}/../docs/source/framework +Sming = ${SMING_HOME}/Core + +# Directories to mount other object stores +[mountpoints] +config = 1 + +# Rules for file metadata. All rules are evaluated in sequence for every file +# The masks apply to the full target path +[rules] +*: read=guest,write=admin +\.*: read=admin +*.html: readonly=true +*.js, *.png, *.ico, *.jpg, *.jpeg, *.html, *.css, *.txt, *.md: compress=gzip +/layout.json, /full-layout.json: compress=gzip +/Data/*, /framework/*, /Sming/*: compress=gzip +/index.html, /index.js, /stswsio.js, /styles.css, *.ico, *.png: read=any +# This is a template file so firmware needs to read it +/error.html: compress=none diff --git a/samples/Basic_IFS/resource/listing.html b/samples/Basic_IFS/resource/listing.html new file mode 100644 index 0000000000..dba33cb68f --- /dev/null +++ b/samples/Basic_IFS/resource/listing.html @@ -0,0 +1,76 @@ +# +# HTML template to list directory with links +# + +# 0: Header +{SECTION} + + + + + + +Index of '{path}' + + +

Basic IFS demo

+Request count: {request-count} +

{path}

+ + + + + + + + + + + + + + + + +{!ifdef:parent} + + + +{!endif} + +{/SECTION} + + +# 1: Content +{SECTION} +{!iflt:$record:100} + + + + + + +{!ifin:attr:"D"} +{!else} + + +{!endif} + + + + +{!else} +Too many records {$record} +{!endif} +{/SECTION} + + +# 2: Footer +{SECTION} +
IDNameMIME typeLast modifiedSizeOriginal SizeAttributesCompressionAccess R/W
{parent}
{$record}{file_id}{icon} {name}{!mime_type:name}{modified}{size}
{!kb:size} KB
{original_size}
{!kb:original_size} KB
{!replace:attr_long:", ":"
"}
{compression}{access_long}
+

+{!ifeq:{!count:1}}No files found. Last error: {last_error} +{!else}{!count:1} files, {total_size} bytes ({!kb:total_size} KB){!endif} + + +{/SECTION} \ No newline at end of file diff --git a/samples/Basic_IFS/resource/listing.json b/samples/Basic_IFS/resource/listing.json new file mode 100644 index 0000000000..5a20750249 --- /dev/null +++ b/samples/Basic_IFS/resource/listing.json @@ -0,0 +1,24 @@ +{SECTION} +{ + {{!ifdef:parent}}"parent":"{{parent}}", + {{!endif}}"files": [ +{/SECTION} + +{SECTION}{{!ifgt:$record:0}}, +{{!endif}} { + "record": {{!as_int:$record}}, + "name": "{{name}}", + "modified": "{{modified}}", + "size": {{!as_int:size}}, + "original_size": {{!as_int:original_size}}, + "attr": "{{attr}}", + "access": "{{access}}" + }{/SECTION} + +{SECTION} + ], + "count": {{!as_int:{!count:1}}}, + "total_size": {{!as_int:total_size}}, + "last_error": "{{last_error}}" +} +{/SECTION} diff --git a/samples/Basic_IFS/resource/listing.txt b/samples/Basic_IFS/resource/listing.txt new file mode 100644 index 0000000000..9737a1fd5c --- /dev/null +++ b/samples/Basic_IFS/resource/listing.txt @@ -0,0 +1,26 @@ +# +# Sample template to display root directory in a table +# + +{SECTION} +Index of '{path}' +{!ifdef:parent}Parent is '{parent}' (length is {length:parent}){!endif} + +{!pad:"#":5} {!pad:"Name":32} {!pad:"Modified":22} {!pad:"Size":10} {!pad:"Size (KB)":10} {!pad:"Orig Size":10} {!pad:"Attributes":10} {!pad:"Access":10} +{!repeat:"-":5} {!repeat:"-":32} {!repeat:"-":22} {!repeat:"-":10} {!repeat:"-":10} {!repeat:"-":10} {!repeat:"-":10} {!repeat:"-":10} +{/SECTION} + +# Data section follows + +{SECTION}{!pad:$record:5} {!pad:name:32} {!pad:modified:22} {!pad:size:10} {!pad:{!kb:size}:10} {!pad:original_size:10} {!pad:attr:10} {!pad:access:10} +{/SECTION} + +{SECTION}{!repeat:"-":10} +{!count:1} files, {total_size} bytes ({!kb:total_size} KB) +Last error: {last_error} + +LISTING COMPLETE + +{/SECTION} + +# We're all done. diff --git a/samples/Basic_Serial/app/application.cpp b/samples/Basic_Serial/app/application.cpp index 1a7e0ce2a5..02548412d1 100644 --- a/samples/Basic_Serial/app/application.cpp +++ b/samples/Basic_Serial/app/application.cpp @@ -222,7 +222,7 @@ void handleCommand(const String& command) if(command.equalsIgnoreCase(_F("cat"))) { String filename = F("README.md"); FileStream* fileStream = new FileStream; - if(fileStream && fileStream->open(filename, eFO_ReadOnly)) { + if(fileStream && fileStream->open(filename, File::ReadOnly)) { Serial.printf(_F("Sending \"%s\" (%u bytes)\r\n"), filename.c_str(), fileStream->available()); auto demo = new SerialTransmitDemo(Serial1, fileStream); demo->begin(); diff --git a/samples/HttpClient/app/application.cpp b/samples/HttpClient/app/application.cpp index 1078d84445..bb54ee542f 100644 --- a/samples/HttpClient/app/application.cpp +++ b/samples/HttpClient/app/application.cpp @@ -140,7 +140,7 @@ void connectOk(IpAddress ip, IpAddress mask, IpAddress gateway) // Or if you want to directly save the response body to a file then the following can be done // FileStream* responseBodyFile = new FileStream(); - // responseBodyFile->open("file.name", eFO_CreateNewAlways | eFO_WriteOnly); + // responseBodyFile->open("file.name", File::CreateNewAlways | File::WriteOnly); // putRequest->setResponseStream(responseBodyFile); // << the complete body will be stored on your file system // see the implementation of `bool HttpClient::downloadFile(const String& url, const String& saveFileName, ...` for details. diff --git a/samples/LiveDebug/app/application.cpp b/samples/LiveDebug/app/application.cpp index a60f0186f6..dbe12d1ed7 100644 --- a/samples/LiveDebug/app/application.cpp +++ b/samples/LiveDebug/app/application.cpp @@ -1,6 +1,7 @@ #include #include "HardwareTimer.h" #include +#include #include #include #include @@ -61,6 +62,9 @@ static OsMessageInterceptor osMessageInterceptor; // Supports `consoleOff` command to prevent re-enabling when debugger is attached bool consoleOffRequested = false; +// +IFS::Gdb::FileSystem gdbfs; + // Forward declarations bool handleCommand(const String& cmd); void readConsole(); @@ -154,15 +158,15 @@ void onDataReceived(Stream& source, char arrivedChar, unsigned short availableCh */ void readFile(const char* filename, bool display) { - auto start = millis(); - int fd = gdb_syscall_open(filename, O_RDONLY, 0); - Serial.printf(_F("gdb_syscall_open(\"%s\") = %d\r\n"), filename, fd); - if(fd > 0) { + int file = gdbfs.open(filename, File::ReadOnly); + Serial.printf(_F("gdbfs.open(\"%s\") = %d\r\n"), filename, file); + if(file >= 0) { + OneShotFastMs timer; char buf[256]; - size_t total = 0; + size_t total{0}; int len; do { - len = gdb_syscall_read(fd, buf, sizeof(buf)); + len = gdbfs.read(file, buf, sizeof(buf)); if(len > 0) { total += size_t(len); if(display) { @@ -170,11 +174,11 @@ void readFile(const char* filename, bool display) } } } while(len == sizeof(buf)); - auto elapsed = millis() - start; - Serial.printf(_F("\r\ngdb_syscall_read() = %d, total = %u, elapsed = %u ms, av. %u bytes/sec\r\n"), len, total, - elapsed, total == 0 ? 0 : 1000U * total / elapsed); + auto elapsed = timer.elapsedTime(); + Serial.printf(_F("\r\ngdbfs.read() = %d, total = %u, elapsed = %s, av. %u bytes/sec\r\n"), len, total, + elapsed.toString().c_str(), total == 0 ? 0 : 1000U * total / elapsed); - gdb_syscall_close(fd); + gdbfs.close(file); } } @@ -649,7 +653,7 @@ extern "C" void gdb_on_attach(bool attached) debug_i("GdbAttach(%d)", attached); if(attached) { // Open a log file on the host to demonstrate use of GdbFileStream - logFile.open(F(LOG_FILENAME), eFO_WriteOnly | eFO_CreateIfNotExist); + logFile.open(F(LOG_FILENAME), File::WriteOnly | File::Create); debug_i("open log %d", logFile.getLastError()); logFile.println(); diff --git a/tests/HostTests/modules/ArduinoJson6.cpp b/tests/HostTests/modules/ArduinoJson6.cpp index 9c3690045b..544d5a83a8 100644 --- a/tests/HostTests/modules/ArduinoJson6.cpp +++ b/tests/HostTests/modules/ArduinoJson6.cpp @@ -305,7 +305,7 @@ class JsonTest6 : public TestGroup { PSTR_ARRAY(filename, "test.json"); if(!fileExist(filename)) { - FileStream fs(filename, eFO_CreateNewAlways | eFO_WriteOnly); + FileStream fs(filename, File::CreateNewAlways | File::WriteOnly); REQUIRE(fs.isValid()); FSTR::Stream os(Resource::test_json); REQUIRE(fs.copyFrom(&os, os.available()) == Resource::test_json.length()); diff --git a/tests/HostTests/modules/Files.cpp b/tests/HostTests/modules/Files.cpp index 46c59d2b68..6a401b46c6 100644 --- a/tests/HostTests/modules/Files.cpp +++ b/tests/HostTests/modules/Files.cpp @@ -23,7 +23,7 @@ class FilesTest : public TestGroup res = fileSetContent(testFileName, testContent); debug_i("fileSetContent() returned %d", res); REQUIRE(size_t(res) == testContent.length()); - file = fileOpen(testFileName, eFO_ReadWrite); + file = fileOpen(testFileName, File::ReadWrite); size = fileSeek(file, 0, SeekOrigin::End); pos = fileSeek(file, 100, SeekOrigin::Start); debug_i("pos = %d, size = %d", pos, size); @@ -104,7 +104,7 @@ class FilesTest : public TestGroup TEST_CASE("Truncate read/write file stream") { FileStream fs; - fs.open(testFileName, eFO_ReadWrite); + fs.open(testFileName, File::ReadWrite); fs.seek(50); res = fs.truncate(100); pos = fs.getPos(); @@ -122,7 +122,7 @@ class FilesTest : public TestGroup TEST_CASE("Seek file stream past end of file") { FileStream fs; - fs.open(testFileName, eFO_ReadWrite); + fs.open(testFileName, File::ReadWrite); res = fs.seekFrom(101, SeekOrigin::Start); pos = fs.getPos(); size = fs.getSize(); diff --git a/tests/HostTests/modules/TemplateStream.cpp b/tests/HostTests/modules/TemplateStream.cpp index 9fe9011e17..1805d54c02 100644 --- a/tests/HostTests/modules/TemplateStream.cpp +++ b/tests/HostTests/modules/TemplateStream.cpp @@ -3,6 +3,11 @@ #include #include +#ifdef ARCH_HOST +#include +#include +#endif + DEFINE_FSTR_LOCAL(template1, "Stream containing {var1}, {var2} and {var3}. {} {{}} {{12345") DEFINE_FSTR_LOCAL(template1_1, "Stream containing value #1, value #2 and {var3}. {} {{}} {{12345") DEFINE_FSTR_LOCAL(template1_2, "Stream containing value #1, value #2 and [value #3]. {} {{}} {{12345") @@ -89,6 +94,22 @@ class TemplateStreamTest : public TestGroup return nullptr; }); +#ifdef ARCH_HOST + { + HostFileStream fs("test-src1.out", eFO_CreateNewAlways | eFO_WriteOnly); + int res = fs.copyFrom(&tmpl); + debug_e("copyfrom(src) = %d", res); + tmpl.gotoSection(0); + } + + { + HostFileStream fs("test-src2.out", eFO_CreateNewAlways | eFO_WriteOnly); + int res = fs.copyFrom(&tmpl); + debug_e("copyfrom(src) = %d", res); + tmpl.gotoSection(0); + } +#endif + check(tmpl, Resource::ut_template1_out1_rst); } } From d5a78b013172d5feb406a1698171cc35bddb0675 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 14 Feb 2021 10:14:54 +0000 Subject: [PATCH 14/91] Handle SPIFFS metadata more elegantly (#2218) **IFS changes** Define SPIFFS_OBJ_META_LEN as build variable so it's under user control Don't read or write metadata if defined meta length is < 16 Add magic to start of SPIFFS metadata, set dirty flag when initialised Newly created metadata is automatically flushed to storage, but only if file is open in write mode Add filesystem info attribute to indicate if metadata is supported Fix block size assignment for SPIFFS to support use with other device types Define global `IFS::Host::fileSystem&` for easier use Fix Basic_IFS sample handling for FWFS_HYBRID=1 **Replace `spiffy` with `spiffsgen` tool from ESP IDF** Generate default metadata for each file, provided meta len >= 16 Add check in HostTests to compare spiffy output with spiffsgen. Output messages for `samples` and `component-samples` target to make log easier to follow **spiffsgen.py changes** * Add `SPIFFS_ALIGNED_OBJECT_INDEX_TABLES` setting as page indices must be word-aligned * Don't write image file if errors occur * Set `--use-magic` and `--use-magic-len` defaults to False * Allow spiffsgen `base_dir` to be empty * Remove leading / from filenames --- .../Host/Components/spi_flash/flashmem.cpp | 2 +- .../Host/Core/Data/Stream/HostFileStream.cpp | 7 +- Sming/Components/IFS | 2 +- Sming/Components/spiffs/README.rst | 10 + Sming/Components/spiffs/component.mk | 22 +- Sming/Components/spiffs/spiffs_config.h | 6 +- Sming/Components/spiffs/spiffsgen.py | 565 ++++++++++++++++++ Sming/Components/spiffs/spiffy/Makefile | 41 -- Sming/Components/spiffs/spiffy/spiffy.c | 309 ---------- Sming/Components/spiffs/spiffy_host.h | 16 - Sming/Makefile | 3 + Sming/building.rst | 6 +- docs/source/upgrading/4.2-4.3.rst | 13 +- samples/Basic_IFS/app/application.cpp | 8 +- samples/Basic_IFS/basic_ifs.hw | 4 - tests/HostTests/component.mk | 10 + tests/HostTests/modules/Spiffs.cpp | 103 ++++ tests/HostTests/spiffsgen/README.rst | 14 + tests/HostTests/spiffsgen/build/.lastModified | 1 + .../spiffsgen/build/bootstrap-core.css.gz | Bin 0 -> 15861 bytes tests/HostTests/spiffsgen/build/core.js.gz | Bin 0 -> 30474 bytes tests/HostTests/spiffsgen/build/index.html | 4 + tests/HostTests/spiffsgen/build/settings.html | 4 + .../spiffsgen/build/wifi-sprites.png | Bin 0 -> 1769 bytes tests/HostTests/spiffsgen/spiff_rom_meta.bin | Bin 0 -> 65536 bytes tests/HostTests/spiffsgen/spiff_rom_orig.bin | Bin 0 -> 65536 bytes 26 files changed, 749 insertions(+), 401 deletions(-) create mode 100644 Sming/Components/spiffs/spiffsgen.py delete mode 100644 Sming/Components/spiffs/spiffy/Makefile delete mode 100644 Sming/Components/spiffs/spiffy/spiffy.c delete mode 100644 Sming/Components/spiffs/spiffy_host.h create mode 100644 tests/HostTests/spiffsgen/README.rst create mode 100644 tests/HostTests/spiffsgen/build/.lastModified create mode 100644 tests/HostTests/spiffsgen/build/bootstrap-core.css.gz create mode 100644 tests/HostTests/spiffsgen/build/core.js.gz create mode 100644 tests/HostTests/spiffsgen/build/index.html create mode 100644 tests/HostTests/spiffsgen/build/settings.html create mode 100644 tests/HostTests/spiffsgen/build/wifi-sprites.png create mode 100644 tests/HostTests/spiffsgen/spiff_rom_meta.bin create mode 100644 tests/HostTests/spiffsgen/spiff_rom_orig.bin diff --git a/Sming/Arch/Host/Components/spi_flash/flashmem.cpp b/Sming/Arch/Host/Components/spi_flash/flashmem.cpp index d1d645c78a..0237f9eaf4 100644 --- a/Sming/Arch/Host/Components/spi_flash/flashmem.cpp +++ b/Sming/Arch/Host/Components/spi_flash/flashmem.cpp @@ -25,7 +25,7 @@ namespace { -IFS::Host::FileSystem fileSys; +IFS::IFileSystem& fileSys{IFS::Host::fileSystem}; IFS::File::Handle flashFile{-1}; size_t flashFileSize{0x400000U}; char flashFileName[256]; diff --git a/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp b/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp index a467733794..95832ad28a 100644 --- a/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp +++ b/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp @@ -11,11 +11,6 @@ #include "HostFileStream.h" #include -namespace -{ -IFS::Host::FileSystem hostFileSystem; -} - -HostFileStream::HostFileStream() : IFS::FileStream(&hostFileSystem) +HostFileStream::HostFileStream() : IFS::FileStream(&IFS::Host::fileSystem) { } diff --git a/Sming/Components/IFS b/Sming/Components/IFS index ac268fb17a..dce62c0b14 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit ac268fb17a562a6b94fb16df64d43639a3e5aefe +Subproject commit dce62c0b14c4af7e29c087e46dced25dae5799f0 diff --git a/Sming/Components/spiffs/README.rst b/Sming/Components/spiffs/README.rst index 13e5852abc..3ac089846f 100644 --- a/Sming/Components/spiffs/README.rst +++ b/Sming/Components/spiffs/README.rst @@ -49,3 +49,13 @@ custom :ref:`hardware_config`. Default: 7 Number of file descriptors allocated. This sets the maximum number of files which may be opened at once. + + +.. envvar:: SPIFFS_OBJ_META_LEN + + Default: 16 + + Maximum size of metadata which SPIFFS stores in each file index header (after the filename). + If this value is changed, existing SPIFFS images will not be readable. + + The default value given here is provided to support :component:`IFS` extended file attribute information. diff --git a/Sming/Components/spiffs/component.mk b/Sming/Components/spiffs/component.mk index f82bfa9608..bc7787fffc 100644 --- a/Sming/Components/spiffs/component.mk +++ b/Sming/Components/spiffs/component.mk @@ -4,14 +4,6 @@ COMPONENT_SRCDIRS := . spiffs/src COMPONENT_INCDIRS := . spiffs/src COMPONENT_DOXYGEN_INPUT := spiffs/src -## Spiffy tool - -SPIFFY := $(TOOLS_BASE)/spiffy$(TOOL_EXT) -COMPONENT_TARGETS += $(SPIFFY) -$(COMPONENT_RULE)$(SPIFFY): - $(call MakeTarget,spiffy/Makefile) - - ## Application ifdef DISABLE_SPIFFS @@ -29,8 +21,16 @@ COMPONENT_CFLAGS += -DSPIFF_FILEDESC_COUNT=$(SPIFF_FILEDESC_COUNT) COMPONENT_CFLAGS += -Wno-tautological-compare +COMPONENT_RELINK_VARS += SPIFFS_OBJ_META_LEN +SPIFFS_OBJ_META_LEN ?= 16 +COMPONENT_CFLAGS += -DSPIFFS_OBJ_META_LEN=$(SPIFFS_OBJ_META_LEN) + ##@Building +# Spiffs image generation tool +SPIFFSGEN := $(PYTHON) $(COMPONENT_PATH)/spiffsgen.py +SPIFFSGEN_SMING = $(SPIFFSGEN) --meta-len=$(SPIFFS_OBJ_META_LEN) --block-size=8192 + .PHONY: spiffs-image-update spiffs-image-update: spiffs-image-clean $(SPIFF_BIN_OUT) ##Rebuild the SPIFFS filesystem image @@ -38,11 +38,11 @@ spiffs-image-update: spiffs-image-clean $(SPIFF_BIN_OUT) ##Rebuild the SPIFFS fi ifneq (,$(filter spiffsgen,$(MAKECMDGOALS))) PART_TARGET := $(PARTITION_$(PART)_FILENAME) $(eval PART_FILES := $(call HwExpr,part.build['files'])) -SPIFFY_ARGS := $(PARTITION_$(PART)_SIZE_BYTES) "$(or $(PART_FILES),dummy.dir)" .PHONY: spiffsgen -spiffsgen: $(SPIFFY) +spiffsgen: ifneq (,$(PART_TARGET)) + @echo "Creating SPIFFS image '$(PART_TARGET)'" $(Q) mkdir -p $(dir $(PART_TARGET)) - $(Q) $(SPIFFY) $(SPIFFY_ARGS) $(PART_TARGET) + $(Q) $(SPIFFSGEN_SMING) $(PARTITION_$(PART)_SIZE_BYTES) "$(or $(PART_FILES),)" $(PART_TARGET) endif endif diff --git a/Sming/Components/spiffs/spiffs_config.h b/Sming/Components/spiffs/spiffs_config.h index 502ecc0e64..6699389ed5 100644 --- a/Sming/Components/spiffs/spiffs_config.h +++ b/Sming/Components/spiffs/spiffs_config.h @@ -9,11 +9,7 @@ #define SPIFFS_CONFIG_H_ // ----------- 8< ------------ -#ifdef __ets__ - #include -#else - #include "spiffy_host.h" -#endif /* __ets__ */ +#include // ----------- >8 ------------ // compile time switches diff --git a/Sming/Components/spiffs/spiffsgen.py b/Sming/Components/spiffs/spiffsgen.py new file mode 100644 index 0000000000..b161c1c957 --- /dev/null +++ b/Sming/Components/spiffs/spiffsgen.py @@ -0,0 +1,565 @@ +#!/usr/bin/env python +# +# spiffsgen is a tool used to generate a spiffs image from a directory +# +# Copyright 2019 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import division, print_function + +import argparse +import ctypes +import io +import math +import os +import struct +import sys + +SPIFFS_PH_FLAG_USED_FINAL_INDEX = 0xF8 +SPIFFS_PH_FLAG_USED_FINAL = 0xFC + +SPIFFS_PH_FLAG_LEN = 1 +SPIFFS_PH_IX_SIZE_LEN = 4 +SPIFFS_PH_IX_OBJ_TYPE_LEN = 1 +SPIFFS_TYPE_FILE = 1 + +# Based on typedefs under spiffs_config.h +SPIFFS_OBJ_ID_LEN = 2 # spiffs_obj_id +SPIFFS_SPAN_IX_LEN = 2 # spiffs_span_ix +SPIFFS_PAGE_IX_LEN = 2 # spiffs_page_ix +SPIFFS_BLOCK_IX_LEN = 2 # spiffs_block_ix + +# Fixed config (see spiffs_config.h) +SPIFFS_ALIGNED_OBJECT_INDEX_TABLES = True + +class SpiffsBuildConfig(): + def __init__(self, page_size, page_ix_len, block_size, + block_ix_len, meta_len, obj_name_len, obj_id_len, + span_ix_len, packed, aligned, endianness, use_magic, use_magic_len): + if block_size % page_size != 0: + raise RuntimeError('block size should be a multiple of page size') + + self.page_size = page_size + self.block_size = block_size + self.obj_id_len = obj_id_len + self.span_ix_len = span_ix_len + self.packed = packed + self.aligned = aligned + self.obj_name_len = obj_name_len + self.meta_len = meta_len + self.page_ix_len = page_ix_len + self.block_ix_len = block_ix_len + self.endianness = endianness + self.use_magic = use_magic + self.use_magic_len = use_magic_len + + self.PAGES_PER_BLOCK = self.block_size // self.page_size + self.OBJ_LU_PAGES_PER_BLOCK = int(math.ceil(self.block_size / self.page_size * self.obj_id_len / self.page_size)) + self.OBJ_USABLE_PAGES_PER_BLOCK = self.PAGES_PER_BLOCK - self.OBJ_LU_PAGES_PER_BLOCK + + self.OBJ_LU_PAGES_OBJ_IDS_LIM = self.page_size // self.obj_id_len + + self.OBJ_DATA_PAGE_HEADER_LEN = self.obj_id_len + self.span_ix_len + SPIFFS_PH_FLAG_LEN + + pad = 4 - (4 if self.OBJ_DATA_PAGE_HEADER_LEN % 4 == 0 else self.OBJ_DATA_PAGE_HEADER_LEN % 4) + + self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED = self.OBJ_DATA_PAGE_HEADER_LEN + pad + self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED_PAD = pad + self.OBJ_DATA_PAGE_CONTENT_LEN = self.page_size - self.OBJ_DATA_PAGE_HEADER_LEN + + self.OBJ_INDEX_PAGES_HEADER_LEN = (self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED + SPIFFS_PH_IX_SIZE_LEN + + SPIFFS_PH_IX_OBJ_TYPE_LEN + self.obj_name_len + self.meta_len) + self.OBJ_INDEX_PAGES_OBJ_IDS_HEAD_LIM = (self.page_size - self.OBJ_INDEX_PAGES_HEADER_LEN) // self.block_ix_len + self.OBJ_INDEX_PAGES_OBJ_IDS_LIM = (self.page_size - self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED) / self.block_ix_len + + +class SpiffsFullError(RuntimeError): + def __init__(self, message=None): + super(SpiffsFullError, self).__init__(message) + + +class SpiffsPage(): + _endianness_dict = { + 'little': '<', + 'big': '>' + } + + _len_dict = { + 1: 'B', + 2: 'H', + 4: 'I', + 8: 'Q' + } + + _type_dict = { + 1: ctypes.c_ubyte, + 2: ctypes.c_ushort, + 4: ctypes.c_uint, + 8: ctypes.c_ulonglong + } + + def __init__(self, bix, build_config): + self.build_config = build_config + self.bix = bix + + +class SpiffsObjLuPage(SpiffsPage): + def __init__(self, bix, build_config): + SpiffsPage.__init__(self, bix, build_config) + + self.obj_ids_limit = self.build_config.OBJ_LU_PAGES_OBJ_IDS_LIM + self.obj_ids = list() + + def _calc_magic(self, blocks_lim): + # Calculate the magic value mirrorring computation done by the macro SPIFFS_MAGIC defined in + # spiffs_nucleus.h + magic = 0x20140529 ^ self.build_config.page_size + if self.build_config.use_magic_len: + magic = magic ^ (blocks_lim - self.bix) + magic = SpiffsPage._type_dict[self.build_config.obj_id_len](magic) + return magic.value + + def register_page(self, page): + if not self.obj_ids_limit > 0: + raise SpiffsFullError() + + obj_id = (page.obj_id, page.__class__) + self.obj_ids.append(obj_id) + self.obj_ids_limit -= 1 + + def to_binary(self): + global test + img = b'' + + for (obj_id, page_type) in self.obj_ids: + if page_type == SpiffsObjIndexPage: + obj_id ^= (1 << ((self.build_config.obj_id_len * 8) - 1)) + img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + + SpiffsPage._len_dict[self.build_config.obj_id_len], obj_id) + + assert(len(img) <= self.build_config.page_size) + + img += b'\xFF' * (self.build_config.page_size - len(img)) + + return img + + def magicfy(self, blocks_lim): + # Only use magic value if no valid obj id has been written to the spot, which is the + # spot taken up by the last obj id on last lookup page. The parent is responsible + # for determining which is the last lookup page and calling this function. + remaining = self.obj_ids_limit + empty_obj_id_dict = { + 1: 0xFF, + 2: 0xFFFF, + 4: 0xFFFFFFFF, + 8: 0xFFFFFFFFFFFFFFFF + } + if (remaining >= 2): + for i in range(remaining): + if i == remaining - 2: + self.obj_ids.append((self._calc_magic(blocks_lim), SpiffsObjDataPage)) + break + else: + self.obj_ids.append((empty_obj_id_dict[self.build_config.obj_id_len], SpiffsObjDataPage)) + self.obj_ids_limit -= 1 + + +class SpiffsObjIndexPage(SpiffsPage): + def __init__(self, obj_id, span_ix, size, name, build_config): + SpiffsPage.__init__(self, 0, build_config) + self.obj_id = obj_id + self.span_ix = span_ix + self.name = name + self.size = size + + if self.span_ix == 0: + self.pages_lim = self.build_config.OBJ_INDEX_PAGES_OBJ_IDS_HEAD_LIM + else: + self.pages_lim = self.build_config.OBJ_INDEX_PAGES_OBJ_IDS_LIM + + self.pages = list() + + def register_page(self, page): + if not self.pages_lim > 0: + raise SpiffsFullError + + self.pages.append(page.offset) + self.pages_lim -= 1 + + def to_binary(self): + obj_id = self.obj_id ^ (1 << ((self.build_config.obj_id_len * 8) - 1)) + img = struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + + SpiffsPage._len_dict[self.build_config.obj_id_len] + + SpiffsPage._len_dict[self.build_config.span_ix_len] + + SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN], + obj_id, + self.span_ix, + SPIFFS_PH_FLAG_USED_FINAL_INDEX) + + # Add padding before the object index page specific information + img += b'\xFF' * self.build_config.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED_PAD + + # If this is the first object index page for the object, add filname, type + # and size information + if self.span_ix == 0: + img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + + SpiffsPage._len_dict[SPIFFS_PH_IX_SIZE_LEN] + + SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN], + self.size, + SPIFFS_TYPE_FILE) + + # Append name + name = self.name.encode() + img += name + (b'\x00' * (self.build_config.obj_name_len - len(name))) + + if self.build_config.meta_len >= 16: + # Build default IFS metadata + magic = 0xE3457A77 + mtime = self.mtime + attr = 0 + flags = 0xff + userRole_admin = 0x04 + meta = struct.pack(b'> int(math.log(self.build_config.page_size, 2)) + img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + + SpiffsPage._len_dict[self.build_config.page_ix_len], page) + + assert(len(img) <= self.build_config.page_size) + + img += b'\xFF' * (self.build_config.page_size - len(img)) + + return img + + +class SpiffsObjDataPage(SpiffsPage): + def __init__(self, offset, obj_id, span_ix, contents, build_config): + SpiffsPage.__init__(self, 0, build_config) + self.obj_id = obj_id + self.span_ix = span_ix + self.contents = contents + self.offset = offset + + def to_binary(self): + img = struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + + SpiffsPage._len_dict[self.build_config.obj_id_len] + + SpiffsPage._len_dict[self.build_config.span_ix_len] + + SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN], + self.obj_id, + self.span_ix, + SPIFFS_PH_FLAG_USED_FINAL) + + img += self.contents + + assert(len(img) <= self.build_config.page_size) + + img += b'\xFF' * (self.build_config.page_size - len(img)) + + return img + + +class SpiffsBlock(): + def _reset(self): + self.cur_obj_index_span_ix = 0 + self.cur_obj_data_span_ix = 0 + self.cur_obj_id = 0 + self.cur_obj_idx_page = None + + def __init__(self, bix, blocks_lim, build_config): + self.build_config = build_config + self.offset = bix * self.build_config.block_size + self.remaining_pages = self.build_config.OBJ_USABLE_PAGES_PER_BLOCK + self.pages = list() + self.bix = bix + + lu_pages = list() + for i in range(self.build_config.OBJ_LU_PAGES_PER_BLOCK): + page = SpiffsObjLuPage(self.bix, self.build_config) + lu_pages.append(page) + + self.pages.extend(lu_pages) + + self.lu_page_iter = iter(lu_pages) + self.lu_page = next(self.lu_page_iter) + + self._reset() + + def _register_page(self, page): + if isinstance(page, SpiffsObjDataPage): + self.cur_obj_idx_page.register_page(page) # can raise SpiffsFullError + + try: + self.lu_page.register_page(page) + except SpiffsFullError: + self.lu_page = next(self.lu_page_iter) + try: + self.lu_page.register_page(page) + except AttributeError: # no next lookup page + # Since the amount of lookup pages is pre-computed at every block instance, + # this should never occur + raise RuntimeError('invalid attempt to add page to a block when there is no more space in lookup') + + self.pages.append(page) + + def begin_obj(self, obj_id, size, name, mtime, obj_index_span_ix=0, obj_data_span_ix=0): + if not self.remaining_pages > 0: + raise SpiffsFullError() + self._reset() + + self.cur_obj_id = obj_id + self.cur_obj_index_span_ix = obj_index_span_ix + self.cur_obj_data_span_ix = obj_data_span_ix + + page = SpiffsObjIndexPage(obj_id, self.cur_obj_index_span_ix, size, name, self.build_config) + page.mtime = mtime + self._register_page(page) + + self.cur_obj_idx_page = page + + self.remaining_pages -= 1 + self.cur_obj_index_span_ix += 1 + + def update_obj(self, contents): + if not self.remaining_pages > 0: + raise SpiffsFullError() + page = SpiffsObjDataPage(self.offset + (len(self.pages) * self.build_config.page_size), + self.cur_obj_id, self.cur_obj_data_span_ix, contents, self.build_config) + + self._register_page(page) + + self.cur_obj_data_span_ix += 1 + self.remaining_pages -= 1 + + def end_obj(self): + self._reset() + + def is_full(self): + return self.remaining_pages <= 0 + + def to_binary(self, blocks_lim): + img = b'' + + if self.build_config.use_magic: + for (idx, page) in enumerate(self.pages): + if idx == self.build_config.OBJ_LU_PAGES_PER_BLOCK - 1: + page.magicfy(blocks_lim) + img += page.to_binary() + else: + for page in self.pages: + img += page.to_binary() + + assert(len(img) <= self.build_config.block_size) + + img += b'\xFF' * (self.build_config.block_size - len(img)) + return img + + +class SpiffsFS(): + def __init__(self, img_size, build_config): + if img_size % build_config.block_size != 0: + raise RuntimeError('image size should be a multiple of block size (0x%08x, 0x%04x)' % (img_size, build_config.block_size)) + + self.img_size = img_size + self.build_config = build_config + + self.blocks = list() + self.blocks_lim = self.img_size // self.build_config.block_size + self.remaining_blocks = self.blocks_lim + self.cur_obj_id = 1 # starting object id + + def _create_block(self): + if self.is_full(): + raise SpiffsFullError('the image size has been exceeded') + + block = SpiffsBlock(len(self.blocks), self.blocks_lim, self.build_config) + self.blocks.append(block) + self.remaining_blocks -= 1 + return block + + def is_full(self): + return self.remaining_blocks <= 0 + + def create_file(self, img_path, file_path): + contents = None + + if len(img_path) > self.build_config.obj_name_len: + raise RuntimeError("object name '%s' too long" % img_path) + + name = img_path + + with open(file_path, 'rb') as obj: + contents = obj.read() + + stream = io.BytesIO(contents) + + mtime = int(os.path.getmtime(file_path)) + try: + block = self.blocks[-1] + block.begin_obj(self.cur_obj_id, len(contents), name, mtime) + except (IndexError, SpiffsFullError): + block = self._create_block() + block.begin_obj(self.cur_obj_id, len(contents), name, mtime) + + contents_chunk = stream.read(self.build_config.OBJ_DATA_PAGE_CONTENT_LEN) + + while contents_chunk: + try: + block = self.blocks[-1] + try: + # This can fail because either (1) all the pages in block have been + # used or (2) object index has been exhausted. + block.update_obj(contents_chunk) + except SpiffsFullError: + # If its (1), use the outer exception handler + if block.is_full(): + raise SpiffsFullError + # If its (2), write another object index page + block.begin_obj(self.cur_obj_id, len(contents), name, mtime, + obj_index_span_ix=block.cur_obj_index_span_ix, + obj_data_span_ix=block.cur_obj_data_span_ix) + continue + except (IndexError, SpiffsFullError): + # All pages in the block have been exhausted. Create a new block, copying + # the previous state of the block to a new one for the continuation of the + # current object + prev_block = block + block = self._create_block() + block.cur_obj_id = prev_block.cur_obj_id + block.cur_obj_idx_page = prev_block.cur_obj_idx_page + block.cur_obj_data_span_ix = prev_block.cur_obj_data_span_ix + block.cur_obj_index_span_ix = prev_block.cur_obj_index_span_ix + continue + + contents_chunk = stream.read(self.build_config.OBJ_DATA_PAGE_CONTENT_LEN) + + block.end_obj() + + self.cur_obj_id += 1 + + def to_binary(self): + img = b'' + for block in self.blocks: + img += block.to_binary(self.blocks_lim) + bix = len(self.blocks) + if self.build_config.use_magic: + # Create empty blocks with magic numbers + while self.remaining_blocks > 0: + block = SpiffsBlock(bix, self.blocks_lim, self.build_config) + img += block.to_binary(self.blocks_lim) + self.remaining_blocks -= 1 + bix += 1 + else: + # Just fill remaining spaces FF's + img += b'\xFF' * (self.img_size - len(img)) + return img + + +def main(): + if sys.version_info[0] < 3: + print('WARNING: Support for Python 2 is deprecated and will be removed in future versions.', file=sys.stderr) + elif sys.version_info[0] == 3 and sys.version_info[1] < 6: + print('WARNING: Python 3 versions older than 3.6 are not supported.', file=sys.stderr) + parser = argparse.ArgumentParser(description='SPIFFS Image Generator', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('image_size', + help='Size of the created image') + + parser.add_argument('base_dir', + help='Path to directory from which the image will be created') + + parser.add_argument('output_file', + help='Created image output file path') + + parser.add_argument('--page-size', + help='Logical page size. Set to value same as CONFIG_SPIFFS_PAGE_SIZE.', + type=int, + default=256) + + parser.add_argument('--block-size', + help="Logical block size. Set to the same value as the flash chip's sector size (g_rom_flashchip.sector_size).", + type=int, + default=4096) + + parser.add_argument('--obj-name-len', + help='File full path maximum length. Set to value same as CONFIG_SPIFFS_OBJ_NAME_LEN.', + type=int, + default=32) + + parser.add_argument('--meta-len', + help='File metadata length. Set to value same as CONFIG_SPIFFS_META_LENGTH.', + type=int, + default=4) + + parser.add_argument('--use-magic', + help='Use magic number to create an identifiable SPIFFS image. Specify if CONFIG_SPIFFS_USE_MAGIC.', + action='store_true', + default=False) + + parser.add_argument('--follow-symlinks', + help='Take into account symbolic links during partition image creation.', + action='store_true', + default=False) + + parser.add_argument('--use-magic-len', + help='Use position in memory to create different magic numbers for each block. Specify if CONFIG_SPIFFS_USE_MAGIC_LENGTH.', + action='store_true', + default=False) + + parser.add_argument('--big-endian', + help='Specify if the target architecture is big-endian. If not specified, little-endian is assumed.', + action='store_true', + default=False) + + args = parser.parse_args() + + if args.base_dir != '' and not os.path.exists(args.base_dir): + raise RuntimeError('given base directory %s does not exist' % args.base_dir) + + image_size = int(args.image_size, 0) + spiffs_build_default = SpiffsBuildConfig(args.page_size, SPIFFS_PAGE_IX_LEN, + args.block_size, SPIFFS_BLOCK_IX_LEN, args.meta_len, + args.obj_name_len, SPIFFS_OBJ_ID_LEN, SPIFFS_SPAN_IX_LEN, + True, True, 'big' if args.big_endian else 'little', + args.use_magic, args.use_magic_len) + + spiffs = SpiffsFS(image_size, spiffs_build_default) + + if args.base_dir != '': + for root, dirs, files in os.walk(args.base_dir, followlinks=args.follow_symlinks): + for f in files: + full_path = os.path.join(root, f) + rel_path = os.path.relpath(full_path, args.base_dir) + print("'%s', '%s', '%s'" % (rel_path, full_path, args.base_dir)) + spiffs.create_file(rel_path.replace('\\', '/'), full_path) + + image = spiffs.to_binary() + open(args.output_file, 'wb').write(image) + + +if __name__ == '__main__': + main() diff --git a/Sming/Components/spiffs/spiffy/Makefile b/Sming/Components/spiffs/spiffy/Makefile deleted file mode 100644 index b830ed7ce4..0000000000 --- a/Sming/Components/spiffs/spiffy/Makefile +++ /dev/null @@ -1,41 +0,0 @@ -# -# Makefile for spiffy -# -# Pass in TARGET, BUILD_DIR , SPIFFS_SMING, SPIFFS_BASE -# - -HOST_CC ?= gcc -HOST_LD ?= gcc - -INCDIR := -I.. -I../spiffs/src -CFLAGS := -O2 -Wall -Wno-unused-value - -ifeq ("$(V)","1") -Q := -vecho := @true -else -Q := @ -vecho := @echo -endif - -BUILD_DIR ?= . -TARGET ?= spiffy - -OBJS := $(addprefix $(BUILD_DIR)/,spiffy.o spiffs_cache.o spiffs_nucleus.o spiffs_hydrogen.o spiffs_gc.o spiffs_check.o) - -all: $(TARGET) - -$(BUILD_DIR)/%.o: ../spiffs/src/%.c - $(vecho) "CC $<" - $(Q) $(HOST_CC) $(CFLAGS) $(INCDIR) -c $< -o $@ - -$(BUILD_DIR)/spiffy.o: spiffy.c - $(vecho) "CC $<" - $(Q) $(HOST_CC) $(CFLAGS) $(INCDIR) -c $< -o $@ - -$(TARGET): $(OBJS) - $(vecho) "LD $@" - $(Q) $(HOST_LD) -o $@ $^ - -clean: - rm -f $(OBJS) $(TARGET) diff --git a/Sming/Components/spiffs/spiffy/spiffy.c b/Sming/Components/spiffs/spiffy/spiffy.c deleted file mode 100644 index 6dab9cedac..0000000000 --- a/Sming/Components/spiffs/spiffy/spiffy.c +++ /dev/null @@ -1,309 +0,0 @@ -#include -#include -#include -#include -#include - -#define LOG_PAGE_SIZE 256 -#define SPI_FLASH_SEC_SIZE 4096 - -#define ROM_ERASE 0xFF - -#define DEFAULT_FOLDER "files" -#define DEFAULT_ROM_NAME "spiff_rom.bin" -#define DEFAULT_ROM_SIZE 0x30000 - -static spiffs fs; -static u8_t spiffs_work_buf[LOG_PAGE_SIZE*2]; -static u8_t spiffs_fds[32*4]; -static u8_t spiffs_cache_buf[(LOG_PAGE_SIZE+32)*4]; - -#define S_DBG(fmt, ...) -//#define S_DBG printf - -static FILE *rom = 0; - -void hexdump_mem(u8_t *b, u32_t len) { - int i; - for (i = 0; i < len; i++) { - S_DBG("%02x", *b++); - if ((i % 16) == 15) S_DBG("\n"); - else if ((i % 16) == 7) S_DBG(" "); - } - if ((i % 16) != 0) S_DBG("\n"); -} - -static s32_t my_spiffs_read(struct spiffs_t* fs, u32_t addr, u32_t size, u8_t *dst) { - - int res; - - if (fseek(rom, addr, SEEK_SET)) { - printf("Unable to seek to %d.\n", addr); - return SPIFFS_ERR_END_OF_OBJECT; - } - - res = fread(dst, 1, size, rom); - if (res != size) { - printf("Unable to read - tried to get %d bytes only got %d.\n", size, res); - return SPIFFS_ERR_NOT_READABLE; - } - - S_DBG("Read %d bytes from offset %d.\n", size, addr); - //hexdump_mem(dst, size); - return SPIFFS_OK; -} - -static s32_t my_spiffs_write(struct spiffs_t* fs, u32_t addr, u32_t size, u8_t *src) { - - int ret = SPIFFS_OK; - u8_t *buff = 0; - - buff = malloc(size); - if (!buff) { - printf("Unable to malloc %d bytes.\n", size); - ret = SPIFFS_ERR_INTERNAL; - } else { - ret = my_spiffs_read(fs, addr, size, buff); - if (ret == SPIFFS_OK) { - int i; - for(i = 0; i < size; i++) buff[i] &= src[i]; - //hexdump_mem(buff, size); - if (fseek(rom, addr, SEEK_SET)) { - printf("Unable to seek to %d.\n", addr); - ret = SPIFFS_ERR_END_OF_OBJECT; - } else { - if (fwrite(src, 1, size, rom) != size) { - printf("Unable to write.\n"); - ret = SPIFFS_ERR_NOT_WRITABLE; - } else { - fflush(rom); - S_DBG("Wrote %d bytes to offset %d.\n", size, addr); - } - } - } - } - - if (buff) free (buff); - return ret; -} - -static s32_t my_spiffs_erase(struct spiffs_t* fs, u32_t addr, u32_t size) { - - int i; - - if (fseek(rom, addr, SEEK_SET)) { - printf("Unable to seek to %d.\n", addr); - return SPIFFS_ERR_END_OF_OBJECT; - } - - for (i = 0; i < size; i++) { - if (fputc(ROM_ERASE, rom) == EOF) { - printf("Unable to write.\n"); - return SPIFFS_ERR_NOT_WRITABLE; - } - } - - fflush(rom); - S_DBG("Erased %d bytes at offset %d.\n", size, addr); - - return SPIFFS_OK; -} - -int my_spiffs_mount(u32_t msize) { - - spiffs_config cfg; - - cfg.phys_size = msize; - cfg.phys_addr = 0; - - cfg.phys_erase_block = SPI_FLASH_SEC_SIZE; - cfg.log_block_size = SPI_FLASH_SEC_SIZE * 2; - cfg.log_page_size = LOG_PAGE_SIZE; - - cfg.hal_read_f = my_spiffs_read; - cfg.hal_write_f = my_spiffs_write; - cfg.hal_erase_f = my_spiffs_erase; - - int res = SPIFFS_mount(&fs, - &cfg, - spiffs_work_buf, - spiffs_fds, - sizeof(spiffs_fds), - spiffs_cache_buf, - sizeof(spiffs_cache_buf), - 0); - S_DBG("Mount result: %d.\n", res); - - return res; -} - -void my_spiffs_unmount() { - SPIFFS_unmount(&fs); -} - -int my_spiffs_format() { - int res = SPIFFS_format(&fs); - S_DBG("Format result: %d.\n", res); - return res; -} - -int write_to_spiffs(char *fname, u8_t *data, int size) { - - int ret = 0; - - spiffs_file fd = SPIFFS_open(&fs, fname, SPIFFS_CREAT | SPIFFS_TRUNC | SPIFFS_RDWR, 0); - if (fd < 0) { - printf("Unable to open spiffs file '%s', error %d.\n", fname, fd); - } else { - S_DBG("Opened spiffs file '%s'.\n", fname); - if (SPIFFS_write(&fs, fd, (u8_t *)data, size) < SPIFFS_OK) { - printf("Unable to write to spiffs file '%s', errno %d.\n", fname, SPIFFS_errno(&fs)); - } else { - ret = 1; - } - } - - if (fd >= 0) { - SPIFFS_close(&fs, fd); - S_DBG("Closed spiffs file '%s'.\n", fname); - } - return ret; -} - -int add_file(const char* fdir, char* fname) { - - int ret = 0; - u8_t *buff = 0; - FILE *fp = 0; - char *path = 0; - - path = malloc(1024); - if (!path) { - printf("Unable to malloc %d bytes.\n", 1024); - } else { - struct stat st; - sprintf(path, "%s/%s", fdir, fname); - if (stat(path, &st) || !S_ISREG(st.st_mode)) { - S_DBG("Skipping non-file '%s'.\n", fname); - } else { - fp = fopen(path, "rb"); - if (!fp) { - S_DBG("Unable to open '%s'.\n", fname); - } else { - int size = (int)st.st_size; - buff = malloc(size); - if (!buff) { - printf("Unable to malloc %d bytes.\n", size); - } else { - if (fread(buff, 1, size, fp) != size) { - printf("Unable to read file '%s'.\n", fname); - } else { - S_DBG("%d bytes read from '%s'.\n", size, fname); - if (write_to_spiffs(fname, buff, size)) { - printf("Added '%s' to spiffs (%d bytes).\n", fname, size); - ret = 1; - } - } - } - } - } - } - - if (buff) free(buff); - if (path) free(path); - if (fp) fclose(fp); - - return ret; -} - -int get_rom_size (const char *str) { - - long val; - - // accept decimal or hex, but not octal - if ((strlen(str) > 2) && (str[0] == '0') && - (((str[1] == 'x')) || ((str[1] == 'X')))) { - val = strtol(str, NULL, 16); - } else { - val = strtol(str, NULL, 10); - } - - return (int)val; -} - -int main(int argc, char **argv) { - - const char *folder; - const char *romfile; - int romsize; - int ret = EXIT_SUCCESS; - - if (argc == 1) { - romsize = DEFAULT_ROM_SIZE; - folder = DEFAULT_FOLDER; - romfile = DEFAULT_ROM_NAME; - printf("Usage: %s maxFsSizeinByte spiffsBaseDir [outfile.bin]\n" - "There is no specific size or files directory.\n" - "Starting in compatibility mode.\n" - "Default fs size is 0x%x (%d) bytes and directory is '%s'.\n", - argv[0], romsize, romsize, DEFAULT_FOLDER); - } else if (argc == 3) { - romsize = get_rom_size(argv[1]); - folder = argv[2]; - romfile = DEFAULT_ROM_NAME; - } else if (argc == 4) { - romsize = get_rom_size(argv[1]); - folder = argv[2]; - romfile = argv[3]; - } else { - printf ("Usage: %s [OutFile.bin]\n", argv[0]); - printf (" FsSizeInBytes can be in hex (starting with 0x) or decimal.\n"); - printf (" To create an empty filesystem pass 'dummy.dir' as FilesDir.\n"); - exit(EXIT_FAILURE); - } - - printf("Creating rom '%s' of size 0x%x (%d) bytes.\n", romfile, romsize, romsize); - rom = fopen(romfile, "wb+"); - - if (!rom) { - printf("Unable to open file '%s' for writing.\n", romfile); - } else { - - fseek(rom, romsize - 1, SEEK_SET); - fputc(ROM_ERASE, rom); - - // we have to do this before calling format - if (!my_spiffs_mount(romsize)) { - my_spiffs_unmount(); - } - - int res; - if ((res = my_spiffs_format()) != SPIFFS_OK) { - printf("Failed to format spiffs, error %d.\n", res); - ret = EXIT_FAILURE; - } else if ((res = my_spiffs_mount(romsize)) != SPIFFS_OK) { - printf("Failed to mount spiffs, error %d.\n", res); - ret = EXIT_FAILURE; - } else { - DIR *dir; - struct dirent *ent; - if (!strcmp(folder, "dummy.dir")) { - printf("Creating empty filesystem.\n"); - } else if ((dir = opendir(folder)) != NULL) { - printf("Adding files in directory '%s'.\n", folder); - while ((ent = readdir(dir)) != NULL) { - add_file(folder, ent->d_name); - } - closedir(dir); - } else { - printf("Unable to open directory '%s'.\n", folder); - ret = EXIT_FAILURE; - } - my_spiffs_unmount(); - } - } - - if (rom) fclose(rom); - if (ret == EXIT_FAILURE) unlink(romfile); - exit(ret); -} diff --git a/Sming/Components/spiffs/spiffy_host.h b/Sming/Components/spiffs/spiffy_host.h deleted file mode 100644 index f621ef6557..0000000000 --- a/Sming/Components/spiffs/spiffy_host.h +++ /dev/null @@ -1,16 +0,0 @@ - -#pragma once - -#include -#include -#include -#include -#include -#include - -typedef signed int s32_t; -typedef unsigned int u32_t; -typedef signed short s16_t; -typedef unsigned short u16_t; -typedef signed char s8_t; -typedef unsigned char u8_t; diff --git a/Sming/Makefile b/Sming/Makefile index 4ad8476a79..c058d057e6 100644 --- a/Sming/Makefile +++ b/Sming/Makefile @@ -112,6 +112,7 @@ SAMPLE_NAMES = $(shell ls -1 $(SAMPLES_DIR)) samples: | $(SAMPLE_NAMES) ##Build all sample applications $(SAMPLE_NAMES): + @printf "\n\n** Building ${@F} **\n\n" $(Q) $(MAKE) --no-print-directory -C $(SAMPLES_DIR)/$@ PIP_ARGS=-q python-requirements all # Build component samples @@ -127,6 +128,7 @@ COMPONENT_SAMPLE_TARGETS = $(addsuffix $(BUILT_SUFFIX),$(wildcard $(foreach c,$( build-component-samples: $(COMPONENT_SAMPLE_TARGETS) $(COMPONENT_SAMPLE_TARGETS): + @printf "\n\n** Building $(notdir $(@:$(BUILT_SUFFIX)=)) **\n\n" $(Q) $(MAKE) --no-print-directory -C $(@:$(BUILT_SUFFIX)=) PIP_ARGS=-q python-requirements all $(Q) touch $@ @@ -143,6 +145,7 @@ PHONY: build-and-run-tests build-and-run-tests: $(TESTS_COMPLETED) ##Build and run all test applications $(TESTS_COMPLETED): + @printf "\n\n** Building $(notdir $(@D)) **\n\n" $(Q) $(MAKE) -C $(@D) execute $(Q) touch $@ diff --git a/Sming/building.rst b/Sming/building.rst index e5c6513b50..988a84fb50 100644 --- a/Sming/building.rst +++ b/Sming/building.rst @@ -693,10 +693,8 @@ clean and easy to follow. Components can be rebuilt and cleaned individually. For example: -- ``make spiffs-build`` runs the Component ‘make’ for spiffs, which - contains the spiffs library and spiffy tool. -- ``make spiffs-clean`` removes all intermediate build files for the - Component +- ``make spiffs-build`` runs the Component ‘make’ for spiffs, which contains the SPIFFS library. +- ``make spiffs-clean`` removes all intermediate build files for the Component - ``make spiffs-rebuild`` cleans and then re-builds the Component By default, a regular ``make`` performs an incremental build on the diff --git a/docs/source/upgrading/4.2-4.3.rst b/docs/source/upgrading/4.2-4.3.rst index 63be9b39f8..ff7822422d 100644 --- a/docs/source/upgrading/4.2-4.3.rst +++ b/docs/source/upgrading/4.2-4.3.rst @@ -114,8 +114,17 @@ SPIFFS Applications should not use SPIFFS functions directly. - SPIFFS is now built with ``SPIFFS_OBJ_META_LEN=16`` to store extended attribute information, - but existing volumes should still be readable. + .. important:: + + SPIFFS is now built with ``SPIFFS_OBJ_META_LEN=16`` to store extended attribute information. + Existing volumes built with other values will not be directly compatible; the file listing may be + correct but file contents will not. + + To accommodate use of existing pre-built SPIFFS images, :envvar:`SPIFFS_OBJ_META_LEN` has been added:: + + make SPIFFS_OBJ_META_LEN=0 + + You will, however, lose the additional file information (such as modification time). File open flags e.g. eFO_ReadOnly. These will still work but are now deprecated and should be replaced with their diff --git a/samples/Basic_IFS/app/application.cpp b/samples/Basic_IFS/app/application.cpp index 5828f96d24..07f0c8ae98 100644 --- a/samples/Basic_IFS/app/application.cpp +++ b/samples/Basic_IFS/app/application.cpp @@ -130,7 +130,13 @@ bool initFileSystem() IFS::IFileSystem* fs; #ifdef FWFS_HYBRID // Create a read/write filesystem - fs = IFS::createHybridFilesystem(part); + auto spiffsPart = *Storage::findPartition(Storage::Partition::SubType::Data::spiffs); + if(spiffsPart) { + debug_i("Found '%s'", spiffsPart.name().c_str()); + } else { + debug_e("No SPIFFS partition found"); + } + fs = IFS::createHybridFilesystem(part, spiffsPart); #else // Read-only fs = IFS::createFirmwareFilesystem(part); diff --git a/samples/Basic_IFS/basic_ifs.hw b/samples/Basic_IFS/basic_ifs.hw index b001259618..0c4d66a6a5 100644 --- a/samples/Basic_IFS/basic_ifs.hw +++ b/samples/Basic_IFS/basic_ifs.hw @@ -2,10 +2,6 @@ "name": "Basic IFS sample", "base_config": "spiffs", "partitions": { - "spiffs0": { - "size": "0x20000", - "build": {} - }, "fwfs1": { "address": "0x280000", "size": "0x60000", diff --git a/tests/HostTests/component.mk b/tests/HostTests/component.mk index 284b9f0aab..38f213d5e5 100644 --- a/tests/HostTests/component.mk +++ b/tests/HostTests/component.mk @@ -36,3 +36,13 @@ APP_CFLAGS += -DRESTART_DELAY=$(RESTART_DELAY) .PHONY: execute execute: flash run + +SPIFFSGEN_BIN := out/spiff_rom_test.bin +CUSTOM_TARGETS += $(SPIFFSGEN_BIN) +$(SPIFFSGEN_BIN): + $(Q) $(SPIFFSGEN_SMING) 0x10000 spiffsgen/build $@ + +clean: resource-clean +.PHONY: resource-clean +resource-clean: + rm -f $(SPIFFSGEN_BIN) diff --git a/tests/HostTests/modules/Spiffs.cpp b/tests/HostTests/modules/Spiffs.cpp index 67e73475ee..058d0a5a4d 100644 --- a/tests/HostTests/modules/Spiffs.cpp +++ b/tests/HostTests/modules/Spiffs.cpp @@ -1,6 +1,13 @@ #include #include +#ifdef ARCH_HOST +#include +#include +#include +using IFileSystem = IFS::IFileSystem; +#endif + class SpiffsTest : public TestGroup { public: @@ -10,6 +17,13 @@ class SpiffsTest : public TestGroup void execute() override { +#ifdef ARCH_HOST + TEST_CASE("Check Spiffsgen.py") + { + checkSpiffsGen(); + } +#endif + TEST_CASE("Cycle flash") { cycleFlash(); @@ -33,6 +47,7 @@ class SpiffsTest : public TestGroup DEFINE_FSTR_LOCAL(testContent, "Some test content to write to a file"); auto part = *Storage::findPartition(Storage::Partition::SubType::Data::spiffs); + REQUIRE(part.name() == "spiffs0"); // Write to filesystem until sector #0 gets erased unsigned writeCount = 0; @@ -59,6 +74,94 @@ class SpiffsTest : public TestGroup auto content = fileGetContent(testFile); REQUIRE(testContent == content); } + +#ifdef ARCH_HOST + /* + * Verify that a legacy volume (i.e. one generated with spiffy before IFS was introduced) + * can still be read correctly. + * + * Compare content of all files to ensure they are identical. + */ + void checkSpiffsGen() + { + IFS::IFileSystem::Info info; + int err = fileGetSystemInfo(info); + CHECK(err >= 0); + debug_i("fs attr = %s", toString(info.attr).c_str()); + + IFileSystem* fsOld; + if(info.attr[IFileSystem::Attribute::NoMeta]) { + fsOld = mountSpiffsFromFile("old", "spiffsgen/spiff_rom_orig.bin"); + } else { + fsOld = mountSpiffsFromFile("old", "spiffsgen/spiff_rom_meta.bin"); + } + auto fsNew = mountSpiffsFromFile("new", "out/spiff_rom_test.bin"); + + readCheck(fsOld, fsNew); + + delete fsNew; + delete fsOld; + + delete Storage::findDevice("new"); + delete Storage::findDevice("old"); + } + + IFileSystem* mountSpiffsFromFile(const String& tag, const String& filename) + { + auto& hfs = IFS::Host::fileSystem; + auto f = hfs.open(filename, IFS::File::ReadOnly); + if(f < 0) { + debug_e("Failed to open '%s': %s", filename.c_str(), hfs.getErrorString(f).c_str()); + return nullptr; + } + auto dev = new Storage::FileDevice(tag, hfs, f); + Storage::registerDevice(dev); + auto part = dev->createPartition(tag, Storage::Partition::SubType::Data::spiffs, 0, dev->getSize(), + Storage::Partition::Flag::readOnly); + + auto fs = new IFS::SPIFFS::FileSystem(part); + int err = fs->mount(); + if(err < 0) { + debug_e("SPIFFS mount '%s' failed: %s", tag.c_str(), fs->getErrorString(err).c_str()); + delete fs; + fs = nullptr; + delete dev; + } + + debug_i("Mounted '%s' as '%s'", filename.c_str(), tag.c_str()); + return fs; + } + + void readCheck(IFileSystem* fsOld, IFileSystem* fsNew) + { + DirHandle dir{}; + int res = fsOld->opendir(nullptr, dir); + if(res < 0) { + debug_e("opendir failed: %s", fsOld->getErrorString(res).c_str()); + TEST_ASSERT(false); + return; + } + + FileNameStat stat; + while((res = fsOld->readdir(dir, stat)) >= 0) { + FileStat statNew; + fsNew->stat(stat.name.buffer, &statNew); + debug_i("File '%s' size %u / %u", stat.name.buffer, stat.size, statNew.size); + + String oldContent = fsOld->getContent(stat.name.buffer); + CHECK(oldContent); + String newContent = fsNew->getContent(stat.name.buffer); + CHECK(newContent); + + CHECK_EQ(newContent, oldContent); + } + + CHECK_EQ(res, IFS::Error::NoMoreFiles); + debug_i("readdir(): %s", fsOld->getErrorString(res).c_str()); + + fsOld->closedir(dir); + } +#endif }; void REGISTER_TEST(Spiffs) diff --git a/tests/HostTests/spiffsgen/README.rst b/tests/HostTests/spiffsgen/README.rst new file mode 100644 index 0000000000..00d6717feb --- /dev/null +++ b/tests/HostTests/spiffsgen/README.rst @@ -0,0 +1,14 @@ +Spiffsgen +========= + +Test SPIFFS images used by `Spiffs` module. +Content is from `HttpServer_ConfigNetwork` sample, `web/build` directory. + +spiff_rom_orig.bin + Built using SPIFFY with no metadata +spiff_rom_meta.bin + Build using SPIFFY with metadata (16 bytes) + +This test application builds an image using spiffsgen for comparison. + +Will fail if built with SPIFFS_OBJ_META_LEN set to anything other than 0 or 16. diff --git a/tests/HostTests/spiffsgen/build/.lastModified b/tests/HostTests/spiffsgen/build/.lastModified new file mode 100644 index 0000000000..9685abd72b --- /dev/null +++ b/tests/HostTests/spiffsgen/build/.lastModified @@ -0,0 +1 @@ +Mon, 24 Jul 2017 17:20:31 GMT diff --git a/tests/HostTests/spiffsgen/build/bootstrap-core.css.gz b/tests/HostTests/spiffsgen/build/bootstrap-core.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..8483876af3e9ba369e93d223ee6c03ca8ca9f77a GIT binary patch literal 15861 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≺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 F{|_Xg0#E<| literal 0 HcmV?d00001 diff --git a/tests/HostTests/spiffsgen/build/core.js.gz b/tests/HostTests/spiffsgen/build/core.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..6765d5850bb1af07a91fbe34174bbaddefc10c1c GIT binary patch literal 30474 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 RkUbR7{twGu9YXg|0RTa39)AD; literal 0 HcmV?d00001 diff --git a/tests/HostTests/spiffsgen/build/index.html b/tests/HostTests/spiffsgen/build/index.html new file mode 100644 index 0000000000..a89f8c23ad --- /dev/null +++ b/tests/HostTests/spiffsgen/build/index.html @@ -0,0 +1,4 @@ +Sming Framework WiFi Network configuration

Sming Based

Network Settings

Wireless network connection

Networks

\ No newline at end of file diff --git a/tests/HostTests/spiffsgen/build/settings.html b/tests/HostTests/spiffsgen/build/settings.html new file mode 100644 index 0000000000..315f279b8e --- /dev/null +++ b/tests/HostTests/spiffsgen/build/settings.html @@ -0,0 +1,4 @@ +Sming Framework WiFi Network configuration

Sming Based

Network Settings

Wireless network connection

Settings

IP Address
\ No newline at end of file diff --git a/tests/HostTests/spiffsgen/build/wifi-sprites.png b/tests/HostTests/spiffsgen/build/wifi-sprites.png new file mode 100644 index 0000000000000000000000000000000000000000..7a61d71a115bb69eb2a847f6c63c5b35c585aaf6 GIT binary patch literal 1769 zcmZXVX*3)78pi*Y8$nYkf>D|@rJZP1EzKx(1<^88W1Cuv)>0MipvF$3rjFp6mQvM@ zTBb$D672}JgxXt67=zdn!dy!%u_RI|jC<}m_ndp?`S6_g>+gM^=e(%|hpR9-bvXb4 z%+?0)1c1ch2yjryp$omJ%>Y2EZSfW^(G#m|9}2=%LuQ@N@9SeUMzLHSmEx*hK7Q!w ze)n3dQ|s)fp&m(OEC!kxyL58#O=;RYXe!z=?Ht;wb@B0_)gZEB&pAu3*eNKD9C;&> z&&j*Az-q6WU_{+nYnY#XB_?s2!fA#rrVFINNM@cPIQDmCc+Cd5@^zga1>i2m)tlJs zL>0yLtLQBk)Cn;RQri*1>yeUxOlh0pHfp;_)U*;H9;pj)$zv#^$Nx>$nQFaXb~(JG zclY`L!`iO*+5Gt%t{t9re!Snsim_jTHxAi$`Jp@m)icbtY#mO??w{MXVxR#Y9N0KmaGtz2}keE7r8=FRh&a+M>0RNHqt&i z!QaoMY9CYw?K6=IM6)MOszzHq&45|V%5Kv2xxQvGIXA6>P2`pQgY#2Rb>sG}%_!5f zFM^Q9;0~dWP=X+5X)G&Y#JY=zo2GmVFP>(pz$1!vd3`GrhQ9DdX|GkKu!+g=;Zdnt zEuu&S2wza*})8;MVpodUI|cFyuU_=YJXVr z&^wV|4@XXIR2<^ATy#EZvA^1Yeo||Dm;4~NKq;axuk=BG%}P+Ta-vQR4e3mp#CSt+ zK9$E4h!2}Aba|nL3wq=^>{|EIx`yvd|LhPHwyTxn$9YCOpRAl8Zh{LoAk>#|bWd|cW&!+S z4fA(<8Z)Q{1IcZdQYyHbDnHUnd6)Ma5RvOQY4s${is{z3u0xBOPd_8z|G$OlD;)Xt zeP7+RVb^ReeABj_xs0%>k>JQvYtpVu1?M{j%q8QuvA-RH-+w;(pagB4@{{l&dRLIY zT$Kl6APl$rbt@^dc zl_GF}ZljAFeJDKbErn9=K8)^vuX*e+$6YCK@cr0!&51SBRICz=UHM%B zoT`J?Hx_;D_TFB;`DNV0EJslEFOcCJA&piGm7V{=zWbpJHrAaSF{k0`c{cv>uU6d+ ze%_;GUPFCaP3Q0?^EPjz{g6w8_R4y8C~2Wb9y0Wz4l|Z7I2DQUeH~MYBySgN){?Ht z0R2w(HgoQa;HmV55^J#QPKJVqvKQWqVDFHfs6<%?`#_?PAaYp;^Vso4GV`XLP-~bKn=t|VTma%PV6(+Q^ggU7zXuf%A{>!hFHiN7N&78Id z($#6Ul$jW%n0(q!5?|`aOjLiahZ5Fm?)vTP|*&Ej&i+= z<(^l|JrrHk8y&nP>H0G-kVh1s18QZ_aP(saz1m_U>bvjV20roJ&y8;+9eqC^216(n zolA1I75ZYBM;-1t4Ue$iljwsrBiAc78tv1~PbGCdRgHR}2dTY$b4;eRJ93mf&qor( z{`D$i0Xvm_FrI_yP`m4Rqo8s+v{4lG1I?|{X)yfR-d-@fWlpcza)5V6F4&el?&{Oq zY(BU(=;vW{^rV>Dyf5g^U-WJ^5E)9M*o>4?bNt`s$7v}OC(3EaA5j1E$!x71@Ku(# GlKukF<4{Wg literal 0 HcmV?d00001 diff --git a/tests/HostTests/spiffsgen/spiff_rom_meta.bin b/tests/HostTests/spiffsgen/spiff_rom_meta.bin new file mode 100644 index 0000000000000000000000000000000000000000..2ed1940b20e11e91b0e97dd5bff9450eb63c726e GIT binary patch literal 65536 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 EKS%=V-v9sr literal 0 HcmV?d00001 diff --git a/tests/HostTests/spiffsgen/spiff_rom_orig.bin b/tests/HostTests/spiffsgen/spiff_rom_orig.bin new file mode 100644 index 0000000000000000000000000000000000000000..13e02bcec791637142c1362b66df35c446ff17b4 GIT binary patch literal 65536 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,%#JY;XZC3jIV~wv;;$Zky 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 qT`SJHT6F*Qx8< Date: Tue, 16 Feb 2021 07:52:47 +0000 Subject: [PATCH 15/91] Use JSON for IFS build configuration, with schema (#2219) Replaces existing .ini file, consistent with Storage config, more structured. --- Sming/Components/IFS | 2 +- Sming/Components/Storage/README.rst | 3 + .../Storage/Tools/hwconfig/config.py | 4 +- .../Storage/Tools/hwconfig/hwconfig.py | 1 - Sming/Components/Storage/requirements.txt | 1 + Tools/requirements.txt | 1 + samples/Basic_IFS/basic_ifs.hw | 2 +- samples/Basic_IFS/fsimage.fwfs | 71 +++++++++++++++++++ samples/Basic_IFS/fsimage.ini | 32 --------- samples/Basic_Storage/basic_storage.hw | 6 ++ 10 files changed, 87 insertions(+), 36 deletions(-) create mode 100644 samples/Basic_IFS/fsimage.fwfs delete mode 100644 samples/Basic_IFS/fsimage.ini diff --git a/Sming/Components/IFS b/Sming/Components/IFS index dce62c0b14..778a4a973f 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit dce62c0b14c4af7e29c087e46dced25dae5799f0 +Subproject commit 778a4a973ff5fa2eb091db653f66f2d41f737e59 diff --git a/Sming/Components/Storage/README.rst b/Sming/Components/Storage/README.rst index e4afc51031..5b3f4f8bcf 100644 --- a/Sming/Components/Storage/README.rst +++ b/Sming/Components/Storage/README.rst @@ -26,6 +26,9 @@ Hardware configuration Each project has an associated ``Hardware configuration``, specified by the :envvar:`HWCONFIG` setting: this is a JSON file with a ``.hw`` extension. +For user convenience, the configuration file may contain comments however these are stripped before +processing. + The build system locates the file by searching, in order: - ``{PROJECT_DIR}`` the root project directory diff --git a/Sming/Components/Storage/Tools/hwconfig/config.py b/Sming/Components/Storage/Tools/hwconfig/config.py index b4972375f6..9e4e8101c6 100644 --- a/Sming/Components/Storage/Tools/hwconfig/config.py +++ b/Sming/Components/Storage/Tools/hwconfig/config.py @@ -5,6 +5,7 @@ import os, partition, storage from common import * from builtins import classmethod +from rjsmin import jsmin def findConfig(name): dirs = os.environ['HWCONFIG_DIRS'].split(' ') @@ -32,7 +33,8 @@ def from_name(cls, name): def load(self, name): filename = findConfig(name) self.depends.append(filename) - data = json.load(open(filename)) + din = open(filename).read() + data = json.loads(jsmin(din)) self.parse_dict(data) def parse_dict(self, data): diff --git a/Sming/Components/Storage/Tools/hwconfig/hwconfig.py b/Sming/Components/Storage/Tools/hwconfig/hwconfig.py index 5a8cfbdf9b..dc33c3c1aa 100644 --- a/Sming/Components/Storage/Tools/hwconfig/hwconfig.py +++ b/Sming/Components/Storage/Tools/hwconfig/hwconfig.py @@ -59,7 +59,6 @@ def main(): # Validate resulting hardware configuration against schema try: from jsonschema import Draft7Validator - from jsonschema.exceptions import ValidationError except ImportError: critical("hwconfig: `jsonschema` is not installed. Please run `make python-requirements`") sys.exit(1) diff --git a/Sming/Components/Storage/requirements.txt b/Sming/Components/Storage/requirements.txt index d89304b1a8..bcc6ba09ac 100644 --- a/Sming/Components/Storage/requirements.txt +++ b/Sming/Components/Storage/requirements.txt @@ -1 +1,2 @@ jsonschema +rjsmin diff --git a/Tools/requirements.txt b/Tools/requirements.txt index ee96c8f8e1..941e353f13 100644 --- a/Tools/requirements.txt +++ b/Tools/requirements.txt @@ -1,2 +1,3 @@ pyserial jsonschema +rjsmin diff --git a/samples/Basic_IFS/basic_ifs.hw b/samples/Basic_IFS/basic_ifs.hw index 0c4d66a6a5..ee67b4c91f 100644 --- a/samples/Basic_IFS/basic_ifs.hw +++ b/samples/Basic_IFS/basic_ifs.hw @@ -10,7 +10,7 @@ "filename": "out/fwfs1.bin", "build": { "target": "fwfs-build", - "config": "fsimage.ini" + "config": "fsimage.fwfs" } } } diff --git a/samples/Basic_IFS/fsimage.fwfs b/samples/Basic_IFS/fsimage.fwfs new file mode 100644 index 0000000000..bb1a6be2e2 --- /dev/null +++ b/samples/Basic_IFS/fsimage.fwfs @@ -0,0 +1,71 @@ +// Filesystem builder configuration +{ + "name": "Sming IFS demo volume", + "id": "0x12345678", + // Where to read files from + "source": { + "/": "files", + "README.rst": "README.rst", + "test.jsonc": "basic_ifs.hw", + "readme.md": "${SMING_HOME}/Components/IFS/README.rst", + "sming.png": "${SMING_HOME}/../docs/api-logo.png", + "Data": "${SMING_HOME}/Core/Data", + "framework": "${SMING_HOME}/../docs/source/framework", + "Sming": "${SMING_HOME}/Core" + }, + // Directories to mount other object stores + "mountpoints": { + "config": 1 + }, + // Rules for file metadata. All rules are evaluated in sequence for every file + "rules": [ + { + "mask": "*", + "read": "guest", + "write": "admin" + }, + { + "mask": ".*", + "read": "admin" + }, + { + "mask": "*.html", + "readonly": true + }, + { + "mask": [ + "*.js", + "*.png", + "*.ico", + "*.jpg", + "*.jpeg", + "*.html", + "*.css", + "*.txt", + "*.md" + ], + "compress": "gzip" + }, + { + "mask": [ + "/styles.css", + "*.ico", + "*.png" + ], + "read": "any" + }, + { + "mask": [ + "/Data/*", + "/framework/*", + "/Sming/*" + ], + "compress": "gzip" + }, + //This is a template file so firmware needs to read it + { + "mask": "/error.html", + "compress": "none" + } + ] +} \ No newline at end of file diff --git a/samples/Basic_IFS/fsimage.ini b/samples/Basic_IFS/fsimage.ini deleted file mode 100644 index 6e23dc647a..0000000000 --- a/samples/Basic_IFS/fsimage.ini +++ /dev/null @@ -1,32 +0,0 @@ - -# Filesystem builder configuration -[config] - -volumeName = Sming IFS demo volume -volumeID = 0x12345678 - -# Where to read files from -[source] -/ = files -readme.md = ${SMING_HOME}/Components/IFS/README.rst -sming.png = ${SMING_HOME}/../docs/api-logo.png -Data = ${SMING_HOME}/Core/Data -framework = ${SMING_HOME}/../docs/source/framework -Sming = ${SMING_HOME}/Core - -# Directories to mount other object stores -[mountpoints] -config = 1 - -# Rules for file metadata. All rules are evaluated in sequence for every file -# The masks apply to the full target path -[rules] -*: read=guest,write=admin -\.*: read=admin -*.html: readonly=true -*.js, *.png, *.ico, *.jpg, *.jpeg, *.html, *.css, *.txt, *.md: compress=gzip -/layout.json, /full-layout.json: compress=gzip -/Data/*, /framework/*, /Sming/*: compress=gzip -/index.html, /index.js, /stswsio.js, /styles.css, *.ico, *.png: read=any -# This is a template file so firmware needs to read it -/error.html: compress=none diff --git a/samples/Basic_Storage/basic_storage.hw b/samples/Basic_Storage/basic_storage.hw index c878ea9713..aad639cc4f 100644 --- a/samples/Basic_Storage/basic_storage.hw +++ b/samples/Basic_Storage/basic_storage.hw @@ -2,12 +2,14 @@ "name": "Basic Storage sample", "base_config": "spiffs", "devices": { + // Override default (conservative) flash settings for maximum performance "spiFlash": { "mode": "qio", "speed": 80 } }, "partitions": { + // User-defined partition type "user0": { "address": "0x1F0000", "size": "16K", @@ -21,12 +23,15 @@ "type": "user", "subtype": 1 }, + // Override default SPIFFS partition with new address and source content "spiffs0": { "address": "0x200000", "build": { + // target is already defined, just change the folder to pick up files from "files": "files/spiffs0" } }, + // Add a second SPIFFS partition "spiffs1": { "address": "0x280000", "size": "256K", @@ -38,6 +43,7 @@ "files": "files/spiffs1" } }, + // And a third "spiffs2": { "address": "0x2C0000", "size": "256K", From 11836eb08306b3c5548f1c4c394e79d12be37016 Mon Sep 17 00:00:00 2001 From: slaff Date: Thu, 18 Feb 2021 09:05:21 +0100 Subject: [PATCH 16/91] Added protocol buffers support using nanopb. (#2217) --- .gitmodules | 4 ++ Sming/Libraries/nanopb/README.rst | 56 ++++++++++++++++++++++++++ Sming/Libraries/nanopb/component.mk | 4 ++ Sming/Libraries/nanopb/nanopb | 1 + Sming/Libraries/nanopb/src/PbUtils.cpp | 41 +++++++++++++++++++ Sming/Libraries/nanopb/src/PbUtils.h | 27 +++++++++++++ 6 files changed, 133 insertions(+) create mode 100644 Sming/Libraries/nanopb/README.rst create mode 100644 Sming/Libraries/nanopb/component.mk create mode 160000 Sming/Libraries/nanopb/nanopb create mode 100644 Sming/Libraries/nanopb/src/PbUtils.cpp create mode 100644 Sming/Libraries/nanopb/src/PbUtils.h diff --git a/.gitmodules b/.gitmodules index 9bb87d3574..e2a90fbd3b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -227,6 +227,10 @@ path = Sming/Libraries/MultipartParser/multipart-parser url = https://github.com/iafonov/multipart-parser-c.git ignore = dirty +[submodule "Libraries.nanopb"] + path = Sming/Libraries/nanopb/nanopb + url = https://github.com/nanopb/nanopb.git + ignore = dirty [submodule "Libraries.RapidXML"] path = Sming/Libraries/RapidXML url = https://github.com/mikee47/Sming-RapidXML diff --git a/Sming/Libraries/nanopb/README.rst b/Sming/Libraries/nanopb/README.rst new file mode 100644 index 0000000000..5a70236598 --- /dev/null +++ b/Sming/Libraries/nanopb/README.rst @@ -0,0 +1,56 @@ +Nano Protocol-Buffer +==================== + +Introduction +------------ + +This component adds support for `Nano Protocol-Buffer `_ implementation. + + Nanopb is a small code-size `Protocol Buffers `_ implementation in ansi C. It is especially suitable for use in microcontrollers, but fits any memory restricted system. + + +C file generation from Proto files +---------------------------------- + +Once this component is installed you can use it to add Nano Protocol-Buffer support to your project and generate C and header files from Proto files. +One possible way to call the generator is to go to the directory where the proto file is located and run the generator. As shown below:: + + make -C $SMING_HOME fetch nano-pb + cd + python $SMING_HOME/Libraries/nanopb/nanopb/generator/nanopb_generator.py .proto + +After the generator tool is run you will have newly generated C and header files that can be used in your Sming application. + +Using +----- + +1. Add ``COMPONENT_DEPENDS += nanopb`` to your application componenent.mk file. +2. Add these lines to your application:: + + #include + // The line below should be replaced with the generated header file + #include "cast_channel.pb.h" + +3. Example:: + + #include + #include "cast_channel.pb.h" + + void doSomething(const uint8_t* data, size_t length) + { + // ... + + extensions_api_cast_channel_CastMessage message = extensions_api_cast_channel_CastMessage_init_default; + + message.protocol_version = extensions_api_cast_channel_CastMessage_ProtocolVersion_CASTV2_1_0; + message.source_id.funcs.encode = &pbEncodeData; + message.source_id.arg = new PbData(sourceId); + message.destination_id.funcs.encode = &pbEncodeData; + message.destination_id.arg = new PbData(destinationId); + message.nameSpace.funcs.encode = &pbEncodeData; + message.nameSpace.arg = new PbData(ns); + message.payload_type = extensions_api_cast_channel_CastMessage_PayloadType_STRING; + message.payload_utf8.funcs.encode = &pbEncodeData; + message.payload_utf8.arg = new PbData((uint8_t*)data, length); + // ... + } \ No newline at end of file diff --git a/Sming/Libraries/nanopb/component.mk b/Sming/Libraries/nanopb/component.mk new file mode 100644 index 0000000000..7145d462d4 --- /dev/null +++ b/Sming/Libraries/nanopb/component.mk @@ -0,0 +1,4 @@ +COMPONENT_SRCDIRS := nanopb src +COMPONENT_INCDIRS := $(COMPONENT_SRCDIRS) + +COMPONENT_SUBMODULES += nanopb \ No newline at end of file diff --git a/Sming/Libraries/nanopb/nanopb b/Sming/Libraries/nanopb/nanopb new file mode 160000 index 0000000000..049485ff55 --- /dev/null +++ b/Sming/Libraries/nanopb/nanopb @@ -0,0 +1 @@ +Subproject commit 049485ff557178f646d573eca3bd647f543b760b diff --git a/Sming/Libraries/nanopb/src/PbUtils.cpp b/Sming/Libraries/nanopb/src/PbUtils.cpp new file mode 100644 index 0000000000..2ba2047d1b --- /dev/null +++ b/Sming/Libraries/nanopb/src/PbUtils.cpp @@ -0,0 +1,41 @@ +#include "PbUtils.h" + +// See: https://iam777.tistory.com/538 + +bool pbEncodeData(pb_ostream_t *stream, const pb_field_t *field, void * const *arg) +{ + PbData *data = (PbData*) *arg; + if(data == nullptr) { + return false; + } + + if (!pb_encode_tag_for_field(stream, field)) { + return false; + } + + return pb_encode_string(stream, (uint8_t*)data->value, data->length); +} + +bool pbDecodeData(pb_istream_t *stream, const pb_field_t *field, void **arg) +{ + uint8_t buffer[1024] = {0}; + + /* We could read block-by-block to avoid the large buffer... */ + if (stream->bytes_left > sizeof(buffer) - 1) { + return false; + } + + size_t available = stream->bytes_left; + if (!pb_read(stream, buffer, stream->bytes_left)) { + return false; + } + + + MemoryDataStream* data = (MemoryDataStream*) *arg; + if(data == nullptr) { + data = new MemoryDataStream(); + *arg = (void*)data; + } + data->write(buffer, available); + return true; +} diff --git a/Sming/Libraries/nanopb/src/PbUtils.h b/Sming/Libraries/nanopb/src/PbUtils.h new file mode 100644 index 0000000000..78c221e659 --- /dev/null +++ b/Sming/Libraries/nanopb/src/PbUtils.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +class PbData +{ +public: + uint8_t* value = nullptr; + size_t length = 0; + + PbData(const String& text) + { + PbData((uint8_t *)text.c_str(), text.length()); + } + + PbData(uint8_t* data, size_t length) + { + value = data; + this->length = length; + } +}; + +bool pbEncodeData(pb_ostream_t *stream, const pb_field_t *field, void * const *arg); +bool pbDecodeData(pb_istream_t *stream, const pb_field_t *field, void **arg); From 8f3c894ee9a790f1df0d0664fa71404f1b5eacd8 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 19 Feb 2021 07:45:21 +0000 Subject: [PATCH 17/91] Fix BitSet `&` operator, add XOR operator (#2220) --- Sming/Core/Data/BitSet.h | 14 +++++++++++++- tests/HostTests/modules/BitSet.cpp | 4 ++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Sming/Core/Data/BitSet.h b/Sming/Core/Data/BitSet.h index 10dd5164ad..ee7a0b47e1 100644 --- a/Sming/Core/Data/BitSet.h +++ b/Sming/Core/Data/BitSet.h @@ -377,7 +377,7 @@ template class BitSet template inline constexpr BitSet operator&(const BitSet& x, const BitSet& y) { - return BitSet(S(x) & ~S(y)); + return BitSet(S(x) & S(y)); } template @@ -416,6 +416,18 @@ inline constexpr BitSet operator-(const BitSet& x, E b return BitSet(S(x) & ~BitSet::bitVal(b)); } +template +inline constexpr BitSet operator^(BitSet x, BitSet y) +{ + return BitSet(S(x) ^ S(y)); +} + +template +inline constexpr BitSet operator^(BitSet x, E b) +{ + return x ^ BitSet(b); +} + /* * These allow construction of a maximally-sized BitSet in an expression, * which is then copy-constructed to the actual value. For example: diff --git a/tests/HostTests/modules/BitSet.cpp b/tests/HostTests/modules/BitSet.cpp index cfb5b5b2d3..331457e5f4 100644 --- a/tests/HostTests/modules/BitSet.cpp +++ b/tests/HostTests/modules/BitSet.cpp @@ -100,7 +100,11 @@ class BitSetTest : public TestGroup basket -= Fruit::orange; REQUIRE(basket.value() == (_BV(Fruit::banana) | _BV(Fruit::tomato))); + basket |= Fruit::kiwi; + REQUIRE(basket.value() == (_BV(Fruit::kiwi) | _BV(Fruit::banana) | _BV(Fruit::tomato))); + basket &= fixedBasket; + REQUIRE(basket.value() == (_BV(Fruit::banana) | _BV(Fruit::tomato))); basket = ~fixedBasket; debug_e("basket.value = 0x%08x", basket.value()); From 295a5f39af034de1a819915044323b2588666f11 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 20 Feb 2021 07:26:25 +0000 Subject: [PATCH 18/91] Catch missing python modules and print message (#2221) * Add `python-requirements` target for main Sming Makefile to install core requirements * Catch missing optional python modules and print more obvious message --- Sming/Components/IFS | 2 +- Sming/Components/Storage/Tools/hwconfig/config.py | 8 +++++++- Sming/Makefile | 8 ++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Sming/Components/IFS b/Sming/Components/IFS index 778a4a973f..cd16dc51c4 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit 778a4a973ff5fa2eb091db653f66f2d41f737e59 +Subproject commit cd16dc51c49fc792e1ae92f96ab2d8eb74873538 diff --git a/Sming/Components/Storage/Tools/hwconfig/config.py b/Sming/Components/Storage/Tools/hwconfig/config.py index 9e4e8101c6..e0b373ec70 100644 --- a/Sming/Components/Storage/Tools/hwconfig/config.py +++ b/Sming/Components/Storage/Tools/hwconfig/config.py @@ -5,7 +5,13 @@ import os, partition, storage from common import * from builtins import classmethod -from rjsmin import jsmin + +try: + from rjsmin import jsmin +except ImportError as err: + sys.stderr.write("\n** %s: please run `make python-requirements` **\n\n" % str(err)) + sys.exit(1) + def findConfig(name): dirs = os.environ['HWCONFIG_DIRS'].split(' ') diff --git a/Sming/Makefile b/Sming/Makefile index c058d057e6..a64374e5ca 100644 --- a/Sming/Makefile +++ b/Sming/Makefile @@ -175,6 +175,14 @@ $(CLEAN_TESTS): ##@Tools +CACHE_VARS += PIP_ARGS +PIP_ARGS ?= +.PHONY: python-requirements +python-requirements: ##Install Python requirements for framework via pip (use PIP_ARGS=... for additional options) + @echo Installing Python requirements... + $(Q) $(PYTHON) -m pip install $(PIP_ARGS) -r $(SMING_HOME)/../Tools/requirements.txt + + # Recursive wildcard search # $1 -> list of directories # $2 -> file extensions filters (using % as wildcard) From 3fcb3cad57e11955942350d9fc52fdd83d0e9a02 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Mon, 22 Feb 2021 15:29:41 +0000 Subject: [PATCH 19/91] Fix issues with missing `rjsmin` python package Add rjsmin.py directly to source (suggested by author https://github.com/ndparker/rjsmin) Catch missing `jsonschema` and generate warning, but allow compilation to continue IFS: Apply the same changes --- Sming/Components/IFS | 2 +- .../Storage/Tools/hwconfig/config.py | 8 +- .../Storage/Tools/hwconfig/hwconfig.py | 21 +- .../Storage/Tools/hwconfig/rjsmin.py | 543 ++++++++++++++++++ Sming/Components/Storage/requirements.txt | 1 - Tools/requirements.txt | 1 - 6 files changed, 555 insertions(+), 21 deletions(-) create mode 100644 Sming/Components/Storage/Tools/hwconfig/rjsmin.py diff --git a/Sming/Components/IFS b/Sming/Components/IFS index cd16dc51c4..1e50d014fa 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit cd16dc51c49fc792e1ae92f96ab2d8eb74873538 +Subproject commit 1e50d014fac7604026d0d4112f1855431a44496a diff --git a/Sming/Components/Storage/Tools/hwconfig/config.py b/Sming/Components/Storage/Tools/hwconfig/config.py index e0b373ec70..e8cc943156 100644 --- a/Sming/Components/Storage/Tools/hwconfig/config.py +++ b/Sming/Components/Storage/Tools/hwconfig/config.py @@ -3,16 +3,10 @@ # import os, partition, storage +from rjsmin import jsmin from common import * from builtins import classmethod -try: - from rjsmin import jsmin -except ImportError as err: - sys.stderr.write("\n** %s: please run `make python-requirements` **\n\n" % str(err)) - sys.exit(1) - - def findConfig(name): dirs = os.environ['HWCONFIG_DIRS'].split(' ') for d in dirs: diff --git a/Sming/Components/Storage/Tools/hwconfig/hwconfig.py b/Sming/Components/Storage/Tools/hwconfig/hwconfig.py index dc33c3c1aa..a117ff87ff 100644 --- a/Sming/Components/Storage/Tools/hwconfig/hwconfig.py +++ b/Sming/Components/Storage/Tools/hwconfig/hwconfig.py @@ -59,17 +59,16 @@ def main(): # Validate resulting hardware configuration against schema try: from jsonschema import Draft7Validator - except ImportError: - critical("hwconfig: `jsonschema` is not installed. Please run `make python-requirements`") - sys.exit(1) - inst = json.loads(config.to_json()) - schema = json.load(open(args.expr)) - v = Draft7Validator(schema) - errors = sorted(v.iter_errors(inst), key=lambda e: e.path) - if errors != []: - for e in errors: - critical("%s @ %s" % (e.message, e.path)) - sys.exit(3) + inst = json.loads(config.to_json()) + schema = json.load(open(args.expr)) + v = Draft7Validator(schema) + errors = sorted(v.iter_errors(inst), key=lambda e: e.path) + if errors != []: + for e in errors: + critical("%s @ %s" % (e.message, e.path)) + sys.exit(3) + except ImportError as err: + critical("\n** WARNING! %s: Cannot validate '%s', please run `make python-requirements **\n\n" % (str(err), args.input)) elif args.command == 'flashcheck': # Expect list of chunks, such as "0x100000=/out/Esp8266/debug/firmware/spiff_rom.bin 0x200000=custom.bin" list = args.expr.split() diff --git a/Sming/Components/Storage/Tools/hwconfig/rjsmin.py b/Sming/Components/Storage/Tools/hwconfig/rjsmin.py new file mode 100644 index 0000000000..68b65aeef0 --- /dev/null +++ b/Sming/Components/Storage/Tools/hwconfig/rjsmin.py @@ -0,0 +1,543 @@ +#!/usr/bin/env python +# -*- coding: ascii -*- +u""" +===================== + Javascript Minifier +===================== + +rJSmin is a javascript minifier written in python. + +The minifier is based on the semantics of `jsmin.c by Douglas Crockford`_\\. + +:Copyright: + + Copyright 2011 - 2019 + Andr\xe9 Malo or his licensors, as applicable + +:License: + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +The module is a re-implementation aiming for speed, so it can be used at +runtime (rather than during a preprocessing step). Usually it produces the +same results as the original ``jsmin.c``. It differs in the following ways: + +- there is no error detection: unterminated string, regex and comment + literals are treated as regular javascript code and minified as such. +- Control characters inside string and regex literals are left untouched; they + are not converted to spaces (nor to \\n) +- Newline characters are not allowed inside string and regex literals, except + for line continuations in string literals (ECMA-5). +- "return /regex/" is recognized correctly. +- More characters are allowed before regexes. +- Line terminators after regex literals are handled more sensibly +- "+ +" and "- -" sequences are not collapsed to '++' or '--' +- Newlines before ! operators are removed more sensibly +- (Unnested) template literals are supported (ECMA-6) +- Comments starting with an exclamation mark (``!``) can be kept optionally +- rJSmin does not handle streams, but only complete strings. (However, the + module provides a "streamy" interface). + +Since most parts of the logic are handled by the regex engine it's way faster +than the original python port of ``jsmin.c`` by Baruch Even. The speed factor +varies between about 6 and 55 depending on input and python version (it gets +faster the more compressed the input already is). Compared to the +speed-refactored python port by Dave St.Germain the performance gain is less +dramatic but still between 3 and 50 (for huge inputs). See the docs/BENCHMARKS +file for details. + +rjsmin.c is a reimplementation of rjsmin.py in C and speeds it up even more. + +Supported python versions are 2.7 and 3.4+. + +.. _jsmin.c by Douglas Crockford: + http://www.crockford.com/javascript/jsmin.c +""" +__author__ = u"Andr\xe9 Malo" +__docformat__ = "restructuredtext en" +__license__ = "Apache License, Version 2.0" +__version__ = '1.1.0' +__all__ = ['jsmin'] + +import functools as _ft +import re as _re + + +def _make_jsmin(python_only=False): + """ + Generate JS minifier based on `jsmin.c by Douglas Crockford`_ + + .. _jsmin.c by Douglas Crockford: + http://www.crockford.com/javascript/jsmin.c + + :Parameters: + `python_only` : ``bool`` + Use only the python variant. If true, the c extension is not even + tried to be loaded. + + :Return: Minifier + :Rtype: ``callable`` + """ + # pylint: disable = unused-variable + # pylint: disable = too-many-locals + + if not python_only: + try: + import _rjsmin + except ImportError: + pass + else: + # Ensure that the C version is in sync + # https://github.com/ndparker/rjsmin/issues/11 + if getattr(_rjsmin, '__version__', None) == __version__: + return _rjsmin.jsmin + try: + xrange + except NameError: + xrange = range # pylint: disable = redefined-builtin + + space_chars = r'[\000-\011\013\014\016-\040]' + + line_comment = r'(?://[^\r\n]*)' + space_comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)' + space_comment_nobang = r'(?:/\*(?!!)[^*]*\*+(?:[^/*][^*]*\*+)*/)' + bang_comment = r'(?:/\*![^*]*\*+(?:[^/*][^*]*\*+)*/)' + + string1 = r"(?:'[^'\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|\r)[^'\\\r\n]*)*')" + string1 = string1.replace("'", r'\047') # portability + string2 = r'(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|\r)[^"\\\r\n]*)*")' + string3 = r'(?:`[^`\\]*(?:\\(?:[^\r\n]|\r?\n|\r)[^`\\]*)*`)' + string3 = string3.replace('`', r'\140') # portability + strings = r'(?:%s|%s|%s)' % (string1, string2, string3) + + charclass = r'(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\])' + nospecial = r'[^/\\\[\r\n]' + regex = r'(?:/(?![\r\n/*])%s*(?:(?:\\[^\r\n]|%s)%s*)*/)' % ( + nospecial, charclass, nospecial + ) + space = r'(?:%s|%s)' % (space_chars, space_comment) + newline = r'(?:%s?[\r\n])' % line_comment + + def fix_charclass(result): + """ Fixup string of chars to fit into a regex char class """ + pos = result.find('-') + if pos >= 0: + result = r'%s%s-' % (result[:pos], result[pos + 1:]) + + def sequentize(string): + """ + Notate consecutive characters as sequence + + (1-4 instead of 1234) + """ + first, last, result = None, None, [] + for char in map(ord, string): + if last is None: + first = last = char + elif last + 1 == char: + last = char + else: + result.append((first, last)) + first = last = char + if last is not None: + result.append((first, last)) + return ''.join(['%s%s%s' % ( + chr(first), + last > first + 1 and '-' or '', + last != first and chr(last) or '' + ) for first, last in result]) # noqa + + return _re.sub( + r"([\000-\040'`])", # ' and ` for better portability + lambda m: '\\%03o' % ord(m.group(1)), ( + sequentize(result) + .replace('\\', '\\\\') + .replace('[', '\\[') + .replace(']', '\\]') + ) + ) + + def id_literal_(what): + """ Make id_literal like char class """ + match = _re.compile(what).match + result = ''.join([ + chr(c) for c in xrange(127) if not match(chr(c)) + ]) + return '[^%s]' % fix_charclass(result) + + def not_id_literal_(keep): + """ Make negated id_literal like char class """ + match = _re.compile(id_literal_(keep)).match + result = ''.join([ + chr(c) for c in xrange(127) if not match(chr(c)) + ]) + return r'[%s]' % fix_charclass(result) + + not_id_literal = not_id_literal_(r'[a-zA-Z0-9_$]') + preregex1 = r'[(,=:\[!&|?{};\r\n+*-]' + preregex2 = r'%(not_id_literal)sreturn' % locals() + + id_literal = id_literal_(r'[a-zA-Z0-9_$]') + id_literal_open = id_literal_(r'[a-zA-Z0-9_${\[(!+-]') + id_literal_close = id_literal_(r'[a-zA-Z0-9_$}\])"\047\140+-]') + post_regex_off = id_literal_(r'[^\000-\040}\])?:|,;.&=+-]') + + dull = r'[^\047"\140/\000-\040]' + + space_sub_simple = _re.compile(( + # noqa pylint: disable = bad-continuation + + r'(%(dull)s+)' # 0 + r'|(%(strings)s%(dull)s*)' # 1 + r'|(?<=%(preregex1)s)' + r'%(space)s*(?:%(newline)s%(space)s*)*' + r'(%(regex)s)' # 2 + r'(%(space)s*(?:%(newline)s%(space)s*)+' # 3 + r'(?=%(post_regex_off)s))?' + r'|(?<=%(preregex2)s)' + r'%(space)s*(?:(%(newline)s)%(space)s*)*' # 4 + r'(%(regex)s)' # 5 + r'(%(space)s*(?:%(newline)s%(space)s*)+' # 6 + r'(?=%(post_regex_off)s))?' + r'|(?<=%(id_literal_close)s)' + r'%(space)s*(?:(%(newline)s)%(space)s*)+' # 7 + r'(?=%(id_literal_open)s)' + r'|(?<=%(id_literal)s)(%(space)s)+(?=%(id_literal)s)' # 8 + r'|(?<=\+)(%(space)s)+(?=\+)' # 9 + r'|(?<=-)(%(space)s)+(?=-)' # 10 + r'|%(space)s+' + r'|(?:%(newline)s%(space)s*)+' + ) % locals()).sub + + # print(space_sub_simple.__self__.pattern) + + def space_subber_simple(match): + """ Substitution callback """ + # pylint: disable = too-many-return-statements + + groups = match.groups() + if groups[0]: + return groups[0] + elif groups[1]: + return groups[1] + elif groups[2]: + if groups[3]: + return groups[2] + '\n' + return groups[2] + elif groups[5]: + return "%s%s%s" % ( + groups[4] and '\n' or '', + groups[5], + groups[6] and '\n' or '', + ) + elif groups[7]: + return '\n' + elif groups[8] or groups[9] or groups[10]: + return ' ' + else: + return '' + + space_sub_banged = _re.compile(( + # noqa pylint: disable = bad-continuation + + r'(%(dull)s+)' # 0 + r'|(%(strings)s%(dull)s*)' # 1 + r'|(?<=%(preregex1)s)' + r'(%(space)s*(?:%(newline)s%(space)s*)*)' # 2 + r'(%(regex)s)' # 3 + r'(%(space)s*(?:%(newline)s%(space)s*)+' # 4 + r'(?=%(post_regex_off)s))?' + r'|(?<=%(preregex2)s)' + r'(%(space)s*(?:(%(newline)s)%(space)s*)*)' # 5, 6 + r'(%(regex)s)' # 7 + r'(%(space)s*(?:%(newline)s%(space)s*)+' # 8 + r'(?=%(post_regex_off)s))?' + r'|(?<=%(id_literal_close)s)' + r'(%(space)s*(?:%(newline)s%(space)s*)+)' # 9 + r'(?=%(id_literal_open)s)' + r'|(?<=%(id_literal)s)(%(space)s+)(?=%(id_literal)s)' # 10 + r'|(?<=\+)(%(space)s+)(?=\+)' # 11 + r'|(?<=-)(%(space)s+)(?=-)' # 12 + r'|(%(space)s+)' # 13 + r'|((?:%(newline)s%(space)s*)+)' # 14 + ) % locals()).sub + + # print(space_sub_banged.__self__.pattern) + + keep = _re.compile(( + r'%(space_chars)s+|%(space_comment_nobang)s+|%(newline)s+' + r'|(%(bang_comment)s+)' + ) % locals()).sub + keeper = lambda m: m.groups()[0] or '' + + # print(keep.__self__.pattern) + + def space_subber_banged(match): + """ Substitution callback """ + # pylint: disable = too-many-return-statements + + groups = match.groups() + if groups[0]: + return groups[0] + elif groups[1]: + return groups[1] + elif groups[3]: + return "%s%s%s%s" % ( + keep(keeper, groups[2]), + groups[3], + keep(keeper, groups[4] or ''), + groups[4] and '\n' or '', + ) + elif groups[7]: + return "%s%s%s%s%s" % ( + keep(keeper, groups[5]), + groups[6] and '\n' or '', + groups[7], + keep(keeper, groups[8] or ''), + groups[8] and '\n' or '', + ) + elif groups[9]: + return keep(keeper, groups[9]) + '\n' + elif groups[10] or groups[11] or groups[12]: + return keep(keeper, groups[10] or groups[11] or groups[12]) or ' ' + else: + return keep(keeper, groups[13] or groups[14]) + + banged = _ft.partial(space_sub_banged, space_subber_banged) + simple = _ft.partial(space_sub_simple, space_subber_simple) + + def jsmin(script, keep_bang_comments=False): + r""" + Minify javascript based on `jsmin.c by Douglas Crockford`_\. + + Instead of parsing the stream char by char, it uses a regular + expression approach which minifies the whole script with one big + substitution regex. + + .. _jsmin.c by Douglas Crockford: + http://www.crockford.com/javascript/jsmin.c + + :Parameters: + `script` : ``str`` + Script to minify + + `keep_bang_comments` : ``bool`` + Keep comments starting with an exclamation mark? (``/*!...*/``) + + :Return: Minified script + :Rtype: ``str`` + """ + # pylint: disable = redefined-outer-name + + is_bytes, script = _as_str(script) + script = (banged if keep_bang_comments else simple)( + '\n%s\n' % script + ).strip() + if is_bytes: + return script.encode('latin-1') + return script + + return jsmin + +jsmin = _make_jsmin() + + +def _as_str(script): + """ Make sure the script is a text string """ + is_bytes = False + if str is bytes: + if not isinstance(script, basestring): # noqa pylint: disable = undefined-variable + raise TypeError("Unexpected type") + elif isinstance(script, (bytes, bytearray)): + is_bytes = True + script = script.decode('latin-1') + elif not isinstance(script, str): + raise TypeError("Unexpected type") + + return is_bytes, script + + +def jsmin_for_posers(script, keep_bang_comments=False): + r""" + Minify javascript based on `jsmin.c by Douglas Crockford`_\. + + Instead of parsing the stream char by char, it uses a regular + expression approach which minifies the whole script with one big + substitution regex. + + .. _jsmin.c by Douglas Crockford: + http://www.crockford.com/javascript/jsmin.c + + :Warning: This function is the digest of a _make_jsmin() call. It just + utilizes the resulting regexes. It's here for fun and may + vanish any time. Use the `jsmin` function instead. + + :Parameters: + `script` : ``str`` + Script to minify + + `keep_bang_comments` : ``bool`` + Keep comments starting with an exclamation mark? (``/*!...*/``) + + :Return: Minified script + :Rtype: ``str`` + """ + if not keep_bang_comments: + rex = ( + r'([^\047"\140/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^' + r'\r\n]|\r?\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^' + r'\r\n]|\r?\n|\r)[^"\\\r\n]*)*")|(?:\140[^\140\\]*(?:\\(?:[^\r\n' + r']|\r?\n|\r)[^\140\\]*)*\140))[^\047"\140/\000-\040]*)|(?<=[(,=' + r':\[!&|?{};\r\n+*-])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*' + r'\*+(?:[^/*][^*]*\*+)*/))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-' + r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)*(' + r'(?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*' + r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/))((?:[\000-\011' + r'\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:(?:(' + r'?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*' + r']*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040&)+,.:;=?\]|}-]))?|' + r'(?<=[\000-#%-,./:-@\[-^\140{-~-]return)(?:[\000-\011\013\014\0' + r'16-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:((?:(?://[^\r' + r'\n]*)?[\r\n]))(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?' + r':[^/*][^*]*\*+)*/))*)*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^' + r'\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r' + r'\n]*)*/))((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/' + r'*][^*]*\*+)*/))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013' + r'\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000' + r'-\040&)+,.:;=?\]|}-]))?|(?<=[^\000-!#%&(*,./:-@\[\\^{|~])(?:[' + r'\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)' + r')*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\014\016-\040' + r']|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#%-\047' + r')*,./:-@\\-^\140|-~])|(?<=[^\000-#%-,./:-@\[-^\140{-~-])((?:[' + r'\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)' + r'))+(?=[^\000-#%-,./:-@\[-^\140{-~-])|(?<=\+)((?:[\000-\011\013' + r'\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<' + r'=-)((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]' + r'*\*+)*/)))+(?=-)|(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*' + r'+(?:[^/*][^*]*\*+)*/))+|(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-' + r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+' + ) + + def subber(match): + """ Substitution callback """ + groups = match.groups() + return ( + groups[0] or + groups[1] or + (groups[3] and (groups[2] + '\n')) or + groups[2] or + (groups[5] and "%s%s%s" % ( + groups[4] and '\n' or '', + groups[5], + groups[6] and '\n' or '', + )) or + (groups[7] and '\n') or + (groups[8] and ' ') or + (groups[9] and ' ') or + (groups[10] and ' ') or + '' + ) + else: + rex = ( + r'([^\047"\140/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^' + r'\r\n]|\r?\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^' + r'\r\n]|\r?\n|\r)[^"\\\r\n]*)*")|(?:\140[^\140\\]*(?:\\(?:[^\r\n' + r']|\r?\n|\r)[^\140\\]*)*\140))[^\047"\140/\000-\040]*)|(?<=[(,=' + r':\[!&|?{};\r\n+*-])((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]' + r'*\*+(?:[^/*][^*]*\*+)*/))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000' + r'-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)*' + r')((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n' + r']*(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/))((?:[\000-\0' + r'11\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:(?' + r':(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[' + r'^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040&)+,.:;=?\]|}-]))' + r'?|(?<=[\000-#%-,./:-@\[-^\140{-~-]return)((?:[\000-\011\013\01' + r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:((?:(?://[^' + r'\r\n]*)?[\r\n]))(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+' + r'(?:[^/*][^*]*\*+)*/))*)*)((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:' + r'\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/' + r'\\\[\r\n]*)*/))((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+' + r'(?:[^/*][^*]*\*+)*/))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\01' + r'1\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[' + r'^\000-\040&)+,.:;=?\]|}-]))?|(?<=[^\000-!#%&(*,./:-@\[\\^{|~])' + r'((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*' + r'+)*/))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-' + r'\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+)(?=[^\000-\040"#%' + r'-\047)*,./:-@\\-^\140|-~])|(?<=[^\000-#%-,./:-@\[-^\140{-~-])(' + r'(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+' + r')*/))+)(?=[^\000-#%-,./:-@\[-^\140{-~-])|(?<=\+)((?:[\000-\011' + r'\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+)(?=\+)' + r'|(?<=-)((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*]' + r'[^*]*\*+)*/))+)(?=-)|((?:[\000-\011\013\014\016-\040]|(?:/\*[^' + r'*]*\*+(?:[^/*][^*]*\*+)*/))+)|((?:(?:(?://[^\r\n]*)?[\r\n])(?:' + r'[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/' + r'))*)+)' + ) + + keep = _re.compile( + r'[\000-\011\013\014\016-\040]+|(?:/\*(?!!)[^*]*\*+(?:[^/*][^*]*' + r'\*+)*/)+|(?:(?://[^\r\n]*)?[\r\n])+|((?:/\*![^*]*\*+(?:[^/*][^' + r'*]*\*+)*/)+)' + ).sub + keeper = lambda m: m.groups()[0] or '' + + def subber(match): + """ Substitution callback """ + groups = match.groups() + return ( + groups[0] or + groups[1] or + groups[3] and "%s%s%s%s" % ( + keep(keeper, groups[2]), + groups[3], + keep(keeper, groups[4] or ''), + groups[4] and '\n' or '', + ) or + groups[7] and "%s%s%s%s%s" % ( + keep(keeper, groups[5]), + groups[6] and '\n' or '', + groups[7], + keep(keeper, groups[8] or ''), + groups[8] and '\n' or '', + ) or + groups[9] and (keep(keeper, groups[9]) + '\n') or + groups[10] and (keep(keeper, groups[10]) or ' ') or + groups[11] and (keep(keeper, groups[11]) or ' ') or + groups[12] and (keep(keeper, groups[12]) or ' ') or + keep(keeper, groups[13] or groups[14]) + ) + + is_bytes, script = _as_str(script) + script = _re.sub(rex, subber, '\n%s\n' % script).strip() + if is_bytes: + return script.encode('latin-1') + return script + + +if __name__ == '__main__': + def main(): + """ Main """ + import sys as _sys + + argv = _sys.argv[1:] + keep_bang_comments = '-b' in argv or '-bp' in argv or '-pb' in argv + if '-p' in argv or '-bp' in argv or '-pb' in argv: + xjsmin = _make_jsmin(python_only=True) + else: + xjsmin = jsmin + + _sys.stdout.write(xjsmin( + _sys.stdin.read(), keep_bang_comments=keep_bang_comments + )) + + main() diff --git a/Sming/Components/Storage/requirements.txt b/Sming/Components/Storage/requirements.txt index bcc6ba09ac..d89304b1a8 100644 --- a/Sming/Components/Storage/requirements.txt +++ b/Sming/Components/Storage/requirements.txt @@ -1,2 +1 @@ jsonschema -rjsmin diff --git a/Tools/requirements.txt b/Tools/requirements.txt index 941e353f13..ee96c8f8e1 100644 --- a/Tools/requirements.txt +++ b/Tools/requirements.txt @@ -1,3 +1,2 @@ pyserial jsonschema -rjsmin From 25668a2154e3285ecffb4cba7a8fa3301f6ae102 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Tue, 16 Feb 2021 13:02:06 +0000 Subject: [PATCH 20/91] Move `IFS::Directory` and `IFS::FsBase` objects into IFS library Don't belong in Stream. --- Sming/Core/Data/Stream/Directory.h | 2 +- Sming/Core/Data/Stream/IFS/Directory.cpp | 102 ---------------- Sming/Core/Data/Stream/IFS/Directory.h | 111 ------------------ .../Core/Data/Stream/IFS/DirectoryTemplate.h | 2 +- Sming/Core/Data/Stream/IFS/FileStream.h | 2 +- Sming/Core/Data/Stream/IFS/FsBase.h | 72 ------------ 6 files changed, 3 insertions(+), 288 deletions(-) delete mode 100644 Sming/Core/Data/Stream/IFS/Directory.cpp delete mode 100644 Sming/Core/Data/Stream/IFS/Directory.h delete mode 100644 Sming/Core/Data/Stream/IFS/FsBase.h diff --git a/Sming/Core/Data/Stream/Directory.h b/Sming/Core/Data/Stream/Directory.h index e1c203d876..81ee133379 100644 --- a/Sming/Core/Data/Stream/Directory.h +++ b/Sming/Core/Data/Stream/Directory.h @@ -12,7 +12,7 @@ #pragma once -#include "IFS/Directory.h" +#include #include /** diff --git a/Sming/Core/Data/Stream/IFS/Directory.cpp b/Sming/Core/Data/Stream/IFS/Directory.cpp deleted file mode 100644 index 2405e208d7..0000000000 --- a/Sming/Core/Data/Stream/IFS/Directory.cpp +++ /dev/null @@ -1,102 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/anakod/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * Directory.cpp - * - * @author mikee47 May 2019 - * - ****/ - -#include "Directory.h" - -namespace IFS -{ -bool Directory::open(const String& dirName) -{ - auto fs = getFileSystem(); - if(fs == nullptr) { - return false; - } - - DirHandle dir; - int err = fs->opendir(dirName, dir); - if(!check(err)) { - debug_w("Directory '%s' open error: %s", dirName.c_str(), fs->getErrorString(err).c_str()); - return false; - } - - close(); - name = dirName ?: ""; - this->dir = dir; - - return true; -} - -void Directory::close() -{ - if(dir != nullptr) { - auto fs = getFileSystem(); - assert(fs != nullptr); - fs->closedir(dir); - dir = nullptr; - } - lastError = FS_OK; -} - -bool Directory::rewind() -{ - auto fs = getFileSystem(); - if(fs == nullptr) { - return false; - } - - int err = fs->rewinddir(dir); - return err == FS_OK; -} - -String Directory::getPath() const -{ - String path('/'); - path += name; - if(name.length() != 0 && name[name.length() - 1] != '/') { - path += '/'; - } - return path; -} - -String Directory::getParent() const -{ - if(name.length() == 0 || name == "/") { - return nullptr; - } - String path('/'); - int i = name.lastIndexOf('/'); - if(i >= 0) { - path.concat(name.c_str(), i); - } - return path; -} - -bool Directory::next() -{ - auto fs = getFileSystem(); - if(fs == nullptr) { - return false; - } - - int err = fs->readdir(dir, dirStat); - if(check(err)) { - totalSize += dirStat.size; - ++currentIndex; - return true; - } - - debug_w("Directory '%s' read error: %s", name.c_str(), getErrorString(err).c_str()); - - return false; -} - -} // namespace IFS diff --git a/Sming/Core/Data/Stream/IFS/Directory.h b/Sming/Core/Data/Stream/IFS/Directory.h deleted file mode 100644 index 5b1af25adf..0000000000 --- a/Sming/Core/Data/Stream/IFS/Directory.h +++ /dev/null @@ -1,111 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/anakod/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * Directory.h - * - * @author mikee47 May 2019 - * - ****/ - -#pragma once - -#include "FsBase.h" - -namespace IFS -{ -/** - * @brief Wrapper class for enumerating a directory - */ -class Directory : public FsBase -{ -public: - using FsBase::FsBase; - - ~Directory() - { - close(); - } - - /** - * @brief Open a directory and attach this stream object to it - * @param dirName - * @retval bool true on success, false on error - * @note call getLastError() to determine cause of failure - */ - bool open(const String& dirName); - - /** - * @brief Close directory - */ - void close(); - - /** - * @brief Rewind directory stream to start so it can be re-enumerated - * @retval bool true on success, false on error - * @note call getLastError() to determine cause of failure - */ - bool rewind(); - - /** - * @brief Name of directory stream is attached to - * @retval String invalid if stream isn't open - */ - const String& getDirName() const - { - return name; - } - - /** - * @brief Determine if directory exists - * @retval bool true if stream is attached to a directory - */ - bool dirExist() const - { - return dir != nullptr; - } - - /** - * @brief Get path with leading separator /path/to/dir - */ - String getPath() const; - - /** - * @brief Get parent directory - * @retval String invalid if there is no parent directory - */ - String getParent() const; - - int index() const - { - return currentIndex; - } - - bool isValid() const - { - return currentIndex >= 0; - } - - size_t size() const - { - return totalSize; - } - - const FileStat& stat() const - { - return dirStat; - } - - bool next(); - -private: - String name; - DirHandle dir{}; - FileNameStat dirStat; - int currentIndex{-1}; - size_t totalSize{0}; -}; - -} // namespace IFS diff --git a/Sming/Core/Data/Stream/IFS/DirectoryTemplate.h b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.h index 4e88dd5d0c..287c70b422 100644 --- a/Sming/Core/Data/Stream/IFS/DirectoryTemplate.h +++ b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.h @@ -14,7 +14,7 @@ #pragma once #include "../SectionTemplate.h" -#include "Directory.h" +#include #define DIRSTREAM_FIELD_MAP(XX) \ XX(file_id, "File identifier") \ diff --git a/Sming/Core/Data/Stream/IFS/FileStream.h b/Sming/Core/Data/Stream/IFS/FileStream.h index 9fb9491317..3b8863a681 100644 --- a/Sming/Core/Data/Stream/IFS/FileStream.h +++ b/Sming/Core/Data/Stream/IFS/FileStream.h @@ -11,7 +11,7 @@ #pragma once #include "../ReadWriteStream.h" -#include "FsBase.h" +#include namespace IFS { diff --git a/Sming/Core/Data/Stream/IFS/FsBase.h b/Sming/Core/Data/Stream/IFS/FsBase.h deleted file mode 100644 index 98d00f4cf0..0000000000 --- a/Sming/Core/Data/Stream/IFS/FsBase.h +++ /dev/null @@ -1,72 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/SmingHub/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * FsBase.h - common base for file system classes - * - ****/ - -#pragma once - -#include - -namespace IFS -{ -class FsBase -{ -public: - FsBase(IFileSystem* filesys) : fileSystem(filesys) - { - } - - /** @brief determine if an error occurred during operation - * @retval int filesystem error code - */ - int getLastError() - { - return lastError; - } - - String getErrorString(int err) const - { - return fileSystem == nullptr ? Error::toString(err) : fileSystem->getErrorString(err); - } - - String getLastErrorString() const - { - return getErrorString(lastError); - } - - IFileSystem* getFileSystem() const - { - lastError = (fileSystem == nullptr) ? Error::NoFileSystem : FS_OK; - return fileSystem; - } - -protected: - /** @brief Check file operation result and note error code - * @param res result of fileXXX() operation to check - * @retval bool true if operation was successful, false if error occurred - */ - bool check(int res) - { - if(res >= 0) { - return true; - } - - if(lastError >= 0) { - lastError = res; - } - return false; - } - -protected: - mutable int lastError{FS_OK}; - -private: - IFileSystem* fileSystem; -}; - -} // namespace IFS From dd98be29f92ccbe99dedfe82808be64ce69fc46a Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 19 Feb 2021 08:08:44 +0000 Subject: [PATCH 21/91] Add structure for Compression information --- Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp | 4 ++-- Sming/Core/Network/Http/HttpResponse.cpp | 6 +++--- samples/Basic_IFS/app/application.cpp | 6 +++--- samples/Basic_IFS/resource/listing.json | 3 ++- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp index 460aa885a5..63453bd88a 100644 --- a/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp +++ b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp @@ -66,7 +66,7 @@ String DirectoryTemplate::getValue(const char* name) return statValid ? String(s.size) : nullptr; case Field::original_size: - return statValid ? String(s.originalSize) : nullptr; + return statValid ? String(s.compression.originalSize) : nullptr; case Field::attr: return statValid ? IFS::File::getAttributeString(s.attr) : nullptr; @@ -80,7 +80,7 @@ String DirectoryTemplate::getValue(const char* name) } else if(!s.attr[File::Attribute::Compressed]) { return ""; } else { - return toString(s.compression); + return toString(s.compression.type); } case Field::access: diff --git a/Sming/Core/Network/Http/HttpResponse.cpp b/Sming/Core/Network/Http/HttpResponse.cpp index 6990cd6615..78f9173ad7 100644 --- a/Sming/Core/Network/Http/HttpResponse.cpp +++ b/Sming/Core/Network/Http/HttpResponse.cpp @@ -59,9 +59,9 @@ bool HttpResponse::sendString(String&& text) noexcept bool HttpResponse::sendFile(const FileStat& stat) { auto file = new FileStream(stat); - if(stat.compression == File::Compression::GZip) { + if(stat.compression.type == File::Compression::Type::GZip) { headers[HTTP_HEADER_CONTENT_ENCODING] = F("gzip"); - } else if(stat.compression != File::Compression::None) { + } else if(stat.compression.type != File::Compression::Type::None) { debug_e("Unsupported compression type: %u", stat.compression); } @@ -76,7 +76,7 @@ bool HttpResponse::sendFile(const String& fileName, bool allowGzipFileCheck) String fnCompressed = fileName + _F(".gz"); if(fileStats(fnCompressed, stat) >= 0) { debug_d("found %s", stat.name); - stat.compression = File::Compression::GZip; + stat.compression.type = File::Compression::Type::GZip; stat.name = IFS::NameBuffer(fnCompressed); return sendFile(stat); } diff --git a/samples/Basic_IFS/app/application.cpp b/samples/Basic_IFS/app/application.cpp index 07f0c8ae98..57f8a9c0cd 100644 --- a/samples/Basic_IFS/app/application.cpp +++ b/samples/Basic_IFS/app/application.cpp @@ -80,10 +80,10 @@ void onFile(HttpRequest& request, HttpResponse& response) } else { // response.setCache(86400, true); // It's important to use cache for better performance. auto stream = new FileStream(stat); - if(stat.compression == File::Compression::GZip) { + if(stat.compression.type == File::Compression::Type::GZip) { response.headers[HTTP_HEADER_CONTENT_ENCODING] = F("gzip"); - } else if(stat.compression != File::Compression::None) { - debug_e("Unsupported compression type: %u", stat.compression); + } else if(stat.compression.type != File::Compression::Type::None) { + debug_e("Unsupported compression type: %u", stat.compression.type); } auto mimeType = ContentType::fromFullFileName(file.c_str(), MIME_TEXT); diff --git a/samples/Basic_IFS/resource/listing.json b/samples/Basic_IFS/resource/listing.json index 5a20750249..9c15c47968 100644 --- a/samples/Basic_IFS/resource/listing.json +++ b/samples/Basic_IFS/resource/listing.json @@ -12,7 +12,8 @@ "size": {{!as_int:size}}, "original_size": {{!as_int:original_size}}, "attr": "{{attr}}", - "access": "{{access}}" + "access": "{{access}}", + "compression": "{{compression}}" }{/SECTION} {SECTION} From 530e51a137d43b1101a44489ad776cde97c1cc22 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Tue, 16 Feb 2021 08:43:50 +0000 Subject: [PATCH 22/91] Change setattr() to accept filename instead of handle If file is set to read-only, then it can't be changed back! --- Sming/Core/FileSystem.h | 16 ++++++++++++---- samples/Basic_IFS/fsimage.fwfs | 6 +++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Sming/Core/FileSystem.h b/Sming/Core/FileSystem.h index e34e5488a5..c7788e4c78 100644 --- a/Sming/Core/FileSystem.h +++ b/Sming/Core/FileSystem.h @@ -505,15 +505,23 @@ inline int fileSetACL(file_t file, const File::ACL& acl) return fileSystem->setacl(file, acl); } -/** @brief Set file attributes - * @param file handle to open file, must have write access +/** @name Set file attributes + * @param filename + * @param attr * @retval int Error code + * @{ */ -inline int fileSetAttr(file_t file, File::Attributes attr) +inline int fileSetAttr(const char* filename, File::Attributes attr) { CHECK_FS(setattr) - return fileSystem->setattr(file, attr); + return fileSystem->setattr(filename, attr); +} + +inline int fileSetAttr(const String& filename, File::Attributes attr) +{ + return fileSetAttr(filename.c_str(), attr); } +/** @} */ /** @brief Set access control information for file * @param file handle to open file, must have write access diff --git a/samples/Basic_IFS/fsimage.fwfs b/samples/Basic_IFS/fsimage.fwfs index bb1a6be2e2..b8e7cbe19a 100644 --- a/samples/Basic_IFS/fsimage.fwfs +++ b/samples/Basic_IFS/fsimage.fwfs @@ -29,7 +29,11 @@ "read": "admin" }, { - "mask": "*.html", + "mask": [ + "*.html", + "*.css", + "*.png" + ], "readonly": true }, { From 658ee9856fd5b5350d27d356e8345c0bd55bf40b Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sat, 20 Feb 2021 18:09:22 +0000 Subject: [PATCH 23/91] Define `_FILE_OFFSET_BITS=64` in Host builds to ensure file calls function correctly --- Sming/Arch/Host/build.mk | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sming/Arch/Host/build.mk b/Sming/Arch/Host/build.mk index 5700a06743..db1582468f 100644 --- a/Sming/Arch/Host/build.mk +++ b/Sming/Arch/Host/build.mk @@ -21,7 +21,8 @@ GCC_UPGRADE_URL := https://sming.readthedocs.io/en/latest/arch/host/host-emulato CPPFLAGS += \ -m32 \ - -Wno-deprecated-declarations + -Wno-deprecated-declarations \ + -D_FILE_OFFSET_BITS=64 # => Tools MEMANALYZER = size From bd8c48f898360d85724df0e2e4ca866610041a7e Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sat, 20 Feb 2021 11:19:40 +0000 Subject: [PATCH 24/91] CUSTOM_TARGETS handled in project.mk --- Sming/Arch/Esp32/app.mk | 2 +- Sming/Arch/Esp8266/app.mk | 2 +- Sming/Arch/Host/app.mk | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sming/Arch/Esp32/app.mk b/Sming/Arch/Esp32/app.mk index 4c9fca76bd..a4f06d5efb 100644 --- a/Sming/Arch/Esp32/app.mk +++ b/Sming/Arch/Esp32/app.mk @@ -11,7 +11,7 @@ LDFLAGS += \ .PHONY: application -application: $(CUSTOM_TARGETS) $(TARGET_BIN) +application: $(TARGET_BIN) # $1 -> Linker script define LinkTarget diff --git a/Sming/Arch/Esp8266/app.mk b/Sming/Arch/Esp8266/app.mk index 2521c1df13..96ad91ec04 100644 --- a/Sming/Arch/Esp8266/app.mk +++ b/Sming/Arch/Esp8266/app.mk @@ -35,7 +35,7 @@ LDFLAGS += \ .PHONY: application -application: $(CUSTOM_TARGETS) $(FW_FILE_1) $(FW_FILE_2) +application: $(FW_FILE_1) $(FW_FILE_2) # $1 -> Linker script define LinkTarget diff --git a/Sming/Arch/Host/app.mk b/Sming/Arch/Host/app.mk index d5484569cf..f94ae254cf 100644 --- a/Sming/Arch/Host/app.mk +++ b/Sming/Arch/Host/app.mk @@ -15,7 +15,7 @@ TARGET_OUT_0 := $(FW_BASE)/$(APP_NAME)$(TOOL_EXT) # Target definitions .PHONY: application -application: $(CUSTOM_TARGETS) $(TARGET_OUT_0) +application: $(TARGET_OUT_0) $(TARGET_OUT_0): $(COMPONENTS_AR) $(info $(notdir $(PROJECT_DIR)): Linking $@) From bbf246338672d70d0de869c7cf8404ed14a2aab4 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sat, 20 Feb 2021 14:11:52 +0000 Subject: [PATCH 25/91] Define `HOME` in MinGW builds (required for openssl rand) --- Sming/Arch/Host/build.mk | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sming/Arch/Host/build.mk b/Sming/Arch/Host/build.mk index db1582468f..c2fc4c9e8b 100644 --- a/Sming/Arch/Host/build.mk +++ b/Sming/Arch/Host/build.mk @@ -34,6 +34,9 @@ CLI_TARGET_OPTIONS = # $1 -> Command to execute ifeq ($(UNAME),Windows) DetachCommand = start $1 +# May be required by some applications (e.g. openssl) +HOME ?= $(USERPROFILE) +export HOME else DetachCommand = gnome-terminal -- bash -c "sleep 1; $1" endif From 1a4e02e749f274f92e1849b2b82f88c4120fef75 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sun, 21 Feb 2021 14:51:29 +0000 Subject: [PATCH 26/91] Remove `File` namespace and reassign `File` as object for easier file manipulation --- .../Host/Components/spi_flash/flashmem.cpp | 38 +++++----- .../Host/Core/Data/Stream/HostFileStream.cpp | 3 +- .../Host/Core/Data/Stream/HostFileStream.h | 2 +- Sming/Core/Data/Stream/Directory.h | 28 -------- .../Data/Stream/IFS/DirectoryTemplate.cpp | 6 +- Sming/Core/Data/Stream/IFS/FileStream.cpp | 14 ++-- Sming/Core/Data/Stream/IFS/FileStream.h | 8 +-- .../Data/Stream/IFS/HtmlDirectoryTemplate.cpp | 2 +- Sming/Core/FileSystem.h | 70 ++++++++++++------- Sming/Core/Network/Http/HttpResponse.cpp | 6 +- samples/Basic_IFS/app/application.cpp | 7 +- tests/HostTests/modules/Spiffs.cpp | 17 +++-- 12 files changed, 93 insertions(+), 108 deletions(-) delete mode 100644 Sming/Core/Data/Stream/Directory.h diff --git a/Sming/Arch/Host/Components/spi_flash/flashmem.cpp b/Sming/Arch/Host/Components/spi_flash/flashmem.cpp index 0237f9eaf4..6fead07de7 100644 --- a/Sming/Arch/Host/Components/spi_flash/flashmem.cpp +++ b/Sming/Arch/Host/Components/spi_flash/flashmem.cpp @@ -21,12 +21,11 @@ #include "flashmem.h" #include #include -#include +#include namespace { -IFS::IFileSystem& fileSys{IFS::Host::fileSystem}; -IFS::File::Handle flashFile{-1}; +IFS::File flashFile(&IFS::Host::getFileSystem()); size_t flashFileSize{0x400000U}; char flashFileName[256]; const char defaultFlashFileName[]{"flash.bin"}; @@ -59,26 +58,24 @@ bool host_flashmem_init(FlashmemConfig& config) config.filename = flashFileName; } - flashFile = fileSys.open(flashFileName, IFS::File::Create | IFS::File::ReadWrite); - if(flashFile < 0) { + if(!flashFile.open(flashFileName, IFS::File::Create | IFS::File::ReadWrite)) { hostmsg("Error opening \"%s\"", flashFileName); return false; } - int res = fileSys.lseek(flashFile, 0, SeekOrigin::End); + int res = flashFile.seek(0, SeekOrigin::End); if(res < 0) { - hostmsg("Error seeking \"%s\": %s", flashFileName, fileSys.getErrorString(res).c_str()); - fileSys.close(flashFile); - flashFile = -1; + hostmsg("Error seeking \"%s\": %s", flashFileName, flashFile.getErrorString(res).c_str()); + flashFile.close(); return false; } if(res == 0) { size_t size = config.createSize ?: flashFileSize; - res = fileSys.lseek(flashFile, size, SeekOrigin::Start); + res = flashFile.seek(size, SeekOrigin::Start); if(res != int(size)) { hostmsg("Error seeking beyond end of file \"%s\"", flashFileName); - } else if(fileSys.truncate(flashFile, size) < 0) { + } else if(!flashFile.truncate(size)) { hostmsg("Error truncating \"%s\" to %u bytes", flashFileName, size); } else { hostmsg("Created blank \"%s\", %u bytes", flashFileName, size); @@ -95,37 +92,36 @@ bool host_flashmem_init(FlashmemConfig& config) void host_flashmem_cleanup() { - fileSys.close(flashFile); - flashFile = -1; + flashFile.close(); hostmsg("Closed \"%s\"", flashFileName); } static int readFlashFile(uint32_t offset, void* buffer, size_t count) { - if(flashFile < 0) { + if(!flashFile) { return -1; } - int res = fileSys.lseek(flashFile, offset, SeekOrigin::Start); + int res = flashFile.seek(offset, SeekOrigin::Start); if(res >= 0) { - res = fileSys.read(flashFile, buffer, count); + res = flashFile.read(buffer, count); } if(res < 0) { - debug_w("readFlashFile(0x%08x, %u) failed: %s", offset, count, fileSys.getErrorString(res).c_str()); + debug_w("readFlashFile(0x%08x, %u) failed: %s", offset, count, flashFile.getErrorString(res).c_str()); } return res; } static int writeFlashFile(uint32_t offset, const void* data, size_t count) { - if(flashFile < 0) { + if(!flashFile) { return -1; } - int res = fileSys.lseek(flashFile, offset, SeekOrigin::Start); + int res = flashFile.seek(offset, SeekOrigin::Start); if(res >= 0) { - res = fileSys.write(flashFile, data, count); + res = flashFile.write(data, count); } if(res < 0) { - debug_w("writeFlashFile(0x%08x, %u) failed: %s", offset, count, fileSys.getErrorString(res).c_str()); + debug_w("writeFlashFile(0x%08x, %u) failed: %s", offset, count, flashFile.getErrorString(res).c_str()); } return res; } diff --git a/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp b/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp index 95832ad28a..70783b5703 100644 --- a/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp +++ b/Sming/Arch/Host/Core/Data/Stream/HostFileStream.cpp @@ -9,8 +9,7 @@ ****/ #include "HostFileStream.h" -#include -HostFileStream::HostFileStream() : IFS::FileStream(&IFS::Host::fileSystem) +HostFileStream::HostFileStream() : IFS::FileStream(&IFS::Host::getFileSystem()) { } diff --git a/Sming/Arch/Host/Core/Data/Stream/HostFileStream.h b/Sming/Arch/Host/Core/Data/Stream/HostFileStream.h index 0332203d5c..381e955812 100644 --- a/Sming/Arch/Host/Core/Data/Stream/HostFileStream.h +++ b/Sming/Arch/Host/Core/Data/Stream/HostFileStream.h @@ -21,7 +21,7 @@ class HostFileStream : public IFS::FileStream public: HostFileStream(); - HostFileStream(const String& fileName, IFS::File::OpenFlags openFlags = IFS::File::ReadOnly) : HostFileStream() + HostFileStream(const String& fileName, IFS::OpenFlags openFlags = IFS::OpenFlag::Read) : HostFileStream() { open(fileName, openFlags); } diff --git a/Sming/Core/Data/Stream/Directory.h b/Sming/Core/Data/Stream/Directory.h deleted file mode 100644 index 81ee133379..0000000000 --- a/Sming/Core/Data/Stream/Directory.h +++ /dev/null @@ -1,28 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/anakod/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * Directory.h - * - * @author mikee47 May 2019 - * - ****/ - -#pragma once - -#include -#include - -/** - * @brief Directory stream class - * @ingroup stream data - */ -class Directory : public IFS::Directory -{ -public: - Directory() : IFS::Directory(::getFileSystem()) - { - } -}; diff --git a/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp index 63453bd88a..ca667ef846 100644 --- a/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp +++ b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp @@ -69,7 +69,7 @@ String DirectoryTemplate::getValue(const char* name) return statValid ? String(s.compression.originalSize) : nullptr; case Field::attr: - return statValid ? IFS::File::getAttributeString(s.attr) : nullptr; + return statValid ? IFS::getFileAttributeString(s.attr) : nullptr; case Field::attr_long: return statValid ? toString(s.attr) : nullptr; @@ -77,14 +77,14 @@ String DirectoryTemplate::getValue(const char* name) case Field::compression: if(!statValid) { return nullptr; - } else if(!s.attr[File::Attribute::Compressed]) { + } else if(!s.attr[FileAttribute::Compressed]) { return ""; } else { return toString(s.compression.type); } case Field::access: - return statValid ? IFS::File::getAclString(s.acl) : nullptr; + return statValid ? IFS::getAclString(s.acl) : nullptr; case Field::access_long: return statValid ? toString(s.acl) : nullptr; diff --git a/Sming/Core/Data/Stream/IFS/FileStream.cpp b/Sming/Core/Data/Stream/IFS/FileStream.cpp index 66caa0c383..4f83212156 100644 --- a/Sming/Core/Data/Stream/IFS/FileStream.cpp +++ b/Sming/Core/Data/Stream/IFS/FileStream.cpp @@ -14,7 +14,7 @@ namespace IFS { -void FileStream::attach(File::Handle file, size_t size) +void FileStream::attach(FileHandle file, size_t size) { close(); if(file < 0) { @@ -33,7 +33,7 @@ void FileStream::attach(File::Handle file, size_t size) debug_d("attached file: '%s' (%u bytes) #0x%08X", fileName().c_str(), size, this); } -bool FileStream::open(const FileStat& stat, File::OpenFlags openFlags) +bool FileStream::open(const Stat& stat, OpenFlags openFlags) { auto fs = getFileSystem(); if(fs == nullptr) { @@ -42,7 +42,7 @@ bool FileStream::open(const FileStat& stat, File::OpenFlags openFlags) lastError = FS_OK; - File::Handle file = fs->fopen(stat, openFlags); + FileHandle file = fs->fopen(stat, openFlags); if(!check(file)) { return false; } @@ -51,7 +51,7 @@ bool FileStream::open(const FileStat& stat, File::OpenFlags openFlags) return true; } -bool FileStream::open(const String& fileName, File::OpenFlags openFlags) +bool FileStream::open(const String& fileName, OpenFlags openFlags) { auto fs = getFileSystem(); if(fs == nullptr) { @@ -60,7 +60,7 @@ bool FileStream::open(const String& fileName, File::OpenFlags openFlags) lastError = FS_OK; - File::Handle file = fs->open(fileName, openFlags); + FileHandle file = fs->open(fileName, openFlags); if(!check(file)) { debug_w("File '%s' open error: %s", fileName.c_str(), fs->getErrorString(file).c_str()); return false; @@ -179,7 +179,7 @@ String FileStream::fileName() const return nullptr; } - FileNameStat stat; + NameStat stat; int res = fs->fstat(handle, stat); return (res < 0 || stat.name.length == 0) ? nullptr : stat.name.buffer; } @@ -191,7 +191,7 @@ String FileStream::id() const return 0; } - FileStat stat; + Stat stat; int res = fs->fstat(handle, stat); if(res < 0) { return nullptr; diff --git a/Sming/Core/Data/Stream/IFS/FileStream.h b/Sming/Core/Data/Stream/IFS/FileStream.h index 3b8863a681..bac5188ce4 100644 --- a/Sming/Core/Data/Stream/IFS/FileStream.h +++ b/Sming/Core/Data/Stream/IFS/FileStream.h @@ -33,9 +33,9 @@ class FileStream : public FsBase, public ReadWriteStream * @param file * @param size */ - void attach(File::Handle file, size_t size); + void attach(FileHandle file, size_t size); - bool open(const FileStat& stat, File::OpenFlags openFlags = File::ReadOnly); + bool open(const Stat& stat, OpenFlags openFlags = OpenFlag::Read); /** @brief Open a file and attach this stream object to it * @param fileName @@ -43,7 +43,7 @@ class FileStream : public FsBase, public ReadWriteStream * @retval bool true on success, false on error * @note call getLastError() to determine cause of failure */ - bool open(const String& fileName, File::OpenFlags openFlags = File::ReadOnly); + bool open(const String& fileName, IFS::OpenFlags openFlags = OpenFlag::Read); /** @brief Close file */ @@ -138,7 +138,7 @@ class FileStream : public FsBase, public ReadWriteStream } private: - File::Handle handle{-1}; + FileHandle handle{-1}; size_t pos{0}; size_t size{0}; }; diff --git a/Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.cpp b/Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.cpp index 05cdeb47c7..579e709f9e 100644 --- a/Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.cpp +++ b/Sming/Core/Data/Stream/IFS/HtmlDirectoryTemplate.cpp @@ -27,7 +27,7 @@ String HtmlDirectoryTemplate::getValue(const char* name) auto& stat = dir().stat(); if(FS("icon") == name) { - if(stat.attr[File::Attribute::Directory]) { + if(stat.attr[FileAttribute::Directory]) { return F("📁"); } diff --git a/Sming/Core/FileSystem.h b/Sming/Core/FileSystem.h index c7788e4c78..3255f8e959 100644 --- a/Sming/Core/FileSystem.h +++ b/Sming/Core/FileSystem.h @@ -16,32 +16,23 @@ #pragma once #include +#include +#include #include #include "WVector.h" ///< @deprecated see fileList() -namespace File -{ -using namespace IFS::File; -} - -using file_t = IFS::File::Handle; +using file_t = IFS::FileHandle; typedef SeekOrigin SeekOriginFlags; ///< @deprecated Use `SeekOrigin` instead +using FileHandle = IFS::FileHandle; using DirHandle = IFS::DirHandle; -using FileOpenFlag = File::OpenFlag; -using FileOpenFlags = File::OpenFlags; -using FileStat = File::Stat; -using FileNameStat = File::NameStat; +using FileOpenFlag = IFS::OpenFlag; +using FileOpenFlags = IFS::OpenFlags; +using FileAttribute = IFS::FileAttribute; +using FileAttributes = IFS::FileAttributes; +using FileStat = IFS::Stat; +using FileNameStat = IFS::NameStat; constexpr int FS_OK = IFS::FS_OK; -// Various file flag combinations -constexpr FileOpenFlags eFO_ReadOnly SMING_DEPRECATED{File::ReadOnly}; ///< @deprecated use File::ReadOnly -constexpr FileOpenFlags eFO_WriteOnly SMING_DEPRECATED{File::WriteOnly}; ///< @deprecated use File::WriteOnly -constexpr FileOpenFlags eFO_ReadWrite{File::ReadWrite}; ///< @deprecated use File::ReadWrite -constexpr FileOpenFlags eFO_CreateIfNotExist{File::Create}; ///< @deprecated use File::Create -constexpr FileOpenFlags eFO_Append{File::Append}; ///< @deprecated use File::Append -constexpr FileOpenFlags eFO_Truncate{File::Truncate}; ///< @deprecated use File::Truncate -constexpr FileOpenFlags eFO_CreateNewAlways{File::CreateNewAlways}; ///< @deprecated use File::CreateNewAlways - constexpr SeekOrigin eSO_FileStart{SeekOrigin::Start}; ///< @deprecated use SeekOrigin::Start constexpr SeekOrigin eSO_CurrentPos{SeekOrigin::Current}; ///< @deprecated use SeekOrigin::Current constexpr SeekOrigin eSO_FileEnd{SeekOrigin::End}; ///< @deprecated use SeekOrigin::End @@ -60,11 +51,40 @@ extern IFS::IFileSystem* activeFileSystem; } // namespace SmingInternal +class File : public IFS::File +{ +public: + File() : IFS::File(SmingInternal::activeFileSystem) + { + } +}; + +/** + * @brief Directory stream class + * @ingroup stream data + */ +class Directory : public IFS::Directory +{ +public: + Directory() : IFS::Directory(SmingInternal::activeFileSystem) + { + } +}; + +// Various file flag combinations +constexpr FileOpenFlags eFO_ReadOnly{File::ReadOnly}; ///< @deprecated use File::ReadOnly +constexpr FileOpenFlags eFO_WriteOnly{File::WriteOnly}; ///< @deprecated use File::WriteOnly +constexpr FileOpenFlags eFO_ReadWrite{File::ReadWrite}; ///< @deprecated use File::ReadWrite +constexpr FileOpenFlags eFO_CreateIfNotExist{File::Create}; ///< @deprecated use File::Create +constexpr FileOpenFlags eFO_Append{File::Append}; ///< @deprecated use File::Append +constexpr FileOpenFlags eFO_Truncate{File::Truncate}; ///< @deprecated use File::Truncate +constexpr FileOpenFlags eFO_CreateNewAlways{File::CreateNewAlways}; ///< @deprecated use File::CreateNewAlways + /* * Boilerplate check for file function wrappers to catch undefined filesystem. */ #define CHECK_FS(_method) \ - auto fileSystem = SmingInternal::activeFileSystem; \ + auto fileSystem = static_cast(SmingInternal::activeFileSystem); \ if(fileSystem == nullptr) { \ debug_e("ERROR in %s(): No active file system", __FUNCTION__); \ return file_t(IFS::Error::NoFileSystem); \ @@ -74,12 +94,12 @@ extern IFS::IFileSystem* activeFileSystem; * @brief Get the currently active file system, if any * @retval IFS::IFileSystem* */ -inline IFS::IFileSystem* getFileSystem() +inline IFS::FileSystem* getFileSystem() { if(SmingInternal::activeFileSystem == nullptr) { debug_e("ERROR: No active file system"); } - return SmingInternal::activeFileSystem; + return static_cast(SmingInternal::activeFileSystem); } /** @brief Sets the currently active file system @@ -499,7 +519,7 @@ inline int fileSystemCheck() * @param acl * @retval int Error code */ -inline int fileSetACL(file_t file, const File::ACL& acl) +inline int fileSetACL(file_t file, const IFS::ACL& acl) { CHECK_FS(setacl) return fileSystem->setacl(file, acl); @@ -511,13 +531,13 @@ inline int fileSetACL(file_t file, const File::ACL& acl) * @retval int Error code * @{ */ -inline int fileSetAttr(const char* filename, File::Attributes attr) +inline int fileSetAttr(const char* filename, FileAttributes attr) { CHECK_FS(setattr) return fileSystem->setattr(filename, attr); } -inline int fileSetAttr(const String& filename, File::Attributes attr) +inline int fileSetAttr(const String& filename, FileAttributes attr) { return fileSetAttr(filename.c_str(), attr); } diff --git a/Sming/Core/Network/Http/HttpResponse.cpp b/Sming/Core/Network/Http/HttpResponse.cpp index 78f9173ad7..0f8e9e52b5 100644 --- a/Sming/Core/Network/Http/HttpResponse.cpp +++ b/Sming/Core/Network/Http/HttpResponse.cpp @@ -59,9 +59,9 @@ bool HttpResponse::sendString(String&& text) noexcept bool HttpResponse::sendFile(const FileStat& stat) { auto file = new FileStream(stat); - if(stat.compression.type == File::Compression::Type::GZip) { + if(stat.compression.type == IFS::Compression::Type::GZip) { headers[HTTP_HEADER_CONTENT_ENCODING] = F("gzip"); - } else if(stat.compression.type != File::Compression::Type::None) { + } else if(stat.compression.type != IFS::Compression::Type::None) { debug_e("Unsupported compression type: %u", stat.compression); } @@ -76,7 +76,7 @@ bool HttpResponse::sendFile(const String& fileName, bool allowGzipFileCheck) String fnCompressed = fileName + _F(".gz"); if(fileStats(fnCompressed, stat) >= 0) { debug_d("found %s", stat.name); - stat.compression.type = File::Compression::Type::GZip; + stat.compression.type = IFS::Compression::Type::GZip; stat.name = IFS::NameBuffer(fnCompressed); return sendFile(stat); } diff --git a/samples/Basic_IFS/app/application.cpp b/samples/Basic_IFS/app/application.cpp index 57f8a9c0cd..3af196cb5b 100644 --- a/samples/Basic_IFS/app/application.cpp +++ b/samples/Basic_IFS/app/application.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -59,7 +58,7 @@ void onFile(HttpRequest& request, HttpResponse& response) return; } - if(stat.attr[File::Attribute::Directory]) { + if(stat.attr[FileAttribute::Directory]) { auto dir = new Directory; IFS::DirectoryTemplate* tmpl; String fmt = request.uri.Query["format"]; @@ -80,9 +79,9 @@ void onFile(HttpRequest& request, HttpResponse& response) } else { // response.setCache(86400, true); // It's important to use cache for better performance. auto stream = new FileStream(stat); - if(stat.compression.type == File::Compression::Type::GZip) { + if(stat.compression.type == IFS::Compression::Type::GZip) { response.headers[HTTP_HEADER_CONTENT_ENCODING] = F("gzip"); - } else if(stat.compression.type != File::Compression::Type::None) { + } else if(stat.compression.type != IFS::Compression::Type::None) { debug_e("Unsupported compression type: %u", stat.compression.type); } diff --git a/tests/HostTests/modules/Spiffs.cpp b/tests/HostTests/modules/Spiffs.cpp index 058d0a5a4d..75fd9e6eb1 100644 --- a/tests/HostTests/modules/Spiffs.cpp +++ b/tests/HostTests/modules/Spiffs.cpp @@ -3,9 +3,8 @@ #ifdef ARCH_HOST #include -#include #include -using IFileSystem = IFS::IFileSystem; +using FileSystem = IFS::FileSystem; #endif class SpiffsTest : public TestGroup @@ -84,13 +83,13 @@ class SpiffsTest : public TestGroup */ void checkSpiffsGen() { - IFS::IFileSystem::Info info; + FileSystem::Info info; int err = fileGetSystemInfo(info); CHECK(err >= 0); debug_i("fs attr = %s", toString(info.attr).c_str()); - IFileSystem* fsOld; - if(info.attr[IFileSystem::Attribute::NoMeta]) { + FileSystem* fsOld; + if(info.attr[FileSystem::Attribute::NoMeta]) { fsOld = mountSpiffsFromFile("old", "spiffsgen/spiff_rom_orig.bin"); } else { fsOld = mountSpiffsFromFile("old", "spiffsgen/spiff_rom_meta.bin"); @@ -106,9 +105,9 @@ class SpiffsTest : public TestGroup delete Storage::findDevice("old"); } - IFileSystem* mountSpiffsFromFile(const String& tag, const String& filename) + FileSystem* mountSpiffsFromFile(const String& tag, const String& filename) { - auto& hfs = IFS::Host::fileSystem; + auto& hfs = IFS::Host::getFileSystem(); auto f = hfs.open(filename, IFS::File::ReadOnly); if(f < 0) { debug_e("Failed to open '%s': %s", filename.c_str(), hfs.getErrorString(f).c_str()); @@ -129,10 +128,10 @@ class SpiffsTest : public TestGroup } debug_i("Mounted '%s' as '%s'", filename.c_str(), tag.c_str()); - return fs; + return FileSystem::cast(fs); } - void readCheck(IFileSystem* fsOld, IFileSystem* fsNew) + void readCheck(IFS::FileSystem* fsOld, IFS::FileSystem* fsNew) { DirHandle dir{}; int res = fsOld->opendir(nullptr, dir); From 1a32a4b7c74ced8d8472e979df65221a4afb15b6 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sun, 21 Feb 2021 17:22:49 +0000 Subject: [PATCH 27/91] Remove x permission --- .ci/build.cmd | 0 .ci/install.cmd | 0 Sming/Core/DateTime.cpp | 0 Sming/Core/DateTime.h | 0 Sming/Core/SmingLocale.h | 0 5 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 .ci/build.cmd mode change 100755 => 100644 .ci/install.cmd mode change 100755 => 100644 Sming/Core/DateTime.cpp mode change 100755 => 100644 Sming/Core/DateTime.h mode change 100755 => 100644 Sming/Core/SmingLocale.h diff --git a/.ci/build.cmd b/.ci/build.cmd old mode 100755 new mode 100644 diff --git a/.ci/install.cmd b/.ci/install.cmd old mode 100755 new mode 100644 diff --git a/Sming/Core/DateTime.cpp b/Sming/Core/DateTime.cpp old mode 100755 new mode 100644 diff --git a/Sming/Core/DateTime.h b/Sming/Core/DateTime.h old mode 100755 new mode 100644 diff --git a/Sming/Core/SmingLocale.h b/Sming/Core/SmingLocale.h old mode 100755 new mode 100644 From a68081cfe2f7128f72072f6296656b2caaab4d11 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Mon, 22 Feb 2021 09:18:35 +0000 Subject: [PATCH 28/91] Rename `truncate` -> `ftruncate` for consistency --- Sming/Core/Data/Stream/IFS/FileStream.cpp | 2 +- Sming/Core/FileSystem.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sming/Core/Data/Stream/IFS/FileStream.cpp b/Sming/Core/Data/Stream/IFS/FileStream.cpp index 4f83212156..d4f9e5be6d 100644 --- a/Sming/Core/Data/Stream/IFS/FileStream.cpp +++ b/Sming/Core/Data/Stream/IFS/FileStream.cpp @@ -210,7 +210,7 @@ bool FileStream::truncate(size_t newSize) return 0; } - bool res = check(fs->truncate(handle, newSize)); + bool res = check(fs->ftruncate(handle, newSize)); if(res) { size = newSize; if(pos > size) { diff --git a/Sming/Core/FileSystem.h b/Sming/Core/FileSystem.h index 3255f8e959..3c1feb5e33 100644 --- a/Sming/Core/FileSystem.h +++ b/Sming/Core/FileSystem.h @@ -277,7 +277,7 @@ template inline uint32_t fileGetSize(const TFileName& fileN inline int fileTruncate(file_t file, size_t newSize) { CHECK_FS(truncate); - return fileSystem->truncate(file, newSize); + return fileSystem->ftruncate(file, newSize); } /** @brief Truncate an open file at the current cursor position @@ -287,7 +287,7 @@ inline int fileTruncate(file_t file, size_t newSize) inline int fileTruncate(file_t file) { CHECK_FS(truncate); - return fileSystem->truncate(file); + return fileSystem->ftruncate(file); } /** @brief Truncate (reduce) the size of a file From 4a47a8cbb279451c0b9856064f52db1ea422226a Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sat, 20 Feb 2021 18:09:49 +0000 Subject: [PATCH 29/91] Update IFS library --- Sming/Components/IFS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Components/IFS b/Sming/Components/IFS index 1e50d014fa..4d6fcdb7d1 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit 1e50d014fac7604026d0d4112f1855431a44496a +Subproject commit 4d6fcdb7d1e808b66acc8f03a99b77b79abac67d From 723c5cb4bd56392160972d1d0213929564ffb7bb Mon Sep 17 00:00:00 2001 From: mikee47 Date: Mon, 22 Feb 2021 22:31:54 +0000 Subject: [PATCH 30/91] Update spiffsgen.py to reflect revised metadata layout --- Sming/Components/spiffs/spiffsgen.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sming/Components/spiffs/spiffsgen.py b/Sming/Components/spiffs/spiffsgen.py index b161c1c957..0ca153dcca 100644 --- a/Sming/Components/spiffs/spiffsgen.py +++ b/Sming/Components/spiffs/spiffsgen.py @@ -228,9 +228,10 @@ def to_binary(self): magic = 0xE3457A77 mtime = self.mtime attr = 0 - flags = 0xff userRole_admin = 0x04 - meta = struct.pack(b' Date: Tue, 23 Feb 2021 10:32:58 +0100 Subject: [PATCH 31/91] Revert removed spiffs-image-clean (#2225) It was removed in https://github.com/SmingHub/Sming/commit/fdb772a76316c2697d3e0aa795bd67efd206ecd1?branch=fdb772a76316c2697d3e0aa795bd67efd206ecd1&diff=unified#diff-47632947d7b1a3ebdc4dc2b67f68638bf31eda6ef22844a7edda489de702acbdL62 but is required by `spiffs-image-update` --- Sming/Components/spiffs/component.mk | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sming/Components/spiffs/component.mk b/Sming/Components/spiffs/component.mk index bc7787fffc..e82a68d993 100644 --- a/Sming/Components/spiffs/component.mk +++ b/Sming/Components/spiffs/component.mk @@ -25,6 +25,13 @@ COMPONENT_RELINK_VARS += SPIFFS_OBJ_META_LEN SPIFFS_OBJ_META_LEN ?= 16 COMPONENT_CFLAGS += -DSPIFFS_OBJ_META_LEN=$(SPIFFS_OBJ_META_LEN) +##@Cleaning + +.PHONY: spiffs-image-clean +spiffs-image-clean: ##Remove SPIFFS image file + $(info Cleaning $(SPIFF_BIN_OUT)) + $(Q) rm -f $(SPIFF_BIN_OUT) + ##@Building # Spiffs image generation tool From ca3b7a1e425ff36f162ef5a49354c6b6470b8de9 Mon Sep 17 00:00:00 2001 From: slaff Date: Wed, 24 Feb 2021 08:50:55 +0100 Subject: [PATCH 32/91] Fix handling of `.gz` files in HttpResponse (#2226) * Most browsers expect correct mime type of data otherwise they refuse to load CSS, JavaScript and so on. * Override `FileStream::getMimeType()` to account for compressed files Fixes incorrect behaviour of `Basic_WebSkeletonApp` sample. Co-authored-by: mikee47 --- Sming/Core/Data/Stream/IFS/FileStream.cpp | 9 +++++++++ Sming/Core/Data/Stream/IFS/FileStream.h | 2 ++ Sming/Core/Network/Http/HttpResponse.cpp | 4 ++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Sming/Core/Data/Stream/IFS/FileStream.cpp b/Sming/Core/Data/Stream/IFS/FileStream.cpp index d4f9e5be6d..ef4ebd06a4 100644 --- a/Sming/Core/Data/Stream/IFS/FileStream.cpp +++ b/Sming/Core/Data/Stream/IFS/FileStream.cpp @@ -184,6 +184,15 @@ String FileStream::fileName() const return (res < 0 || stat.name.length == 0) ? nullptr : stat.name.buffer; } +MimeType FileStream::getMimeType() const +{ + String name = fileName(); + if(name.endsWith(".gz")) { + name.remove(name.length() - 3); + } + return ContentType::fromFullFileName(name, MIME_UNKNOWN); +} + String FileStream::id() const { auto fs = getFileSystem(); diff --git a/Sming/Core/Data/Stream/IFS/FileStream.h b/Sming/Core/Data/Stream/IFS/FileStream.h index bac5188ce4..f8d0430547 100644 --- a/Sming/Core/Data/Stream/IFS/FileStream.h +++ b/Sming/Core/Data/Stream/IFS/FileStream.h @@ -92,6 +92,8 @@ class FileStream : public FsBase, public ReadWriteStream return fileName(); } + MimeType getMimeType() const override; + bool isValid() const override { return fileExist(); diff --git a/Sming/Core/Network/Http/HttpResponse.cpp b/Sming/Core/Network/Http/HttpResponse.cpp index 0f8e9e52b5..b0877e1902 100644 --- a/Sming/Core/Network/Http/HttpResponse.cpp +++ b/Sming/Core/Network/Http/HttpResponse.cpp @@ -77,14 +77,14 @@ bool HttpResponse::sendFile(const String& fileName, bool allowGzipFileCheck) if(fileStats(fnCompressed, stat) >= 0) { debug_d("found %s", stat.name); stat.compression.type = IFS::Compression::Type::GZip; - stat.name = IFS::NameBuffer(fnCompressed); + stat.name = IFS::NameBuffer(const_cast(fileName)); return sendFile(stat); } } if(fileStats(fileName, stat) >= 0) { debug_d("found %s", fileName.c_str()); - stat.name = IFS::NameBuffer((char*)fileName.c_str(), fileName.length(), fileName.length()); + stat.name = IFS::NameBuffer(const_cast(fileName)); return sendFile(stat); } From 7d66eb9e1f67320eeda473aa83ef38adc094005f Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 24 Feb 2021 20:14:01 +0000 Subject: [PATCH 33/91] Add web test support to HostTests CI application (#2228) * Enable loopback on host network interfaces and add CI web test module * Enable support for Windows loopback, but don't enable in CI Requires manual network adapter setup, no need to repeat tests already handling in Linux * Serialise file requests and add more checks * Prevent Windows host run from stalling --- .../Host/Components/lwip/Linux/host_lwip.c | 1 + .../Host/Components/lwip/Windows/host_lwip.c | 1 + Sming/Arch/Host/Components/lwip/lwipopts.h | 3 + Sming/Arch/Host/Tools/ci/build.setup.sh | 5 + tests/HostTests/app/application.cpp | 39 ++++-- tests/HostTests/component.mk | 4 +- tests/HostTests/fwfs0.json | 9 ++ tests/HostTests/host-tests.hw | 11 ++ tests/HostTests/include/modules.h | 3 +- tests/HostTests/modules/HttpRequest.cpp | 116 ++++++++++++++++++ 10 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 tests/HostTests/fwfs0.json create mode 100644 tests/HostTests/modules/HttpRequest.cpp diff --git a/Sming/Arch/Host/Components/lwip/Linux/host_lwip.c b/Sming/Arch/Host/Components/lwip/Linux/host_lwip.c index 5de4cf3891..d5c38414c4 100644 --- a/Sming/Arch/Host/Components/lwip/Linux/host_lwip.c +++ b/Sming/Arch/Host/Components/lwip/Linux/host_lwip.c @@ -170,6 +170,7 @@ void host_lwip_service(void) { /* poll netif, pass packet to lwIP */ tapif_select(&netif); + netif_poll(&netif); sys_check_timeouts(); } diff --git a/Sming/Arch/Host/Components/lwip/Windows/host_lwip.c b/Sming/Arch/Host/Components/lwip/Windows/host_lwip.c index 404e049739..521be8507b 100644 --- a/Sming/Arch/Host/Components/lwip/Windows/host_lwip.c +++ b/Sming/Arch/Host/Components/lwip/Windows/host_lwip.c @@ -216,6 +216,7 @@ void host_lwip_service(void) { /* check for packets and link status*/ pcapif_poll(&netif); + netif_poll(&netif); sys_check_timeouts(); } diff --git a/Sming/Arch/Host/Components/lwip/lwipopts.h b/Sming/Arch/Host/Components/lwip/lwipopts.h index 6a66da4e4e..b94fa08ccc 100644 --- a/Sming/Arch/Host/Components/lwip/lwipopts.h +++ b/Sming/Arch/Host/Components/lwip/lwipopts.h @@ -382,6 +382,9 @@ */ #define LWIP_HAVE_LOOPIF 0 +// Enable loopback on interfaces +#define LWIP_NETIF_LOOPBACK 1 + /* ---------------------------------------------- ---------- Sequential layer options ---------- diff --git a/Sming/Arch/Host/Tools/ci/build.setup.sh b/Sming/Arch/Host/Tools/ci/build.setup.sh index 692cdae27a..2dc3d0c044 100755 --- a/Sming/Arch/Host/Tools/ci/build.setup.sh +++ b/Sming/Arch/Host/Tools/ci/build.setup.sh @@ -19,5 +19,10 @@ if [ -n "$SMING_SECRET" ]; then fi set -x +# Setup networking +sudo ip tuntap add dev tap0 mode tap user $(whoami) +sudo ip a a dev tap0 192.168.13.1/24 +sudo ip link set tap0 up + # Build documentation make -C $SMING_HOME docs diff --git a/tests/HostTests/app/application.cpp b/tests/HostTests/app/application.cpp index 522e6e2332..e1668b2b3c 100644 --- a/tests/HostTests/app/application.cpp +++ b/tests/HostTests/app/application.cpp @@ -8,11 +8,18 @@ #include #include +#ifndef WIFI_SSID +#define WIFI_SSID "PleaseEnterSSID" +#define WIFI_PWD "PleaseEnterPass" +#endif + #define XX(t) extern void REGISTER_TEST(t); TEST_MAP(XX) #undef XX -static void registerTests() +namespace +{ +void registerTests() { #define XX(t) \ REGISTER_TEST(t); \ @@ -21,7 +28,7 @@ static void registerTests() #undef XX } -static void testsComplete() +void testsComplete() { #if RESTART_DELAY == 0 System.restart(); @@ -30,6 +37,14 @@ static void testsComplete() #endif } +void beginTests() +{ + SmingTest::runner.setGroupIntervalMs(TEST_GROUP_INTERVAL); + System.onReady([]() { SmingTest::runner.execute(testsComplete); }); +} + +} // namespace + void init() { Serial.setTxBufferSize(1024); @@ -40,13 +55,19 @@ void init() spiffs_mount(); -#ifndef DISABLE_WIFI - WifiStation.enable(false, false); - WifiAccessPoint.enable(false, false); -#endif - registerTests(); - SmingTest::runner.setGroupIntervalMs(TEST_GROUP_INTERVAL); - System.onReady([]() { SmingTest::runner.execute(testsComplete); }); +#ifdef DISABLE_WIFI + beginTests(); +#else + WifiAccessPoint.enable(false); + if(netif_default == nullptr) { + WifiStation.enable(false); + beginTests(); + } else { + WifiStation.enable(true); + WifiStation.config(WIFI_SSID, WIFI_PWD); + WifiEvents.onStationGotIP([](IpAddress ip, IpAddress netmask, IpAddress gateway) { beginTests(); }); + } +#endif } diff --git a/tests/HostTests/component.mk b/tests/HostTests/component.mk index 38f213d5e5..91d000f57d 100644 --- a/tests/HostTests/component.mk +++ b/tests/HostTests/component.mk @@ -14,8 +14,10 @@ COMPONENT_DEPENDS := \ axtls-8266 \ bearssl-esp8266 -# Don't need network +ifeq ($(UNAME),Windows) +# Network tests run on Linux only HOST_NETWORK_OPTIONS := --nonet +endif # Time in milliseconds to pause after a test group has completed CONFIG_VARS += TEST_GROUP_INTERVAL diff --git a/tests/HostTests/fwfs0.json b/tests/HostTests/fwfs0.json new file mode 100644 index 0000000000..7ba67a8fab --- /dev/null +++ b/tests/HostTests/fwfs0.json @@ -0,0 +1,9 @@ +{ + "name": "Test files for web requests", + "id": "0x12345678", + "source": { + "/": "../../samples/Basic_WebSkeletonApp/files" + }, + "mountpoints": {}, + "rules": [] +} \ No newline at end of file diff --git a/tests/HostTests/host-tests.hw b/tests/HostTests/host-tests.hw index 954c4865ff..33131de523 100644 --- a/tests/HostTests/host-tests.hw +++ b/tests/HostTests/host-tests.hw @@ -12,6 +12,17 @@ "size": "0x10000", "filename": "" }, + "fwfs0": { + "address": "0x220000", + "size": "0x40000", + "type": "data", + "subtype": "fwfs", + "filename": "$(FW_BASE)/fwfs0.bin", + "build": { + "target": "fwfs-build", + "config": "fwfs0.json" + } + }, "external1": { "device": "testDevice", "address": 0, diff --git a/tests/HostTests/include/modules.h b/tests/HostTests/include/modules.h index 97583b28b3..93e0b83d4b 100644 --- a/tests/HostTests/include/modules.h +++ b/tests/HostTests/include/modules.h @@ -25,4 +25,5 @@ XX(Spiffs) \ XX(Rational) \ XX(Clocks) \ - XX(Timers) + XX(Timers) \ + XX(HttpRequest) diff --git a/tests/HostTests/modules/HttpRequest.cpp b/tests/HostTests/modules/HttpRequest.cpp new file mode 100644 index 0000000000..bf061a3b36 --- /dev/null +++ b/tests/HostTests/modules/HttpRequest.cpp @@ -0,0 +1,116 @@ +#include + +#include "Network/HttpServer.h" +#include "Network/HttpClient.h" +#include +#include + +namespace +{ +struct TestFile { + const char* name; // Name of file requested via HTTP + const char* realName; // Actual filename on disk (optional, defaults to name) + MimeType mimeType; + const char* contentEncoding; + + size_t getSize() const + { + return fileGetSize(realName ?: name); + } +}; + +TestFile testFiles[]{ + {"bootstrap.min.css", "bootstrap.min.css.gz", MimeType::CSS, "gzip"}, + {"bootstrap.min.css.gz", nullptr, MimeType::UNKNOWN, ""}, + {"index.html", nullptr, MimeType::HTML, ""}, + {"index.js", nullptr, MimeType::JS, ""}, +}; + +} // namespace + +class HttpRequestTest : public TestGroup +{ +public: + HttpRequestTest() : TestGroup(_F("HTTP")) + { + server = new HttpServer; + } + + void execute() override + { + if(!WifiStation.isConnected()) { + Serial.println("No network, skipping tests"); + return; + } + + auto fs = IFS::createFirmwareFilesystem(*Storage::findPartition(Storage::Partition::SubType::Data::fwfs)); + CHECK(fs != nullptr); + CHECK(fs->mount() == FS_OK); + fileSetFileSystem(fs); + + server->listen(80); + server->paths.setDefault([](HttpRequest& request, HttpResponse& response) { + auto path = request.uri.getRelativePath(); + bool ok = response.sendFile(path); + debug_i("Request from '%s' for '%s': %s", request.uri.Host.c_str(), path.c_str(), ok ? "OK" : "FAIL"); + }); + + requestNextFile(); + pending(); + } + + void requestNextFile() + { + if(fileIndex >= ARRAY_SIZE(testFiles)) { + shutdown(); + return; + } + + auto& file = testFiles[fileIndex++]; + Url url; + url.Host = WifiStation.getIP().toString(); + url.Port = 80; + url.Path = String('/') + file.name; + + auto req = new HttpRequest(url); + req->onRequestComplete([this, file](HttpConnection& connection, bool success) -> int { + auto response = connection.getResponse(); + debug_i("Client received '%s'", connection.getRequest()->uri.toString().c_str()); + Serial.print(response->toString()); + + REQUIRE(response->code == HTTP_STATUS_OK); + REQUIRE(response->headers[HTTP_HEADER_CONTENT_TYPE] == toString(file.mimeType)); + REQUIRE(response->headers[HTTP_HEADER_CONTENT_ENCODING] == file.contentEncoding); + REQUIRE(response->headers[HTTP_HEADER_CONTENT_LENGTH] == String(file.getSize())); + + Serial.println(); + + requestNextFile(); + return 0; + }); + bool ok = client.send(req); + debug_i("Requested '%s': %s", file.name, ok ? "OK" : "FAIL"); + } + + void shutdown() + { + server->shutdown(); + server = nullptr; + timer.initializeMs<1000>([this]() { complete(); }); + timer.startOnce(); + } + +private: + HttpServer* server{nullptr}; + unsigned fileIndex{0}; + HttpClient client; + Timer timer; +}; + +void REGISTER_TEST(HttpRequest) +{ + // Currently only supported for Host CI +#if defined(ARCH_HOST) + registerGroup(); +#endif +} From 90908fed7ed40f779f44c33dae3fcbf7dd18470e Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 25 Feb 2021 07:43:45 +0000 Subject: [PATCH 34/91] Replace `toString(uint8_t)` with template to avoid unintentional truncation of values (#2229) e.g. uint32_t -> uint8_t gets truncated without warning --- Sming/Core/Data/BitSet.h | 5 ++++- tests/HostTests/modules/BitSet.cpp | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Sming/Core/Data/BitSet.h b/Sming/Core/Data/BitSet.h index ee7a0b47e1..27aaef3d76 100644 --- a/Sming/Core/Data/BitSet.h +++ b/Sming/Core/Data/BitSet.h @@ -453,7 +453,10 @@ constexpr return a | b; } -String toString(uint8_t value); +template typename std::enable_if::value, String>::type toString(T value) +{ + return String(value); +} /** * @brief Class template to print the contents of a BitSet to a String diff --git a/tests/HostTests/modules/BitSet.cpp b/tests/HostTests/modules/BitSet.cpp index 331457e5f4..df0b238ff1 100644 --- a/tests/HostTests/modules/BitSet.cpp +++ b/tests/HostTests/modules/BitSet.cpp @@ -144,6 +144,13 @@ class BitSetTest : public TestGroup REQUIRE(sizeof(large) == 8); REQUIRE(large.domain().value() == 0x7FFFFFFFFULL); } + + TEST_CASE("toString") + { + REQUIRE(toString(12) == "12"); + REQUIRE(toString(12345678) == "12345678"); + REQUIRE(toString(12345678912345ULL) == "12345678912345"); + } } }; From 3d3ddabb6f8920e41c5497d212ef954ab99c47b9 Mon Sep 17 00:00:00 2001 From: slaff Date: Thu, 25 Feb 2021 08:58:53 +0100 Subject: [PATCH 35/91] Fixed coverity scan compilation. The coverity scan compilation of HttpServer_FirmwareUpload was failing on Host. (#2230) Replaced it with HttpServer_WebSockets. [scan:coverity] --- Sming/Arch/Host/Tools/ci/coverity-scan.sh | 2 +- Sming/Arch/Host/Tools/travis/coverity-scan.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sming/Arch/Host/Tools/ci/coverity-scan.sh b/Sming/Arch/Host/Tools/ci/coverity-scan.sh index f53b2afb6a..984660af48 100755 --- a/Sming/Arch/Host/Tools/ci/coverity-scan.sh +++ b/Sming/Arch/Host/Tools/ci/coverity-scan.sh @@ -4,7 +4,7 @@ set -e COVERITY_SCAN_PROJECT_NAME=SmingHub/Sming COVERITY_SCAN_NOTIFICATION_EMAIL="slaff@attachix.com" -COVERITY_SCAN_BUILD_COMMAND="$MAKE_PARALLEL Basic_Blink Basic_DateTime Basic_Delegates Basic_Interrupts Basic_ProgMem Basic_Serial Basic_Servo Basic_Ssl HttpServer_FirmwareUpload SMING_ARCH=Host DEBUG_VERBOSE_LEVEL=3" +COVERITY_SCAN_BUILD_COMMAND="$MAKE_PARALLEL Basic_Blink Basic_DateTime Basic_Delegates Basic_Interrupts Basic_ProgMem Basic_Serial Basic_Servo Basic_Ssl HttpServer_WebSockets SMING_ARCH=Host DEBUG_VERBOSE_LEVEL=3" set +x source /tmp/secrets.sh diff --git a/Sming/Arch/Host/Tools/travis/coverity-scan.sh b/Sming/Arch/Host/Tools/travis/coverity-scan.sh index 11a1248da8..f4fd35c2ef 100755 --- a/Sming/Arch/Host/Tools/travis/coverity-scan.sh +++ b/Sming/Arch/Host/Tools/travis/coverity-scan.sh @@ -4,7 +4,7 @@ set -e COVERITY_SCAN_PROJECT_NAME=${TRAVIS_REPO_SLUG} COVERITY_SCAN_NOTIFICATION_EMAIL="slaff@attachix.com" -COVERITY_SCAN_BUILD_COMMAND="$MAKE_PARALLEL Basic_Blink Basic_DateTime Basic_Delegates Basic_Interrupts Basic_ProgMem Basic_Serial Basic_Servo Basic_Ssl HttpServer_FirmwareUpload SMING_ARCH=Host DEBUG_VERBOSE_LEVEL=3" +COVERITY_SCAN_BUILD_COMMAND="$MAKE_PARALLEL Basic_Blink Basic_DateTime Basic_Delegates Basic_Interrupts Basic_ProgMem Basic_Serial Basic_Servo Basic_Ssl HttpServer_WebSockets SMING_ARCH=Host DEBUG_VERBOSE_LEVEL=3" # Environment check [ -z "$COVERITY_SCAN_PROJECT_NAME" ] && echo "ERROR: COVERITY_SCAN_PROJECT_NAME must be set" && exit 1 From 3dcc6250b3af916aa1e150a57cdbe6e9312c4472 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 26 Feb 2021 13:02:24 +0000 Subject: [PATCH 36/91] Fix IFS code quality issues (#2232) --- Sming/Components/IFS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Components/IFS b/Sming/Components/IFS index 4d6fcdb7d1..fbe21e8965 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit 4d6fcdb7d1e808b66acc8f03a99b77b79abac67d +Subproject commit fbe21e8965af1955d5881b841938079e4e55855d From ef18984d86a3e49e0b6f8b9e72962ecc061f3a67 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 26 Feb 2021 13:14:20 +0000 Subject: [PATCH 37/91] Add vscode configuration support (#2231) * Add `vscode` build target * Update docs Add new help topic called "IDE tools" Rename vscode target to ide-vscode. This way the autocompletion in bash can hint the user to find easily all supported ide operations by typing make ide[PRESS-TAB] Co-authored-by: Slavey Karadzhov --- .gitignore | 1 + Sming/project.mk | 19 ++ Tools/vscode/setup.py | 149 ++++++++++++ .../template/intellisense/configuration.json | 9 + .../template/intellisense/properties.json | 4 + Tools/vscode/template/launch.json | 41 ++++ Tools/vscode/template/tasks.json | 80 +++++++ Tools/vscode/template/workspace.json | 28 +++ docs/source/tools/vscode.rst | 222 ++++++++---------- docs/source/tools/vscode1.png | Bin 0 -> 33508 bytes docs/source/tools/vscode2.png | Bin 0 -> 27504 bytes lgtm.yml | 4 + 12 files changed, 434 insertions(+), 123 deletions(-) create mode 100644 Tools/vscode/setup.py create mode 100644 Tools/vscode/template/intellisense/configuration.json create mode 100644 Tools/vscode/template/intellisense/properties.json create mode 100644 Tools/vscode/template/launch.json create mode 100644 Tools/vscode/template/tasks.json create mode 100644 Tools/vscode/template/workspace.json create mode 100644 docs/source/tools/vscode1.png create mode 100644 docs/source/tools/vscode2.png create mode 100644 lgtm.yml diff --git a/.gitignore b/.gitignore index f112376880..a30086cd31 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ language.settings.xml nbproject .*.swp .vscode +*.code-workspace GTAGS GRTAGS GPATH diff --git a/Sming/project.mk b/Sming/project.mk index 91b7db27df..39f0b48561 100644 --- a/Sming/project.mk +++ b/Sming/project.mk @@ -515,6 +515,25 @@ else $(Q) $(PYTHON) -m pip install $(PIP_ARGS) $(foreach reqfile,$(PYTHON_REQUIREMENTS),-r $(reqfile)) endif +##@IDE Tools + +.PHONY: ide-vscode +ide-vscode: checkdirs submodules ##Create/update vscode project configuration + $(Q) $(MAKE) --no-print-directory ide-vscode-update + +.PHONY: ide-vscode-update +ide-vscode-update: + $(Q) CXX=$(CXX) \ + SMING_HOME=$(SMING_HOME) \ + ESP_HOME=$(ESP_HOME) \ + IDF_PATH=$(IDF_PATH) \ + IDF_TOOLS_PATH=$(IDF_TOOLS_PATH) \ + SMING_ARCH=$(SMING_ARCH) \ + GDB=$(GDB) \ + COM_SPEED_GDB=$(COM_SPEED_GDB) \ + WSL_ROOT=$(WSL_ROOT) \ + $(PYTHON) $(SMING_HOME)/../Tools/vscode/setup.py + ##@Testing # OTA Server diff --git a/Tools/vscode/setup.py b/Tools/vscode/setup.py new file mode 100644 index 0000000000..8746079ea6 --- /dev/null +++ b/Tools/vscode/setup.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# +# Sming hardware configuration tool +# + +import os, sys, json + +def fix_path(path): + if path[1:3] == ':/': + return '/' + path[0] + path[2:] + return path + +def env_replace(path, name, prefix): + if name in os.environ: + s = fix_path(path) + value = fix_path(os.environ[name]) + if value != '' and s.startswith(value): + return '${%s%s}%s' % (prefix, name, s[len(value):]) + else: + print("env['%s'] not found" % name) + return path + +def env_subst_path(path, prefix=''): + path = env_replace(path, 'SMING_HOME', prefix) + path = env_replace(path, 'ESP_HOME', prefix) + path = env_replace(path, 'IDF_PATH', prefix) + path = env_replace(path, 'IDF_TOOLS_PATH', prefix) + return path + +def load_json(filename, must_exist=True): + if must_exist or os.path.exists(filename): + import rjsmin + s = open(filename, 'r').read() + return json.loads(rjsmin.jsmin(s)) + return None + +def save_json(data, filename): + dir = os.path.dirname(filename) + if dir != '': + os.makedirs(dir, exist_ok=True) + with open(filename, 'w') as f: + json.dump(data, f, indent=4) + +def load_template(name): + dir = os.path.dirname(__file__) + filename = os.path.join(dir, 'template', name) + return load_json(filename) + +def find_object(data, name): + for o in data: + if o['name'] == name: + return o + return None + +def get_property(data, name, default): + if not name in data: + data[name] = default + return data[name] + +def update_intellisense(): + dirs = os.environ['COMPONENTS_EXTRA_INCDIR'].split() + for i, d in enumerate(dirs): + dirs[i] = env_subst_path(d) + + propertiesFile = '.vscode/c_cpp_properties.json' + if os.path.exists(propertiesFile): + properties = load_json(propertiesFile) + else: + properties = load_template('intellisense/properties.json') + + configurations = get_property(properties, 'configurations', []) + + arch = os.environ['SMING_ARCH'] + config = find_object(configurations, arch) + + if config is None: + config = load_template('intellisense/configuration.json') + config['name'] = arch + config['defines'].append('ARCH_%s=1' % arch.upper()) + configurations.append(config) + + config['compilerPath'] = env_subst_path(os.environ['CXX']) + config['includePath'] = dirs + + save_json(properties, propertiesFile) + + +def update_tasks(): + filename = '.vscode/tasks.json' + launch = load_json(filename, False) + if launch is None: + template = load_template('tasks.json') + launch = template.copy() + # TODO: Make any changes as required + save_json(launch, filename) + +def update_launch(): + filename = '.vscode/launch.json' + launch = load_json(filename, False) + template = load_template('launch.json') + if launch is None: + launch = template.copy() + configurations = get_property(launch, 'configurations', []) + for template_config in template['configurations']: + config = find_object(configurations, template_config['name']) + if not config is None: + configurations.remove(config) + config = template_config.copy() + configurations.append(config) + + arch = os.environ['SMING_ARCH'] + is_wsl = (os.environ['WSL_ROOT'] != '') + config = find_object(configurations, "%s GDB" % arch) + if not config is None: + path = env_subst_path(os.environ['GDB'], 'env:') + if sys.platform == 'win32': + path = os.path.splitext(path)[0] + '.exe' + config['miDebuggerPath'] = path + args = "-x ${env:SMING_HOME}/Arch/%s/Components/gdbstub/gdbcmds" % arch + if arch == 'Esp8266': + if not is_wsl: + args += " -b %s" % os.environ['COM_SPEED_GDB'] + config['miDebuggerServerAddress'] = os.environ['COM_PORT_GDB'] + config['miDebuggerArgs'] = args + + save_json(launch, filename) + +def update_workspace(): + filename = 'sming.code-workspace' + ws = load_json(filename, False) + if ws is None: + template = load_template('workspace.json') + ws = template.copy() + # TODO: Make any required changes to generated + save_json(ws, filename) + +def main(): + # So we can find rjsmin.py + sys.path.append(os.path.join(os.environ['SMING_HOME'], 'Components/Storage/Tools/hwconfig')) + + update_intellisense() + update_tasks() + update_launch() + update_workspace() + + + +if __name__ == '__main__': + main() diff --git a/Tools/vscode/template/intellisense/configuration.json b/Tools/vscode/template/intellisense/configuration.json new file mode 100644 index 0000000000..a884e7ee5d --- /dev/null +++ b/Tools/vscode/template/intellisense/configuration.json @@ -0,0 +1,9 @@ +{ + "name": "", + "includePath": [], + "defines": [], + "compilerPath": "", + "cStandard": "c11", + "cppStandard": "c++17", + "intelliSenseMode": "gcc-x86" +} \ No newline at end of file diff --git a/Tools/vscode/template/intellisense/properties.json b/Tools/vscode/template/intellisense/properties.json new file mode 100644 index 0000000000..144b7d5268 --- /dev/null +++ b/Tools/vscode/template/intellisense/properties.json @@ -0,0 +1,4 @@ +{ + "configurations": [], + "version": 4 +} \ No newline at end of file diff --git a/Tools/vscode/template/launch.json b/Tools/vscode/template/launch.json new file mode 100644 index 0000000000..5d623e3415 --- /dev/null +++ b/Tools/vscode/template/launch.json @@ -0,0 +1,41 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Esp8266 GDB", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/out/Esp8266/debug/build/app_0.out", + "args": [], + "stopAtEntry": true, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": true, + "MIMode": "gdb", + "miDebuggerPath": "${env:ESP_HOME}/xtensa-lx106-elf/bin/xtensa-lx106-elf-gdb", + "miDebuggerArgs": "-x ${env:SMING_HOME}/Arch/Esp8266/Components/gdbstub/gdbcmds -b 115200", + "miDebuggerServerAddress": "192.168.1.101:7778" + }, + { + "name": "Host GDB", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/out/Host/debug/firmware/app", + "args": [], + "stopAtEntry": true, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "miDebuggerPath": "/usr/bin/gdb", + "miDebuggerArgs": "-x ${env:SMING_HOME}/Arch/Host/Components/gdbstub/gdbcmds" + } + ] +} \ No newline at end of file diff --git a/Tools/vscode/template/tasks.json b/Tools/vscode/template/tasks.json new file mode 100644 index 0000000000..b67d903ec2 --- /dev/null +++ b/Tools/vscode/template/tasks.json @@ -0,0 +1,80 @@ +{ + "version": "2.0.0", + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "shared", + "showReuseMessage": true, + "clear": false + }, + "tasks": [ + { + "label": "Build", + "detail": "Normal build", + "type": "shell", + "command": "make -j SMING_ARCH=${command:cpptools.activeConfigName}", + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Full rebuild (no debugging0", + "detail": "Rebuild application and all Components", + "type": "shell", + "command": "make clean components-clean SMING_ARCH=${command:cpptools.activeConfigName} && make -j ENABLE_GDB=", + "problemMatcher": [] + }, + { + "label": "Full rebuild (with debugging)", + "detail": "Rebuild application and all Components", + "type": "shell", + "command": "make clean components-clean SMING_ARCH=${command:cpptools.activeConfigName} && make -j ENABLE_GDB=1", + "problemMatcher": [] + }, + { + "label": "flash", + "detail": "Write all partitions to device", + "type": "shell", + "command": "make flash SMING_ARCH=${command:cpptools.activeConfigName}", + "problemMatcher": [] + }, + { + "label": "run", + "detail": "Run application for Host", + "type": "shell", + "command": "make -j run SMING_ARCH=Host", + "problemMatcher": [] + }, + { + "label": "Flash and Run", + "detail": "Build, flash and run application for Host", + "type": "shell", + "command": "make -j flash run SMING_ARCH=Host", + "problemMatcher": [] + }, + { + "label": "clean", + "detail": "Clean just the application", + "type": "shell", + "command": "make clean SMING_ARCH=${command:cpptools.activeConfigName}", + "problemMatcher": [] + }, + { + "label": "Full clean", + "detail": "Clean application and all Components", + "type": "shell", + "command": "make clean components-clean SMING_ARCH=${command:cpptools.activeConfigName}", + "problemMatcher": [] + }, + { + "label": "Distribution Clean", + "detail": "Cleans application, all components and re-initialises all submodules", + "type": "shell", + "command": "make dist-clean", + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/Tools/vscode/template/workspace.json b/Tools/vscode/template/workspace.json new file mode 100644 index 0000000000..f5d5823978 --- /dev/null +++ b/Tools/vscode/template/workspace.json @@ -0,0 +1,28 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "files.associations": { + "*.hw": "jsonc", + "*.fwfs": "jsonc", + "optional": "cpp" + }, + "json.schemas": [ + { + "fileMatch": [ + "*.hw" + ], + "url": "{$SMING_HOME}/Components/Storage/schema.json" + }, + { + "fileMatch": [ + "*.fwfs" + ], + "url": "${SMING_HOME}/Components/IFS/fsbuild/schema.json" + } + ] + } +} \ No newline at end of file diff --git a/docs/source/tools/vscode.rst b/docs/source/tools/vscode.rst index 9ddb197ab1..015efd36d5 100644 --- a/docs/source/tools/vscode.rst +++ b/docs/source/tools/vscode.rst @@ -1,131 +1,107 @@ -******************************** Using with MS Visual Studio Code -******************************** +================================ +.. highlight:: bash -`Visual Studio Code `__ is free (as in -“free beerâ€, and they claim the code is Open Source) code editor for -Windows, Linux and Mac. While not as sophisticated in C/C++ support as -full featured Visual Studio, it already has some official support with -`C/C++ extension from -Microsoft `__. -GNU Global replaces venerable ``ctags`` tool and help collecting symbols -for Intellisense engine. +Microsoft `Visual Studio Code `__ is a free (as in +"free beer") and Open Source code editor for Windows, Linux and Mac. + +For easier integration make sure you have both :envvar:`ESP_HOME` and +:envvar:`SMING_HOME` exported in your working environment. -For easier integration make sure you have both ``ESP_HOME`` and -``SMING_HOME`` exported in your working environment. -``${workspaceRoot}`` below is the directory with your project, this -notation is used also in VS Code config files. All environment variables -are available in configuration using this notation, eg. ``${HOME}`` etc. Software involved -================= +----------------- - `Visual Studio Code `__ -- `C/C++ - extension `__ -- `RunOnSave - extension `__ -- `C++ Intellisense - extension `__ -- `GNU Global `__ - -Fire and forget way -=================== - -- install VS Code, extensions and tools -- download sming-qstart.py from - https://github.com/zgoda/sming-qstart/blob/master/sming-qstart.py -- run it from terminal, eg. ``python sming-qstart.py myproject``, see - the `program - documentation `__ - for detailed invocation options - -Easy way -======== - -- install VS Code, extensions and tools -- clone project skeleton using https://github.com/zgoda/sming-skel.git - as source -- remove unwanted bits (at least ``.git`` directory) -- update paths configuration in - ``${workspaceRoot}/.vscode/c_cpp_properties.json`` - should already - work out of the box with Linux providing you have ``ESP_HOME`` and - ``SMING_HOME`` properly exported - -Step by step -============ - -- install VS Code, extensions and tools -- update paths configuration in - ``${workspaceRoot}/.vscode/c_cpp_properties.json`` so the list - includes toolchain include path - (``${ESP_HOME}/xtensa-lx106-elf/xtensa-lx106-elf/include``), Sming - framework include paths (``${SMING_HOME}`` and - ``${SMING_HOME}/system/include``) and possibly your project - additional paths (eg. ``${workspaceRoot}/lib``), if you screw your - configuration just close VS Code, delete this file and start from - scratch -- make sure ``path`` list in ``browse`` section contains the same - entries as ``includePath`` list in root section -- define RunOnSave task in your - ``${workspaceRoot}/.vscode/settings.json`` (create file if does not - exist) to regenerate GNU Global database on every save, eg: - -.. code-block:: json - - { - "emeraldwalk.runonsave": { - "commands": [ - { - "match": "\\.(c|cpp|h|hpp)$", - "isAsync": true, - "cmd": "gtags ${workspaceRoot}" - } - ] - } - } - -- create file ``${workspaceRoot}/.vscode/tasks.json`` and define tasks - you want to run from command palette, eg minimal set: - -.. code-block:: json - - { - "version": "0.1.0", - "command": "make", - "isShellCommand": true, - "showOutput": "always", - "echoCommand": true, - "suppressTaskName": true, - "tasks": [ - { - "taskName": "Build", - "isBuildCommand": true - }, - { - "taskName": "Clean", - "args": [ - "clean" - ] - }, - { - "taskName": "Flash", - "args": [ - "flash" - ] - } - ] - } - -- add tools and binary artifacts to ``.gitignore``, eg: - -:: - - out - - # development tools - .vscode - GTAGS - GRTAGS - GPATH +- `C/C++ extension `__ + + +Installation +------------ + +- Install VS Code, extensions and tools +- Navigate to project folder and create configuration as described below +- Open workspace. If vscode is in your system path you can do this:: + + code . + + +Configuration +------------- + +One of the strengths of vscode is the use of well-documented configuration files. +You can find comprehensive documentation for these online. + +However, setting these up is time-consuming so the build system can create them for you. +The vscode workspace root directory is your project directory. + +Change to your project directory (e.g. ``samples/Basic_Blink``) and run these commands:: + + make ide-vscode SMING_ARCH=Esp8266 + make ide-vscode SMING_ARCH=Host + +Now open the workspace in vscode, and open a source file (.c, .cpp, .h). +You should now be able to select the architecture from the icon in the bottom-right corner: + +.. figure:: vscode1.png + + VS Code language selection + +A selection of tasks are provided which you can view via ``Terminal`` -> ``Run Task``. + +To debug your application, follow these steps: + +- Select the appropriate architecture (e.g. Host, Esp8266) +- Select ``Terminal`` -> ``Run Task`` -> ``Full rebuild (with debugging)`` +- Confirm that the baud rate (:envvar:`COM_SPEED_GDB`) and port (:envvar:`COM_PORT_GDB`) are + set correctly:: + + make gdb SMING_ARCH=Esp8266 COM_PORT_GDB=/dev/ttyUSB0 COM_SPEED_GDB=115200 + +- Update the vscode configuration:: + + make ide-vscode + +- In vscode, select the require 'Run' task: + +.. figure:: vscode2.png + + VS Code debug selection + + +Manual configuration changes +---------------------------- + +When you run ``make ide-vscode`` the configuration files are actually generated using a python script +``Tools/vscode/setup.py``. Configuration variables are passed from the project makefile. + +If you make any changes to the configuration files, please note the following behaviour: + +- The ``Host``, ``Esp32`` or ``Esp8266`` intellisense settings will be overwritten. +- The ``Esp8266 GDB`` and ``Host GDB`` launch configurations will be overwritten +- The ``sming.code-workspace`` and ``.vscode/tasks.json`` files will be created if they do not already exist. + To re-create these they must first be deleted. + +Ideally the vscode configuration files should not need to be kept under configuration control, +but generated when required. + +Some settings are necessarily configured via the ``setup.py`` script, however most settings can +be changed by editing the files in ``Tools/vscode/template``. + +If you do this, remember to keep a copy as they'll be overwritten otherwise. + +And, please consider contributing any changes or suggestions to the community! + + +Known issues / features +----------------------- + +- The vscode configuration files are only updated when you manually run ``make ide-vscode``. + If you update change critical build variables or add/remove Components to your project, + you may need to run it again to update them. +- When running ``make ide-vscode``, comments in the configuration files will be discarded. +- ``make ide-vscode`` may overwrite parts of your configuration: be warned! +- When debugging for esp8266 output in the console is not formatted correctly. + Lines appear with @ in front of them. +- A debugging configuration is not currently provided for ESP32. diff --git a/docs/source/tools/vscode1.png b/docs/source/tools/vscode1.png new file mode 100644 index 0000000000000000000000000000000000000000..7ecaa35b1636dc1e47dfcaf1bdd8c3efd7edc9ad GIT binary patch literal 33508 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 literal 0 HcmV?d00001 diff --git a/docs/source/tools/vscode2.png b/docs/source/tools/vscode2.png new file mode 100644 index 0000000000000000000000000000000000000000..ae880e72b5a891066c3e327019f45d8dc3a60044 GIT binary patch literal 27504 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 literal 0 HcmV?d00001 diff --git a/lgtm.yml b/lgtm.yml new file mode 100644 index 0000000000..a3d9cfe7ff --- /dev/null +++ b/lgtm.yml @@ -0,0 +1,4 @@ +extraction: + python: + python_setup: + version: 3 From f0298d331291bd9f10bab138761bbb61c862b813 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 1 Mar 2021 08:19:11 +0000 Subject: [PATCH 38/91] Add wrapper classes to support nanopb (#2233) * Add nanopb requirements * Add `Protobuf` namespace, with Callback/Stream class wrappers Handle detail of encode/decode using `InputStream` and `OutputStream` classes. `Callback` classes wrap individual data elements * Add `getCapacity()` method to LimitedMemoryStream Co-authored-by: Slavey Karadzhov --- Sming/Core/Data/Stream/LimitedMemoryStream.h | 5 + Sming/Libraries/nanopb/component.mk | 2 +- Sming/Libraries/nanopb/requirements.txt | 1 + Sming/Libraries/nanopb/src/Callback.cpp | 42 ++++++ Sming/Libraries/nanopb/src/PbUtils.cpp | 41 ------ Sming/Libraries/nanopb/src/PbUtils.h | 27 ---- Sming/Libraries/nanopb/src/Stream.cpp | 46 +++++++ Sming/Libraries/nanopb/src/include/Protobuf.h | 23 ++++ .../nanopb/src/include/Protobuf/Callback.h | 123 ++++++++++++++++++ .../nanopb/src/include/Protobuf/Stream.h | 90 +++++++++++++ 10 files changed, 331 insertions(+), 69 deletions(-) create mode 100644 Sming/Libraries/nanopb/requirements.txt create mode 100644 Sming/Libraries/nanopb/src/Callback.cpp delete mode 100644 Sming/Libraries/nanopb/src/PbUtils.cpp delete mode 100644 Sming/Libraries/nanopb/src/PbUtils.h create mode 100644 Sming/Libraries/nanopb/src/Stream.cpp create mode 100644 Sming/Libraries/nanopb/src/include/Protobuf.h create mode 100644 Sming/Libraries/nanopb/src/include/Protobuf/Callback.h create mode 100644 Sming/Libraries/nanopb/src/include/Protobuf/Stream.h diff --git a/Sming/Core/Data/Stream/LimitedMemoryStream.h b/Sming/Core/Data/Stream/LimitedMemoryStream.h index 4efc62726d..b414b6edef 100644 --- a/Sming/Core/Data/Stream/LimitedMemoryStream.h +++ b/Sming/Core/Data/Stream/LimitedMemoryStream.h @@ -66,6 +66,11 @@ class LimitedMemoryStream : public ReadWriteStream return writePos - readPos; } + size_t getCapacity() const + { + return capacity; + } + uint16_t readMemoryBlock(char* data, int bufSize) override; int seekFrom(int offset, SeekOrigin origin) override; diff --git a/Sming/Libraries/nanopb/component.mk b/Sming/Libraries/nanopb/component.mk index 7145d462d4..28bb9259eb 100644 --- a/Sming/Libraries/nanopb/component.mk +++ b/Sming/Libraries/nanopb/component.mk @@ -1,4 +1,4 @@ COMPONENT_SRCDIRS := nanopb src -COMPONENT_INCDIRS := $(COMPONENT_SRCDIRS) +COMPONENT_INCDIRS := nanopb src/include COMPONENT_SUBMODULES += nanopb \ No newline at end of file diff --git a/Sming/Libraries/nanopb/requirements.txt b/Sming/Libraries/nanopb/requirements.txt new file mode 100644 index 0000000000..b0c79cc0ec --- /dev/null +++ b/Sming/Libraries/nanopb/requirements.txt @@ -0,0 +1 @@ +protobuf diff --git a/Sming/Libraries/nanopb/src/Callback.cpp b/Sming/Libraries/nanopb/src/Callback.cpp new file mode 100644 index 0000000000..9dad3d4881 --- /dev/null +++ b/Sming/Libraries/nanopb/src/Callback.cpp @@ -0,0 +1,42 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Callback.cpp + * + ****/ + +#include "include/Protobuf/Callback.h" +#include +#include + +// See: https://iam777.tistory.com/538 + +namespace Protobuf +{ +bool OutputCallback::encode(pb_ostream_t* stream, const pb_field_t* field) +{ + if(!pb_encode_tag_for_field(stream, field)) { + return false; + } + + return pb_encode_string(stream, data, length); +} + +bool InputCallback::decode(pb_istream_t* stream, const pb_field_t* field) +{ + size_t available = stream->bytes_left; + auto new_buf = realloc(data, length + available); + if(new_buf == nullptr) { + return false; + } + data = static_cast(new_buf); + auto old_length{length}; + length += available; + + return pb_read(stream, &data[old_length], available); +} + +} // namespace Protobuf diff --git a/Sming/Libraries/nanopb/src/PbUtils.cpp b/Sming/Libraries/nanopb/src/PbUtils.cpp deleted file mode 100644 index 2ba2047d1b..0000000000 --- a/Sming/Libraries/nanopb/src/PbUtils.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "PbUtils.h" - -// See: https://iam777.tistory.com/538 - -bool pbEncodeData(pb_ostream_t *stream, const pb_field_t *field, void * const *arg) -{ - PbData *data = (PbData*) *arg; - if(data == nullptr) { - return false; - } - - if (!pb_encode_tag_for_field(stream, field)) { - return false; - } - - return pb_encode_string(stream, (uint8_t*)data->value, data->length); -} - -bool pbDecodeData(pb_istream_t *stream, const pb_field_t *field, void **arg) -{ - uint8_t buffer[1024] = {0}; - - /* We could read block-by-block to avoid the large buffer... */ - if (stream->bytes_left > sizeof(buffer) - 1) { - return false; - } - - size_t available = stream->bytes_left; - if (!pb_read(stream, buffer, stream->bytes_left)) { - return false; - } - - - MemoryDataStream* data = (MemoryDataStream*) *arg; - if(data == nullptr) { - data = new MemoryDataStream(); - *arg = (void*)data; - } - data->write(buffer, available); - return true; -} diff --git a/Sming/Libraries/nanopb/src/PbUtils.h b/Sming/Libraries/nanopb/src/PbUtils.h deleted file mode 100644 index 78c221e659..0000000000 --- a/Sming/Libraries/nanopb/src/PbUtils.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -class PbData -{ -public: - uint8_t* value = nullptr; - size_t length = 0; - - PbData(const String& text) - { - PbData((uint8_t *)text.c_str(), text.length()); - } - - PbData(uint8_t* data, size_t length) - { - value = data; - this->length = length; - } -}; - -bool pbEncodeData(pb_ostream_t *stream, const pb_field_t *field, void * const *arg); -bool pbDecodeData(pb_istream_t *stream, const pb_field_t *field, void **arg); diff --git a/Sming/Libraries/nanopb/src/Stream.cpp b/Sming/Libraries/nanopb/src/Stream.cpp new file mode 100644 index 0000000000..2b44e3b4c1 --- /dev/null +++ b/Sming/Libraries/nanopb/src/Stream.cpp @@ -0,0 +1,46 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Stream.cpp + * + ****/ + +#include "include/Protobuf/Stream.h" + +// See: https://iam777.tistory.com/538 + +namespace Protobuf +{ +bool InputStream::decode(const pb_msgdesc_t* fields, void* dest_struct) +{ + int avail = stream.available(); + if(avail <= 0) { + return false; + } + pb_istream_t is{}; + is.callback = [](pb_istream_t* stream, pb_byte_t* buf, size_t count) -> bool { + auto self = static_cast(stream->state); + assert(self != nullptr); + size_t read = self->stream.readBytes(reinterpret_cast(buf), count); + return true; + }; + is.state = this; + is.bytes_left = size_t(avail); + is.errmsg = nullptr; + return pb_decode(&is, fields, dest_struct); +} + +size_t OutputStream::encode(const pb_msgdesc_t* fields, const void* src_struct) +{ + pb_ostream_t os{}; + os.state = const_cast(this); + os.callback = buf_write; + os.max_size = SIZE_MAX; + + return pb_encode(&os, fields, src_struct) ? os.bytes_written : 0; +} + +} // namespace Protobuf diff --git a/Sming/Libraries/nanopb/src/include/Protobuf.h b/Sming/Libraries/nanopb/src/include/Protobuf.h new file mode 100644 index 0000000000..b8a1b57fcf --- /dev/null +++ b/Sming/Libraries/nanopb/src/include/Protobuf.h @@ -0,0 +1,23 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Protobuf.h + * + ****/ + +#pragma once + +#include "Protobuf/Stream.h" +#include "Protobuf/Callback.h" + +namespace Protobuf +{ +inline size_t getEncodeSize(const pb_msgdesc_t* fields, const void* src_struct) +{ + return DummyOutputStream().encode(fields, src_struct); +} + +} // namespace Protobuf diff --git a/Sming/Libraries/nanopb/src/include/Protobuf/Callback.h b/Sming/Libraries/nanopb/src/include/Protobuf/Callback.h new file mode 100644 index 0000000000..dad64b7665 --- /dev/null +++ b/Sming/Libraries/nanopb/src/include/Protobuf/Callback.h @@ -0,0 +1,123 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Callback.h + * + ****/ + +#pragma once + +#include +#include + +namespace Protobuf +{ +/** + * @brief Base class to manage handling of pb_callback_t instances + */ +class Callback +{ +public: + Callback(pb_callback_t& cb) : cb(cb) + { + cb.arg = this; + } + + virtual ~Callback() + { + free(data); + cb.arg = nullptr; + cb.funcs.encode = nullptr; + cb.funcs.decode = nullptr; + } + + bool operator==(const String& s) const + { + return s.equals(reinterpret_cast(data), length); + } + + bool operator!=(const String& s) const + { + return !operator==(s); + } + + uint8_t* getData() + { + return data; + } + + size_t getLength() + { + return length; + } + + explicit operator String() const + { + return String(reinterpret_cast(data), length); + } + +protected: + pb_callback_t& cb; + uint8_t* data{nullptr}; + size_t length{0}; +}; + +/** + * @brief Handles input data decoding + */ +class InputCallback : public Callback +{ +public: + /** + * @brief Construct a decoding instance + */ + InputCallback(pb_callback_t& cb) : Callback(cb) + { + cb.funcs.decode = static_decode; + } + +protected: + static bool static_decode(pb_istream_t* stream, const pb_field_t* field, void** arg) + { + auto self = static_cast(*arg); + return self ? self->decode(stream, field) : false; + } + bool decode(pb_istream_t* stream, const pb_field_t* field); +}; + +/** + * @brief Handles output data encoding + */ +class OutputCallback : public Callback +{ +public: + /** + * @brief Construct an encoding instance + * @param data + * @param len + */ + OutputCallback(pb_callback_t& cb, const void* data, size_t len) : Callback(cb) + { + cb.funcs.encode = static_encode; + this->data = new uint8_t[len]; + memcpy(this->data, data, len); + length = len; + } + + OutputCallback(pb_callback_t& cb, const String& value) : OutputCallback(cb, value.c_str(), value.length()) + { + } + +protected: + static bool static_encode(pb_ostream_t* stream, const pb_field_t* field, void* const* arg) + { + auto self = static_cast(*arg); + return self ? self->encode(stream, field) : false; + } + bool encode(pb_ostream_t* stream, const pb_field_t* field); +}; + +} // namespace Protobuf diff --git a/Sming/Libraries/nanopb/src/include/Protobuf/Stream.h b/Sming/Libraries/nanopb/src/include/Protobuf/Stream.h new file mode 100644 index 0000000000..6e7cfbbcab --- /dev/null +++ b/Sming/Libraries/nanopb/src/include/Protobuf/Stream.h @@ -0,0 +1,90 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * Stream.h + * + ****/ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Protobuf +{ +class Stream +{ +public: + ~Stream() + { + } +}; + +class InputStream : public Stream +{ +public: + InputStream(IDataSourceStream& source) : stream(source) + { + } + + bool decode(const pb_msgdesc_t* fields, void* dest_struct); + + String getErrorString() const + { + return errmsg; + } + +private: + const char* errmsg{nullptr}; + IDataSourceStream& stream; +}; + +class OutputStream : public Stream +{ +public: + size_t encode(const pb_msgdesc_t* fields, const void* src_struct); + +protected: + static bool buf_write(pb_ostream_t* stream, const pb_byte_t* buf, size_t count) + { + auto self = static_cast(stream->state); + assert(self != nullptr); + return self->write(buf, count); + } + virtual bool write(const pb_byte_t* buf, size_t count) = 0; +}; + +class DummyOutputStream : public OutputStream +{ +protected: + bool write(const pb_byte_t* buf, size_t count) override + { + return true; + } +}; + +class TcpClientOutputStream : public OutputStream +{ +public: + TcpClientOutputStream(TcpClient& client) : client(client) + { + } + +protected: + bool write(const pb_byte_t* buf, size_t count) override + { + int err = client.send(reinterpret_cast(buf), count); + // debug_i("client.send(%u): %d", count, err); + return err > 0; + } + + TcpClient& client; +}; + +} // namespace Protobuf From e321fe90de060aed63e6c76b61077fe98eef4a0c Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 1 Mar 2021 08:19:56 +0000 Subject: [PATCH 39/91] Provide `LinkedObject` for general use (#2234) Move `Storage::Object`, etc. into `Core/Data` and rename as `LinkedObject...` --- .../Storage/src/include/Storage/Device.h | 8 ++-- .../Object.h => Core/Data/LinkedObject.h} | 32 +++++++-------- .../Data/LinkedObjectList.cpp} | 14 +++---- .../Data/LinkedObjectList.h} | 39 ++++++++----------- 4 files changed, 42 insertions(+), 51 deletions(-) rename Sming/{Components/Storage/src/include/Storage/Object.h => Core/Data/LinkedObject.h} (76%) rename Sming/{Components/Storage/src/ObjectList.cpp => Core/Data/LinkedObjectList.cpp} (81%) rename Sming/{Components/Storage/src/include/Storage/ObjectList.h => Core/Data/LinkedObjectList.h} (66%) diff --git a/Sming/Components/Storage/src/include/Storage/Device.h b/Sming/Components/Storage/src/include/Storage/Device.h index cf54588b19..b680b960a3 100644 --- a/Sming/Components/Storage/src/include/Storage/Device.h +++ b/Sming/Components/Storage/src/include/Storage/Device.h @@ -10,7 +10,7 @@ #pragma once #include -#include "ObjectList.h" +#include #include "PartitionTable.h" #define STORAGE_TYPE_MAP(XX) \ @@ -29,11 +29,11 @@ class SpiFlash; /** * @brief Represents a storage device (e.g. flash memory) */ -class Device : public ObjectTemplate +class Device : public LinkedObjectTemplate { public: - using List = ObjectListTemplate; - using OwnedList = OwnedObjectListTemplate; + using List = LinkedObjectListTemplate; + using OwnedList = OwnedLinkedObjectListTemplate; /** * @brief Storage type diff --git a/Sming/Components/Storage/src/include/Storage/Object.h b/Sming/Core/Data/LinkedObject.h similarity index 76% rename from Sming/Components/Storage/src/include/Storage/Object.h rename to Sming/Core/Data/LinkedObject.h index 0d6914f643..7b4cecbd86 100644 --- a/Sming/Components/Storage/src/include/Storage/Object.h +++ b/Sming/Core/Data/LinkedObject.h @@ -4,7 +4,7 @@ * http://github.com/SmingHub/Sming * All files of the Sming Core are provided under the LGPL v3 license. * - * Object.h - Base Storage object definition + * LinkedObject.h * ****/ #pragma once @@ -12,46 +12,48 @@ #include #include -namespace Storage -{ -class ObjectList; - -class Object +/** + * @brief Base virtual class to allow objects to be linked together + * + * This can be more efficient than defining a separate list, as each object + * requires only an additional pointer field for 'next'. + */ +class LinkedObject { public: - virtual ~Object() + virtual ~LinkedObject() { } - virtual Object* next() const + virtual LinkedObject* next() const { return mNext; } - Object* getNext() const + LinkedObject* getNext() const { return mNext; } - bool operator==(const Object& other) const + bool operator==(const LinkedObject& other) const { return this == &other; } - bool operator!=(const Object& other) const + bool operator!=(const LinkedObject& other) const { return this != &other; } private: - friend class ObjectList; - Object* mNext{nullptr}; + friend class LinkedObjectList; + LinkedObject* mNext{nullptr}; }; /** * @brief Base class template for linked items with type casting */ -template class ObjectTemplate : public Object +template class LinkedObjectTemplate : public LinkedObject { public: template @@ -140,5 +142,3 @@ template class ObjectTemplate : public Object return ConstIterator(nullptr); } }; - -} // namespace Storage diff --git a/Sming/Components/Storage/src/ObjectList.cpp b/Sming/Core/Data/LinkedObjectList.cpp similarity index 81% rename from Sming/Components/Storage/src/ObjectList.cpp rename to Sming/Core/Data/LinkedObjectList.cpp index 4ec5cc3d15..a1634d7e2f 100644 --- a/Sming/Components/Storage/src/ObjectList.cpp +++ b/Sming/Core/Data/LinkedObjectList.cpp @@ -4,21 +4,19 @@ * http://github.com/SmingHub/Sming * All files of the Sming Core are provided under the LGPL v3 license. * - * ObjectList.cpp + * LinkedObjectList.cpp * ****/ -#include "include/Storage/ObjectList.h" +#include "LinkedObjectList.h" -namespace Storage -{ -bool ObjectList::add(Object* object) +bool LinkedObjectList::add(LinkedObject* object) { if(object == nullptr) { return false; } - Object* prev = nullptr; + LinkedObject* prev = nullptr; auto it = mHead; while(it != nullptr) { if(it == object) { @@ -38,7 +36,7 @@ bool ObjectList::add(Object* object) return true; } -bool ObjectList::remove(Object* object) +bool LinkedObjectList::remove(LinkedObject* object) { if(object == nullptr || mHead == nullptr) { return false; @@ -61,5 +59,3 @@ bool ObjectList::remove(Object* object) return false; } - -} // namespace Storage diff --git a/Sming/Components/Storage/src/include/Storage/ObjectList.h b/Sming/Core/Data/LinkedObjectList.h similarity index 66% rename from Sming/Components/Storage/src/include/Storage/ObjectList.h rename to Sming/Core/Data/LinkedObjectList.h index da015dd0d8..2014756ba7 100644 --- a/Sming/Components/Storage/src/include/Storage/ObjectList.h +++ b/Sming/Core/Data/LinkedObjectList.h @@ -4,51 +4,48 @@ * http://github.com/SmingHub/Sming * All files of the Sming Core are provided under the LGPL v3 license. * - * Object.h - Base Storage object definition + * LinkedObjectList.h * ****/ #pragma once -#include "Object.h" -#include +#include "LinkedObject.h" -namespace Storage -{ /** * @brief Singly-linked list of objects * @note We don't own the items, just keep references to them */ -class ObjectList +class LinkedObjectList { public: - ObjectList() + LinkedObjectList() { } - ObjectList(Object* object) : mHead(object) + LinkedObjectList(LinkedObject* object) : mHead(object) { } - bool add(Object* object); + bool add(LinkedObject* object); - bool add(const Object* object) + bool add(const LinkedObject* object) { - return add(const_cast(object)); + return add(const_cast(object)); } - bool remove(Object* object); + bool remove(LinkedObject* object); void clear() { mHead = nullptr; } - Object* head() + LinkedObject* head() { return mHead; } - const Object* head() const + const LinkedObject* head() const { return mHead; } @@ -59,15 +56,15 @@ class ObjectList } protected: - Object* mHead{nullptr}; + LinkedObject* mHead{nullptr}; }; -template class ObjectListTemplate : public ObjectList +template class LinkedObjectListTemplate : public LinkedObjectList { public: - ObjectListTemplate() = default; + LinkedObjectListTemplate() = default; - ObjectListTemplate(ObjectType* object) : ObjectList(object) + LinkedObjectListTemplate(ObjectType* object) : LinkedObjectList(object) { } @@ -116,12 +113,12 @@ template class ObjectListTemplate : public ObjectList * @brief Class template for singly-linked list of objects * @note We own the objects so are responsible for destroying them when removed */ -template class OwnedObjectListTemplate : public ObjectListTemplate +template class OwnedLinkedObjectListTemplate : public LinkedObjectListTemplate { public: bool remove(ObjectType* object) { - bool res = ObjectList::remove(object); + bool res = LinkedObjectList::remove(object); delete object; return res; } @@ -133,5 +130,3 @@ template class OwnedObjectListTemplate : public ObjectList } } }; - -} // namespace Storage From 7c6fbf05efd6b6e7017d9fa167bff2f0c95031ca Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 1 Mar 2021 08:20:43 +0000 Subject: [PATCH 40/91] Use esp82xx-nonos-linklayer github fork (#2235) Link to savannah keeps breaking during CI --- .gitmodules | 2 +- Sming/Arch/Esp8266/Components/lwip2/lwip2 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index e2a90fbd3b..e5ea539ace 100644 --- a/.gitmodules +++ b/.gitmodules @@ -93,7 +93,7 @@ ignore = dirty [submodule "Esp8266.lwip2"] path = Sming/Arch/Esp8266/Components/lwip2/lwip2 - url = https://github.com/d-a-v/esp82xx-nonos-linklayer.git + url = https://github.com/mikee47/esp82xx-nonos-linklayer.git ignore = dirty [submodule "Esp8266.new-pwm"] path = Sming/Arch/Esp8266/Components/driver/new-pwm diff --git a/Sming/Arch/Esp8266/Components/lwip2/lwip2 b/Sming/Arch/Esp8266/Components/lwip2/lwip2 index 5b73bb11d5..8bd8607b4e 160000 --- a/Sming/Arch/Esp8266/Components/lwip2/lwip2 +++ b/Sming/Arch/Esp8266/Components/lwip2/lwip2 @@ -1 +1 @@ -Subproject commit 5b73bb11d59285d49a6efeb20726186cc5cc2b74 +Subproject commit 8bd8607b4ed3b81cf656a72729aa70a09b5f4d6e From 1ad27fab3c57dbed3bcdbdad5c824a8fb19df9b6 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 1 Mar 2021 15:09:52 +0000 Subject: [PATCH 41/91] Revise install scripts and documentation (#2204) * add install scripts for faster and easier installation Co-authored-by: Slavey Karadzhov --- .ci/install.sh | 13 - .github/workflows/codeql-analysis.yml | 18 +- .travis.yml | 6 +- .travis/tools/esptool2 | Bin 22261 -> 0 bytes CONTRIBUTING.md | 2 +- Sming/Arch/Esp32/README.rst | 19 +- Sming/Arch/Esp32/Tools/ci/install.cmd | 3 + Sming/Arch/Esp32/Tools/ci/install.sh | 14 -- Sming/Arch/Esp32/Tools/install.sh | 50 ++++ Sming/Arch/Esp8266/README.rst | 1 + Sming/Arch/Esp8266/Tools/ci/install.cmd | 5 +- Sming/Arch/Esp8266/Tools/ci/install.sh | 14 -- Sming/Arch/Esp8266/Tools/docker/Dockerfile | 15 -- Sming/Arch/Esp8266/Tools/install.sh | 26 ++ Sming/Arch/Host/Components/vflash/README.rst | 2 +- Sming/Arch/Host/README.rst | 11 +- Sming/Arch/Host/Tools/ci/build.setup.sh | 4 +- Sming/Arch/Host/Tools/ci/install.cmd | 14 +- Sming/Arch/Host/Tools/ci/install.sh | 8 - Sming/Arch/Host/Tools/install.sh | 8 + Sming/Libraries/Adafruit_ILI9341/component.mk | 2 +- Sming/README.rst | 2 +- Sming/building.rst | 2 +- Sming/project.mk | 14 +- .../Tools/docker => Tools/Docker}/README.md | 0 Tools/Docker/cli/Dockerfile | 43 ++++ Tools/Docker/cli/docker-compose.yml | 16 ++ Tools/Docker/ide/Dockerfile | 42 ++++ Tools/Docker/ide/LICENSE | 22 ++ .../Docker/ide}/assets/welcome.html | 13 +- .../Docker/ide}/assets/welcome.js | 0 .../Docker/ide}/docker-compose.yml | 20 +- Tools/Docker/ide/supervisord.conf | 14 ++ Tools/choco-install.cmd | 19 ++ {.ci => Tools/ci}/build.cmd | 0 {.ci => Tools/ci}/build.sh | 0 {.ci => Tools/ci}/deploy.sh | 0 {.ci => Tools/ci}/install.cmd | 2 + {.ci => Tools/ci}/secrets.sh.enc | 0 Tools/export.sh | 35 +++ Tools/install.sh | 184 ++++++++++++++ Tools/tcp_serial_redirect.py | 232 ++++++++++++++++++ {.travis => Tools/travis}/build.sh | 0 {.travis => Tools/travis}/deploy.sh | 0 {.travis => Tools/travis}/install.sh | 0 Tools/vscode/setup.py | 21 +- appveyor.yml | 18 +- docs/Tools/install.sh | 26 ++ docs/requirements.txt | 3 +- .../arch/esp32/getting-started/index.rst | 52 ---- .../arch/esp8266/getting-started/index.rst | 30 --- .../arch/esp8266/getting-started/linux.rst | 109 -------- .../host/{host-emulator.rst => index.rst} | 20 +- .../esp8266 => }/getting-started/config.rst | 15 +- .../docker}/c9-1.png | Bin .../docker/index.rst} | 52 ++-- .../index.rst} | 26 +- docs/source/getting-started/linux/index.rst | 78 ++++++ .../macos/index.rst} | 10 +- .../windows/index.rst} | 89 +++---- .../windows/manual.rst} | 2 +- .../windows/wsl.rst} | 28 ++- docs/source/index.rst | 4 +- .../develop}/clang-tools.rst | 0 .../source/information/develop/components.rst | 2 +- docs/source/information/flash.rst | 2 +- docs/source/link-roles.py | 1 + docs/source/samples.rst | 3 + docs/source/tools/index.rst | 2 + .../source/troubleshooting/random-restart.rst | 2 +- samples/Basic_rBoot/README.rst | 2 +- 71 files changed, 1046 insertions(+), 446 deletions(-) delete mode 100755 .ci/install.sh delete mode 100755 .travis/tools/esptool2 delete mode 100755 Sming/Arch/Esp32/Tools/ci/install.sh create mode 100755 Sming/Arch/Esp32/Tools/install.sh delete mode 100755 Sming/Arch/Esp8266/Tools/ci/install.sh delete mode 100644 Sming/Arch/Esp8266/Tools/docker/Dockerfile create mode 100755 Sming/Arch/Esp8266/Tools/install.sh delete mode 100755 Sming/Arch/Host/Tools/ci/install.sh create mode 100755 Sming/Arch/Host/Tools/install.sh rename {Sming/Arch/Esp8266/Tools/docker => Tools/Docker}/README.md (100%) create mode 100644 Tools/Docker/cli/Dockerfile create mode 100644 Tools/Docker/cli/docker-compose.yml create mode 100644 Tools/Docker/ide/Dockerfile create mode 100644 Tools/Docker/ide/LICENSE rename {Sming/Arch/Esp8266/Tools/docker => Tools/Docker/ide}/assets/welcome.html (64%) rename {Sming/Arch/Esp8266/Tools/docker => Tools/Docker/ide}/assets/welcome.js (100%) rename {Sming/Arch/Esp8266/Tools/docker => Tools/Docker/ide}/docker-compose.yml (53%) create mode 100644 Tools/Docker/ide/supervisord.conf create mode 100644 Tools/choco-install.cmd rename {.ci => Tools/ci}/build.cmd (100%) rename {.ci => Tools/ci}/build.sh (100%) rename {.ci => Tools/ci}/deploy.sh (100%) rename {.ci => Tools/ci}/install.cmd (76%) rename {.ci => Tools/ci}/secrets.sh.enc (100%) create mode 100755 Tools/export.sh create mode 100755 Tools/install.sh create mode 100644 Tools/tcp_serial_redirect.py rename {.travis => Tools/travis}/build.sh (100%) rename {.travis => Tools/travis}/deploy.sh (100%) rename {.travis => Tools/travis}/install.sh (100%) create mode 100644 docs/Tools/install.sh delete mode 100644 docs/source/arch/esp32/getting-started/index.rst delete mode 100644 docs/source/arch/esp8266/getting-started/index.rst delete mode 100644 docs/source/arch/esp8266/getting-started/linux.rst rename docs/source/arch/host/{host-emulator.rst => index.rst} (92%) rename docs/source/{arch/esp8266 => }/getting-started/config.rst (76%) rename docs/source/{arch/esp8266/getting-started => getting-started/docker}/c9-1.png (100%) rename docs/source/{arch/esp8266/getting-started/docker.rst => getting-started/docker/index.rst} (51%) rename docs/source/{getting-started.rst => getting-started/index.rst} (91%) create mode 100644 docs/source/getting-started/linux/index.rst rename docs/source/{arch/esp8266/getting-started/macos.rst => getting-started/macos/index.rst} (95%) rename docs/source/{arch/esp8266/getting-started/windows.rst => getting-started/windows/index.rst} (54%) rename docs/source/{arch/esp8266/getting-started/windows-manual.rst => getting-started/windows/manual.rst} (99%) rename docs/source/{windows-wsl.rst => getting-started/windows/wsl.rst} (78%) rename docs/source/{contribute => information/develop}/clang-tools.rst (100%) diff --git a/.ci/install.sh b/.ci/install.sh deleted file mode 100755 index 21a90b2d2c..0000000000 --- a/.ci/install.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -set -ex # exit with nonzero exit code if anything fails - -# Common install - -sudo apt-get update - -sudo update-alternatives --set gcc /usr/bin/gcc-9 -python -m pip install --upgrade pip -r $SMING_HOME/../Tools/requirements.txt - -sudo apt-get install -y gcc-9-multilib g++-9-multilib python3-setuptools - -source $SMING_HOME/Arch/$SMING_ARCH/Tools/ci/install.sh diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 634f26726d..da50404adc 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -17,7 +17,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: fail-fast: false @@ -61,16 +61,10 @@ jobs: - if: matrix.language == 'cpp' || matrix.language == 'c' name: Build Sming C/C++ run: | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test - sudo apt-get update -y - sudo apt-get install gcc-9-multilib g++-9-multilib python3-wheel python3-setuptools - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 60 --slave /usr/bin/g++ g++ /usr/bin/g++-9 - python3 -m pip install --upgrade pip wheel -r Tools/requirements.txt - env - cd Sming - export SMING_HOME=$(pwd) - cd ../samples/Basic_Blink - make -j3 SMING_ARCH=Host - + sudo Tools/install.sh host + . Tools/export.sh + cd samples/Basic_Blink + make -j3 SMING_ARCH=Host + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 diff --git a/.travis.yml b/.travis.yml index 1228bc56d0..97b2443e0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,11 +56,11 @@ addons: update: true packages: - xmlstarlet -install: ".travis/install.sh" -script: ".travis/build.sh" +install: "Tools/travis/install.sh" +script: "Tools/travis/build.sh" deploy: provider: script - script: ".travis/deploy.sh $TRAVIS_TAG" + script: "Tools/travis/deploy.sh $TRAVIS_TAG" skip_cleanup: true on: tags: true diff --git a/.travis/tools/esptool2 b/.travis/tools/esptool2 deleted file mode 100755 index 7de8b1f191fbec7b1576216b89f1ec5f2826dfad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22261 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 WFy2KO8jrQ?B|Z$A;>F8+RQ`VyzOW?# diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19039ad569..a7f6067b20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ __Sming Contributing flow__: 4. Document your code - As a bare minimum, please include a `README.rst` or `README.md` file. See :doc:`/contribute/documentation` for further information. + As a bare minimum, please include a `README.rst` or `README.md` file. See :doc:`/information/develop/documentation` for further information. 5. Commit changes diff --git a/Sming/Arch/Esp32/README.rst b/Sming/Arch/Esp32/README.rst index 6a39bb46d8..5c249ba8a9 100644 --- a/Sming/Arch/Esp32/README.rst +++ b/Sming/Arch/Esp32/README.rst @@ -15,15 +15,19 @@ Requirements ------------ In order to be able to compile for the ESP32 architecture you should have ESP-IDF v4.1 installed. -A detailed installation manual can be found in the `ESP-IDF documentation `__. +The Sming installers can do this for you - see :doc:`/getting-started/index`. + +You can find further details in the `ESP-IDF documentation `__. Building -------- -Make sure that the IDF_PATH environmental variable is set. Also make sure that the other ESP-IDF environmental variables are set. -For example on Linux this can be done using the following command:: +Make sure that the :envvar:`IDF_PATH` is set. +Also make sure that the other ESP-IDF environmental variables are set. + +In Linux this can be done using the following command:: - source $IDF_PATH/export.sh + source $SMING_HOME/Tools/export.sh Build the framework and application as usual, specifying :envvar:`SMING_ARCH` =Esp32. For example:: @@ -51,6 +55,13 @@ If you want to revert to using the default pre-compiled SDK then issue the follo make SMING_ARCH=Esp32 sdk-default +.. note:: + + If you have an ESP32-S2 device you'll need to change :envvar:`ESP_VARIANT`:: + + make ESP_VARIANT=esp32s2 + +See :component-esp32:`esp32` for further details. Components ---------- diff --git a/Sming/Arch/Esp32/Tools/ci/install.cmd b/Sming/Arch/Esp32/Tools/ci/install.cmd index 087ec4fa00..7007813206 100644 --- a/Sming/Arch/Esp32/Tools/ci/install.cmd +++ b/Sming/Arch/Esp32/Tools/ci/install.cmd @@ -1,5 +1,8 @@ REM Esp32 install.cmd +if "%IDF_PATH%"=="" goto :EOF +if "%IDF_TOOLS_PATH%"=="" goto :EOF + git clone -b release/v4.1 https://github.com/espressif/esp-idf.git %IDF_PATH% REM Espressif downloads very slow, fetch from SmingTools diff --git a/Sming/Arch/Esp32/Tools/ci/install.sh b/Sming/Arch/Esp32/Tools/ci/install.sh deleted file mode 100755 index 17a7577081..0000000000 --- a/Sming/Arch/Esp32/Tools/ci/install.sh +++ /dev/null @@ -1,14 +0,0 @@ -# Esp32 install.sh - -sudo apt-get install -y git wget flex bison gperf ninja-build ccache libffi-dev libssl-dev dfu-util - -git clone -b release/v4.1 https://github.com/espressif/esp-idf.git $IDF_PATH - -# Espressif downloads very slow, fetch from SmingTools -mkdir -p $IDF_TOOLS_PATH -ESPTOOLS=esp32-tools-linux-4.1.zip -wget --no-verbose $SMINGTOOLS/$ESPTOOLS -unzip $ESPTOOLS -d $IDF_TOOLS_PATH/dist - -python $IDF_PATH/tools/idf_tools.py install -python -m pip install -r $IDF_PATH/requirements.txt diff --git a/Sming/Arch/Esp32/Tools/install.sh b/Sming/Arch/Esp32/Tools/install.sh new file mode 100755 index 0000000000..6639c15ffc --- /dev/null +++ b/Sming/Arch/Esp32/Tools/install.sh @@ -0,0 +1,50 @@ +# Esp32 install.sh + +if [ -n "$IDF_PATH" ] && [ -n "$IDF_TOOLS_PATH" ]; then + +PACKAGES=(\ + bison \ + ccache \ + dfu-util \ + flex \ + gperf \ + ninja-build \ + ) + +case $DIST in + debian) + PACKAGES+=(\ + libffi-dev \ + libssl-dev \ + ) + ;; + + fedora) + PACKAGES+=(\ + libffi-devel \ + ) + ;; +esac + +$PKG_INSTALL ${PACKAGES[*]} + +if [ -d "$IDF_PATH" ]; then + printf "\n\n** Skipping ESP-IDF clone: '$IDF_PATH' exists\n\n" +else + git clone -b release/v4.1 https://github.com/espressif/esp-idf.git $IDF_PATH +fi + +# Espressif downloads very slow, fetch from SmingTools +mkdir -p $IDF_TOOLS_PATH +ESPTOOLS=esp32-tools-linux-4.1.zip +$WGET $SMINGTOOLS/$ESPTOOLS -O $DOWNLOADS/$ESPTOOLS +unzip $DOWNLOADS/$ESPTOOLS -d $IDF_TOOLS_PATH/dist + +python3 $IDF_PATH/tools/idf_tools.py install +python3 -m pip install -r $IDF_PATH/requirements.txt + +if [ -z "$KEEP_DOWNLOADS" ]; then + rm -rf $IDF_TOOLS_PATH/dist +fi + +fi diff --git a/Sming/Arch/Esp8266/README.rst b/Sming/Arch/Esp8266/README.rst index b97391a15c..a001a3c628 100644 --- a/Sming/Arch/Esp8266/README.rst +++ b/Sming/Arch/Esp8266/README.rst @@ -3,6 +3,7 @@ Sming Esp8266 Architecture Support building Sming for the Esp8266 architecture. +See also :doc:`/arch/esp8266/getting-started/eqt`. Build variables --------------- diff --git a/Sming/Arch/Esp8266/Tools/ci/install.cmd b/Sming/Arch/Esp8266/Tools/ci/install.cmd index 8aa477c552..e7525a4b2a 100644 --- a/Sming/Arch/Esp8266/Tools/ci/install.cmd +++ b/Sming/Arch/Esp8266/Tools/ci/install.cmd @@ -1,10 +1,11 @@ REM Esp8266 install.cmd -call :install %UDK_ROOT% esp-udk-win32.7z -call :install %EQT_ROOT% x86_64-w64-mingw32.xtensa-lx106-elf-e6a192b.201211.zip +call :install "%UDK_ROOT%" esp-udk-win32.7z +call :install "%EQT_ROOT%" x86_64-w64-mingw32.xtensa-lx106-elf-e6a192b.201211.zip goto :EOF :install +if "%1"=="" goto :EOF mkdir %1 curl -LO %SMINGTOOLS%/%2 7z -o%1 x %2 diff --git a/Sming/Arch/Esp8266/Tools/ci/install.sh b/Sming/Arch/Esp8266/Tools/ci/install.sh deleted file mode 100755 index 87a3918c0f..0000000000 --- a/Sming/Arch/Esp8266/Tools/ci/install.sh +++ /dev/null @@ -1,14 +0,0 @@ -# Esp8266 install.sh - -# Old toolchain -TOOLCHAIN=esp-open-sdk-linux-x86_64.tar.gz -wget --no-verbose $SMINGTOOLS/$TOOLCHAIN -tar -zxf $TOOLCHAIN -mkdir -p $UDK_ROOT -ln -s $(pwd)/esp-open-sdk/xtensa-lx106-elf $UDK_ROOT/. - -# New toolchain -TOOLCHAIN=x86_64-linux-gnu.xtensa-lx106-elf-e6a192b.201211.tar.gz -wget --no-verbose $SMINGTOOLS/$TOOLCHAIN -mkdir -p $EQT_ROOT -tar -zxf $TOOLCHAIN -C $EQT_ROOT --totals diff --git a/Sming/Arch/Esp8266/Tools/docker/Dockerfile b/Sming/Arch/Esp8266/Tools/docker/Dockerfile deleted file mode 100644 index 98b53dfe02..0000000000 --- a/Sming/Arch/Esp8266/Tools/docker/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# ------------------------------------ -# Fast dockerized development environment -# for the Sming Framework: https://github.com/SmingHub/Sming.git -# ------------------------------------ -FROM attachix/c9-esp8266-sdk:latest -MAINTAINER Slavey Karadzhov - -COPY assets/welcome.html /cloud9/plugins/c9.ide.welcome/welcome.html -COPY assets/welcome.js /cloud9/plugins/c9.ide.welcome/welcome.js - -RUN git clone https://github.com/SmingHub/Sming.git /workspace/Sming - -ENV SMING_HOME /workspace/Sming/Sming - -ENTRYPOINT /usr/bin/supervisord diff --git a/Sming/Arch/Esp8266/Tools/install.sh b/Sming/Arch/Esp8266/Tools/install.sh new file mode 100755 index 0000000000..c940fcd930 --- /dev/null +++ b/Sming/Arch/Esp8266/Tools/install.sh @@ -0,0 +1,26 @@ +# Esp8266 install.sh + +# Old toolchain +if [ -n "$UDK_ROOT" ]; then + if [ -d "$EQT_ROOT" ]; then + printf "\n\n** Skipping Esp8266 tools installation: '$EQT_ROOT' exists\n\n" + else + TOOLCHAIN=esp-open-sdk-linux-x86_64.tar.gz + $WGET $SMINGTOOLS/$TOOLCHAIN -O $DOWNLOADS/$TOOLCHAIN + tar -zxf $DOWNLOADS/$TOOLCHAIN + mkdir -p $UDK_ROOT + ln -s $(pwd)/esp-open-sdk/xtensa-lx106-elf $UDK_ROOT/. + fi +fi + +# New toolchain +if [ -n "$EQT_ROOT" ]; then + if [ -d "$EQT_ROOT" ]; then + printf "\n\n** Skipping Esp8266 tools installation: '$EQT_ROOT' exists\n\n" + else + TOOLCHAIN=x86_64-linux-gnu.xtensa-lx106-elf-e6a192b.201211.tar.gz + $WGET $SMINGTOOLS/$TOOLCHAIN -O $DOWNLOADS/$TOOLCHAIN + mkdir -p $EQT_ROOT + tar -zxf $DOWNLOADS/$TOOLCHAIN -C $EQT_ROOT --totals + fi +fi diff --git a/Sming/Arch/Host/Components/vflash/README.rst b/Sming/Arch/Host/Components/vflash/README.rst index 862312c3af..5fa2f06fa0 100644 --- a/Sming/Arch/Host/Components/vflash/README.rst +++ b/Sming/Arch/Host/Components/vflash/README.rst @@ -21,7 +21,7 @@ The following options are interpreted and used to provide command-line parameter The size of the flash memory is set via :ref:`hardware_config`. -See :component-esp8266:`esptool` for details and other applicable variables. +See :component:`esptool` for details and other applicable variables. Build targets diff --git a/Sming/Arch/Host/README.rst b/Sming/Arch/Host/README.rst index 7e20eca7b6..96867e170f 100644 --- a/Sming/Arch/Host/README.rst +++ b/Sming/Arch/Host/README.rst @@ -26,7 +26,7 @@ For Linux, you may require the ``gcc-multilib`` and ``g++-multilib`` packages to build 32-bit executables on a 64-bit OS. For Windows, make sure your ``MinGW`` distro is up to date. -See :doc:`/arch/esp8266/getting-started/windows-manual` for further details. +See :doc:`/getting-started/windows` for further details. Building -------- @@ -58,12 +58,3 @@ Components Components/sming-arch/index Components/*/index - - -todo ----- - -* Add passthrough support for real serial ports to permit connection of physical devices. -* Consider how this mechanism might be used to support emulation of other devices (SPI, I2C, etc). -* Development platforms with SPI or I2C (e.g. Raspberry Pi) could be supported. -* Are there any generic device emulators available? For example, to simulate specific types of SPI slave. diff --git a/Sming/Arch/Host/Tools/ci/build.setup.sh b/Sming/Arch/Host/Tools/ci/build.setup.sh index 2dc3d0c044..a2370b039b 100755 --- a/Sming/Arch/Host/Tools/ci/build.setup.sh +++ b/Sming/Arch/Host/Tools/ci/build.setup.sh @@ -3,7 +3,7 @@ # Check coding style make cs DIFFS=$(git diff) -if [ "$DIFFS" != "" ]; then +if [ -n "$DIFFS" ]; then echo "!!! Coding Style issues Found!!!" echo " Run: 'make cs' to fix them. " echo "$DIFFS" @@ -13,7 +13,7 @@ fi # Make deployment keys, etc. available set +x if [ -n "$SMING_SECRET" ]; then - openssl aes-256-cbc -d -a -iter 100 -in $CI_BUILD_DIR/.ci/secrets.sh.enc -out /tmp/secrets.sh -pass pass:$SMING_SECRET + openssl aes-256-cbc -d -a -iter 100 -in $CI_BUILD_DIR/Tools/ci/secrets.sh.enc -out /tmp/secrets.sh -pass pass:$SMING_SECRET source /tmp/secrets.sh unset SMING_SECRET fi diff --git a/Sming/Arch/Host/Tools/ci/install.cmd b/Sming/Arch/Host/Tools/ci/install.cmd index 3cee90ae9a..556cb4dd2f 100644 --- a/Sming/Arch/Host/Tools/ci/install.cmd +++ b/Sming/Arch/Host/Tools/ci/install.cmd @@ -1,8 +1,14 @@ REM Host install.cmd -curl -LO https://doxygen.nl/files/doxygen-1.9.1-setup.exe -doxygen-1.9.1-setup /silent - -choco install -y graphviz +call :install "c:\tools\doxygen" doxygen-1.9.1.windows.bin.zip +call :install "c:\tools" stable_windows_10_msbuild_Release_Win32_graphviz-2.46.1-win32.zip python -m pip install -r %SMING_HOME%/../docs/requirements.txt + +goto :EOF + +:install +if "%~1"=="" goto :EOF +mkdir %1 +curl -LO %SMINGTOOLS%/%2 +7z -o%1 -y x %2 diff --git a/Sming/Arch/Host/Tools/ci/install.sh b/Sming/Arch/Host/Tools/ci/install.sh deleted file mode 100755 index 673729e2fc..0000000000 --- a/Sming/Arch/Host/Tools/ci/install.sh +++ /dev/null @@ -1,8 +0,0 @@ -# Host install.sh - -sudo apt-get install -y clang-format-6.0 \ - python3-sphinx python3-setuptools python3-cairocffi \ - doxygen graphviz-dev xmlstarlet jq -sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/bin/clang-format-6.0 100 - -python -m pip install -r $SMING_HOME/../docs/requirements.txt diff --git a/Sming/Arch/Host/Tools/install.sh b/Sming/Arch/Host/Tools/install.sh new file mode 100755 index 0000000000..4b21178d6b --- /dev/null +++ b/Sming/Arch/Host/Tools/install.sh @@ -0,0 +1,8 @@ +# Host install.sh + +# Required by deployment script +if [ -n "$APPVEYOR" ]; then + $PKG_INSTALL \ + jq \ + xmlstarlet +fi diff --git a/Sming/Libraries/Adafruit_ILI9341/component.mk b/Sming/Libraries/Adafruit_ILI9341/component.mk index 169766e78e..91e2591dae 100644 --- a/Sming/Libraries/Adafruit_ILI9341/component.mk +++ b/Sming/Libraries/Adafruit_ILI9341/component.mk @@ -10,7 +10,7 @@ else ifeq ($(SMING_ARCH),Esp32) TFT_CS_PIN ?= 18 TFT_DC_PIN ?= 19 TFT_RESET_PIN ?= 21 -else +else ifndef MAKE_DOCS $(warning Arch unsupported) endif diff --git a/Sming/README.rst b/Sming/README.rst index f199106129..bc32255df2 100644 --- a/Sming/README.rst +++ b/Sming/README.rst @@ -18,7 +18,7 @@ Serial Communications This will recompile your application to use the revised baud rate. Note that this will change the default speed used for both flashing and serial comms. - See also :component-esp8266:`esptool` and :component:`terminal` for further details. + See also :component:`esptool` and :component:`terminal` for further details. The default rate for serial ports is 115200 baud. You can change it like this:: diff --git a/Sming/building.rst b/Sming/building.rst index 988a84fb50..397d553f0a 100644 --- a/Sming/building.rst +++ b/Sming/building.rst @@ -132,7 +132,7 @@ Hardware configuration The appropriate hardware configuration should be selected in the project's component.mk file. Use one of the standard configurations -or create your own. See :ref:`hardware-config`. +or create your own. See :ref:`hardware_config`. Configuration variables ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/Sming/project.mk b/Sming/project.mk index 39f0b48561..9833848499 100644 --- a/Sming/project.mk +++ b/Sming/project.mk @@ -486,7 +486,7 @@ cs-dev: ##Apply coding style to all files changed from current upstream develop $(SMING_MAKE) $@ .PHONY: gdb -gdb: kill_term ##Run the debugger console +gdb: ##Run the debugger console $(GDB_CMDLINE) .PHONY: fetch @@ -544,6 +544,18 @@ otaserver: all ##Launch a simple python HTTP server for testing OTA updates $(info Starting OTA server for TESTING) $(Q) cd $(FW_BASE) && $(PYTHON) -m SimpleHTTPServer $(SERVER_OTA_PORT) + +# +.PHONY: tcp-serial-redirect +tcp-serial-redirect: ##Redirect COM port to TCP port + $(info Starting serial redirector) +ifdef WSL_ROOT + $(Q) cmd.exe /c start /MIN python3 $(WSL_ROOT)/$(SMING_HOME)/../Tools/tcp_serial_redirect.py $(COM_PORT) $(COM_SPEED_SERIAL) +else + $(Q) gnome-terminal -- bash -c "$(PYTHON) $(SMING_HOME)/../Tools/tcp_serial_redirect.py $(COM_PORT) $(COM_SPEED_SERIAL)" +endif + + ##@Help .PHONY: list-config diff --git a/Sming/Arch/Esp8266/Tools/docker/README.md b/Tools/Docker/README.md similarity index 100% rename from Sming/Arch/Esp8266/Tools/docker/README.md rename to Tools/Docker/README.md diff --git a/Tools/Docker/cli/Dockerfile b/Tools/Docker/cli/Dockerfile new file mode 100644 index 0000000000..4a49c5cd20 --- /dev/null +++ b/Tools/Docker/cli/Dockerfile @@ -0,0 +1,43 @@ +FROM ubuntu + +# ------------------------------------------------------------------------------ +# Set Environment +# ------------------------------------------------------------------------------ + +# Common +ENV SMING_HOME /opt/Sming/Sming +ENV PYTHON /usr/bin/python3 + +# Esp8266 +#ENV UDK_ROOT /opt/esp-open-sdk +ENV EQT_ROOT /opt/esp-quick-toolchain +ENV ESP_HOME $EQT_ROOT + +# Esp32 +ENV IDF_PATH /opt/esp-idf +ENV IDF_TOOLS_PATH /opt/esp32 +ENV ESP32_PYTHON_PATH $PYTHON + +# ------------------------------------------------------------------------------ +# Pre-requisites +# ------------------------------------------------------------------------------ + +RUN apt-get -y update \ + && DEBIAN_FRONTEND=noninteractive \ + TZ=Europe/London \ + apt-get install -y \ + git \ + sudo \ + tzdata + +# ------------------------------------------------------------------------------ +# Fetch Sming and install tools +# ------------------------------------------------------------------------------ + +ARG SMING_BRANCH=develop +ARG SMING_REPO=SmingHub/Sming +ARG INSTALL_ARGS=all + +RUN git clone -b $SMING_BRANCH -- https://github.com/$SMING_REPO $(readlink -m $SMING_HOME/..) + +RUN $SMING_HOME/../Tools/install.sh $INSTALL_ARGS diff --git a/Tools/Docker/cli/docker-compose.yml b/Tools/Docker/cli/docker-compose.yml new file mode 100644 index 0000000000..15c63a5d56 --- /dev/null +++ b/Tools/Docker/cli/docker-compose.yml @@ -0,0 +1,16 @@ +sming-cli: + + build: . + +# Uncomment the lines below if you want to use the source code in your host computer +# volumes: +# - ../../Sming/:/workspace/Sming/ + + ports: + - "10080:80" + +# Modify the line below if you serial adapter is mapped to a different port +# devices: +# - "/dev/ttyUSB0:/dev/ttyUSB0" + + privileged: true diff --git a/Tools/Docker/ide/Dockerfile b/Tools/Docker/ide/Dockerfile new file mode 100644 index 0000000000..bc046b46a3 --- /dev/null +++ b/Tools/Docker/ide/Dockerfile @@ -0,0 +1,42 @@ +# ------------------------------------ +# Fast dockerized development environment +# for the Sming Framework: https://github.com/SmingHub/Sming.git +# ------------------------------------ +FROM docker_sming-cli +MAINTAINER Slavey Karadzhov "slav@attachix.com" + +# ------------------------------------------------------------------------------ +# Install Cloud9 and Supervisor +# ------------------------------------------------------------------------------ + +RUN apt-get update -y \ + && apt-get install -y \ + apache2-utils \ + libxml2-dev \ + locales-all \ + npm \ + nodejs \ + sshfs \ + supervisor \ + tmux + +RUN git clone https://github.com/c9/core.git /cloud9 \ + && curl -s -L https://raw.githubusercontent.com/c9/install/master/install.sh | bash \ + && /cloud9/scripts/install-sdk.sh \ + && sed -i -e 's_127.0.0.1_0.0.0.0_g' /cloud9/configs/standalone.js \ + && mkdir -p /var/log/supervisor + +ADD supervisord.conf /etc/ + +# VOLUME /workspace + +EXPOSE 80 + +SHELL ["/bin/bash", "-c"] + +CMD ["/usr/bin/supervisord", "--nodaemon", "--configuration", "/etc/supervisord.conf"] + +COPY assets/welcome.html /cloud9/plugins/c9.ide.welcome/welcome.html +COPY assets/welcome.js /cloud9/plugins/c9.ide.welcome/welcome.js + +ENTRYPOINT /usr/bin/supervisord diff --git a/Tools/Docker/ide/LICENSE b/Tools/Docker/ide/LICENSE new file mode 100644 index 0000000000..725e164f24 --- /dev/null +++ b/Tools/Docker/ide/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2017 Slavey Karadzhov +Copyright (c) 2014 Kevin Delfour + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Sming/Arch/Esp8266/Tools/docker/assets/welcome.html b/Tools/Docker/ide/assets/welcome.html similarity index 64% rename from Sming/Arch/Esp8266/Tools/docker/assets/welcome.html rename to Tools/Docker/ide/assets/welcome.html index eae672a9f8..1dbce867cf 100644 --- a/Sming/Arch/Esp8266/Tools/docker/assets/welcome.html +++ b/Tools/Docker/ide/assets/welcome.html @@ -6,7 +6,7 @@